Completing the Main Program
The file tfeapplication.c is the main program of
Tfe. Its main roles are:
- Application initialization, including command-line support (opening files from the terminal).
- Building the user interface using a UI resource file.
- Managing multiple text views with tabs (GtkNotebook).
- Providing signal handlers for buttons and file dialog operations.
The Function main
The program tfe is executed from the command
line.
$ tfe file1 file2 ...
The function main is called first.
int
main (int argc, char **argv) {
GtkApplication *app;
int stat;
app = gtk_application_new (APPLICATION_ID, G_APPLICATION_HANDLES_OPEN);
g_signal_connect (app, "activate", G_CALLBACK (app_activate), NULL);
g_signal_connect (app, "open", G_CALLBACK (app_open), NULL);
stat = g_application_run (G_APPLICATION (app), argc, argv);
g_object_unref (app);
return stat;
}- 6: Creates a GtkApplication object.
APPLICATION_IDis defined as “com.github.ToshioCP.tfe5” before themainfunction. The second argumentG_APPLICATION_HANDLES_OPENis a flag and that means that the application accepts commandline arguments but only pathnames. - 7-8: Connects the “activate” and “open” signals to their handlers.
- 9: Runs the application.
- 10-11: Releases the application and returns the status.
Startup Signal Handler
A startup signal is emitted just after the GtkApplication instance is registered. The purpose of this handler is to initialize the application. This does not include window initialization, which is handled by the “activate” or “open” signal handlers.
This application does not need a “startup” handler for now, but it will be implemented in a later version.
Activate and open signal handler
The “activate” and “open” signal handlers are named
app_activate and app_open,
respectively. When the application is launched without any
command-line arguments, the “activate” signal is emitted.
Conversely, if arguments are provided, the “open” signal is
emitted.
static GtkWidget *
tfe_get_notebook (GtkWidget *win) {
GtkWidget *boxv = gtk_window_get_child (GTK_WINDOW (win));
return gtk_widget_get_last_child (boxv);
}
void
app_activate (GApplication *application) {
GtkWidget *win = get_main_window (application);
GtkWidget *nb = tfe_get_notebook (win);
notebook_page_new_with_file (GTK_NOTEBOOK (nb), NULL);
gtk_window_present (GTK_WINDOW (win));
}
void
app_open (GApplication *application, GFile **files, gint n_files, const gchar *hint) {
GtkWidget *win = get_main_window (application);
GtkWidget *nb = tfe_get_notebook (win);
int i;
for (i = 0; i < n_files; i++)
notebook_page_new_with_file (GTK_NOTEBOOK (nb), files[i]);
if (gtk_notebook_get_n_pages (GTK_NOTEBOOK (nb)) == 0) /* No files were opened */
notebook_page_new_with_file (GTK_NOTEBOOK (nb), NULL);
gtk_window_present (GTK_WINDOW (win));
}- 1-5: The private function
tfe_get_notebookreturns the notebook instance. This function is a private utility used byapp_activateandapp_open. This function relies on the assumption that the notebook is the last child of the box. Therefore, it will no longer work if another widget is added to the box in the future version. There are several ways to solve this issue, which will be explained in a later section. - 7-14:
app_activate. - 9: The main window instance is obtained via the
get_main_windowfunction, which will be discussed later. - 10: The notebook instance
nbis obtained by calling the functiontfe_get_notebook. - 12: A new empty page is created, and finally, the main
window is displayed. The
notebook_page_new_with_filefunction will be explained later. - 16-27: The
app_openfunction. - 22-23: In this for-loop, notebook pages are created with
files using the
notebook_page_new_with_filefunction.files[i]is the i-th commandline argument. - 24-25: If no page has been created, maybe because of read error, then it creates an empty page.
- 26: Shows the window.
The Notebook page building function
The function notebook_page_new_with_file builds
a new notebook page from a GFile. If the GFile is NULL, it
creates a new empty page. The function is called by the
“activate” and “open” handlers when the application starts.
static void
notebook_page_new_with_file (GtkNotebook *nb, GFile *file) {
g_return_if_fail (GTK_IS_NOTEBOOK (nb));
g_return_if_fail (G_IS_FILE (file) || file == NULL);
GtkWidget *win;
GtkNotebookPage *nbp;
GtkWidget *scr;
GtkWidget *tv;
GtkWidget *lab;
int i;
GError *err = NULL;
if ((tv = tfe_text_view_new_with_file (file, &err)) == NULL) {
win = gtk_widget_get_ancestor (GTK_WIDGET (nb), GTK_TYPE_WINDOW);
tfe_error_alert (GTK_WINDOW (win), err);
g_clear_error (&err);
return;
}
lab = tfe_label_from_file (file); /* lab is floating. lab can be NULL */
scr = gtk_scrolled_window_new ();
gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (scr), GTK_WIDGET (tv));
i = gtk_notebook_append_page (nb, scr, lab);
nbp = gtk_notebook_get_page (nb, scr);
g_object_set (nbp, "tab-expand", TRUE, NULL);
gtk_notebook_set_current_page (nb, i);
}- 14-19: Calls
tfe_text_view_new_with_fileto create a new TfeTextView instance. If it returns NULL, shows the error message through the alert dialog usingtfe_error_alert. -20: Gets the GtkLabel instance with the filename. If no file is assigned to TfeTextView,tfe_label_from_filereturns NULL. - 21-22: assigns
tvto the newly created scrolled windowscr. Then, appends the scrolled window and the label to the notebook as a child and tab respectively. - 23-25: Sets the page property “tab-expand” to TRUE.
- 26: Sets the current page to the new page.
Primary and Secondary Instances
Only one GApplication instance can be run at a time in a session. The session is a somewhat complex concept and also platform-dependent, but roughly speaking, it corresponds to a graphical desktop login. When you use your PC, you probably log in first, and then your desktop appears until you log out. This is the session.
However, Linux is a multi-process OS and you can run two or more instances of the same application. Isn’t it a contradiction?
When the first instance is launched, it registers itself with its application ID (for example, “com.github.ToshioCP.tfe5”). Just after registration, the “startup” signal is emitted, followed by the “activate” or “open” signal, and finally, the instance’s main loop starts.
If another instance with the same application ID is launched, it also tries to register itself. Because this is the second instance, the registration of the ID has already been done, so it fails. Because of the failure, the “startup” signal isn’t emitted. After that, the “activate” or “open” signal is emitted in the primary instance, not in the second instance. The primary instance receives the signal and its handler is invoked. On the other hand, the second instance doesn’t receive the signal and immediately quits.
Building a Single Window
The activate and open signal
handlers are responsible for creating and displaying the window.
However, because these handlers can be called multiple times,
creating a window unconditionally would result in multiple main
windows. Our TFE application is designed to have only one main
window, using notebook pages to manage multiple files.
Therefore, the handlers must follow this logic:
- If the application already has a window, use it.
- If it does not, create a new one.
Since the window is built from a UI file, creating it also
involves instantiating child widgets and connecting button
signals to their handlers. The get_main_window
helper function takes care of all these steps.
static GtkWidget *
get_main_window (GApplication *application) {
GtkApplication *app = GTK_APPLICATION (application);
GList *windows = gtk_application_get_windows (app);
GtkWidget *win;
GtkWidget *nb;
GtkButton *btno;
GtkButton *btnn;
GtkButton *btns;
GtkButton *btnc;
GtkBuilder *build;
/* Return the existing window if one already exists */
if (windows)
return GTK_WIDGET (windows->data);
build = gtk_builder_new_from_resource ("/com/github/ToshioCP/tfe5/tfe.ui");
win = GTK_WIDGET (gtk_builder_get_object (build, "win"));
gtk_window_set_application (GTK_WINDOW (win), app);
nb = GTK_WIDGET (gtk_builder_get_object (build, "nb"));
btno = GTK_BUTTON (gtk_builder_get_object (build, "btno"));
btnn = GTK_BUTTON (gtk_builder_get_object (build, "btnn"));
btns = GTK_BUTTON (gtk_builder_get_object (build, "btns"));
btnc = GTK_BUTTON (gtk_builder_get_object (build, "btnc"));
g_signal_connect_swapped (btno, "clicked", G_CALLBACK (open_cb), nb);
g_signal_connect_swapped (btnn, "clicked", G_CALLBACK (new_cb), nb);
g_signal_connect_swapped (btns, "clicked", G_CALLBACK (save_cb), nb);
g_signal_connect_swapped (btnc, "clicked", G_CALLBACK (close_cb), nb);
g_object_unref (build);
return win;
}- 4: An application can generally have multiple main windows.
These are stored in a list, which can be retrieved using the
gtk_application_get_windowsfunction. The return type is aGList *pointer. AGListstructure containsdata(pointing to the window in this case),next, andprevpointers, forming a doubly linked list. If the list is empty, the function returnsNULL. For more details, refer to the GLib Documentation - Doubly Linked Lists and GList. - 14-15: If
windowsis notNULL, the application already has a window. The function simply returns this existing window (windows->data). - 17-32: Otherwise, it uses
GtkBuilderto generate all the widgets, registers the new window with the application, connects the “clicked” signals to their handlers, and finally returns the window.
Running and Observing Primary and Secondary Instances
For compiling, see “Build and Execute the Program” subsection below.
Try running two instances in a row:
$ ./_build/tfe & ./_build/tfe tfeapplication.c
$
First, the primary instance opens a window. Then, after the
second instance is run, a new notebook page with the contents of
tfeapplication.c appears in the primary instance’s
window. This is because the “open” signal is emitted in the
primary instance. The second instance immediately quits, so the
shell prompt soon appears.
New and CLose Button Signal Handlers
Open and save button handlers were explained in the previous section. Now we will explore the remaining handlers: the new and close handlers
static void
new_cb (GtkNotebook *nb) {
notebook_page_new_with_file (nb, NULL);
}
static void
close_cb (GtkNotebook *nb) {
if (gtk_notebook_get_n_pages (nb) >= 2)
gtk_notebook_remove_page (nb, gtk_notebook_get_current_page (nb));
else /* If the page is the last page or no page exists */
gtk_window_destroy (GTK_WINDOW (gtk_widget_get_ancestor (GTK_WIDGET (nb), GTK_TYPE_WINDOW)));
}- 1-4: Handles the “clicked” signal for the “New” button. The
new_cbfunction simply callsnotebook_page_new_with_fileto create a new, empty page. - 6-12: Handles the “clicked” signal for the “Close” button. If two or more pages exist, it removes the current page. If it is the last remaining page, the program destroys the window, which consequently quits the application.
meson.build
project('tfe', 'c')
gtkdep = dependency('gtk4')
gnome=import('gnome')
resources = gnome.compile_resources('resources','tfe.gresource.xml')
# Main application
sourcefiles=files('tfeapplication.c', 'tfetextview.c')
executable('tfe', sourcefiles, resources, dependencies: gtkdep)
# Test executable for tfetextview
test_sourcefiles = files('test/test_tfetextview.c', 'tfetextview.c')
test_exe = executable('test_tfetextview', test_sourcefiles, dependencies: gtkdep)
# Test registration
test('tfetextview_test', test_exe)- 1-10: In this file, just the source file names are modified from the prior version.
- 12-17: These lines are for a test program for
tfetextview.c. Test is not covered by this tutorial. If you want to know Glib test framework, See the GLib documentation.
Source Files
You can download the files from the repository.
The source files are under the /src/tfe5 directory.
Build and Execute the Program
You can build and execute the program using Meson and Ninja. To install the applications, type:
$ sudo apt install meson ninja-build
Change your current directory to
Gtk4-tutorial/src/tfe5 and type the following to
build the program.
$ meson setup _build
$ ninja -C _build
To execute the program, type:
$ _build/tfe
You can add pathnames as arguments. A window will appear, containing four buttons and an editing area.
If you want to run the test program for
tfetextview.c, type:
$ meson test -C _build
ninja: Entering directory `/home/username/Gtk4-tutorial/src/tfe5/_build'
ninja: no work to do.
1/1 tfetextview_test OK 0.53s
Ok: 1
Expected Fail: 0
Fail: 0
Unexpected Pass: 0
Skipped: 0
Timeout: 0
Full log written to /home/username/Gtk4-tutorial/src/tfe5/_build/meson-logs/testlog.txt