Gtk4-tutorial/gfm/sec19.md
2021-02-18 00:08:05 +09:00

43 KiB

Up: Readme.md, Prev: Section 18, Next: Section 20

Upgrade text file editor

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:

  • Put open, save and close buttons to the toolbar. In addition, GtkMenuButton is added to the toolbar. This button shows a popup menu when clicked on. Here, popup means widely, including pull-down menu.
  • Put new, save as, preference and quit items to the menu under the menu button.

Signal elements in ui files

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 attribute. Swapped attribute has the same meaning as g_signal_connect_swapped function. So, the signal tag above works the same as the function below.

g_signal_connect_swapped (btno, "clicked", G_CALLBACK (open_cb), nb);

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
open_cb (GtkNotebook *nb) {
  notebook_page_open (nb);
}

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.

Menu and GkMenuButton

Menus are described in menu.ui file.

 1 <?xml version="1.0" encoding="UTF-8"?>
 2 <interface>
 3   <menu id="menu">
 4     <section>
 5       <item>
 6         <attribute name="label">New</attribute>
 7         <attribute name="action">win.new</attribute>
 8       </item>
 9       <item>
10         <attribute name="label">Save As…</attribute>
11         <attribute name="action">win.saveas</attribute>
12       </item>
13     </section>
14     <section>
15       <item>
16         <attribute name="label">Preference</attribute>
17         <attribute name="action">win.pref</attribute>
18       </item>
19     </section>
20     <section>
21       <item>
22         <attribute name="label">Quit</attribute>
23         <attribute name="action">win.close-all</attribute>
24       </item>
25     </section>
26   </menu>
27 </interface>

There are four items, "New", "Saveas", "Preference" and "Quit".

  • New menu creates a new empty page.
  • Saveas menu saves the current page as a new filename.
  • Preference menu sets preference items. This version of tfe has only font preference.
  • Quit menu quits the application.

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.

  build = gtk_builder_new_from_resource ("/com/github/ToshioCP/tfe/menu.ui");
  menu = G_MENU_MODEL (gtk_builder_get_object (build, "menu"));
  gtk_menu_button_set_menu_model (btnm, menu);

Actions and Accelerators

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_add_action_entries (G_ACTION_MAP (win), win_entries, G_N_ELEMENTS (win_entries), nb);

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_set_accels_for_action(GTK_APPLICATION(app), action_accels[i].action, action_accels[i].accels);

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.

  • The member action is "win.open". This specifies the action "open" belongs to the window object.
  • The member accels is an array of two strings, "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 "o", "F2", "minus" and so on. If you want to use symbol key like "-", use "minus" instead. Such symbol to lower case name relation is specified in gdkkeysyms.h in the gtk4 source code.

Saveas handler

TfeTextView has already had a saveas function. So, only we need to write is the wrapper function in tfenotebook.c.

 1 static TfeTextView *
 2 get_current_textview (GtkNotebook *nb) {
 3   int i;
 4   GtkWidget *scr;
 5   GtkWidget *tv;
 6 
 7   i = gtk_notebook_get_current_page (nb);
 8   scr = gtk_notebook_get_nth_page (nb, i);
 9   tv = gtk_scrolled_window_get_child (GTK_SCROLLED_WINDOW (scr));
10   return TFE_TEXT_VIEW (tv);
11 }
12 
13 void
14 notebook_page_saveas (GtkNotebook *nb) {
15   g_return_if_fail(GTK_IS_NOTEBOOK (nb));
16 
17   TfeTextView *tv;
18 
19   tv = get_current_textview (nb);
20   tfe_text_view_saveas (TFE_TEXT_VIEW (tv));
21 }

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.

1 static void
2 saveas_activated (GSimpleAction *action, GVariant *parameter, gpointer nb) {
3   notebook_page_saveas (GTK_NOTEBOOK (nb));
4 }

Preference and alert dialog

Preference dialog

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>
  • Preference dialog is an independent dialog. It is not a descendant widget of the top GtkApplicationwindow win. Therefore, no child tag of the dialog object.
  • There are four properties of the dialog specified. GtkDialog is a child object (not child widget) of GtkWindow, so it inherits all the properties from GtkWindow. Title, resizable, modal and transient-for properties are inherited from GtkWindow. Transient-for specifies a temporary parent window, which the dialog's location is based on.
  • internal-child attribute is used in the child tag above.. GtkDialog has a GtkBox child widget. Its id is "content_area" in gtkdialog.ui, which is the ui file of GtkDialog in gtk4 source files. This box is provided to 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.
  • In the content area, defines GtkBox, GtkLabel and GtkFontButton.

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.

pref_close_cb (GtkDialog *pref, gpointer user_data) {
  return TRUE;
}

g_signal_connect (GTK_DIALOG (pref), "close-request", G_CALLBACK (pref_close_cb), NULL);

Generally, signal emission consists of five stages.

  1. Default handler is invoked if the signal's flag is 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_connectseries 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.
  2. Signal handlers are invoked, unless it is connected by g_signal_connect_after.
  3. Default handler is invoked if the signal's flag is G_SIGNAL_RUN_LAST.
  4. Signal handlers are invoked, if it is connected by g_signal_connect_after.
  5. Default handler is invoked if the signal's flag is 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.

  1. Signal handler pref_close_cb is invoked.
  2. Default handler 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
dialog_close_cb (GtkDialog *dialog, gpointer user_data) {
  gtk_widget_hide (GTK_WIDGET (dialog));
  return TRUE;
}

... ...

static void
pref_activated (GSimpleAction *action, GVariant *parameter, gpointer nb) {
  gtk_widget_show (GTK_WIDGET (pref));
}

... ...

/* ----- quit application ----- */
void
tfe_application_quit (GtkWindow *win) {
  if (pref_close_request_handler_id > 0)
    g_signal_handler_disconnect (pref, pref_close_request_handler_id);
  if (alert_close_request_handler_id > 0)
    g_signal_handler_disconnect (alert, alert_close_request_handler_id);
  g_clear_object (&settings);
  gtk_window_destroy (GTK_WINDOW (alert));
  gtk_window_destroy (GTK_WINDOW (pref));
  gtk_window_destroy (win);
}

... ...

static void
tfe_startup (GApplication *application) {

  ... ...

  pref = GTK_DIALOG (gtk_builder_get_object (build, "pref"));
  pref_close_request_handler_id = g_signal_connect (GTK_DIALOG (pref), "close-request", G_CALLBACK (dialog_close_cb), NULL);

  ... ... 
}

The function tfe_application_quit destroys top level windows and quits the application. It first disconnects the handlers from the signal "close-request".

Alert dialog

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.

dialog-warning icon is like ...

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.

Close and quit handlers

If a user closes a page or quits the application without saving the contents, the application alerts.

static gboolean is_quit;

... ...

void
close_cb (GtkNotebook *nb) {
  is_quit = false;
  if (has_saved (GTK_NOTEBOOK (nb)))
    notebook_page_close (GTK_NOTEBOOK (nb));
  else {
    gtk_label_set_text (lb_alert, "Contents aren't saved yet.\nAre you sure to close?");
    gtk_button_set_label (close_btn_close, "Close");
    gtk_widget_show (GTK_WIDGET (alert));
  }
}

... ...

static void
close_activated (GSimpleAction *action, GVariant *parameter, gpointer nb) {
  close_cb (GTK_NOTEBOOK (nb));
}

... ...

void
alert_response_cb (GtkDialog *alert, int response_id, gpointer user_data) {
  GtkNotebook *nb = GTK_NOTEBOOK (user_data);
  GtkWidget *win = gtk_widget_get_ancestor (GTK_WIDGET (nb), GTK_TYPE_WINDOW);

  gtk_widget_hide (GTK_WIDGET (alert));
  if (response_id == GTK_RESPONSE_ACCEPT) {
    if (is_quit)
      tfe_application_quit (GTK_WINDOW (win));
    else
      notebook_page_close (nb);
  }
}

static void
quit_activated (GSimpleAction *action, GVariant *parameter, gpointer nb) {
  GtkWidget *win = gtk_widget_get_ancestor (GTK_WIDGET (nb), GTK_TYPE_WINDOW);

  is_quit = true;
  if (has_saved_all (GTK_NOTEBOOK (nb)))
    tfe_application_quit (GTK_WINDOW (win));
  else {
    gtk_label_set_text (lb_alert, "Contents aren't saved yet.\nAre you sure to quit?");
    gtk_button_set_label (close_btn_close, "Quit");
    gtk_widget_show (GTK_WIDGET (alert));
  }
}

static void
tfe_startup (GApplication *application) {

  ... ...

  alert = GTK_DIALOG (gtk_builder_get_object (build, "alert"));
  alert_close_request_handler_id = g_signal_connect (GTK_DIALOG (alert), "close-request", G_CALLBACK (dialog_close_cb), NULL);
  lb_alert = GTK_LABEL (gtk_builder_get_object (build, "lb_alert"));
  btn_accept = GTK_BUTTON (gtk_builder_get_object (build, "btn_accept"));

  ... ...

}

The static variable is_quit is true when user tries to quit the application and FAULT 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 text in the label and 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 check the response_id. If it is GTK_RESPONSE_ACCEPT, which means user clicked on the close button, then closes the current page.

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 text in the label and button, then shows the alert dialog.

Now alert_response_cb works similar but it calls tfe_application_quitinstead of notebook_page_close.

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.

 1 gboolean
 2 has_saved (GtkNotebook *nb) {
 3   g_return_val_if_fail (GTK_IS_NOTEBOOK (nb), false);
 4 
 5   TfeTextView *tv;
 6   GtkTextBuffer *tb;
 7 
 8   tv = get_current_textview (nb);
 9   tb = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv));
10   if (gtk_text_buffer_get_modified (tb))
11     return false;
12   else
13     return true;
14 }
15 
16 gboolean
17 has_saved_all (GtkNotebook *nb) {
18   g_return_val_if_fail (GTK_IS_NOTEBOOK (nb), false);
19 
20   int i, n;
21   GtkWidget *scr;
22   GtkWidget *tv;
23   GtkTextBuffer *tb;
24 
25   n = gtk_notebook_get_n_pages (nb);
26   for (i = 0; i < n; ++i) {
27     scr = gtk_notebook_get_nth_page (nb, i);
28     tv = gtk_scrolled_window_get_child (GTK_SCROLLED_WINDOW (scr));
29     tb = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv));
30     if (gtk_text_buffer_get_modified (tb))
31       return false;
32   }
33   return true;
34 }
  • 1-14: has_saved function.
  • 10: The 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 the buffer is generated. It is reset to false when it is replaced with a new contents from a file or it is saved to a file.
  • 11-13: This function returns true if the contents of the current page has been saved and no modification has been made. If it returns false, then the user tries to close the current page without saving the modified contents.
  • 16-33: 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.

Notebook page tab

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
notebook_page_build (GtkNotebook *nb, GtkWidget *tv, char *filename) {
  ... ...
  g_signal_connect (GTK_TEXT_VIEW (tv), "change-file", G_CALLBACK (file_changed_cb), NULL);
  g_signal_connect (tb, "modified-changed", G_CALLBACK (modified_changed_cb), tv);
}

When a page is built, connect "change-file" and "modified-changed" signals to file_changed_cb and modified_changed_cb handlers respectively.

 1 static void
 2 file_changed_cb (TfeTextView *tv) {
 3   GtkWidget *nb =  gtk_widget_get_ancestor (GTK_WIDGET (tv), GTK_TYPE_NOTEBOOK);
 4   GtkWidget *scr;
 5   GtkWidget *label;
 6   GFile *file;
 7   char *filename;
 8 
 9   if (! GTK_IS_NOTEBOOK (nb)) /* tv not connected to nb yet */
10     return;
11   file = tfe_text_view_get_file (tv);
12   scr = gtk_widget_get_parent (GTK_WIDGET (tv));
13   if (G_IS_FILE (file)) {
14     filename = g_file_get_basename (file);
15     g_object_unref (file);
16   } else
17     filename = get_untitled ();
18   label = gtk_label_new (filename);
19   gtk_notebook_set_tab_label (GTK_NOTEBOOK (nb), scr, label);
20 }
21 
22 static void
23 modified_changed_cb (GtkTextBuffer *tb, gpointer user_data) {
24   TfeTextView *tv = TFE_TEXT_VIEW (user_data);
25   GtkWidget *scr = gtk_widget_get_parent (GTK_WIDGET (tv));
26   GtkWidget *nb =  gtk_widget_get_ancestor (GTK_WIDGET (tv), GTK_TYPE_NOTEBOOK);
27   GtkWidget *label;
28   const char *filename;
29   char *text;
30 
31   if (! GTK_IS_NOTEBOOK (nb)) /* tv not connected to nb yet */
32     return;
33   else if (gtk_text_buffer_get_modified (tb)) {
34     filename = gtk_notebook_get_tab_label_text (GTK_NOTEBOOK (nb), scr);
35     text = g_strdup_printf ("*%s", filename);
36     label = gtk_label_new (text);
37     gtk_notebook_set_tab_label (GTK_NOTEBOOK (nb), scr, label);
38   } else
39     file_changed_cb (tv);
40 }
  • 1-20: file_changed_cb handler.
  • 9-10: If the signal emits during the page is being built, it is possible that 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.
  • 13-15: If file is GFile, then it gets the filename and unrefs file.
  • 16-17: Otherwise, file is probably NULL and it assigns "Untitled" related name to filename
  • 18-19: Generates GtkLabel with filename and sets the tab of the page with the GtkLabel.
  • 22-40: modified_changed_cb handler.
  • 31-32: If tv isn't a descendant of nb, then nothing needs to be done.
  • 33-35: If the content is modified, then it gets the text of the tab and adds asterisk at the beginning of the text.
  • 36-37: Sets the tab with the asterisk prepended text.
  • 38-39: Otherwise the modified bit is off. It is because content is saved. It calls file_changed_cb and resets the filename, that means it leaves out the asterisk.

Font

GtkFontButton and GtkFontChooser

The GtkFontButton is a button which displays the current font. It opens a font chooser dialog if a user clocked 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 is inserted to the preference dialog in the initialization process.

static void
font_set_cb (GtkFontButton *fontbtn, gpointer user_data) {
  GtkWindow *win = GTK_WINDOW (user_data);
  PangoFontDescription *pango_font_desc;

  pango_font_desc = gtk_font_chooser_get_font_desc (GTK_FONT_CHOOSER (fontbtn));
  set_font_for_display_with_pango_font_desc (win, pango_font_desc);
}

static void
tfe_startup (GApplication *application) {

  ... ...

  fontbtn = GTK_FONT_BUTTON (gtk_builder_get_object (build, "fontbtn"));
  g_signal_connect (fontbtn, "font-set", G_CALLBACK (font_set_cb), win);

  ... ...

}

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 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.

CSS and Pango

A new file css.c is made for functions related to CSS.

 1 #include "tfe.h"
 2 
 3 void
 4 set_css_for_display (GtkWindow *win, char *css) {
 5   GdkDisplay *display;
 6 
 7   display = gtk_widget_get_display (GTK_WIDGET (win));
 8   GtkCssProvider *provider = gtk_css_provider_new ();
 9   gtk_css_provider_load_from_data (provider, css, -1);
10   gtk_style_context_add_provider_for_display (display, GTK_STYLE_PROVIDER (provider), GTK_STYLE_PROVIDER_PRIORITY_USER);
11 }
12 
13 void
14 set_font_for_display (GtkWindow *win, const char *fontfamily, const char *fontstyle, const char *fontweight, int fontsize) {
15   char *textview_css;
16 
17   textview_css = g_strdup_printf ("textview {padding: 10px; font-family: \"%s\"; font-style: %s; font-weight: %s; font-size: %dpt;}",
18                                       fontfamily, fontstyle, fontweight, fontsize);
19   set_css_for_display (win, textview_css);
20 } 
21 
22 void
23 set_font_for_display_with_pango_font_desc (GtkWindow *win, PangoFontDescription *pango_font_desc) {
24   int pango_style;
25   int pango_weight; 
26   const char *family;
27   const char *style;
28   const char *weight;
29   int fontsize;
30 
31   family = pango_font_description_get_family (pango_font_desc);
32   pango_style = pango_font_description_get_style (pango_font_desc);
33   switch (pango_style) {
34   case PANGO_STYLE_NORMAL:
35     style = "normal";
36     break;
37   case PANGO_STYLE_ITALIC:
38     style = "italic";
39     break;
40   case PANGO_STYLE_OBLIQUE:
41     style = "oblique";
42     break;
43   default:
44     style = "normal";
45     break;
46   }
47   pango_weight = pango_font_description_get_weight (pango_font_desc);
48   switch (pango_weight) {
49   case PANGO_WEIGHT_THIN:
50     weight = "100";
51     break;
52   case PANGO_WEIGHT_ULTRALIGHT:
53     weight = "200";
54     break;
55   case PANGO_WEIGHT_LIGHT:
56     weight = "300";
57     break;
58   case PANGO_WEIGHT_SEMILIGHT:
59     weight = "350";
60     break;
61   case PANGO_WEIGHT_BOOK:
62     weight = "380";
63     break;
64   case PANGO_WEIGHT_NORMAL:
65     weight = "400"; /* or "normal" */
66     break;
67   case PANGO_WEIGHT_MEDIUM:
68     weight = "500";
69     break;
70   case PANGO_WEIGHT_SEMIBOLD:
71     weight = "600";
72     break;
73   case PANGO_WEIGHT_BOLD:
74     weight = "700"; /* or "bold" */
75     break;
76   case PANGO_WEIGHT_ULTRABOLD:
77     weight = "800";
78     break;
79   case PANGO_WEIGHT_HEAVY:
80     weight = "900";
81     break;
82   case PANGO_WEIGHT_ULTRAHEAVY:
83     weight = "900"; /* In PangoWeight definition, the weight is 1000. But CSS allows the weight below 900. */
84     break;
85   default:
86     weight = "normal";
87     break;
88   }
89   fontsize = pango_font_description_get_size (pango_font_desc) / PANGO_SCALE;
90   set_font_for_display (win, family, style, weight, fontsize);
91 }
  • 3-11: 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.
  • 13-20: set_font_for_display. This function sets CSS with font-family, font-style, font-weight and font-size.
    • font-family is a name of font. For example, sans-serif, monospace, Helvetica and "Noto Sans" are font-family. It is recommended to quote font family names that contain white space, digits, or punctuation characters other than hyphens.
    • font-style is one of normal, italic and oblique.
    • font-weight specifies the thickness of a font. It is normal or bold. It can be specified with a number between 100 and 900. Normal is the same as 400. Bold is 700.
    • font-size specifies the size of a font. Small, medium, large and 12pt are font-size.
  • 17: Makes CSS text. The function g_strdup_printf generates a new string with printf-like formatting.
  • 22-91: 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.
  • 31: Gets the font-family of pango_font_desc.
  • 32-46: Gets the font-style of pango_font_desc. The functions pango_font_description_get_style returns a enumerated value.
  • 47-88: Gets the font-weight of pango_font_desc. The function pango_font_description_get_weight returns a enumerated value. They corresponds to the numbers from 100 to 900.
  • 89: Gets the font-size of 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. -90: calls set_font_for_display to set CSS for the GdkDisplay.

GSettings

We want to maintain the font data after the application quits. There are some ways to implement it.

  • Make a configuration file. For example, a text file "~/.config/tfe/font.cfg" keeps font information.
  • Use GSettings object. The basic idea of GSettings are similar to configuration file. Configuration information data is put into a database file.

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.

GSettings schema

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.

  • A schema has an id. The id must be unique. We often use the same string as application id, but schema id and application id are different. You can use different name from application id. Schema id is a string delimited by periods. For example, "com.github.ToshioCP.tfe" is a correct schema id.
  • A schema usually has a path. The path is a location in the database. Each key is stored under the path. For example, if a key 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.
  • GSettings save information as key-value style. Key is a string begins with lower case characters followed by lower case, digit or dash - 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 integer, double, boolean, string or complex types like an array. The type of values needs to be defined in the schema.
  • A default value needs to be set for each key.
  • A summery and description can be set for each key.

Schemas are described in an XML format. For example,

 1 <?xml version="1.0" encoding="UTF-8"?>
 2 <schemalist>
 3   <schema path="/com/github/ToshioCP/tfe/" id="com.github.ToshioCP.tfe">
 4     <key name="font" type="s">
 5       <default>'Monospace 12'</default>
 6       <summary>Font</summary>
 7       <description>The font to be used for textview.</description>
 8     </key>
 9   </schema>
10 </schemalist>
  • 4: The type attribute is "s". It is GVariant format string. Other common types are:
    • "b" gboolean
    • "i" gint32.
    • "d" double Further information is in the website GVariant format string above.

gsettings

First, let's try gsetting 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

gnome-calculator basic mode

Then, change the mode to advanced and quit.

gnome-calculator advanced mode

Run gsettongs 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 sets button-mode key to "advanced" which is the current mode. The value remains even the calculator quits. So when the calculator is run again, it will appear as an advanced mode calculator.

glib-compile-schemas

GSettings schemas are specified with XML format. The XML schema files must have the filename extension .gschema.xml. The following is the XML schema file for the application tfe.

 1 <?xml version="1.0" encoding="UTF-8"?>
 2 <schemalist>
 3   <schema path="/com/github/ToshioCP/tfe/" id="com.github.ToshioCP.tfe">
 4     <key name="font" type="s">
 5       <default>'Monospace 12'</default>
 6       <summary>Font</summary>
 7       <description>The font to be used for textview.</description>
 8     </key>
 9   </schema>
10 </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.

  • 2: The top level element is <schemalist>.
  • 3: schema tag has path and id attributes. A path determines where the settings are stored in the conceptual global tree of settings. An id identifies the schema.
  • 4: Key tag has two attributes. Name is the name of the key. Type us the type of the key' value and specified with GVariant format string.
  • 5: default value of the key font is Monospace 12.
  • 6: Summery and description elements describes the key.

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 tfe6directory.

$ 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.

  • It searches glib-2.0/schemas subdirectories of all the directories specified in XDG_DATA_DIRS. Most common directory is /usr/share/glib-2.0/schemas.
  • If 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.

  1. Make .gschema.xml file.
  2. Copy it to one of the directories above. For example, /usr/share/glib-2.0/schemas.
  3. Run `glib-compile-schemas' on the directory above.

Meson.build

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

GSettings object and g_setting_bind

Write gsettings related codes to `tfeapplication.c'.

... ...
static GSettings *settings;
... ...

void
tfe_application_quit (GtkWindow *win) {
  ... ...
  g_clear_object (&settings);
  ... ...
}

static void
tfe_startup (GApplication *application) {
  ... ...
  settings = g_settings_new ("com.github.ToshioCP.tfe");
  g_settings_bind (settings, "font", fontbtn, "font", G_SETTINGS_BIND_DEFAULT);
  ... ...
}

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 generate a GSettings object and bind it to a property of an object.

Installation

It is a good idea to install your application in $HOME/local/bin directory if you have installed gtk4 under the instruction in Section 2. Then you need to put --prefix=$HOME/local option to meson.

$ meson --prefix=$HOME/local _build

Add install option and set it true in executable function.

executable('tfe', sourcefiles, resources, dependencies: gtkdep, export_dynamic: true, install: true)

Then, 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)
  • get_option: This function returns the value of build options. The default value of the option 'prefix' is "/usr/local", but it is "$HOME/local" because we have run meson with prefix option. The default value of the option 'datadir' is "share". The operator '/' connects the strings with '/' separator. So, $HOME/local/share/glib-2.0/schemas is assigned to the varable schema_dir.
  • install_data: This function installs the data to the install directory.

Meson can runs a post compile script.

meson.add_install_script('glib-compile-schemas', schema_dir)

This method runs 'glib-compile-schemas' with an argument schema_dir.

 1 project('tfe', 'c')
 2 
 3 gtkdep = dependency('gtk4')
 4 
 5 gnome=import('gnome')
 6 resources = gnome.compile_resources('resources','tfe.gresource.xml')
 7 gnome.compile_schemas(build_by_default: true, depend_files: 'com.github.ToshioCP.tfe.gschema.xml')
 8 
 9 sourcefiles=files('tfeapplication.c', 'tfenotebook.c', 'css.c', '../tfetextview/tfetextview.c')
10 
11 executable('tfe', sourcefiles, resources, dependencies: gtkdep, export_dynamic: true, install: true)
12 
13 schema_dir = get_option('prefix') / get_option('datadir') / 'glib-2.0/schemas/'
14 install_data('com.github.ToshioCP.tfe.gschema.xml', install_dir: schema_dir)
15 meson.add_install_script('glib-compile-schemas', schema_dir)
16 

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
... ...

tfe6

Up: Readme.md, Prev: Section 18, Next: Section 20