Traditional menu structure is fine. However, buttons or menu items we often use are not so many. Some mightn’t be clicked at all. Therefore, it’s a good idea to put some frequently used buttons on the toolbar and put the rest of the less frequently used operations into the menu. Such menu are often connected to GtkMenuButton.
We will restructure tfe text file editor in this section. It will be more practical. The buttons are changed to:
The four buttons are included in the ui file tfe.ui
. The
difference from prior sections is signal tag. The following is extracted
from tfe.ui
and it describes the open button.
object class="GtkButton" id="btno">
<property name="label">Open</property>
<signal name="clicked" handler="open_cb" swapped="TRUE" object="nb"></signal>
<object> </
Signal tag specifies the name of the signal, handler and user_data
object. They are the value of name, handler and object attributes.
Swapped attribute has the same meaning as
g_signal_connect_swapped
function. So, the signal tag above
works the same as the function below.
(btno, "clicked", G_CALLBACK (open_cb), nb); g_signal_connect_swapped
You need to compile the source file with “-WI, –export-dynamic”
options. You can achieve this by adding “export_dynamic: true” argument
to executable function in meson.build
. And remove static
class from the handler.
void
(GtkNotebook *nb) {
open_cb (nb);
notebook_page_open }
If you add static, the function is in the scope of the file and it can’t be seen from outside. Then the signal tag can’t find the function.
Menus are described in menu.ui
file.
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<menu id="menu">
<section>
<item>
<attribute name="label">New</attribute>
<attribute name="action">win.new</attribute>
</item>
<item>
<attribute name="label">Save As…</attribute>
<attribute name="action">win.saveas</attribute>
</item>
</section>
<section>
<item>
<attribute name="label">Preference</attribute>
<attribute name="action">win.pref</attribute>
</item>
</section>
<section>
<item>
<attribute name="label">Quit</attribute>
<attribute name="action">win.close-all</attribute>
</item>
</section>
</menu>
</interface>
There are four items, “New”, “Saveas”, “Preference” and “Quit”.
tfe
has only font preference.These four menus are not used so often. That’s why they are put to the menu behind the menu button.
The menus and the menu button are connected with
gtk_menu_button_set_menu_model
function. The variable
btnm
below points a GtkMenuButton object.
= gtk_builder_new_from_resource ("/com/github/ToshioCP/tfe/menu.ui");
build = G_MENU_MODEL (gtk_builder_get_object (build, "menu"));
menu (btnm, menu); gtk_menu_button_set_menu_model
Menus are connected to actions. Actions are defined with an array and
g_action_map_add_action_entries
function.
const GActionEntry win_entries[] = {
{ "open", open_activated, NULL, NULL, NULL },
{ "save", save_activated, NULL, NULL, NULL },
{ "close", close_activated, NULL, NULL, NULL },
{ "new", new_activated, NULL, NULL, NULL },
{ "saveas", saveas_activated, NULL, NULL, NULL },
{ "pref", pref_activated, NULL, NULL, NULL },
{ "close-all", quit_activated, NULL, NULL, NULL }
};
(G_ACTION_MAP (win), win_entries, G_N_ELEMENTS (win_entries), nb); g_action_map_add_action_entries
There are seven actions, open, save, close, new, saveas, pref and close-all. But there were only four menus. New, saveas, pref and close-all actions correspond to new, saveas, preference and quit menu respectively. The three actions open, save and close doesn’t have corresponding menus. Are thy necessary? These actions are defined because of accelerators.
Accelerators are a kind of short cut key function. They are defined
with arrays and gtk_application_set_accels_for_action
function.
struct {
const char *action;
const char *accels[2];
} action_accels[] = {
{ "win.open", { "<Control>o", NULL } },
{ "win.save", { "<Control>s", NULL } },
{ "win.close", { "<Control>w", NULL } },
{ "win.new", { "<Control>n", NULL } },
{ "win.saveas", { "<Shift><Control>s", NULL } },
{ "win.close-all", { "<Control>q", NULL } },
};
for (i = 0; i < G_N_ELEMENTS(action_accels); i++)
(GTK_APPLICATION(app), action_accels[i].action, action_accels[i].accels); gtk_application_set_accels_for_action
This code is a bit complicated. The array
action-accels[]
is an array of structures. The structure
is:
struct {
const char *action;
const char *accels[2];
}
The member action
is a string. The member
accels
is an array of two strings. For example,
{ "win.open", { "<Control>o", NULL } },
This is the first element of the array
action_accels
.
action
is “win.open”. This specifies the
action “open” belongs to the window object.accels
is an array of two strings,
“<Control>o” and NULL. The first string specifies a key
combination. Control key and ‘o’. If you keep pressing the control key
and push ‘o’ key, then it activates the action win.open
.
The second string NULL (or zero) means the end of the list (array). You
can define more than one accelerator keys and the list must ends with
NULL (zero). If you want to do so, the array length needs to be three or
more. The parser recognizes “<control>o”,
“<Shift><Alt>F2”, “<Ctrl>minus” and so on. If you want
to use symbol key like “<Ctrl>-”, use “<Ctrl>minus” instead.
Such relation between lower case and symbol (its character code) is
specified in gdkkeysyms.h
in the GTK 4 source code.TfeTextView has already had a saveas function. So, only we need to
write is the wrapper function in tfenotebook.c
.
static TfeTextView *
get_current_textview (GtkNotebook *nb) {
int i;
GtkWidget *scr;
GtkWidget *tv;
i = gtk_notebook_get_current_page (nb);
scr = gtk_notebook_get_nth_page (nb, i);
tv = gtk_scrolled_window_get_child (GTK_SCROLLED_WINDOW (scr));
return TFE_TEXT_VIEW (tv);
}
void
notebook_page_saveas (GtkNotebook *nb) {
g_return_if_fail(GTK_IS_NOTEBOOK (nb));
TfeTextView *tv;
tv = get_current_textview (nb);
tfe_text_view_saveas (TFE_TEXT_VIEW (tv));
}
The function get_current_textview
is the same as before.
The function notebook_page_saveas
simply calls
tfe_text_view_saveas
.
In tfeapplication.c
, saveas handler just call
notebook_page_saveas
.
static void
saveas_activated (GSimpleAction *action, GVariant *parameter, gpointer user_data) {
GtkNotebook *nb = GTK_NOTEBOOK (user_data);
notebook_page_saveas (nb);
}
Preference dialog xml definition is added to tfe.ui
.
object class="GtkDialog" id="pref">
<property name="title">Preferences</property>
<property name="resizable">FALSE</property>
<property name="modal">TRUE</property>
<property name="transient-for">win</property>
<child internal-child="content_area">
<object class="GtkBox" id="content_area">
<child>
<object class="GtkBox" id="pref_boxh">
<property name="orientation">GTK_ORIENTATION_HORIZONTAL</property>
<property name="spacing">12</property>
<property name="margin-start">12</property>
<property name="margin-end">12</property>
<property name="margin-top">12</property>
<property name="margin-bottom">12</property>
<child>
<object class="GtkLabel" id="fontlabel">
<property name="label">Font:</property>
<property name="xalign">1</property>
<object>
</child>
</child>
<object class="GtkFontButton" id="fontbtn">
<object>
</child>
</object>
</child>
</object>
</child>
</object> </
win
.
Therefore, There’s no child tag that surrounds the dialog object.gtkdialog.ui
, which is the ui file of GtkDialog. (It is in
the GTK 4 source files.) This box is provided for users to add content
widgets in it. The tag
<child internal-child="content_area">
is put at the
top of the contents. Then you need to specify an object tag and define
its class as GtkBox and its id as content_area. This object is defined
in gtkdialog.ui
but you need to define it again in the
child tag.I want the preference dialog to keep alive during the application lives. So, it is necessary to catch “close-request” signal from the dialog and stop the signal propagation. This is accomplished by returning TRUE by the signal handler.
(GtkDialog *pref, gpointer user_data) {
pref_close_cb return TRUE;
}
(GTK_DIALOG (pref), "close-request", G_CALLBACK (pref_close_cb), NULL); g_signal_connect
Generally, signal emission consists of five stages.
G_SIGNAL_RUN_FIRST
. Default handler is set when a signal is
registered. It is different from user signal handler, simply called
signal handler, connected by g_signal_connect
series
function. Default handler can be invoked in either stage 1, 3 or 5. Most
of the default handlers are G_SIGNAL_RUN_FIRST
or
G_SIGNAL_RUN_LAST
.g_signal_connect_after
.G_SIGNAL_RUN_LAST
.g_signal_connect_after
.G_SIGNAL_RUN_CLEANUP
.In the case of “close-request” signal, the default handler’s flag is
G_SIGNAL_RUN_LAST
. The handler pref_close_cb
is not connected by g_signal_connect_after
. So the number
of stages are two.
pref_close_cb
is invoked.And If the user signal handler returns TRUE, then other handlers will be stopped being invoked. Therefore, the program above prevents the invocation of the default handler and stop the closing process of the dialog.
The following codes are extracted from
tfeapplication.c
.
static gulong pref_close_request_handler_id = 0;
static gulong alert_close_request_handler_id = 0;
... ...
static gboolean
(GtkDialog *dialog, gpointer user_data) {
dialog_close_cb (GTK_WIDGET (dialog));
gtk_widget_hide return TRUE;
}
... ...
static void
(GSimpleAction *action, GVariant *parameter, gpointer nb) {
pref_activated (GTK_WIDGET (pref));
gtk_widget_show }
... ...
/* ----- quit application ----- */
void
(GtkWindow *win) {
tfe_application_quit if (pref_close_request_handler_id > 0)
(pref, pref_close_request_handler_id);
g_signal_handler_disconnect if (alert_close_request_handler_id > 0)
(alert, alert_close_request_handler_id);
g_signal_handler_disconnect (&settings);
g_clear_object (GTK_WINDOW (alert));
gtk_window_destroy (GTK_WINDOW (pref));
gtk_window_destroy (win);
gtk_window_destroy }
... ...
static void
(GApplication *application) {
tfe_startup
... ...
= GTK_DIALOG (gtk_builder_get_object (build, "pref"));
pref = g_signal_connect (GTK_DIALOG (pref), "close-request", G_CALLBACK (dialog_close_cb), NULL);
pref_close_request_handler_id
... ...
}
The function tfe_application_quit
destroys top-level
windows and quits the application. It first disconnects the handlers
from the signal “close-request”.
If a user closes a page which hasn’t been saved, it is advisable to show an alert to confirm it. Alert dialog is used in this application for such a situation.
object class="GtkDialog" id="alert">
<property name="title">Are you sure?</property>
<property name="resizable">FALSE</property>
<property name="modal">TRUE</property>
<property name="transient-for">win</property>
<child internal-child="content_area">
<object class="GtkBox">
<child>
<object class="GtkBox">
<property name="orientation">GTK_ORIENTATION_HORIZONTAL</property>
<property name="spacing">12</property>
<property name="margin-start">12</property>
<property name="margin-end">12</property>
<property name="margin-top">12</property>
<property name="margin-bottom">12</property>
<child>
<object class="GtkImage">
<property name="icon-name">dialog-warning</property>
<property name="icon-size">GTK_ICON_SIZE_LARGE</property>
<object>
</child>
</child>
<object class="GtkLabel" id="lb_alert">
<object>
</child>
</object>
</child>
</object>
</child>
</child type="action">
<object class="GtkButton" id="btn_cancel">
<property name="label">Cancel</property>
<object>
</child>
</child type="action">
<object class="GtkButton" id="btn_accept">
<property name="label">Close</property>
<object>
</child>
</action-widgets>
<action-widget response="cancel" default="true">btn_cancel</action-widget>
<action-widget response="accept">btn_accept</action-widget>
<action-widgets>
</signal name="response" handler="alert_response_cb" swapped="NO" object="nb"></signal>
<object> </
This ui file describes the alert dialog. Some part are the same as preference dialog. There are two objects in the content area, GtkImage and GtkLabel.
GtkImage shows an image. The image can comes from files, resources,
icon theme and so on. The image above displays an icon from the current
icon theme. You can see icons in the theme by
gtk4-icon-browser
.
$ gtk4-icon-browser
The icon named “dialog-warning” is something like this.
These are made by my hand. The real image on the alert dialog is nicer.
The GtkLabel lb_alert
has no text yet. An alert message
will be inserted by the program later.
There are two child tags which have “action” type. They are button
objects located in the action area. Action-widgets tag describes the
actions of the buttons. btn_cancel
button emits response
signal with cancel response (GTK_RESPONSE_CANCEL
) if it is
clicked on. btn_accept
button emits response signal with
accept response (GTK_RESPONSE_ACCEPT
) if it is clicked on.
The response signal is connected to alert_response_cb
handler.
The alert dialog keeps alive while the application lives. The
“close-request” signal is stopped by the handler
dialog_close_cb
like the preference dialog.
If a user closes a page or quits the application without saving the contents, the application alerts.
static gboolean is_quit;
... ...
void
(GtkNotebook *nb) {
close_cb = false;
is_quit if (has_saved (GTK_NOTEBOOK (nb)))
(GTK_NOTEBOOK (nb));
notebook_page_close else {
(lb_alert, "Contents aren't saved yet.\nAre you sure to close?");
gtk_label_set_text (close_btn_close, "Close");
gtk_button_set_label (GTK_WIDGET (alert));
gtk_widget_show }
}
... ...
static void
(GSimpleAction *action, GVariant *parameter, gpointer user_data) {
close_activated *nb = GTK_NOTEBOOK (user_data);
GtkNotebook (nb);
close_cb }
... ...
void
(GtkDialog *alert, int response_id, gpointer user_data) {
alert_response_cb *nb = GTK_NOTEBOOK (user_data);
GtkNotebook *win = gtk_widget_get_ancestor (GTK_WIDGET (nb), GTK_TYPE_WINDOW);
GtkWidget
(GTK_WIDGET (alert));
gtk_widget_hide if (response_id == GTK_RESPONSE_ACCEPT) {
if (is_quit)
(GTK_WINDOW (win));
tfe_application_quit else
(nb);
notebook_page_close }
}
static void
(GSimpleAction *action, GVariant *parameter, gpointer user_data) {
quit_activated *nb = GTK_NOTEBOOK (user_data);
GtkNotebook *win = gtk_widget_get_ancestor (GTK_WIDGET (nb), GTK_TYPE_WINDOW);
GtkWidget
= true;
is_quit if (has_saved_all (nb))
(GTK_WINDOW (win));
tfe_application_quit else {
(lb_alert, "Contents aren't saved yet.\nAre you sure to quit?");
gtk_label_set_text (btn_accept, "Quit");
gtk_button_set_label (GTK_WIDGET (alert));
gtk_widget_show }
}
static void
(GApplication *application) {
tfe_startup
... ...
= GTK_DIALOG (gtk_builder_get_object (build, "alert"));
alert = g_signal_connect (GTK_DIALOG (alert), "close-request", G_CALLBACK (dialog_close_cb), NULL);
alert_close_request_handler_id = GTK_LABEL (gtk_builder_get_object (build, "lb_alert"));
lb_alert = GTK_BUTTON (gtk_builder_get_object (build, "btn_accept"));
btn_accept
... ...
}
The static variable is_quit
is true when user tries to
quit the application and false otherwise. When user presses “Ctrl-w”,
close_activated
handler is invoked. It just calls
close_cb
. When user clicks on the close button,
close_cb
handler is invoked.
The handler sets is_quit
to false. The function
has_saved
returns true if the current page has been saved.
If it is true, it calls notebook_page_close
to close the
current page. Otherwise, it sets the message of the dialog and the label
of the button, then shows the alert dialog.
The response signal of the dialog is connected to the handler
alert_response_cb
. It hides the dialog first. Then checks
the response_id
. If it is GTK_RESPONSE_ACCEPT
,
which means user clicked on the close button, then it closes the current
page. Otherwise it does nothing.
When user press “Ctrl-q” or clicked on the quit menu, then
quit_activated
handler is invoked. The handler sets
is_quit
to true. The function has_saved_all
returns true if all the pages have been saved. If it is true, it calls
tfe_application_quit
to quit the application. Otherwise, it
sets the message of the dialog and the label of the button, then shows
the alert dialog.
If the user clicked on the buttons on the alert dialog,
alert_resoponse_cb
is invoked. It hides the dialog and
checks the response_id
. If it is
GTK_RESPONSE_ACCEPT
, which means user clicked on the quit
button, then it calls tfe_application_quit
to quit the
application. Otherwise it does nothing.
The static variables alert
, lb_alert
and
btn_accept
are set in the startup handler. And the signal
“close-request” and dialog_close_cb
handler are
connected.
gboolean
has_saved (GtkNotebook *nb) {
g_return_val_if_fail (GTK_IS_NOTEBOOK (nb), false);
TfeTextView *tv;
GtkTextBuffer *tb;
tv = get_current_textview (nb);
tb = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv));
if (gtk_text_buffer_get_modified (tb))
return false;
else
return true;
}
gboolean
has_saved_all (GtkNotebook *nb) {
g_return_val_if_fail (GTK_IS_NOTEBOOK (nb), false);
int i, n;
GtkWidget *scr;
GtkWidget *tv;
GtkTextBuffer *tb;
n = gtk_notebook_get_n_pages (nb);
for (i = 0; i < n; ++i) {
scr = gtk_notebook_get_nth_page (nb, i);
tv = gtk_scrolled_window_get_child (GTK_SCROLLED_WINDOW (scr));
tb = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv));
if (gtk_text_buffer_get_modified (tb))
return false;
}
return true;
}
has_saved
function.gtk_text_buffer_get_modified
returns
true if the content of the buffer has been modified since the modified
flag had set false. The flag is set to false when:
has_saved_all
function. This function is similar
to has_saved
function. It returns true if all the pages
have been saved. It returns false if at least one page has been modified
since it last had been saved.If you have some pages and edit them together, you might be confused which file needs to be saved. Common file editors changes the tab when the contents are modified. GtkTextBuffer provides “modified-changed” signal to notify the modification.
static void
(GtkNotebook *nb, GtkWidget *tv, char *filename) {
notebook_page_build ... ...
(GTK_TEXT_VIEW (tv), "change-file", G_CALLBACK (file_changed_cb), NULL);
g_signal_connect (tb, "modified-changed", G_CALLBACK (modified_changed_cb), tv);
g_signal_connect }
When a page is built, connect “change-file” and “modified-changed”
signals to file_changed_cb
and
modified_changed_cb
handlers respectively.
static void
file_changed_cb (TfeTextView *tv) {
GtkWidget *nb = gtk_widget_get_ancestor (GTK_WIDGET (tv), GTK_TYPE_NOTEBOOK);
GtkWidget *scr;
GtkWidget *label;
GFile *file;
char *filename;
if (! GTK_IS_NOTEBOOK (nb)) /* tv not connected to nb yet */
return;
file = tfe_text_view_get_file (tv);
scr = gtk_widget_get_parent (GTK_WIDGET (tv));
if (G_IS_FILE (file)) {
filename = g_file_get_basename (file);
g_object_unref (file);
} else
filename = get_untitled ();
label = gtk_label_new (filename);
gtk_notebook_set_tab_label (GTK_NOTEBOOK (nb), scr, label);
}
static void
modified_changed_cb (GtkTextBuffer *tb, gpointer user_data) {
TfeTextView *tv = TFE_TEXT_VIEW (user_data);
GtkWidget *scr = gtk_widget_get_parent (GTK_WIDGET (tv));
GtkWidget *nb = gtk_widget_get_ancestor (GTK_WIDGET (tv), GTK_TYPE_NOTEBOOK);
GtkWidget *label;
const char *filename;
char *text;
if (! GTK_IS_NOTEBOOK (nb)) /* tv not connected to nb yet */
return;
else if (gtk_text_buffer_get_modified (tb)) {
filename = gtk_notebook_get_tab_label_text (GTK_NOTEBOOK (nb), scr);
text = g_strdup_printf ("*%s", filename);
label = gtk_label_new (text);
g_free (text);
gtk_notebook_set_tab_label (GTK_NOTEBOOK (nb), scr, label);
} else
file_changed_cb (tv);
}
file_changed_cb
handler.tv
isn’t a descendant of nb
.
That is, there’s no page corresponds to tv
. Then, it isn’t
necessary to change the name of the tab because no tab exists.file
is GFile, then it gets the filename and
unrefs file
.file
is probably NULL and it assigns
“Untitled” related name to filename
filename
and sets the tab
of the page with the GtkLabel.modified_changed_cb
handler.tv
isn’t a descendant of nb
,
then nothing needs to be done.file_changed_cb
and resets the filename,
that means it leaves out the asterisk.The GtkFontButton is a button which displays the current font. It opens a font chooser dialog if a user clicked on the button. A user can change the font (family, style, weight and size) with the dialog. Then the button keeps the new font and displays it.
The button and its signal “font-set” is initialized in the application startup process.
static void
(GtkFontButton *fontbtn, gpointer user_data) {
font_set_cb *win = GTK_WINDOW (user_data);
GtkWindow *pango_font_desc;
PangoFontDescription
= gtk_font_chooser_get_font_desc (GTK_FONT_CHOOSER (fontbtn));
pango_font_desc (win, pango_font_desc);
set_font_for_display_with_pango_font_desc }
static void
(GApplication *application) {
tfe_startup
... ...
= GTK_FONT_BUTTON (gtk_builder_get_object (build, "fontbtn"));
fontbtn (fontbtn, "font-set", G_CALLBACK (font_set_cb), win);
g_signal_connect
... ...
}
In the startup handler, set the variable fontbtn
to
point the GtkFontButton object. Then connect the “font-set” signal to
font_set_cb
handler. The signal “font-set” is emitted when
the user selects a font.
GtkFontChooser is an interface implemented by GtkFontButton. The
function gtk_font_chooser_get_font_desc
gets the
PangoFontDescription of the currently selected font.
Another function gtk_font_chooser_get_font
returns a
font name which includes family, style, weight and size. I thought it
might be able to be applied to tfe editor. The font name can be used to
the font
property of GtkTextTag as it is. But it can’t be
used to the CSS without converting the string to fit. CSS is appropriate
to change the font of entire text in all the buffers. I think GtkTextTag
is less appropriate. If you know a good solution, please post it to issue and
let me know.
It takes many codes to set the CSS from the PangoFontDescription so
the task is left to the function
set_font_for_display_with_pango_font_desc
.
A new file css.c
is made for functions related to
CSS.
#include "tfe.h"
void
set_css_for_display (GtkWindow *win, const char *css) {
GdkDisplay *display;
display = gtk_widget_get_display (GTK_WIDGET (win));
GtkCssProvider *provider = gtk_css_provider_new ();
gtk_css_provider_load_from_data (provider, css, -1);
gtk_style_context_add_provider_for_display (display, GTK_STYLE_PROVIDER (provider), GTK_STYLE_PROVIDER_PRIORITY_USER);
}
void
set_font_for_display (GtkWindow *win, const char *fontfamily, const char *fontstyle, const char *fontweight, int fontsize) {
char *textview_css;
textview_css = g_strdup_printf ("textview {padding: 10px; font-family: \"%s\"; font-style: %s; font-weight: %s; font-size: %dpt;}",
fontfamily, fontstyle, fontweight, fontsize);
set_css_for_display (win, textview_css);
g_free (textview_css);
}
void
set_font_for_display_with_pango_font_desc (GtkWindow *win, PangoFontDescription *pango_font_desc) {
PangoStyle pango_style;
PangoWeight pango_weight;
const char *family;
const char *style;
const char *weight;
int fontsize;
family = pango_font_description_get_family (pango_font_desc);
pango_style = pango_font_description_get_style (pango_font_desc);
switch (pango_style) {
case PANGO_STYLE_NORMAL:
style = "normal";
break;
case PANGO_STYLE_ITALIC:
style = "italic";
break;
case PANGO_STYLE_OBLIQUE:
style = "oblique";
break;
default:
style = "normal";
break;
}
pango_weight = pango_font_description_get_weight (pango_font_desc);
switch (pango_weight) {
case PANGO_WEIGHT_THIN:
weight = "100";
break;
case PANGO_WEIGHT_ULTRALIGHT:
weight = "200";
break;
case PANGO_WEIGHT_LIGHT:
weight = "300";
break;
case PANGO_WEIGHT_SEMILIGHT:
weight = "350";
break;
case PANGO_WEIGHT_BOOK:
weight = "380";
break;
case PANGO_WEIGHT_NORMAL:
weight = "400"; /* or "normal" */
break;
case PANGO_WEIGHT_MEDIUM:
weight = "500";
break;
case PANGO_WEIGHT_SEMIBOLD:
weight = "600";
break;
case PANGO_WEIGHT_BOLD:
weight = "700"; /* or "bold" */
break;
case PANGO_WEIGHT_ULTRABOLD:
weight = "800";
break;
case PANGO_WEIGHT_HEAVY:
weight = "900";
break;
case PANGO_WEIGHT_ULTRAHEAVY:
weight = "900"; /* In PangoWeight definition, the weight is 1000. But CSS allows the weight below 900. */
break;
default:
weight = "normal";
break;
}
fontsize = pango_font_description_get_size (pango_font_desc) / PANGO_SCALE;
set_font_for_display (win, family, style, weight, fontsize);
}
set_css_for_display
. This function sets CSS for
GdkDisplay. The content of the function is the same as the part of
startup handler in the previous version of
tfeapplication.c
.set_font_for_display
. This function sets CSS
with font-family, font-style, font-weight and font-size.
g_strdup_printf
creates a new string with printf-like formatting.set_font_for_display_with_pango_font_desc
. This
function takes out font-family, font-style, font-weight and font-size
from the PangoFontDescription object and calls
set_font
for_display`.pango_font_desc
.pango_font_desc
. The
functions pango_font_description_get_style
returns an
enumerated value.pango_font_desc
. The
function pango_font_description_get_weight
returns an
enumerated value. They corresponds to the numbers from 100 to 900.pango_font_desc
. The function
pango_font_description_get_size
returns the size of a font.
The unit of this size is (1/PANGO_SCALE)pt. If the font size is 10pt,
the function returns 10PANGO_SCALE. PANGO_SCALE is defined as 1024.
Therefore, 10PANGO_SCALE is 10240.set_font_for_display
to set CSS for the
GdkDisplay.For further information, see Pango API Reference.
We want to maintain the font data after the application quits. There are some ways to implement it.
The coding with GSettings object is simple and easy. However, it is a bit hard to understand the concept. This subsection describes the concept first and then how to program it.
GSettings schema describes a set of keys, value types and some other information. GSettings object uses this schema and it writes/reads the value of a key to/from the right place in the database.
font
is defined with a path
/com/github/ToshioCP/tfe/
, the key’s location in the
database is /com/github/ToshioCP/tfe/font
. Path is a string
begins with and ends with a slash (/
). And it is delimited
by slashes.-
) and ends with lower case or digit. No consecutive
dashes are allowed. Values can be any type. GSettings stores values as
GVariant type, which may contain, for example, integer, double, boolean,
string or complex types like an array. The type of values needs to be
defined in the schema.Schemas are described in an XML format. For example,
<?xml version="1.0" encoding="UTF-8"?>
<schemalist>
<schema path="/com/github/ToshioCP/tfe/" id="com.github.ToshioCP.tfe">
<key name="font" type="s">
<default>'Monospace 12'</default>
<summary>Font</summary>
<description>The font to be used for textview.</description>
</key>
</schema>
</schemalist>
Further information is in GLib API Reference, VarientType.
First, let’s try gsettings
application. It is a
configuration tool for GSettings.
$ gsettings help
Usage:
gsettings --version
gsettings [--schemadir SCHEMADIR] COMMAND [ARGS?]
Commands:
help Show this information
list-schemas List installed schemas
list-relocatable-schemas List relocatable schemas
list-keys List keys in a schema
list-children List children of a schema
list-recursively List keys and values, recursively
range Queries the range of a key
describe Queries the description of a key
get Get the value of a key
set Set the value of a key
reset Reset the value of a key
reset-recursively Reset all values in a given schema
writable Check if a key is writable
monitor Watch for changes
Use "gsettings help COMMAND" to get detailed help.
List schemas.
$ gsettings list-schemas
org.gnome.rhythmbox.podcast
ca.desrt.dconf-editor.Demo.Empty
org.gnome.gedit.preferences.ui
org.gnome.evolution-data-server.calendar
org.gnome.rhythmbox.plugins.generic-player
... ...
Each line is an id of a schema. Each schema has a key-value
configuration data. You can see them with list-recursively command.
Let’s look at the keys and values of org.gnome.calculator
schema.
$ gsettings list-recursively org.gnome.calculator
org.gnome.calculator source-currency ''
org.gnome.calculator source-units 'degree'
org.gnome.calculator button-mode 'basic'
org.gnome.calculator target-currency ''
org.gnome.calculator base 10
org.gnome.calculator angle-units 'degrees'
org.gnome.calculator word-size 64
org.gnome.calculator accuracy 9
org.gnome.calculator show-thousands false
org.gnome.calculator window-position (122, 77)
org.gnome.calculator refresh-interval 604800
org.gnome.calculator target-units 'radian'
org.gnome.calculator precision 2000
org.gnome.calculator number-format 'automatic'
org.gnome.calculator show-zeroes false
This schema is used by GNOME Calculator. Run the calculator and change the mode, then check the schema again.
$ gnome-calculator
Then, change the mode to advanced and quit.
Run gsettings and check whether the value of button-mode
changes.
$ gsettings list-recursively org.gnome.calculator
... ...
org.gnome.calculator button-mode 'advanced'
... ...
Now we know that GNOME Calculator used gsettings and it has set
button-mode
key to “advanced”. The value remains even the
calculator quits. So when the calculator is run again, it will appear as
an advanced mode calculator.
GSettings schemas are specified with an XML format. The XML schema
files must have the filename extension .gschema.xml
. The
following is the XML schema file for the application
tfe
.
<?xml version="1.0" encoding="UTF-8"?>
<schemalist>
<schema path="/com/github/ToshioCP/tfe/" id="com.github.ToshioCP.tfe">
<key name="font" type="s">
<default>'Monospace 12'</default>
<summary>Font</summary>
<description>The font to be used for textview.</description>
</key>
</schema>
</schemalist>
The filename is “com.github.ToshioCP.tfe.gschema.xml”. Schema XML filenames are usually the schema id followed by “.gschema.xml” suffix. You can use different name from schema id, but it is not recommended.
<schemalist>
.path
and id
attributes.
A path determines where the settings are stored in the conceptual global
tree of settings. An id identifies the schema.font
is
Monospace 12
.The XML file is compiled by glib-compile-schemas. When compiling,
glib-compile-schemas
compiles all the XML files which have
“.gschema.xml” file extension in the directory given as an argument. It
converts the XML file into a binary file gschemas.compiled
.
Suppose the XML file above is under tfe6
directory.
$ glib-compile-schemas tfe6
Then, gschemas.compiled
is generated under
tfe6
. When you test your application, set
GSETTINGS_SCHEMA_DIR
so that GSettings objet can find
gschemas.compiled
.
$ GSETTINGS_SCHEMA_DIR=(the directory gschemas.compiled is located):$GSETTINGS_SCHEMA_DIR (your application name)
This is because GSettings object searches
GSETTINGS_SCHEMA_DIR
for
gschemas.compiled
.
GSettings object looks for this file by the following process.
glib-2.0/schemas
subdirectories of all the
directories specified in the environment variable
XDG_DATA_DIRS
. Most common directory is
/usr/share/glib-2.0/schemas
.GSETTINGS_SCHEMA_DIR
environment variable is
defined, it searches all the directories specified in the variable.
GSETTINGS_SCHEMA_DIR
can specify multiple directories
delimited by colon (:).In the directories above, all the .gschema.xml
files are
stored. Therefore, when you install your application, follow the
instruction below to install your schemas.
.gschema.xml
file./usr/local/share/glib-2.0/schemas
.glib-compile-schemas
on the directory above.Meson provides gnome.compile_schemas
method to compile
XML file in the build directory. This is used to test the application.
Write the following to the meson.build
file.
gnome.compile_schemas(build_by_default: true, depend_files: 'com.github.ToshioCP.tfe.gschema.xml')
build_by_default
: If it is true, the target will be
build by default.depend_files
: XML files to be compiled.In the example above, this method runs
glib-compile-schemas
to generate
gschemas.compiled
from the XML file
com.github.ToshioCP.tfe.gschema.xml
. The file
gschemas.compiled
is located under the build directory. If
you run meson as meson _build
and ninja as
ninja -C _build
, then it is under _build
directory.
After compilation, you can test your application like this:
$ GSETTINGS_SCHEMA_DIR=_build:$GSETTINGS_SCHEMA_DIR _build/tfe
Write gsettings related codes to `tfeapplication.c’.
... ...
static GSettings *settings;
... ...
void
(GtkWindow *win) {
tfe_application_quit ... ...
(&settings);
g_clear_object ... ...
}
static void
(GApplication *application) {
tfe_startup ... ...
= g_settings_new ("com.github.ToshioCP.tfe");
settings (settings, "font", fontbtn, "font", G_SETTINGS_BIND_DEFAULT);
g_settings_bind ... ...
}
Static variable settings
keeps a pointer to GSettings
instance. Before application quits, the application releases the
GSettings instance. The function g_clear_object
is
used.
Startup handler creates GSettings instance with the schema id
“com.github.ToshioCP.tfe” and assigns the pointer to
settings
. The function g_settings_bind
connects the settings keys (key and value) and the “font” property of
fontbtn
. Then the two values will be always the same. If
one value changes then the other will automatically change.
You need to make an effort to understand GSettings concept, but coding is very simple. Just create a GSettings object and bind it to a property of an object.
It is a good idea to install your application in
$HOME/local/bin
directory if you have installed GTK 4 from
the source (See Section 2). Then you need to put
--prefix=$HOME/local
option to meson like this.
$ meson --prefix=$HOME/local _build
If you’ve installed GTK 4 from the distribution package,
--prefix
option isn’t necessary. You just install
tfe
to the default bin directory like
/usr/local/bin
.
Modify meson.build
and add install option and set it
true in executable function.
executable('tfe', sourcefiles, resources, dependencies: gtkdep, export_dynamic: true, install: true)
You can install your application by:
$ ninja -C _build install
However, you need to do one more thing. Copy your XML file to
$HOME/local/share/glib-2.0/schemas/
, which is specified in
GSETTINGS_SCHEMA_DIR
environment variable, and run
glib-compile-schemas
on that directory.
schema_dir = get_option('prefix') / get_option('datadir') / 'glib-2.0/schemas/'
install_data('com.github.ToshioCP.tfe.gschema.xml', install_dir: schema_dir)
$HOME/local/share/glib-2.0/schemas
is assigned to the
variable schema_dir
.Meson can run a post compile script.
meson.add_install_script('glib-compile-schemas', schema_dir)
This method runs ‘glib-compile-schemas’ with an argument
schema_dir
. The following is meson.build
.
project('tfe', 'c')
gtkdep = dependency('gtk4')
gnome=import('gnome')
resources = gnome.compile_resources('resources','tfe.gresource.xml')
gnome.compile_schemas(build_by_default: true, depend_files: 'com.github.ToshioCP.tfe.gschema.xml')
sourcefiles=files('tfeapplication.c', 'tfenotebook.c', 'css.c', '../tfetextview/tfetextview.c')
executable('tfe', sourcefiles, resources, dependencies: gtkdep, export_dynamic: true, install: true)
schema_dir = get_option('prefix') / get_option('datadir') / 'glib-2.0/schemas/'
install_data('com.github.ToshioCP.tfe.gschema.xml', install_dir: schema_dir)
meson.add_install_script('glib-compile-schemas', schema_dir)
Source files of tfe
is under src/tfe6 directory. Copy
them to your temporary directory and try to compile and install.
$ meson --prefix=$HOME/local _build
$ ninja -C _build
$ GSETTINGS_SCHEMA_DIR=_build:$GSETTINGS_SCHEMA_DIR _build/tfe
$ ninja -C _build install
$ tfe
$ ls $HOME/local/bin
... ...
... tfe
... ...
$ ls $HOME/local/share/glib-2.0/schemas
com.github.ToshioCP.tfe.gschema.xml
gschema.dtd
gschemas.compiled
... ...
The screenshot is as follows.