We made a very simple file viewer in the previous section. Now we go
on to rewrite it and turn it into very simple editor. Its source file is
tfe1.c
(text file editor 1) under tfe
directory.
GtkTextView is a multi-line editor. So, we don’t need to write the editor from scratch. We just add two things to the file viewer:
There are a couple of ways to store the pointers.
Using global variables is easy to implement. Define a sufficient size pointer array to GFile. For example,
*f[20]; GFile
The variable f[i]
corresponds to the file associated
with the i-th GtkNotebookPage.
However, There are two problems. The first is the size of the array. If a user gives too many arguments (more than 20 in the example above), it is impossible to store all the pointers to the GFile instances. The second is difficulty to maintain the program. We have a small program so far. But, the more you develop the program, the bigger its size grows. Generally speaking, it is very difficult to maintain global variables in a big program. When you check the global variable, you need to check all the codes that use the variable.
Making a child class is a good idea in terms of maintenance. And we prefer it rather than a global variable.
Be careful that we are thinking about “child class”, not “child widget”. Child class and child widget are totally different. Class is a term of GObject system. If you are not familiar with GObject, see:
A child class inherits everything from the parent and, in addition, extends its performance. We will define TfeTextView as a child class of GtkTextView. It has everything that GtkTextView has and adds a pointer to a GFile.
You need to know GObject system convention. First, look at the program below.
#define TFE_TYPE_TEXT_VIEW tfe_text_view_get_type ()
(TfeTextView, tfe_text_view, TFE, TEXT_VIEW, GtkTextView)
G_DECLARE_FINAL_TYPE
struct _TfeTextView
{
;
GtkTextView parent*file;
GFile };
(TfeTextView, tfe_text_view, GTK_TYPE_TEXT_VIEW);
G_DEFINE_FINAL_TYPE
static void
(TfeTextView *tv) {
tfe_text_view_init }
static void
(TfeTextViewClass *class) {
tfe_text_view_class_init }
void
(TfeTextView *tv, GFile *f) {
tfe_text_view_set_file -> file = f;
tv }
*
GFile (TfeTextView *tv) {
tfe_text_view_get_file return tv -> file;
}
*
GtkWidget (void) {
tfe_text_view_new return GTK_WIDGET (g_object_new (TFE_TYPE_TEXT_VIEW, NULL));
}
TFE_TYPE_TEXT_VIEW
macro as
tfe_text_view_get_type ()
. The name is always
(prefix)_TYPE_(object) and the letters are upper case. And the
replacement text is always (prefix)_(object)_get_type () and the letters
are lower case. This definition is put before
G_DECLARE_FINAL_TYPE
macro.G_DECLARE_FINAL_TYPE
macro are the
child class name in camel case, lower case with underscore, prefix
(upper case), object (upper case with underscore) and parent class name
(camel case). The following two C structures are declared in the
expansion of the macro.
typedef struct _TfeTextView TfeTextView
typedef struct {GtkTextViewClass parent_class; } TfeTextViewClass;
_TfeTextView
. The underscore is
necessary. The first member is the parent object (C structure). Notice
this is not a pointer but the object itself. The second member and after
are members of the child object. TfeTextView structure has a pointer to
a GFile instance as a member.G_DEFINE_FINEL_TYPE
macro. The arguments are the child
object name in camel case, lower case with underscore and parent object
type (prefix)_TYPE_(module). This macro is mainly used to register the
new class to the type system. Type system is a base system of GObject.
Every class has its own type. The types of GObject, GtkWidget and
TfeTextView are G_TYPE_OBJECT
, GTK_TYPE_WIDGET
and TFE_TYPE_TEXT_VIEW
respectively. For example,
TFE_TYPE_TEXT_VIEW
is a macro and it is expanded to a
function tfe_text_view_get_type()
. It returns a integer
which is unique among all GObject system classes.tfe_text_view_init
is called
when the instance is created. It is the same as a constructor in other
object oriented languages.tfe_text_view_class_init
is
called when the class is created.tfe_text_view_set_file
and
tfe_text_view_get_file
are public functions. Public
functions are open and you can call them anywhere. They are the same as
public method in other object oriented languages. tv
is a
pointer to the TfeTextView object (C structure). It has a member
file
and it is pointed by tv->file
.tfe_text_view_new
. Its name is (prefix)_(object)_new. It
uses g_object_new
function to create the instance. The
arguments are (prefix)_TYPE_(object), a list to initialize properties
and NULL. NULL is the end mark of the property list. No property is
initialized here. And the return value is casted to GtkWidget.This program shows the outline how to define a child class.
Imagine that you are using this editor. First, you run the editor with arguments. The arguments are filenames. The editor reads the files and shows the window with the text of files in it. Then you edit the text. After you finish editing, you click on the close button of the window and quit the editor. The editor updates files just before the window closes.
GtkWindow emits the “close-request” signal when the close button is
clicked. We will connect the signal and the handler
before_close
. (A handler is a C function which is connected
to a signal.) The function before_close
is called when the
signal “close-request” is emitted.
(win, "close-request", G_CALLBACK (before_close), NULL); g_signal_connect
The argument win
is a GtkApplicationWindow, in which the
signal “close-request” is defined, and before_close
is the
handler. The G_CALLBACK
cast is necessary for the handler.
The program of before_close
is as follows.
static gboolean
before_close (GtkWindow *win, GtkWidget *nb) {
GtkWidget *scr;
GtkWidget *tv;
GFile *file;
char *pathname;
GtkTextBuffer *tb;
GtkTextIter start_iter;
GtkTextIter end_iter;
char *contents;
unsigned int n;
unsigned int i;
GError *err = NULL;
n = gtk_notebook_get_n_pages (GTK_NOTEBOOK (nb));
for (i = 0; i < n; ++i) {
scr = gtk_notebook_get_nth_page (GTK_NOTEBOOK (nb), i);
tv = gtk_scrolled_window_get_child (GTK_SCROLLED_WINDOW (scr));
file = tfe_text_view_get_file (TFE_TEXT_VIEW (tv));
tb = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv));
gtk_text_buffer_get_bounds (tb, &start_iter, &end_iter);
contents = gtk_text_buffer_get_text (tb, &start_iter, &end_iter, FALSE);
if (! g_file_replace_contents (file, contents, strlen (contents), NULL, TRUE, G_FILE_CREATE_NONE, NULL, NULL, &err)) {
g_printerr ("%s.\n", err->message);
g_clear_error (&err);
}
g_free (contents);
g_object_unref (file);
}
return FALSE;
}
The numbers on the left are line numbers.
n
.scr
, tv
and file
is
assigned pointers to the GtkScrolledWindow, TfeTextView and GFile. The
GFile of TfeTextView was stored when app_open
handler was
called. It will be shown later.tb
is assigned the GtkTextBuffer of the
TfeTextView. The contents of the buffer are accessed with iterators.
Iterators points somewhere in the buffer. The function
gtk_text_buffer_get_bounds
assigns the start and end of the
buffer to start_iter
and end_iter
respectively. Then the function gtk_text_buffer_get_text
returns the text between start_iter
and
end_iter
, which is the whole text in the buffer.err
needs to be NULL for the next run in the loop.contents
are freed.g_object_unref
decreases the
reference count of the GFile. Reference count will be explained in the
later section. The reference count will be zero and the GFile instance
will destroy itself.The following is the whole source code of tfe1.c
.
#include <gtk/gtk.h>
/* Define TfeTextView Widget which is the child class of GtkTextView */
#define TFE_TYPE_TEXT_VIEW tfe_text_view_get_type ()
G_DECLARE_FINAL_TYPE (TfeTextView, tfe_text_view, TFE, TEXT_VIEW, GtkTextView)
struct _TfeTextView
{
GtkTextView parent;
GFile *file;
};
G_DEFINE_FINAL_TYPE (TfeTextView, tfe_text_view, GTK_TYPE_TEXT_VIEW);
static void
tfe_text_view_init (TfeTextView *tv) {
tv->file = NULL;
}
static void
tfe_text_view_class_init (TfeTextViewClass *class) {
}
void
tfe_text_view_set_file (TfeTextView *tv, GFile *f) {
tv->file = f;
}
GFile *
tfe_text_view_get_file (TfeTextView *tv) {
return tv -> file;
}
GtkWidget *
tfe_text_view_new (void) {
return GTK_WIDGET (g_object_new (TFE_TYPE_TEXT_VIEW, NULL));
}
/* ---------- end of the definition of TfeTextView ---------- */
static gboolean
before_close (GtkWindow *win, GtkWidget *nb) {
GtkWidget *scr;
GtkWidget *tv;
GFile *file;
char *pathname;
GtkTextBuffer *tb;
GtkTextIter start_iter;
GtkTextIter end_iter;
char *contents;
unsigned int n;
unsigned int i;
GError *err = NULL;
n = gtk_notebook_get_n_pages (GTK_NOTEBOOK (nb));
for (i = 0; i < n; ++i) {
scr = gtk_notebook_get_nth_page (GTK_NOTEBOOK (nb), i);
tv = gtk_scrolled_window_get_child (GTK_SCROLLED_WINDOW (scr));
file = tfe_text_view_get_file (TFE_TEXT_VIEW (tv));
tb = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv));
gtk_text_buffer_get_bounds (tb, &start_iter, &end_iter);
contents = gtk_text_buffer_get_text (tb, &start_iter, &end_iter, FALSE);
if (! g_file_replace_contents (file, contents, strlen (contents), NULL, TRUE, G_FILE_CREATE_NONE, NULL, NULL, &err)) {
g_printerr ("%s.\n", err->message);
g_clear_error (&err);
}
g_free (contents);
g_object_unref (file);
}
return FALSE;
}
static void
app_activate (GApplication *app) {
g_print ("You need to give filenames as arguments.\n");
}
static void
app_open (GApplication *app, GFile ** files, gint n_files, gchar *hint) {
GtkWidget *win;
GtkWidget *nb;
GtkWidget *lab;
GtkNotebookPage *nbp;
GtkWidget *scr;
GtkWidget *tv;
GtkTextBuffer *tb;
char *contents;
gsize length;
char *filename;
int i;
GError *err = NULL;
win = gtk_application_window_new (GTK_APPLICATION (app));
gtk_window_set_title (GTK_WINDOW (win), "file editor");
gtk_window_set_default_size (GTK_WINDOW (win), 600, 400);
nb = gtk_notebook_new ();
gtk_window_set_child (GTK_WINDOW (win), nb);
for (i = 0; i < n_files; i++) {
if (g_file_load_contents (files[i], NULL, &contents, &length, NULL, &err)) {
scr = gtk_scrolled_window_new ();
tv = tfe_text_view_new ();
tb = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv));
gtk_text_view_set_wrap_mode (GTK_TEXT_VIEW (tv), GTK_WRAP_WORD_CHAR);
gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (scr), tv);
tfe_text_view_set_file (TFE_TEXT_VIEW (tv), g_file_dup (files[i]));
gtk_text_buffer_set_text (tb, contents, length);
g_free (contents);
filename = g_file_get_basename (files[i]);
lab = gtk_label_new (filename);
gtk_notebook_append_page (GTK_NOTEBOOK (nb), scr, lab);
nbp = gtk_notebook_get_page (GTK_NOTEBOOK (nb), scr);
g_object_set (nbp, "tab-expand", TRUE, NULL);
g_free (filename);
} else {
g_printerr ("%s.\n", err->message);
g_clear_error (&err);
}
}
if (gtk_notebook_get_n_pages (GTK_NOTEBOOK (nb)) > 0) {
g_signal_connect (win, "close-request", G_CALLBACK (before_close), nb);
gtk_window_present (GTK_WINDOW (win));
} else
gtk_window_destroy (GTK_WINDOW (win));
}
int
main (int argc, char **argv) {
GtkApplication *app;
int stat;
app = gtk_application_new ("com.github.ToshioCP.tfe1", 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;
}
files[i]
, which is a GFile created with the command line
argument. The GFile will be destroyed by the system later. So it needs
to be copied before the assignment. g_file_dup
duplicates
the GFile. Note: GFile is not thread safe. Duplicating GFile
avoids a trouble comes from the different thread.before_close
handler. The fourth argument is called “user
data” and it will be the second argument of the signal handler. So,
nb
is given to before_close
as the second
argument.Now it’s time to compile and run.
$ cd src/tfe
$ comp tfe1
$ ./a.out taketori.txt`.
Modify the contents and close the window. Make sure that the file is modified.
Now we got a very simple editor. It’s not smart. We need more features like open, save, saveas, change font and so on. We will add them in the next section and after.