GtkMenuButton, accelerators, font, pango and gsettings

Tfe text editor will be restructured in this section.

This makes the most frequently used operation bound to the tool bar buttons. And the others are stored in behind the menus. So, it is more practical.

In addition, the following features are added.

tfe6

Static variables shared by functions in tfeapplication.c

The next version of tfe has static variables in tfeapplication.c. Static variables are convenient but not good for maintenance. So, the final version will remove them and take another way to cover the static variables.

Anyway, the following is the code with regard to the static variables.

static GtkDialog *pref; // preference dialog
static GtkFontButton *fontbtn; // font button
static GSettings *settings; // GSetting
static GtkDialog *alert; // alert dialog
static GtkLabel *lb_alert;  // label in the alert dialog
static GtkButton *btn_accept; // accept button in the alert dialog
static GtkCssProvider *provider0; //CSS provider for textview padding
static GtkCssProvider *provider; // CSS provider for fonts

static gulong pref_close_request_handler_id = 0;
static gulong alert_close_request_handler_id = 0;
static gboolean is_quit; // flag whether to quit or close

These variables can be referred by any functions in the file.

Signal tags in ui files

The four buttons are included in the ui file tfe.ui. A difference from prior sections is signal tags. 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.

Swapped attribute has the same effect as g_signal_connect_swapped function. So, the signal tag above works the same as:

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

This function swaps the button and the forth argument (btno and nb) in the handler. If g_signal_connect is used, the handler is like this:

/* The parameter user_data is assigned with nb */
static void
open_cb (GtkButton *btno, gpointer user_data) { ... ... }

If g_signal_connect_swapped is used, the button and the user data are swapped.

/* btno and user_data (nb) are exchanged */
static void
open_cb (GtkNoteBook *nb) { ... ... }

It is good if the button instance is useless in the handler.

When you use a signal tag in your ui file, you need “-WI, –export-dynamic” options to compile. 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.

Traditional menu structure is fine. However, We don’t use all the menus or buttons so often. Some mightn’t be clicked at all. Therefore, it’s a good idea to put some frequently used buttons on the toolbar and the rest into the menu. Such menu are often connected to GtkMenuButton.

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

These four menus are not used so often. That’s why they are put into the menu behind the menu button.

All the actions above have “win” scope. Tfe has only one window even if the second application runs. So, the scope “app” and “win” have very little difference in this application.

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", close_all_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 they necessary? Yes, because they correspond to accelerators.

Accelerators are kinds of short cut key functions. 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.

Open, save and close handlers

There are two open handlers. One is a handler for the clicked signal on the button. The other is for the activate signal on the action.

Open button ==(clicked)==> open.cb handler
Ctrl-o key (accerelator) ==(key down)==> open action activated ==> open_activated handler

But the behavior of the two handlers are the same. So, open_activate just call open.cb.

void
open_cb (GtkNotebook *nb) {
  notebook_page_open (nb);
}

static void
open_activated (GSimpleAction *action, GVariant *parameter, gpointer user_data) {
  GtkNotebook *nb = GTK_NOTEBOOK (user_data);
  open_cb (nb);
}

The same goes on with the save and close handlers.

Saveas handler

TfeTextView has a saveas function. So we just write a 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 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>

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 signal is emitted when the close button, right upper x button of the window, is clicked.) This is accomplished by returning TRUE by the signal handler.

static gboolean
dialog_close_cb (GtkDialog *dialog) {
  gtk_widget_set_visible (GTK_WIDGET (dialog), false);
  return TRUE;
}
... ...
( in app_startup function )
pref_close_request_handler_id = g_signal_connect (GTK_DIALOG (pref), "close-request", G_CALLBACK (dialog_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.

The “close-request” signal is G_SIGNAL_RUN_LAST. So, the order of the invocation is:

  1. Signal handler dialog_close_cb
  2. Default handler

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_set_visible (GTK_WIDGET (dialog), false);
  return TRUE;
}
... ...
static void
pref_activated (GSimpleAction *action, GVariant *parameter, gpointer nb) {
  gtk_window_present (GTK_WINDOW (pref));
}
... ...
void
app_shutdown (GApplication *application) {
   ... ... ...
  if (pref_close_request_handler_id > 0)
    g_signal_handler_disconnect (pref, pref_close_request_handler_id);
  gtk_window_destroy (GTK_WINDOW (pref));
   ... ... ...
}
... ...
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);
  ... ... 
}

Alert dialog

If a user closes a page without saving, it is advisable to show an alert for a user to confirm it. Alert dialog is used in 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 “dialog-warning” icon 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 in the program.

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. The button btn_cancel emits response signal with cancel response (GTK_RESPONSE_CANCEL) if it is clicked on. The button btn_accept 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.

Alert dialog and close handlers

If a user closes a page or quits the application without saving the contents, the alert dialog appears. There are four handlers, close_cb, close_activated, win_close_request_cb and close_all_activated. The first two are called when a notebook page is closed. The others are called when the main window is closed — so, all the notebooks are closed.

static gboolean is_quit;
... ...
static gboolean
win_close_request_cb (GtkWindow *win, GtkNotebook *nb) {
  is_quit = true;
  if (has_saved_all (nb))
    return false;
  else {
    gtk_label_set_text (lb_alert, "Contents aren't saved yet.\nAre you sure to quit?");
    gtk_button_set_label (btn_accept, "Quit");
    gtk_window_present (GTK_WINDOW (alert));
    return true;
  }
}
... ...
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 (btn_accept, "Close");
    gtk_window_present (GTK_WINDOW (alert));
  }
}
... ...
static void
close_activated (GSimpleAction *action, GVariant *parameter, gpointer user_data) {
  GtkNotebook *nb = GTK_NOTEBOOK (user_data);
  close_cb (nb);
}
... ...
static void
close_all_activated (GSimpleAction *action, GVariant *parameter, gpointer user_data) {
  GtkNotebook *nb = GTK_NOTEBOOK (user_data);
  GtkWidget *win = gtk_widget_get_ancestor (GTK_WIDGET (nb), GTK_TYPE_WINDOW);

  if (! win_close_request_cb (GTK_WINDOW (win), nb)) // checks whether contents are saved
    gtk_window_destroy (GTK_WINDOW (win));
}
... ...
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_set_visible (GTK_WIDGET (alert), false);
  if (response_id == GTK_RESPONSE_ACCEPT) {
    if (is_quit)
      gtk_window_destroy (GTK_WINDOW (win));
    else
      notebook_page_close (nb);
  }
}
... ...
static void
app_startup (GApplication *application) {
  ... ...
  build = gtk_builder_new_from_resource ("/com/github/ToshioCP/tfe/tfe.ui");
  win = GTK_APPLICATION_WINDOW (gtk_builder_get_object (build, "win"));
  ... ...
  g_signal_connect (GTK_WINDOW (win), "close-request", G_CALLBACK (win_close_request_cb), nb);
  ... ...
}

The static variable is_quit is true when user tries to quit the application and false otherwise.

Has_saved and has_saved_all functions

The two functions are defined in the file tfenotebook.c. They are public functions.

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;
}

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.

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);
}

The file_changed_cb handler gives a new file name to the notebook page tag. The modified_changed_cb handler inserts an asterisk at the beginning of the filename. It is a sign that indicates the file has been modified but not saved yet.

Font

GtkFontButton and GtkFontChooser

GtkFontButton is a button class which displays the current font and a user can change the font with the button. 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 is set with a builder in the application startup process. And the signal “font-set” is connected to the handler font_set_cb. The signal “font-set” is emitted when the user selects a font.

static void
font_set_cb (GtkFontButton *fontbtn) {
  PangoFontDescription *pango_font_desc;
  char *s, *css;

  pango_font_desc = gtk_font_chooser_get_font_desc (GTK_FONT_CHOOSER (fontbtn));
  s = pfd2css (pango_font_desc); // converts Pango Font Description into CSS style string
  css = g_strdup_printf ("textview {%s}", s);
  gtk_css_provider_load_from_data (provider, css, -1);
  g_free (s);
  g_free (css);
}
... ...
static void
app_startup (GApplication *application) {
  ... ...
  fontbtn = GTK_FONT_BUTTON (gtk_builder_get_object (build, "fontbtn"));
  ... ...
  g_signal_connect (fontbtn, "font-set", G_CALLBACK (font_set_cb), NULL);
  ... ...
}

GtkFontChooser is an interface implemented by GtkFontButton. The function gtk_font_chooser_get_font_desc gets the PangoFontDescription of the currently selected font. PangoFontDescription includes font family, style, weight and size in it. The function pfd2css converts them to CSS style string. The following shows the conversion.

PangoFontDescription:
  font-family: Monospace
  font-style: normal
  font-weight: normal
  font-size: 12pt
=>
"font-family: Monospace; font-style: normal; font-weight: 400; font-size: 12pt;"

Then, font_set_cb creates a CSS string and put it into the provider instance. The provider has been added to the default display in advance. So, the handler effects the font for the contents of the textview immediately.

CSS and Pango

Convertors from PangoFontDescription to CSS are packed in pfd2css.c. The filename means:

All the public functions in the file have “pdf2css” prefix.

#include <pango/pango.h>
#include "pfd2css.h"

// Pango font description to CSS style string
// Returned string is owned by caller. The caller should free it when it is useless.

char*
pfd2css (PangoFontDescription *pango_font_desc) {
  char *fontsize;

  fontsize = pfd2css_size (pango_font_desc);
  return g_strdup_printf ("font-family: \"%s\"; font-style: %s; font-weight: %d; font-size: %s;",
              pfd2css_family (pango_font_desc), pfd2css_style (pango_font_desc),
              pfd2css_weight (pango_font_desc), fontsize);
  g_free (fontsize); 
}

// Each element (family, style, weight and size)

const char*
pfd2css_family (PangoFontDescription *pango_font_desc) {
  return pango_font_description_get_family (pango_font_desc);
}

const char*
pfd2css_style (PangoFontDescription *pango_font_desc) {
  PangoStyle pango_style = pango_font_description_get_style (pango_font_desc);
  switch (pango_style) {
  case PANGO_STYLE_NORMAL:
    return "normal";
  case PANGO_STYLE_ITALIC:
    return "italic";
  case PANGO_STYLE_OBLIQUE:
    return "oblique";
  default:
    return "normal";
  }
}

int
pfd2css_weight (PangoFontDescription *pango_font_desc) {
  PangoWeight pango_weight = pango_font_description_get_weight (pango_font_desc);
  switch (pango_weight) {
  case PANGO_WEIGHT_THIN:
    return 100;
  case PANGO_WEIGHT_ULTRALIGHT:
    return 200;
  case PANGO_WEIGHT_LIGHT:
    return 300;
  case PANGO_WEIGHT_SEMILIGHT:
    return 350;
  case PANGO_WEIGHT_BOOK:
    return 380;
  case PANGO_WEIGHT_NORMAL:
    return 400; /* or "normal" */
  case PANGO_WEIGHT_MEDIUM:
    return 500;
  case PANGO_WEIGHT_SEMIBOLD:
    return 600;
  case PANGO_WEIGHT_BOLD:
    return 700; /* or "bold" */
  case PANGO_WEIGHT_ULTRABOLD:
    return 800;
  case PANGO_WEIGHT_HEAVY:
    return 900;
  case PANGO_WEIGHT_ULTRAHEAVY:
    return 900; /* In PangoWeight definition, the weight is 1000. But CSS allows the weight below 900. */
  default:
    return 400; /* "normal" */
  }
}

char *
pfd2css_size (PangoFontDescription *pango_font_desc) {
  if (pango_font_description_get_size_is_absolute (pango_font_desc))
    return g_strdup_printf ("%dpx", pango_font_description_get_size (pango_font_desc) / PANGO_SCALE);
  else
    return g_strdup_printf ("%dpt", pango_font_description_get_size (pango_font_desc) / PANGO_SCALE);
}

For further information, see Pango API Reference.

GSettings

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

GSettings is simple and easy to use but a bit hard to understand the concept. This subsection describes the concept first and then how to program it.

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.

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>A font to be used for textview.</description>
    </key>
  </schema>
</schemalist>

Further information is in:

gsettings

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
gnome-calculator basic mode

Change the mode to advanced and quit.

gnome-calculator advanced mode

Run gsettings and check the value of button-mode.

$ 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 runs again, it will appear as an advanced mode.

glib-compile-schemas

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

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 environment variable so that GSettings objet can find gschemas.compiled.

$ GSETTINGS_SCHEMA_DIR=(the directory gschemas.compiled is located):$GSETTINGS_SCHEMA_DIR (your application name)

GSettings object looks for this file by the following process.

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/local/share/glib-2.0/schemas.
  3. Run glib-compile-schemas on the directory above. You maybe need sudo.

GSettings object and g_settings_bind

Now, we go on to the next topic — how to program GSettings.

... ...
static GSettings *settings;
... ...
void
app_shutdown (GApplication *application) {
  ... ...
  g_clear_object (&settings);
  ... ...
}
... ...
static void
app_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 a GSettings instance. Before application quits, the application releases the GSettings instance. The function g_clear_object decreases the reference count of the GSettings instance and assigns NULL to the variable settings.

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.

For further information, refer to GIO API rference – GSettings.

Build with Meson

Build and test

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')

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

installation

It is a good idea to install your application in $HOME/bin or $HOME/.local/bin directory. They are local bin directories and work like system bin directories such as /bin, /usr/bin or usr/local/bin. You need to put --prefix=$HOME or --prefix=$HOME/.local option to meson.

$ meson --prefix=$HOME/.local _build

If you want to install your application to a system bin directory, for example /usr/local/bin, --prefix option isn’t necessary.

Meson recognizes options like this:

options values (default) values (–prefix=$HOME/.local)
prefix /usr/local $HOME/.local
bindir bin bin
datadir share share
install directory /usr/local/bin $HOME/.local/bin

The function executable needs install: true to install your program.

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

However, you need to do one more thing. Copy your XML file to your schema directory and execute glib-compile-schemas on the 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)
gnome.post_install (glib_compile_schemas: true)

The function get_option returns the value of build options. See Meson Reference Manual. The operator ‘/’ connects the strings with ‘/’ separator.

options values (default) values (–prefix=$HOME/.local)
prefix /usr/local $HOME/.local
datadir share share
schema_dir /usr/local/share/glib-2.0/schemas $HOME/.local/share/glib-2.0/schemas

The source code of meson.build is as follows.

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', 'pfd2css.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)
gnome.post_install (glib_compile_schemas: true)

Source files of tfe is under src/tfe6 directory. Copy them to your temporary directory and compile and install it.

$ meson --prefix=$HOME/.local _build
$ ninja -C _build
$ GSETTINGS_SCHEMA_DIR=_build:$GSETTINGS_SCHEMA_DIR _build/tfe # test
$ ninja -C _build install
$ ls $HOME/.local/bin
... ...
... tfe
... ...
$ ls $HOME/.local/share/glib-2.0/schemas
com.github.ToshioCP.tfe.gschema.xml
gschema.dtd
gschemas.compiled
... ...
$ tfe

The screenshot is as follows.

tfe6