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.
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.
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:
(btno, "clicked", G_CALLBACK (open_cb), nb); g_signal_connect_swapped
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
(GtkButton *btno, gpointer user_data) { ... ... } open_cb
If g_signal_connect_swapped
is used, the button and the
user data are swapped.
/* btno and user_data (nb) are exchanged */
static void
(GtkNoteBook *nb) { ... ... } open_cb
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
(GtkNotebook *nb) {
open_cb (nb);
notebook_page_open }
If you add static, the function is in the scope of the file and it can’t be seen from outside. Then the signal tag can’t find the function.
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”.
tfe
has only font preference.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.
= gtk_builder_new_from_resource ("/com/github/ToshioCP/tfe/menu.ui");
build = G_MENU_MODEL (gtk_builder_get_object (build, "menu"));
menu (btnm, menu); gtk_menu_button_set_menu_model
Menus are connected to actions. Actions are defined with an array and
g_action_map_add_action_entries
function.
const GActionEntry win_entries[] = {
{ "open", open_activated, NULL, NULL, NULL },
{ "save", save_activated, NULL, NULL, NULL },
{ "close", close_activated, NULL, NULL, NULL },
{ "new", new_activated, NULL, NULL, NULL },
{ "saveas", saveas_activated, NULL, NULL, NULL },
{ "pref", pref_activated, NULL, NULL, NULL },
{ "close-all", close_all_activated, NULL, NULL, NULL }
};
(G_ACTION_MAP (win), win_entries, G_N_ELEMENTS (win_entries), nb); g_action_map_add_action_entries
There are seven actions, open, save, close, new, saveas, pref and close-all. But there were only four menus. New, saveas, pref and close-all actions correspond to new, saveas, preference and quit menu respectively. The three actions open, save and close doesn’t have corresponding menus. Are 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(app), action_accels[i].action, action_accels[i].accels); gtk_application_set_accels_for_action
This code is a bit complicated. The array
action-accels[]
is an array of structures. The structure
is:
struct {
const char *action;
const char *accels[2];
}
The member action
is a string. The member
accels
is an array of two strings. For example,
{ "win.open", { "<Control>o", NULL } },
This is the first element of the array
action_accels
.
action
is “win.open”. This specifies the
action “open” belongs to the window object.accels
is an array of two strings,
“<Control>o” and NULL. The first string specifies a key
combination. Control key and ‘o’. If you keep pressing the control key
and push ‘o’ key, then it activates the action win.open
.
The second string NULL (or zero) means the end of the list (array). You
can define more than one accelerator keys and the list must ends with
NULL (zero). If you want to do so, the array length needs to be three or
more. The parser recognizes “<control>o”,
“<Shift><Alt>F2”, “<Ctrl>minus” and so on. If you want
to use symbol key like “<Ctrl>-”, use “<Ctrl>minus” instead.
Such relation between lower case and symbol (character code) is
specified in gdkkeysyms.h
in the GTK 4 source code.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.
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 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> </
<child internal-child="content_area">
is
put at the top of the contents of the dialog. You need to specify a
GtkBox object tag with content_area id. This object is defined in
gtkdialog.ui
(composite widget) but you need to define it
again in the child tag. Composite widget will be explained in the next
section. For further information about GtkDialog ui tags, see:
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
(GtkDialog *dialog) {
dialog_close_cb (GTK_WIDGET (dialog), false);
gtk_widget_set_visible return TRUE;
}
... ...
( in app_startup function )
= g_signal_connect (GTK_DIALOG (pref), "close-request", G_CALLBACK (dialog_close_cb), NULL);
pref_close_request_handler_id ... ...
Generally, signal emission consists of five stages.
G_SIGNAL_RUN_FIRST
. Default handler is set when a signal is
registered. It is different from user signal handler, simply called
signal handler, connected by g_signal_connect
series
function. Default handler can be invoked in either stage 1, 3 or 5. Most
of the default handlers are G_SIGNAL_RUN_FIRST
or
G_SIGNAL_RUN_LAST
.g_signal_connect_after
.G_SIGNAL_RUN_LAST
.g_signal_connect_after
.G_SIGNAL_RUN_CLEANUP
.The “close-request” signal is G_SIGNAL_RUN_LAST
. So, the
order of the invocation is:
dialog_close_cb
And If the user signal handler returns TRUE, then other handlers will be stopped being invoked. Therefore, the program above prevents the invocation of the default handler and stop the closing process of the dialog.
The following codes are extracted from
tfeapplication.c
.
static gulong pref_close_request_handler_id = 0;
static gulong alert_close_request_handler_id = 0;
... ...
static gboolean
(GtkDialog *dialog, gpointer user_data) {
dialog_close_cb (GTK_WIDGET (dialog), false);
gtk_widget_set_visible return TRUE;
}
... ...
static void
(GSimpleAction *action, GVariant *parameter, gpointer nb) {
pref_activated (GTK_WINDOW (pref));
gtk_window_present }
... ...
void
(GApplication *application) {
app_shutdown ... ... ...
if (pref_close_request_handler_id > 0)
(pref, pref_close_request_handler_id);
g_signal_handler_disconnect (GTK_WINDOW (pref));
gtk_window_destroy ... ... ...
}
... ...
static void
(GApplication *application) {
tfe_startup ... ...
= GTK_DIALOG (gtk_builder_get_object (build, "pref"));
pref = g_signal_connect (GTK_DIALOG (pref), "close-request", G_CALLBACK (dialog_close_cb), NULL);
pref_close_request_handler_id ... ...
}
dialog_close_cb
. It changes the close behavior
of the dialog. When the signal is emitted, the visibility is set to
false and the default handler is canceled. So, the dialog just
disappears but exists.pref_activate
shows the preference
dialog.app_shutdown
disconnects the
handlers from the “close-request”signal and destroys pref
window.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.
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.
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
(GtkWindow *win, GtkNotebook *nb) {
win_close_request_cb = true;
is_quit if (has_saved_all (nb))
return false;
else {
(lb_alert, "Contents aren't saved yet.\nAre you sure to quit?");
gtk_label_set_text (btn_accept, "Quit");
gtk_button_set_label (GTK_WINDOW (alert));
gtk_window_present return true;
}
}
... ...
void
(GtkNotebook *nb) {
close_cb = false;
is_quit if (has_saved (GTK_NOTEBOOK (nb)))
(GTK_NOTEBOOK (nb));
notebook_page_close else {
(lb_alert, "Contents aren't saved yet.\nAre you sure to close?");
gtk_label_set_text (btn_accept, "Close");
gtk_button_set_label (GTK_WINDOW (alert));
gtk_window_present }
}
... ...
static void
(GSimpleAction *action, GVariant *parameter, gpointer user_data) {
close_activated *nb = GTK_NOTEBOOK (user_data);
GtkNotebook (nb);
close_cb }
... ...
static void
(GSimpleAction *action, GVariant *parameter, gpointer user_data) {
close_all_activated *nb = GTK_NOTEBOOK (user_data);
GtkNotebook *win = gtk_widget_get_ancestor (GTK_WIDGET (nb), GTK_TYPE_WINDOW);
GtkWidget
if (! win_close_request_cb (GTK_WINDOW (win), nb)) // checks whether contents are saved
(GTK_WINDOW (win));
gtk_window_destroy }
... ...
void
(GtkDialog *alert, int response_id, gpointer user_data) {
alert_response_cb *nb = GTK_NOTEBOOK (user_data);
GtkNotebook *win = gtk_widget_get_ancestor (GTK_WIDGET (nb), GTK_TYPE_WINDOW);
GtkWidget
(GTK_WIDGET (alert), false);
gtk_widget_set_visible if (response_id == GTK_RESPONSE_ACCEPT) {
if (is_quit)
(GTK_WINDOW (win));
gtk_window_destroy else
(nb);
notebook_page_close }
}
... ...
static void
(GApplication *application) {
app_startup ... ...
= gtk_builder_new_from_resource ("/com/github/ToshioCP/tfe/tfe.ui");
build = GTK_APPLICATION_WINDOW (gtk_builder_get_object (build, "win"));
win ... ...
(GTK_WINDOW (win), "close-request", G_CALLBACK (win_close_request_cb), nb);
g_signal_connect ... ...
}
The static variable is_quit
is true when user tries to
quit the application and false otherwise.
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 shows the alert dialog. The
response signal of the dialog is connected to the handler
alert_response_cb
. It hides the dialog first. Then checks
the response_id
. If it is GTK_RESPONSE_ACCEPT
,
which means the user has clicked on the close button, then it closes the
current page. Otherwise it does nothing.close_activated
handler
is invoked. It just calls close_cb
.win_close_request_cb
handler in advance.
The connection is done in the start up handler on the application. The
win_close_request_cb
handler sets is_quit
to
be true. If has_save_all
returns true, it returns false,
which means the signal moves to the default handler and the main window
will close. Otherwise, It shows the alert dialog and returns true. So,
the signal stops and the default handler won’t be called. But if the
user clicked accept button in the alert dialog, the response handler
alert_response_cb
calls gtk_window_destroy
and
the main window will be closed.close_all_activated
handler is invoked. It calls
win_close_request_cb
. If the return value is false, it
destroys the main window. Otherwise it does nothing, but
win_close_request_cb
has shown the alert dialog.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;
}
has_saved
function.gtk_text_buffer_get_modified
returns
true if the content of the buffer has been modified since the modified
flag had set false. The flag is set to false when:
has_saved_all
function. This function is similar
to has_saved
function. It returns true if all the pages
have been saved. It returns false if at least one page has been modified
since it last had been saved.If you have some pages and edit them together, you might be confused which file needs to be saved. Common file editors changes the tab when the contents are modified. GtkTextBuffer provides “modified-changed” signal to notify the modification.
static void
(GtkNotebook *nb, GtkWidget *tv, char *filename) {
notebook_page_build ... ...
(GTK_TEXT_VIEW (tv), "change-file", G_CALLBACK (file_changed_cb), NULL);
g_signal_connect (tb, "modified-changed", G_CALLBACK (modified_changed_cb), tv);
g_signal_connect }
When a page is built, connect “change-file” and “modified-changed”
signals to file_changed_cb
and
modified_changed_cb
handlers respectively.
static void
file_changed_cb (TfeTextView *tv) {
GtkWidget *nb = gtk_widget_get_ancestor (GTK_WIDGET (tv), GTK_TYPE_NOTEBOOK);
GtkWidget *scr;
GtkWidget *label;
GFile *file;
char *filename;
if (! GTK_IS_NOTEBOOK (nb)) /* tv not connected to nb yet */
return;
file = tfe_text_view_get_file (tv);
scr = gtk_widget_get_parent (GTK_WIDGET (tv));
if (G_IS_FILE (file)) {
filename = g_file_get_basename (file);
g_object_unref (file);
} else
filename = get_untitled ();
label = gtk_label_new (filename);
gtk_notebook_set_tab_label (GTK_NOTEBOOK (nb), scr, label);
}
static void
modified_changed_cb (GtkTextBuffer *tb, gpointer user_data) {
TfeTextView *tv = TFE_TEXT_VIEW (user_data);
GtkWidget *scr = gtk_widget_get_parent (GTK_WIDGET (tv));
GtkWidget *nb = gtk_widget_get_ancestor (GTK_WIDGET (tv), GTK_TYPE_NOTEBOOK);
GtkWidget *label;
const char *filename;
char *text;
if (! GTK_IS_NOTEBOOK (nb)) /* tv not connected to nb yet */
return;
else if (gtk_text_buffer_get_modified (tb)) {
filename = gtk_notebook_get_tab_label_text (GTK_NOTEBOOK (nb), scr);
text = g_strdup_printf ("*%s", filename);
label = gtk_label_new (text);
g_free (text);
gtk_notebook_set_tab_label (GTK_NOTEBOOK (nb), scr, label);
} else
file_changed_cb (tv);
}
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.
file_changed_cb
handler.tv
isn’t a descendant of nb
.
That is to say, there’s no page corresponds to tv
. Then, it
is unnecessary to change the name of the tab because no tab exists.file
is GFile, then it gets the filename and
release the reference to file
.filename
filename
and sets the tab
of the page with the GtkLabel.modified_changed_cb
handler.tv
isn’t a descendant of nb
,
then nothing needs to be done.file_changed_cb
and updates
the filename (without an asterisk).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
(GtkFontButton *fontbtn) {
font_set_cb *pango_font_desc;
PangoFontDescription char *s, *css;
= gtk_font_chooser_get_font_desc (GTK_FONT_CHOOSER (fontbtn));
pango_font_desc = pfd2css (pango_font_desc); // converts Pango Font Description into CSS style string
s = g_strdup_printf ("textview {%s}", s);
css (provider, css, -1);
gtk_css_provider_load_from_data (s);
g_free (css);
g_free }
... ...
static void
(GApplication *application) {
app_startup ... ...
= GTK_FONT_BUTTON (gtk_builder_get_object (build, "fontbtn"));
fontbtn ... ...
(fontbtn, "font-set", G_CALLBACK (font_set_cb), NULL);
g_signal_connect ... ...
}
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.
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);
}
pango/pango.h
.pdf2css.h
makes it possible to call public
functions anywhere in the file pdf2css.c
. Because the
header file includes declarations of all the public functions.pdf2css
function. This function gets font family,
style, weight and size from a PangoFontDescription instance given as an
argument. And it builds them to a string. The returned string is owned
by caller. The caller should free the string when it is useless.pfd2css_famili
function. This function gets
font-family string from a PangoFontDescription instance. The string is
owned by the PFD instance so caller can’t modify or free the
string.pdf2css_style
function. This function gets
font-style string from a PangoFontDescription instance. The string is
static and caller can’t modify or free it.pfd2css_weight
function. This function gets
font-weight integer value from a PangoFontDescription instance. The
value is in between 100 to 900. It is defined in CSS Fonts Module Level
3 specification.
pdf2css_size
function. This function gets
font-size string from a PangoFontDescription instance. The string is
owned by caller, so the caller should free it when it is useless.
PangoFontDescription has absolute or non-absolute size.
PANGO_SCALE
is the scale between dimensions used for Pango
distances and device units. PANGO_SCALE
is currently 1024,
but this may be changed in the future. When setting font sizes, device
units are always considered to be points rather than pixels. If the font
size is 12pt, the size in pango is
12*PANGO_SCALE=12*1024=12288
.For further information, see Pango API Reference.
We want to maintain the font data after the application quits. There are some ways to implement it.
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 describes a set of keys, value types and some other information. GSettings object uses this schema and it writes/reads the value of a key to/from the right place in the database.
font
is defined with a path
/com/github/ToshioCP/tfe/
, the key’s location in the
database is /com/github/ToshioCP/tfe/font
. Path is a string
begins with and ends with a slash (/
). And it is delimited
by slashes.-
) and ends with lower case or digit. No consecutive
dashes are allowed. Values can be any type. GSettings stores values as
GVariant type, which can be, for example, integer, double, boolean,
string or complex types like an array. The type of values needs to be
defined in the schema.Schemas are described in an XML format. For example,
<?xml version="1.0" encoding="UTF-8"?>
<schemalist>
<schema path="/com/github/ToshioCP/tfe/" id="com.github.ToshioCP.tfe">
<key name="font" type="s">
<default>'Monospace 12'</default>
<summary>Font</summary>
<description>A font to be used for textview.</description>
</key>
</schema>
</schemalist>
Further information is in:
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
Change the mode to advanced and quit.
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.
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.
<schemalist>
.path
and id
attributes.
A path determines where the settings are stored in the conceptual global
tree of settings. An id identifies the schema.font
is
Monospace 12
.The XML file is compiled by glib-compile-schemas. When compiling,
glib-compile-schemas
compiles all the XML files which have
“.gschema.xml” file extension in the directory given as an argument. It
converts the XML file into a binary file gschemas.compiled
.
Suppose the XML file above is under tfe6
directory.
$ glib-compile-schemas tfe6
Then, gschemas.compiled
is generated under
tfe6
. When you test your application, set
GSETTINGS_SCHEMA_DIR
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.
glib-2.0/schemas
subdirectories of all the
directories specified in the environment variable
XDG_DATA_DIRS
. Common directores are
/usr/share/glib-2.0/schemas
and
`/usr/local/share/glib-2.0/schemas
.GSETTINGS_SCHEMA_DIR
environment variable is
defined, it searches all the directories specified in the variable.
GSETTINGS_SCHEMA_DIR
can specify multiple directories
delimited by colon (:).In the directories above, all the .gschema.xml
files are
stored. Therefore, when you install your application, follow the
instruction below to install your schemas.
.gschema.xml
file./usr/local/share/glib-2.0/schemas
.glib-compile-schemas
on the directory above. You
maybe need sudo
.Now, we go on to the next topic — how to program GSettings.
... ...
static GSettings *settings;
... ...
void
(GApplication *application) {
app_shutdown ... ...
(&settings);
g_clear_object ... ...
}
... ...
static void
(GApplication *application) {
app_startup ... ...
= g_settings_new ("com.github.ToshioCP.tfe");
settings (settings, "font", fontbtn, "font", G_SETTINGS_BIND_DEFAULT);
g_settings_bind ... ...
}
Static variable settings
keeps a pointer to 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.
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
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.
install_data
function copies a file into a target
directory.gnome.post_install
function executes
‘glib-compile-schemas’ with an argument schema_dir
as post
installation. This function is available since Meson 0.57.0. If the
version is earlier than that, use meson.add_install_script
instead.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.