本节将重新构造Tfe文本编辑器。
- 在工具栏上放置了打开、保存和关闭按钮。此外,GtkMenuButton被添加到工具栏中。当点击这个按钮时会显示一个弹出式菜单。在这里,弹出式的含义很广泛,包括下拉式菜单。
- 新建、另存为、偏好和退出项目被放入菜单中。
这将最常用的操作绑定到工具栏按钮上。其他的则存储在菜单后面。因此,它更实用。
此外,还增加了以下特性。
- 加速器。例如,Ctrl-O读取文件并创建一个新页面。
- 字体选择的首选对话框。
- 警告对话框,确认关闭或退出,不保存内容。
- GSettings保留字体选择。
Static variables shared by functions in tfeapplication.c
tfe的下一个版本在tfeapplication.c中有静态变量。静态变量很方便,但不利于维护。因此,最终版本将删除它们,并采用另一种方式来覆盖静态变量。
无论如何,下面是关于静态变量的代码。
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
文件中的任何函数都可以使用这些变量。
Signal tags in ui files
这四个按钮包含在ui文件tfe.ui中。与前面章节不同的是信号标签。以下内容是从tfe.ui中提取的,它描述了打开按钮。
<object class="GtkButton" id="btno">
<property name="label">Open</property>
<signal name="clicked" handler="open_cb" swapped="TRUE" object="nb"></signal>
</object>
Signal tag指定了信号、处理程序和user_data对象的名称。
- 信号名称为“clicked”。
- 处理程序是“open_cb”。
- 用户数据对象是“nb”(GtkNoteBook实例)。
swapped属性与g_signal_connect_swap函数具有相同的效果。所以,上面的信号标签的工作原理是一样的:
g_signal_connect_swapped (btno, "clicked", G_CALLBACK (open_cb), nb);
这个函数在处理程序中交换了按钮和第四个参数(btno和nb)。如果使用g_signal_connect,处理程序如下所示:
/* The parameter user_data is assigned with nb */
static void
open_cb (GtkButton *btno, gpointer user_data) { ... ... }
如果使用g_signal_connect_swapped,则交换按钮和用户数据。
/* btno and user_data (nb) are exchanged */
static void
open_cb (GtkNoteBook *nb) { ... ... }
如果button实例在处理程序中无用,那就好了。
当你在ui文件中使用signal标签时,你需要"-WI, --export-dynamic" 选项来编译。你可以通过在meson.build的可执行函数中添加“export_dynamic: true”参数来实现这一点。并从处理程序中删除static类。
void
open_cb (GtkNotebook *nb) {
notebook_page_open (nb);
}
如果你添加了static,函数就在文件的作用域中,从外部是看不到的。信号tag无法找到函数。
Menu and GkMenuButton
传统的菜单结构是好的。然而,我们并不经常使用所有的菜单或按钮。有些可能根本不会被点击。因此,将一些常用的按钮放在工具栏上,将其他按钮放在菜单中是一个好主意。这样的菜单通常连接到GtkMenuButton。
菜单被描述在menu.ui文件。
1 <?xml version="1.0" encoding="UTF-8"?>
2 <interface>
3 <menu id="menu">
4 <section>
5 <item>
6 <attribute name="label">New</attribute>
7 <attribute name="action">win.new</attribute>
8 </item>
9 <item>
10 <attribute name="label">Save As…</attribute>
11 <attribute name="action">win.saveas</attribute>
12 </item>
13 </section>
14 <section>
15 <item>
16 <attribute name="label">Preference</attribute>
17 <attribute name="action">win.pref</attribute>
18 </item>
19 </section>
20 <section>
21 <item>
22 <attribute name="label">Quit</attribute>
23 <attribute name="action">win.close-all</attribute>
24 </item>
25 </section>
26 </menu>
27 </interface>
这里有4个元素,“New”、“Saveas”、“Preference"和"Quit”。
- “新建”菜单创建一个新的空白页面。
- “Saveas”菜单将当前页面保存为与原始页面不同的文件名。
- “偏好”菜单设置偏好项。这个版本的tfe只有字体首选项。
- “Quit”菜单退出应用程序。
这四个菜单不太常用。这就是为什么它们被放在菜单按钮后面的菜单中。
以上所有操作都具有“win”范围。即使运行第二个应用程序,Tfe也只有一个窗口。因此,在这个应用程序中,作用域“app”和“win”差别很小。
菜单和菜单按钮连接到gtk_menu_button_set_menu_model函数。下面的变量btnm指向一个GtkMenuButton对象。
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
菜单与操作相连接。动作由一个数组和g_action_map_add_action_entries函数定义。
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);
有7个操作:打开、保存、关闭、新建、保存、优先和关闭所有。但是只有四份菜单。New、saveas、pref和close-all操作分别对应New、saveas、preference和quit菜单。“打开”、“保存”和“关闭”三个操作没有对应的菜单。它们是必要的吗?是的,因为它们对应于加速器。
加速器是一种快捷键功能。它们由数组和gtk_application_set_accs_for_action函数定义。
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);
这段代码有点复杂。数组action-accels[]是一个结构体数组。其结构如下:
struct {
const char *action;
const char *accels[2];
}
成员操作是一个字符串。成员accels是一个包含两个字符串的数组。例如,
{ "win.open", { "<Control>o", NULL } },
这是数组action_accels的第一个元素。
- 成员行为是"win.open"。这指定了"open"操作属于window对象。
- 成员accels是一个由两个字符串组成的数组,
"<Control>o"和NULL
。第一个字符串指定了一个键组合。控制键和“o”。如果你一直按control键并按o键,就会激活win.open动作。第二个字符串NULL(或零)表示列表(数组)结束。你可以定义多个加速键,列表必须以NULL(零)结尾。如果你想这样做,数组长度必须大于等于3。解析器识别"<control>o"
,"<Shift><Alt>F2"
, "<Ctrl>minus
"等等。如果你想使用类似“-”的符号键,请使用“-”。小写字母和符号(字符代码)之间的这种关系在GTK 4源代码中的gdkkeysyms.h中指定。
Open, save and close handlers
有两个打开的处理程序。一个是按钮上单击信号的处理程序。另一个是动作上的激活信号。
Open button (clicked)> open.cb handler
Ctrl-o key (accerelator) (key down)> open action activated ==> open_activated handler
但这两个处理程序的行为是相同的。open_activate调用open_cb。
1 void
2 open_cb (GtkNotebook *nb) {
3 notebook_page_open (nb);
4 }
5
6 static void
7 open_activated (GSimpleAction *action, GVariant *parameter, gpointer user_data) {
8 GtkNotebook *nb = GTK_NOTEBOOK (user_data);
9 open_cb (nb);
10 }
保存和关闭处理程序也是如此。
Saveas handler
TfeTextView有一个saveas函数。因此,我们只需在tfenotebook.c中编写一个包装器函数。
1 static TfeTextView *
2 get_current_textview (GtkNotebook *nb) {
3 int i;
4 GtkWidget *scr;
5 GtkWidget *tv;
6
7 i = gtk_notebook_get_current_page (nb);
8 scr = gtk_notebook_get_nth_page (nb, i);
9 tv = gtk_scrolled_window_get_child (GTK_SCROLLED_WINDOW (scr));
10 return TFE_TEXT_VIEW (tv);
11 }
12
13 void
14 notebook_page_saveas (GtkNotebook *nb) {
15 g_return_if_fail(GTK_IS_NOTEBOOK (nb));
16
17 TfeTextView *tv;
18
19 tv = get_current_textview (nb);
20 tfe_text_view_saveas (TFE_TEXT_VIEW (tv));
21 }
函数get_current_textview和之前一样。函数notebook_page_saveas只是调用了tfe_text_view_saveas。
在tfeapplication.c中,保存处理程序只调用notebook_page_saveas。
1 static void
2 saveas_activated (GSimpleAction *action, GVariant *parameter, gpointer user_data) {
3 GtkNotebook *nb = GTK_NOTEBOOK (user_data);
4 notebook_page_saveas (nb);
5 }
Preference and alert dialog
Preference dialog
首选项对话框xml定义被添加到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>
- 偏好对话框是独立的对话框。它不是顶级GtkApplicationwindow的后代部件。因此,对话框对象周围没有子标签。
- 这个对话框有四个属性。GtkDialog是GtkWindow的一个子对象(不是子构件),因此它继承了GtkWindow的所有属性。Title、resizable、modal和transient-for属性继承自GtkWindow。Transient-for指定一个临时的父窗口,对话框的位置就是基于这个窗口。
- 标签
<child internal-child="content_area">
放在对话框内容的顶部。你需要用content_area id指定一个GtkBox对象标签。这个对象在gtkdialog.ui((复合部件))中定义。但您需要在子标签中再次定义它。复合小部件将在下一节中解释。有关GtkDialog ui标签的更多信息,请参阅:- GTK 4 API reference – GtkBuilder
- GTK 4 API reference – GtkDialog
- GtkDialog ui file
- 在内容区域中有一个水平的GtkBox。
- GtkLabel和GtkFontButton在GtkBox中。
我希望首选项对话框在应用程序的生命周期内保持活跃。因此,有必要从对话框中捕获“close-request”信号并停止该信号的传播。(当点击窗口右上角的x按钮关闭按钮时,会发出这个信号。)这是通过信号处理程序返回TRUE来完成的。
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);
... ...
信号发射一般分为五个阶段。
- 如果信号的标志是G_SIGNAL_RUN_FIRST,则调用默认处理程序。在注册信号时设置默认处理程序。它不同于用户信号处理程序,简称为信号处理程序,由g_signal_connectseries函数连接。默认处理程序可以在阶段1、3或5中调用。大多数默认处理程序是G_SIGNAL_RUN_FIRST或G_SIGNAL_RUN_LAST。
- 调用信号处理程序,除非通过g_signal_connect_after连接。
- 如果信号的标志是G_SIGNAL_RUN_LAST,则调用默认处理程序。
- 如果通过g_signal_connect_after连接,则调用信号处理程序。
- 如果信号的标志是G_SIGNAL_RUN_CLEANUP,则调用默认处理程序。
“close-request”信号是G_SIGNAL_RUN_LAST。因此,调用的顺序是:
- 信号处理程序dialog_close_cb
- 默认的处理程序
如果用户信号处理程序返回TRUE,那么将停止调用其他处理程序。因此,上面的程序阻止了对默认处理程序的调用,并停止了对话框的关闭过程。
下列代码是从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);
... ...
}
- 首选项对话框上的close- request信号连接到处理程序dialog_close_cb。它改变了对话框的close行为。当信号发出时,可见性被设置为false,默认的处理程序被取消。因此,对话框消失但存在。
- 处理程序pref_activate显示首选项对话框。
- 关闭处理程序app_shutdown将处理程序与“close-request”信号断开连接,并销毁pref window。
Alert dialog
如果用户没有保存就关闭了一个页面,建议显示一个警告,让用户确认。警报对话框就是在这种情况下使用的。
<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>
这个ui文件描述了警告对话框。有些部分与选项对话框相同。在内容区域中有两个对象,GtkImage和GtkLabel。
GtkImage显示一个图像。图片可以来自文件、资源、图标主题等。上图显示了当前图标主题中的一个图标。您可以通过gtk4-icon-browser查看主题中的图标。
$ gtk4-icon-browser
“对话框警告”图标类似于下面这样。
这些是我亲手做的。警告对话框中的真实图像更漂亮。
-GtkLabel lb_alert还没有文本。一个警告消息将被插入到程序中。
有两个子标签具有“action”类型。它们是位于操作区域的按钮对象。Action-widgets标签描述按钮的操作。如果单击按钮btn_cancel,则发出带有cancel响应(GTK_RESPONSE_CANCEL)的响应信号。如果单击按钮btn_accept,则用accept响应(GTK_RESPONSE_ACCEPT)发出响应信号。响应信号连接到alert_response_cb处理程序。
在应用程序存活期间,警告对话框保持活跃。“close-request”信号被处理程序停止
Alert dialog and close handlers
如果用户关闭页面或退出应用程序而没有保存内容,则会出现警告对话框。有4个处理程序,close_cb, close_activated, win_close_request_cb和close_all_activated。前两个函数在notebook页面关闭时调用。其他的在主窗口关闭时调用——因此,所有的notebook都关闭了。
- close button => close_cb (=> alert dialog)
- Ctrl-W => close_activated => close_cb (=> alert dialog)
- Close button (x button at the right top of the main window) => win_close_request_cb (=> alert dialog)
- Quit menu or Ctrl-Q => close_all_activated => win_close_request_cb (=> alert dialog)
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);
... ...
}
当用户试图退出应用程序时,静态变量is_quit为true,否则为false。
- 当用户单击关闭按钮时,close_cb处理程序将被调用。处理程序将is_quit设置为false。如果当前页已经保存,函数has_saved返回true。如果为true,它调用notebook_page_close关闭当前页。否则,显示警告对话框。对话框的响应信号连接到处理程序alert_response_cb。它首先隐藏对话框。然后检查response_id。如果是GTK_RESPONSE_ACCEPT,表示用户单击了关闭按钮,则关闭当前页面。否则它什么都不做。
- 当用户按下"Ctrl-w"时,close_activated处理程序会被调用。它只是调用close_cb。
- 当用户点击主窗口的关闭按钮时,窗口会发出“关闭请求”信号。该信号事先已经连接到win_close_request_cb处理程序。连接是在应用程序上的启动处理程序中完成的。win_close_request_cb处理程序将is_quit设置为true。如果has_save_all返回true,它就返回false,这意味着信号移动到默认处理程序,主窗口将关闭。否则,显示警告对话框并返回true。因此,信号停止,默认处理程序不会被调用。但是如果用户点击了警告对话框中的accept按钮,响应处理程序alert_response_cb会调用gtk_window_destroy,主窗口将被关闭。
- 当用户单击quit菜单或按下"Ctrl-q"时,则调用close_all_activated处理程序。它调用了win_close_request_cb。如果返回值为false,它会销毁主窗口。否则它什么也不做,但是win_close_request_cb显示了警告对话框。
Has_saved and has_saved_all functions
这两个函数定义在文件tfenotebook.c中。它们是公共函数。
1 gboolean
2 has_saved (GtkNotebook *nb) {
3 g_return_val_if_fail (GTK_IS_NOTEBOOK (nb), false);
4
5 TfeTextView *tv;
6 GtkTextBuffer *tb;
7
8 tv = get_current_textview (nb);
9 tb = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv));
10 if (gtk_text_buffer_get_modified (tb))
11 return false;
12 else
13 return true;
14 }
15
16 gboolean
17 has_saved_all (GtkNotebook *nb) {
18 g_return_val_if_fail (GTK_IS_NOTEBOOK (nb), false);
19
20 int i, n;
21 GtkWidget *scr;
22 GtkWidget *tv;
23 GtkTextBuffer *tb;
24
25 n = gtk_notebook_get_n_pages (nb);
26 for (i = 0; i < n; ++i) {
27 scr = gtk_notebook_get_nth_page (nb, i);
28 tv = gtk_scrolled_window_get_child (GTK_SCROLLED_WINDOW (scr));
29 tb = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv));
30 if (gtk_text_buffer_get_modified (tb))
31 return false;
32 }
33 return true;
34 }
- 1-14: has_saved函数。
- 10:如果缓冲区的内容已经被修改,函数gtk_text_buffer_get_modified返回true,因为modified标志设置为false。在以下情况下,该标志设置为false:
- 创建缓冲区。
- 缓冲区的内容被替换
- 缓冲区的内容保存到文件中。
- 10-13:如果保存了当前页面的内容且没有对其进行任何修改,则此函数返回true。如果当前页面被修改过且没有保存,则返回false。
- 16-34: has_saved_all函数。该函数类似于has_saved函数。如果所有的页都保存了,则返回true。如果在上次保存页之后,至少有一页被修改过,则返回false。
Notebook page tab
如果你有一些页面,并将它们编辑在一起,你可能会弄不清需要保存哪个文件。普通文件编辑器在修改内容时更改选项卡。GtkTextBuffer提供了"modified-changed"信号来通知修改。
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);
}
在建立页时,将“change-file”和“modified-changed”信号分别连接到file_changed_cb和modified_changed_cb处理程序。
1 static void
2 file_changed_cb (TfeTextView *tv) {
3 GtkWidget *nb = gtk_widget_get_ancestor (GTK_WIDGET (tv), GTK_TYPE_NOTEBOOK);
4 GtkWidget *scr;
5 GtkWidget *label;
6 GFile *file;
7 char *filename;
8
9 if (! GTK_IS_NOTEBOOK (nb)) /* tv not connected to nb yet */
10 return;
11 file = tfe_text_view_get_file (tv);
12 scr = gtk_widget_get_parent (GTK_WIDGET (tv));
13 if (G_IS_FILE (file)) {
14 filename = g_file_get_basename (file);
15 g_object_unref (file);
16 } else
17 filename = get_untitled ();
18 label = gtk_label_new (filename);
19 gtk_notebook_set_tab_label (GTK_NOTEBOOK (nb), scr, label);
20 }
21
22 static void
23 modified_changed_cb (GtkTextBuffer *tb, gpointer user_data) {
24 TfeTextView *tv = TFE_TEXT_VIEW (user_data);
25 GtkWidget *scr = gtk_widget_get_parent (GTK_WIDGET (tv));
26 GtkWidget *nb = gtk_widget_get_ancestor (GTK_WIDGET (tv), GTK_TYPE_NOTEBOOK);
27 GtkWidget *label;
28 const char *filename;
29 char *text;
30
31 if (! GTK_IS_NOTEBOOK (nb)) /* tv not connected to nb yet */
32 return;
33 else if (gtk_text_buffer_get_modified (tb)) {
34 filename = gtk_notebook_get_tab_label_text (GTK_NOTEBOOK (nb), scr);
35 text = g_strdup_printf ("*%s", filename);
36 label = gtk_label_new (text);
37 g_free (text);
38 gtk_notebook_set_tab_label (GTK_NOTEBOOK (nb), scr, label);
39 } else
40 file_changed_cb (tv);
41 }
file_changed_cb处理程序为notebook的page标记提供了一个新文件名。modified_changed_cb处理程序在文件名的开头插入一个星号。这是一个标志,表明文件已经被修改,但还没有保存。
- 1-20: file_changed_cb处理程序。
- 9-10:如果信号是在页面构建过程中发出的,那么tv可能不是nb的后代。也就是说,没有对应于tv的页面。这样就不需要修改选项卡的名称,因为不存在选项卡。
- 13-15:如果file是GFile,那么它获取文件名并释放对file的引用。
- 16-17:否则,它将"Untitled"(+一个数字)赋值给filename
- 18-19:用文件名创建GtkLabel,并用GtkLabel设置页面的选项卡。
- 22-41: modified_changed_cb处理程序。
- 31-32:如果tv不是nb的后代,那么什么都不需要做。
- 33-35:如果内容被修改了,那么它获取选项卡的文本并在文本的开头添加星号。
- 36-38:设置带有星号的文件名选项卡
- 39-40:否则调用file_changed_cb并更新文件名(不带星号)。
Font
GtkFontButton and GtkFontChooser
GtkFontButton是一个按钮类,它显示当前字体,用户可以使用按钮更改字体。如果用户点击按钮,它会打开一个字体选择对话框。用户可以在对话框中改变字体(字体族、样式、粗细和大小)。然后按钮保持新的字体并显示它。
该按钮在应用程序启动过程中使用构建器设置。信号“font-set”连接到处理程序font_set_cb。当用户选择字体时,会发出信号“font-set”。
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是由GtkFontButton实现的接口。函数gtk_font_chooser_get_font_desc获取当前选择字体的PangoFontDescription。PangoFontDescription包含字体族、样式、粗细和大小。函数pfd2css将它们转换为CSS样式字符串。转换如下所示。
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;”
然后,font_set_cb创建一个CSS字符串并将其放入provider实例中。提供程序已提前添加到默认显示。因此,该处理程序立即影响textview内容的字体。
CSS and Pango
pfd2css.c中包含了从PangoFontDescription到CSS的转换器。文件名的意思是:
- pfd => PangoFontDescripter
- 2 => to
- css => CSS (Cascade Style Sheet)
文件中的所有公共函数都有“pdf2css”前缀。
1 #include <pango/pango.h>
2 #include "pfd2css.h"
3
4 // Pango font description to CSS style string
5 // Returned string is owned by caller. The caller should free it when it is useless.
6
7 char*
8 pfd2css (PangoFontDescription *pango_font_desc) {
9 char *fontsize;
10
11 fontsize = pfd2css_size (pango_font_desc);
12 return g_strdup_printf ("font-family: \"%s\"; font-style: %s; font-weight: %d; font-size: %s;",
13 pfd2css_family (pango_font_desc), pfd2css_style (pango_font_desc),
14 pfd2css_weight (pango_font_desc), fontsize);
15 g_free (fontsize);
16 }
17
18 // Each element (family, style, weight and size)
19
20 const char*
21 pfd2css_family (PangoFontDescription *pango_font_desc) {
22 return pango_font_description_get_family (pango_font_desc);
23 }
24
25 const char*
26 pfd2css_style (PangoFontDescription *pango_font_desc) {
27 PangoStyle pango_style = pango_font_description_get_style (pango_font_desc);
28 switch (pango_style) {
29 case PANGO_STYLE_NORMAL:
30 return "normal";
31 case PANGO_STYLE_ITALIC:
32 return "italic";
33 case PANGO_STYLE_OBLIQUE:
34 return "oblique";
35 default:
36 return "normal";
37 }
38 }
39
40 int
41 pfd2css_weight (PangoFontDescription *pango_font_desc) {
42 PangoWeight pango_weight = pango_font_description_get_weight (pango_font_desc);
43 switch (pango_weight) {
44 case PANGO_WEIGHT_THIN:
45 return 100;
46 case PANGO_WEIGHT_ULTRALIGHT:
47 return 200;
48 case PANGO_WEIGHT_LIGHT:
49 return 300;
50 case PANGO_WEIGHT_SEMILIGHT:
51 return 350;
52 case PANGO_WEIGHT_BOOK:
53 return 380;
54 case PANGO_WEIGHT_NORMAL:
55 return 400; /* or "normal" */
56 case PANGO_WEIGHT_MEDIUM:
57 return 500;
58 case PANGO_WEIGHT_SEMIBOLD:
59 return 600;
60 case PANGO_WEIGHT_BOLD:
61 return 700; /* or "bold" */
62 case PANGO_WEIGHT_ULTRABOLD:
63 return 800;
64 case PANGO_WEIGHT_HEAVY:
65 return 900;
66 case PANGO_WEIGHT_ULTRAHEAVY:
67 return 900; /* In PangoWeight definition, the weight is 1000. But CSS allows the weight below 900. */
68 default:
69 return 400; /* "normal" */
70 }
71 }
72
73 char *
74 pfd2css_size (PangoFontDescription *pango_font_desc) {
75 if (pango_font_description_get_size_is_absolute (pango_font_desc))
76 return g_strdup_printf ("%dpx", pango_font_description_get_size (pango_font_desc) / PANGO_SCALE);
77 else
78 return g_strdup_printf ("%dpt", pango_font_description_get_size (pango_font_desc) / PANGO_SCALE);
79 }
- 1: Pango的公共函数、常量和结构定义在pango/pango.h中。
- 2:包含pdf2css.h使得在pdf2css.c文件的任何地方调用公共函数成为可能。因为头文件包含了所有公共函数的声明。
- 7-16: pdf2css功能。这个函数从作为参数的PangoFontDescription实例中获取字体族、样式、粗细和大小。它将它们构建成一根弦。返回的字符串归调用者所有。调用者应该在字符串无用时释放它。
- 20-23: pfd2css_famili函数。这个函数从PangoFontDescription实例中获取font-family字符串。该字符串属于PFD实例,因此调用者不能修改或释放该字符串。
- 25-38: pdf2css_style函数。这个函数从PangoFontDescription实例中获取font-style字符串。字符串是静态的,调用者不能修改或释放它。
- 40-71: pfd2css_weight函数。这个函数从PangoFontDescription实例中获取font-weight整数值。取值范围为100 ~ 900。它定义在[CSS字体模块第3级](…/src/CSS字体模块(第三级)规范。
- 100 -薄
- 200 -超短(超短)
- 300 -短
- 400 -正常
- 500 -中等
- 600 -半加粗(半加粗)
- 700 -粗体
- 800 -加粗(超加粗)
- 900 -黑色(重)
- 73-79: pdf2css_size函数。这个函数从PangoFontDescription实例中获取字体大小字符串。字符串是由调用者拥有的,因此调用者应该在它无用时释放它。PangoFontDescription具有绝对大小或非绝对大小。
- 如果是绝对的,则大小以设备为单位。
- 如果它是非绝对的,则大小以点为单位。
- 设备单位的定义依赖于输出设备。它通常是屏幕的像素,打印机的点。
- Pango将尺寸作为自己的尺寸。常量PANGO_SCALE是用于Pango距离的尺寸和设备单位之间的比例。PANGO_SCALE当前的值是1024,但将来可能会改变。在设置字体大小时,设备单位总是被认为是点而不是像素。如果字体大小为12pt,则pango中的大小为12PANGO_SCALE=121024=12288。
有关更多信息,请参阅Pango API参考。
GSettings
我们希望在应用程序退出后保持字体数据。有一些方法可以实现它。
- 制作配置文件。例如,一个文本文件“~/.config/tfe/font.cfg”保存字体信息。
- 使用GSettings对象。GSettings的基本思想类似于configuration file。配置信息数据放入数据库文件中。
GSettings简单易用,但概念有点难以理解。这一小节首先描述概念,然后如何编程。
GSettings schema
GSettings schema描述了一组键、值类型和其他一些信息。GSettings对象使用这种模式,它将键的值写入/读取到数据库中的正确位置。
- schema有id。id不能重复。我们经常使用与应用程序id相同的字符串,但是schema id和应用程序id是不同的。您可以使用不同于应用程序id的名称。schema id是由句点分隔的字符串。例如,com.github.ToshioCP.Tfe”是正确的schema id。
- schema通常有一个路径path。路径是数据库中的一个位置。每个键都存储在该路径下。例如,如果在路径/com/github/ToshioCP/tfe/中定义了一个key = font,那么该key’s在数据库中的位置就是/com/github/ToshioCP/tfe/font。Path是一个以斜杠(/)开始和结束的字符串。它由斜线分隔。
- GSettings将信息保存为键值(key-value)样式。Key是一个字符串,以小写字母开始,然后是小写字母、数字或破折号(-),以小写字母或数字结束。不允许连续的破折号。值可以是任何类型。GSettings将值存储为GVariant类型,可以是整型、双精度、布尔型、字符串,也可以是数组等复杂类型。值的类型需要在模式中定义。
- 每个键都需要设置一个默认值。
- 可以为每个键设置可选的摘要和描述。
schema以XML格式描述。例如,
1 <?xml version="1.0" encoding="UTF-8"?>
2 <schemalist>
3 <schema path="/com/github/ToshioCP/tfe/" id="com.github.ToshioCP.tfe">
4 <key name="font" type="s">
5 <default>'Monospace 12'</default>
6 <summary>Font</summary>
7 <description>A font to be used for textview.</description>
8 </key>
9 </schema>
10 </schemalist>
- 4: type属性为“s”。它是GVariant类型的字符串。对于GVariant类型字符串,请参阅GLib API参考——GVariant类型字符串。其他常见的类型有:
- “b”:gboolean
- “i”:gint32。
- “d”:double。
更多资料请参阅:
- GLib API Reference – GVariant Format Strings
- GLib API Reference – GVariant Text Format
- GLib API Reference – GVariant
- GLib API Reference – VariantType
gsettings
首先,让我们尝试gsettings应用程序。它是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
... ...
每一行都是一个schema id。每个schema都有一个key-value配置数据。你可以使用list- recur命令查看它们。让我们看一下org.gnome.calculator模式的键和值。
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
GNOME计算器使用此schema。运行计算器并更改schema,然后再次检查schema。
$ gnome-calculator
修改为“高级模式”并退出。
运行gsettings并检查button-mode的值。
gsettings list-recursively org.gnome.calculator
... ...
org.gnome.calculator button-mode 'advanced'
... ...
现在我们知道GNOME计算器使用了gsettings,它将button-mode键设置为“advanced”。即使计算器退出,value仍然存在。因此,当计算器再次运行时,它将显示为高级模式。
glib-compile-schemas
GSettings schema 使用XML格式指定。XML模式文件必须具有文件名扩展名 .gschema.xml。下面是应用程序tfe的XML schema文件。
1 <?xml version="1.0" encoding="UTF-8"?>
2 <schemalist>
3 <schema path="/com/github/ToshioCP/tfe/" id="com.github.ToshioCP.tfe">
4 <key name="font" type="s">
5 <default>'Monospace 12'</default>
6 <summary>Font</summary>
7 <description>A font to be used for textview.</description>
8 </key>
9 </schema>
10 </schemalist>
文件名是"com.github.ToshioCP.tfe.gschema.xml"。Schema XML文件名通常是Schema id后面加上".gschema.xml "后缀。您可以使用与模式id不同的名称,但不建议这样做。
- 2:顶层元素为
<schemalist>
。 - 3: schema标签具有path和id属性。路径确定settings存储在概念全局设置树中的位置。id标识模式。
- 4: Key标签有两个属性。Name是键的名称。Type是键值的类型,它是一个GVariant格式字符串。
- 5:默认值font = Monospace 12
6:概述和描述元素用于描述key。它们是可选的,但建议将它们添加到XML文件中。
XML文件由glib-compile-schemas编译。编译时,glib-compile-schemas编译所有在给定的目录中具有“.gschema.xml”文件扩展名作为参数的XML文件。它将XML文件转换为二进制文件gschema .compiled。假设上面的XML文件在tfe6目录下。
$ glib-compile-schemas tfe6
然后,在tfe6下生成gschemas.compiled。测试应用时,设置环境变量GSETTINGS_SCHEMA_DIR,这样GSettings对象就能找到gschemas.compiled。
$ GSETTINGS_SCHEMA_DIR=(the directory gschemas.compiled is located):$GSETTINGS_SCHEMA_DIR (your application name)
GSettings对象通过以下步骤查找此文件。
- 它搜索在环境变量XDG_DATA_DIRS中指定的所有目录的glib-2.0/schemas子目录。常见的目录是/usr/share/glib-2.0/schemas和/usr/local/share/glib-2.0/schemas。
- 如果定义了GSETTINGS_SCHEMA_DIR环境变量,它将搜索该变量中指定的所有目录。GSETTINGS_SCHEMA_DIR可以指定多个以冒号(:)分隔的目录。
在上面的目录中,存储了所有的.gschema.xml文件。因此,在安装应用程序时,请按照下面的说明安装模式。
- 1.创建.gschema.xml文件。
- 2.将其复制到上面的目录之一。例如,/usr/local/share/glib-2.0/schemas。
- 3.在上面的目录上运行glib-compile-schemas。你可能需要sudo。
GSettings object and g_settings_bind
现在,我们进入下一个主题——如何编写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);
... ...
}
静态变量settings保存了一个指向GSettings实例的指针。在应用程序退出之前,应用程序释放GSettings实例。函数g_clear_object减少GSettings实例的引用计数,并将NULL赋值给变量settings。
Startup handler创建模式id为“com.github.ToshioCP”的GSettings实例。并将指针赋值给settings。函数g_settings_bind连接settings键(key和value)和fontbtn的"font"属性。那么这两个值将始终相同。如果一个值改变了,那么另一个也会自动改变。
有关更多信息,请参阅GIO API参考——GSettings。
Build with Meson
Build and test
Meson提供了gnome.compile_schemas方法在构建目录中编译XML文件。这用于测试应用程序。写下面的meson.build文件。
gnome.compile_schemas(build_by_default: true, depend_files: 'com.github.ToshioCP.tfe.gschema.xml')
- build_by_default:如果为true,将默认构建目标。
- depend_files:要编译的XML文件。
在上面的例子中,这个方法运行glib-compile-schemas,从XML文件com.github.ToshioCP.tfe.gschema.xml生成gschemas.compiled。gschemas.compiled文件位于build目录下。如果你将meson运行为meson _build,将ninja运行为ninja -C _build,那么它就在_build目录下。
编译后,你可以像这样测试你的应用程序:
$ GSETTINGS_SCHEMA_DIR=_build:$GSETTINGS_SCHEMA_DIR _build/tfe
installation
将应用程序安装在
H
O
M
E
/
b
i
n
或
HOME/bin或
HOME/bin或HOME/中是一个好主意。本地/ bin目录。它们是本地bin目录,工作方式类似于系统bin目录,如/bin、/usr/bin或usr/local/bin。你需要添加--prefix=$HOME or --prefix=$HOME/.local
到meson。
$ meson --prefix=$HOME/.local _build
如果你想将应用程序安装到系统的bin目录中,例如/usr/local/bin,则不需要——prefix选项。
Meson识别这样的选项:
函数executable需要install: true才能安装程序。
executable('tfe', sourcefiles, resources, dependencies: gtkdep, export_dynamic: true, install: true)
然而,你还需要做一件事。将XML文件复制到schema目录,并在该目录上执行glib-compile-schemas。
- Install_data函数将文件复制到目标目录。
- gnome.post_install函数在安装后以schema_dir作为参数执行
glib-compile-schemas
。该函数从Meson 0.57.0开始可用。如果版本早于此,则使用meson.add_install_script代替。
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)
函数get_option返回构建选项的值。参见Meason参考手册。运算符/
用/
分隔符连接字符串。
Meson.buid 的源代码如下。
1 project('tfe', 'c')
2
3 gtkdep = dependency('gtk4')
4
5 gnome=import('gnome')
6 resources = gnome.compile_resources('resources','tfe.gresource.xml')
7 gnome.compile_schemas(build_by_default: true, depend_files: 'com.github.ToshioCP.tfe.gschema.xml')
8
9 sourcefiles=files('tfeapplication.c', 'tfenotebook.c', 'pfd2css.c', '../tfetextview/tfetextview.c')
10
11 executable('tfe', sourcefiles, resources, dependencies: gtkdep, export_dynamic: true, install: true)
12
13 schema_dir = get_option('prefix') / get_option('datadir') / 'glib-2.0/schemas/'
14 install_data('com.github.ToshioCP.tfe.gschema.xml', install_dir: schema_dir)
15 gnome.post_install (glib_compile_schemas: true)
tfe的源文件在src/tfe6目录下。将它们复制到临时目录,然后编译并安装它。
$ 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