GtkFontDialogButton and Gsettings

The preference dialog

If the user clicks on the preference menu, a preference dialog appears.

Preference dialog

It has only one button, which is a GtkFontDialogButton widget. You can add more widgets on the dialog but this simple dialog isn’t so bad for the first example program.

If the button is clicked, a FontDialog appears like this.

Font dialog

If the user chooses a font and clicks on the select button, the font is changed.

GtkFontDialogButton and GtkFontDialog are available since GTK version 4.10. They replace GtkFontButton and GtkFontChooserDialog, which are deprecated since 4.10.

A composite widget

The preference dialog has GtkBox, GtkLabel and GtkFontButton in it and is defined as a composite widget. The following is the template ui file for TfePref.

<?xml version="1.0" encoding="UTF-8"?>
<interface>
  <template class="TfePref" parent="GtkWindow">
    <property name="title">Preferences</property>
    <property name="resizable">FALSE</property>
    <property name="modal">TRUE</property>
    <child>
      <object class="GtkBox">
        <property name="orientation">GTK_ORIENTATION_HORIZONTAL</property>
        <property name="spacing">12</property>
        <property name="halign">GTK_ALIGN_CENTER</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">
            <property name="label">Font:</property>
            <property name="xalign">1</property>
          </object>
        </child>
        <child>
          <object class="GtkFontDialogButton" id="font_dialog_btn">
            <property name="dialog">
              <object class="GtkFontDialog"/>
            </property>
          </object>
        </child>
      </object>
    </child>
  </template>
</interface>

The header file

The file tfepref.h defines types and declares a public function.

#pragma once

#include <gtk/gtk.h>

#define TFE_TYPE_PREF tfe_pref_get_type ()
G_DECLARE_FINAL_TYPE (TfePref, tfe_pref, TFE, PREF, GtkWindow)

GtkWidget *
tfe_pref_new (void);

The C file for composite widget

The following codes are extracted from the file tfepref.c.

#include <gtk/gtk.h>
#include "tfepref.h"

struct _TfePref
{
  GtkWindow parent;
  GtkFontDialogButton *font_dialog_btn;
};

G_DEFINE_FINAL_TYPE (TfePref, tfe_pref, GTK_TYPE_WINDOW);

static void
tfe_pref_dispose (GObject *gobject) {
  TfePref *pref = TFE_PREF (gobject);
  gtk_widget_dispose_template (GTK_WIDGET (pref), TFE_TYPE_PREF);
  G_OBJECT_CLASS (tfe_pref_parent_class)->dispose (gobject);
}

static void
tfe_pref_init (TfePref *pref) {
  gtk_widget_init_template (GTK_WIDGET (pref));
}

static void
tfe_pref_class_init (TfePrefClass *class) {
  G_OBJECT_CLASS (class)->dispose = tfe_pref_dispose;
  gtk_widget_class_set_template_from_resource (GTK_WIDGET_CLASS (class), "/com/github/ToshioCP/tfe/tfepref.ui");
  gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (class), TfePref, font_dialog_btn);
}

GtkWidget *
tfe_pref_new (void) {
  return GTK_WIDGET (g_object_new (TFE_TYPE_PREF, NULL));
}

GtkFontDialogButton and Pango

If the GtkFontDialogButton button is clicked, the GtkFontDialog dialog appears. A user can choose a font on the dialog. If the user clicks on the “select” button, the dialog disappears. And the font information is given to the GtkFontDialogButton instance. The font data is taken with the method gtk_font_dialog_button_get_font_desc. It returns a pointer to the PangoFontDescription structure.

Pango is a text layout engine. The documentation is on the internet.

PangoFontDescription is a C structure and it isn’t allowed to access directly. The document is here. If you want to retrieve the font information, there are several functions.

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-desc" 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 command

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 accuracy 9
org.gnome.calculator angle-units 'degrees'
org.gnome.calculator base 10
org.gnome.calculator button-mode 'basic'
org.gnome.calculator number-format 'automatic'
org.gnome.calculator precision 2000
org.gnome.calculator refresh-interval 604800
org.gnome.calculator show-thousands false
org.gnome.calculator show-zeroes false
org.gnome.calculator source-currency ''
org.gnome.calculator source-units 'degree'
org.gnome.calculator target-currency ''
org.gnome.calculator target-units 'radian'
org.gnome.calculator window-position (-1, -1)
org.gnome.calculator word-size 64

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 utility

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

The directories above includes more than one .gschema.xml file. 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, $HOME/.local/share/glib-2.0/schemas.
  3. Run glib-compile-schemas on the directory. It compiles all the schema files in the directory and creates or updates the database file gschemas.compiled.

GSettings object and binding

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

You need to compile your schema file in advance.

Suppose id, key, class name and a property name are:

The example below uses g_settings_bind. If you use it, GSettings key and instance property must have the same the type. In the example, it is assumed that the type of “sample_key” and “sample_property” are the same.

GSettings *settings;
Sample *sample_object;

settings = g_settings_new ("com.github.ToshioCP.sample");
sample_object = sample_new ();
g_settings_bind (settings, "sample_key", sample_object, "sample_property", G_SETTINGS_BIND_DEFAULT);

The function g_settings_bind binds the GSettings value and the property of the instance. If the property value is changed, the GSettings value is also changed, and vice versa. The two values are always the same.

The function g_settings_bind is simple and easy but it isn’t always possible. The type of GSettings are restricted to the type GVariant has. Some property types are out of GVariant. For example, GtkFontDialogButton has “font-desc” property and its type is PangoFontDescription. PangoFontDescription is a C structure and it is wrapped in a boxed type GValue to store in the property. GVariant doesn’t support boxed type.

In that case, another function g_settings_bind_with_mapping is used. It binds GSettings GVariant value and object property via GValue with mapping functions.

void
g_settings_bind_with_mapping (
  GSettings* settings,
  const gchar* key,
  GObject* object,
  const gchar* property,
  GSettingsBindFlags flags, // G_SETTINGS_BIND_DEFAULT is commonly used
  GSettingsBindGetMapping get_mapping, // GSettings => property, See the example below
  GSettingsBindSetMapping set_mapping, // property => GSettings, See the example below
  gpointer user_data, // NULL if unnecessary
  GDestroyNotify destroy //NULL if unnecessary
)

The mapping functions are defined like these:

gboolean
(* GSettingsBindGetMapping) (
  GValue* value,
  GVariant* variant,
  gpointer user_data
)

GVariant*
(* GSettingsBindSetMapping) (
  const GValue* value,
  const GVariantType* expected_type,
  gpointer user_data
)

The following codes are extracted from tfepref.c.

static gboolean // GSettings => property
get_mapping (GValue* value, GVariant* variant, gpointer user_data) {
  const char *s = g_variant_get_string (variant, NULL);
  PangoFontDescription *font_desc = pango_font_description_from_string (s);
  g_value_take_boxed (value, font_desc);
  return TRUE;
}

static GVariant* // Property => GSettings
set_mapping (const GValue* value, const GVariantType* expected_type, gpointer user_data) {
  char*font_desc_string = pango_font_description_to_string (g_value_get_boxed (value));
  return g_variant_new_take_string (font_desc_string);
}

static void
tfe_pref_init (TfePref *pref) {
  gtk_widget_init_template (GTK_WIDGET (pref));
  pref->settings = g_settings_new ("com.github.ToshioCP.tfe");
  g_settings_bind_with_mapping (pref->settings, "font-desc", pref->font_dialog_btn, "font-desc", G_SETTINGS_BIND_DEFAULT,
      get_mapping, set_mapping, NULL, NULL);
}

C file

The following is the full codes of tfepref.c

#include <gtk/gtk.h>
#include "tfepref.h"

struct _TfePref
{
  GtkWindow parent;
  GSettings *settings;
  GtkFontDialogButton *font_dialog_btn;
};

G_DEFINE_FINAL_TYPE (TfePref, tfe_pref, GTK_TYPE_WINDOW);

static void
tfe_pref_dispose (GObject *gobject) {
  TfePref *pref = TFE_PREF (gobject);

  /* GSetting bindings are automatically removed when the object is finalized, so it isn't necessary to unbind them explicitly.*/
  g_clear_object (&pref->settings);
  gtk_widget_dispose_template (GTK_WIDGET (pref), TFE_TYPE_PREF);
  G_OBJECT_CLASS (tfe_pref_parent_class)->dispose (gobject);
}

/* ---------- get_mapping/set_mapping ---------- */
static gboolean // GSettings => property
get_mapping (GValue* value, GVariant* variant, gpointer user_data) {
  const char *s = g_variant_get_string (variant, NULL);
  PangoFontDescription *font_desc = pango_font_description_from_string (s);
  g_value_take_boxed (value, font_desc);
  return TRUE;
}

static GVariant* // Property => GSettings
set_mapping (const GValue* value, const GVariantType* expected_type, gpointer user_data) {
  char*font_desc_string = pango_font_description_to_string (g_value_get_boxed (value));
  return g_variant_new_take_string (font_desc_string);
}

static void
tfe_pref_init (TfePref *pref) {
  gtk_widget_init_template (GTK_WIDGET (pref));
  pref->settings = g_settings_new ("com.github.ToshioCP.tfe");
  g_settings_bind_with_mapping (pref->settings, "font-desc", pref->font_dialog_btn, "font-desc", G_SETTINGS_BIND_DEFAULT,
      get_mapping, set_mapping, NULL, NULL);
}

static void
tfe_pref_class_init (TfePrefClass *class) {
  G_OBJECT_CLASS (class)->dispose = tfe_pref_dispose;
  gtk_widget_class_set_template_from_resource (GTK_WIDGET_CLASS (class), "/com/github/ToshioCP/tfe/tfepref.ui");
  gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (class), TfePref, font_dialog_btn);
}

GtkWidget *
tfe_pref_new (void) {
  return GTK_WIDGET (g_object_new (TFE_TYPE_PREF, NULL));
}

Test program

There’s a test program located at src/tfe6/test directory.

#include <gtk/gtk.h>
#include "../tfepref.h"

GSettings *settings;

// "changed::font-desc" signal handler
static void
changed_font_desc_cb (GSettings *settings, char *key, gpointer user_data) {
  char *s;
  s = g_settings_get_string (settings, key);
  g_print ("%s\n", s);
  g_free (s);
}

static void
app_shutdown (GApplication *application) {
  g_object_unref (settings);
}

static void
app_activate (GApplication *application) {
  GtkWidget *pref = tfe_pref_new ();

  gtk_window_set_application (GTK_WINDOW (pref), GTK_APPLICATION (application));
  gtk_window_present (GTK_WINDOW (pref));
}

static void
app_startup (GApplication *application) {
  settings = g_settings_new ("com.github.ToshioCP.tfe");
  g_signal_connect (settings, "changed::font-desc", G_CALLBACK (changed_font_desc_cb), NULL);
  g_print ("%s\n", "Change the font with the font button. Then the new font will be printed out.\n");
}

#define APPLICATION_ID "com.github.ToshioCP.test_tfe_pref"

int
main (int argc, char **argv) {
  GtkApplication *app;
  int stat;

  app = gtk_application_new (APPLICATION_ID, G_APPLICATION_DEFAULT_FLAGS);
  g_signal_connect (app, "startup", G_CALLBACK (app_startup), NULL);
  g_signal_connect (app, "activate", G_CALLBACK (app_activate), NULL);
  g_signal_connect (app, "shutdown", G_CALLBACK (app_shutdown), NULL);
  stat =g_application_run (G_APPLICATION (app), argc, argv);
  g_object_unref (app);
  return stat;
}

This program sets its active window to TfePref instance, which is a child object of GtkWindow.

It sets the “changed::font-desc” signal handler in the startup function. The process from the user’s font selection to the handler is:

The program building is divided into four steps.

Commands are shown in the next four sub-subsections. You don’t need to try them. The final sub-subsection shows the meson-ninja way, which is the easiest.

Compile the schema file

$ cd src/tef6/test
$ cp ../com.github.ToshioCP.tfe.gschema.xml com.github.ToshioCP.tfe.gschema.xml
$ glib-compile-schemas .

Be careful. The commands glib-compile-schemas has an argument “.”, which means the current directory. This results in creating gschemas.compiled file.

Compile the XML file

$ glib-compile-resources --sourcedir=.. --generate-source --target=resource.c ../tfe.gresource.xml

Compile the C file

$ gcc `pkg-config --cflags gtk4` test_pref.c ../tfepref.c resource.c `pkg-config --libs gtk4`

Run the executable file

$ GSETTINGS_SCHEMA_DIR=. ./a.out

Jamrul Italic Semi-Expanded 12 # <= select Jamrul Italic 12
Monospace 12 #<= select Monospace Regular 12

Meson-ninja way

Meson wraps up the commands above. Create the following text and save it to meson.build.

Note: Gtk4-tutorial repository has meson.build file that defines several tests. So you can try it instead of the following text.

project('tfe_pref_test', 'c')

gtkdep = dependency('gtk4')

gnome=import('gnome')
resources = gnome.compile_resources('resources','../tfe.gresource.xml', source_dir: '..')
gnome.compile_schemas(build_by_default: true, depend_files: 'com.github.ToshioCP.tfe.gschema.xml')

executable('test_pref', ['test_pref.c', '../tfepref.c'], resources, dependencies: gtkdep, export_dynamic: true, install: false)

Type like this to build and test the program.

$ cd src/tef6/test
$ cp ../com.github.ToshioCP.tfe.gschema.xml com.github.ToshioCP.tfe.gschema.xml
$ meson setup _build
$ ninja -C _build
$ GSETTINGS_SCHEMA_DIR=_build _build/test_pref

A window appears and you can choose a font via GtkFontDialog. If you select a new font, the font string is output through the standard output.