diff --git a/.gitignore b/.gitignore index 0c57d22..31cc088 100755 --- a/.gitignore +++ b/.gitignore @@ -12,8 +12,10 @@ src/tfv/a.out src/tfe/a.out src/tfe/hello.txt src/tfe/resources.c +src/tfe4/_build src/tfe5/_build src/tfe5/hello.txt +src/tfe6/_build src/menu/a.out src/color/_build src/turtle/_build diff --git a/Readme.md b/Readme.md index c01eaf9..7e704dc 100644 --- a/Readme.md +++ b/Readme.md @@ -32,5 +32,6 @@ You can read it without download. 1. [Menu and action](gfm/sec16.md) 1. [Stateful action](gfm/sec17.md) 1. [Ui file for menu and action entries](gfm/sec18.md) -1. [GtkDrawingArea and Cairo](gfm/sec19.md) -1. [Combine GtkDrawingArea and TfeTextView](gfm/sec20.md) +1. [Upgrade text file editor](gfm/sec19.md) +1. [GtkDrawingArea and Cairo](gfm/sec20.md) +1. [Combine GtkDrawingArea and TfeTextView](gfm/sec21.md) diff --git a/gfm/sec10.md b/gfm/sec10.md index 590c06b..cabca7c 100644 --- a/gfm/sec10.md +++ b/gfm/sec10.md @@ -372,7 +372,7 @@ Then C destructs itself and finally the memories allocated to C is freed. The idea above is based on an assumption that an object referred by nothing has reference count of zero. When the reference count drops to zero, the object starts its destruction process. -The destruction process is spitted into two phases: disposing and finalizing. +The destruction process is split into two phases: disposing and finalizing. In the disposing process, the object invokes the function pointed by `dispose` in its class to release all references to other objects. In the finalizing process, it invokes the function pointed by `finalize` in its class to complete the destruction process. These functions are also called handlers or methods. diff --git a/gfm/sec12.md b/gfm/sec12.md index a0fe78d..1fbb348 100644 --- a/gfm/sec12.md +++ b/gfm/sec12.md @@ -158,73 +158,57 @@ The error is managed only in the TfeTextView instance and no information is noti 2 saveas_dialog_response (GtkWidget *dialog, gint response, TfeTextView *tv) { 3 GtkTextBuffer *tb = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv)); 4 GFile *file; - 5 - 6 if (response == GTK_RESPONSE_ACCEPT) { - 7 file = gtk_file_chooser_get_file (GTK_FILE_CHOOSER (dialog)); - 8 if (G_IS_FILE(file)) { - 9 if (G_IS_FILE (tv->file)) -10 g_object_unref (tv->file); -11 tv->file = file; -12 gtk_text_buffer_set_modified (tb, TRUE); -13 g_signal_emit (tv, tfe_text_view_signals[CHANGE_FILE], 0); -14 tfe_text_view_save (TFE_TEXT_VIEW (tv)); -15 } -16 } -17 gtk_window_destroy (GTK_WINDOW (dialog)); -18 } -19 -20 void -21 tfe_text_view_save (TfeTextView *tv) { -22 g_return_if_fail (TFE_IS_TEXT_VIEW (tv)); -23 -24 GtkTextBuffer *tb = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv)); -25 GtkTextIter start_iter; -26 GtkTextIter end_iter; -27 gchar *contents; -28 GtkWidget *message_dialog; -29 GtkWidget *win = gtk_widget_get_ancestor (GTK_WIDGET (tv), GTK_TYPE_WINDOW); -30 GError *err = NULL; -31 -32 if (! gtk_text_buffer_get_modified (tb)) -33 return; /* no need to save it */ -34 else if (tv->file == NULL) -35 tfe_text_view_saveas (tv); + 5 GtkWidget *win = gtk_widget_get_ancestor (GTK_WIDGET (tv), GTK_TYPE_WINDOW); + 6 + 7 gtk_window_destroy (GTK_WINDOW (dialog)); + 8 if (response == GTK_RESPONSE_ACCEPT) { + 9 file = gtk_file_chooser_get_file (GTK_FILE_CHOOSER (dialog)); +10 if (! G_IS_FILE (file)) +11 g_warning ("TfeTextView: gtk_file_chooser_get_file returns non GFile object.\n"); +12 else { +13 save_file(file, tb, GTK_WINDOW (win)); +14 if (G_IS_FILE (tv->file)) +15 g_object_unref (tv->file); +16 tv->file = file; +17 gtk_text_buffer_set_modified (tb, FALSE); +18 g_signal_emit (tv, tfe_text_view_signals[CHANGE_FILE], 0); +19 } +20 } +21 } +22 +23 void +24 tfe_text_view_save (TfeTextView *tv) { +25 g_return_if_fail (TFE_IS_TEXT_VIEW (tv)); +26 +27 GtkTextBuffer *tb = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv)); +28 GtkWidget *win = gtk_widget_get_ancestor (GTK_WIDGET (tv), GTK_TYPE_WINDOW); +29 +30 if (! gtk_text_buffer_get_modified (tb)) +31 return; /* no need to save it */ +32 else if (tv->file == NULL) +33 tfe_text_view_saveas (tv); +34 else if (! G_IS_FILE (tv->file)) +35 g_error ("TfeTextView: The pointer in this object isn't NULL nor GFile object.\n"); 36 else { -37 gtk_text_buffer_get_bounds (tb, &start_iter, &end_iter); -38 contents = gtk_text_buffer_get_text (tb, &start_iter, &end_iter, FALSE); -39 if (g_file_replace_contents (tv->file, contents, strlen (contents), NULL, TRUE, G_FILE_CREATE_NONE, NULL, NULL, &err)) -40 gtk_text_buffer_set_modified (tb, FALSE); -41 else { -42 /* It is possible that tv->file is broken or you don't have permission to write. */ -43 /* It is a good idea to set tv->file to NULL. */ -44 if (G_IS_FILE (tv->file)) -45 g_object_unref (tv->file); -46 tv->file =NULL; -47 g_signal_emit (tv, tfe_text_view_signals[CHANGE_FILE], 0); -48 message_dialog = gtk_message_dialog_new (GTK_WINDOW (win), GTK_DIALOG_MODAL, -49 GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, -50 "%s.\n", err->message); -51 g_signal_connect (message_dialog, "response", G_CALLBACK (gtk_window_destroy), NULL); -52 gtk_widget_show (message_dialog); -53 g_error_free (err); -54 } -55 } -56 } -57 -58 void -59 tfe_text_view_saveas (TfeTextView *tv) { -60 g_return_if_fail (TFE_IS_TEXT_VIEW (tv)); -61 -62 GtkWidget *dialog; -63 GtkWidget *win = gtk_widget_get_ancestor (GTK_WIDGET (tv), GTK_TYPE_WINDOW); -64 -65 dialog = gtk_file_chooser_dialog_new ("Save file", GTK_WINDOW (win), GTK_FILE_CHOOSER_ACTION_SAVE, -66 "Cancel", GTK_RESPONSE_CANCEL, -67 "Save", GTK_RESPONSE_ACCEPT, -68 NULL); -69 g_signal_connect (dialog, "response", G_CALLBACK (saveas_dialog_response), tv); -70 gtk_widget_show (dialog); -71 } +37 if (save_file (tv->file, tb, GTK_WINDOW (win))) +38 gtk_text_buffer_set_modified (tb, FALSE); +39 } +40 } +41 +42 void +43 tfe_text_view_saveas (TfeTextView *tv) { +44 g_return_if_fail (TFE_IS_TEXT_VIEW (tv)); +45 +46 GtkWidget *dialog; +47 GtkWidget *win = gtk_widget_get_ancestor (GTK_WIDGET (tv), GTK_TYPE_WINDOW); +48 +49 dialog = gtk_file_chooser_dialog_new ("Save file", GTK_WINDOW (win), GTK_FILE_CHOOSER_ACTION_SAVE, +50 "Cancel", GTK_RESPONSE_CANCEL, +51 "Save", GTK_RESPONSE_ACCEPT, +52 NULL); +53 g_signal_connect (dialog, "response", G_CALLBACK (saveas_dialog_response), tv); +54 gtk_widget_show (dialog); +55 } ~~~ - 20-56: `Tfe_text_view_save` function. @@ -300,45 +284,46 @@ Otherwise probably bad things will happen. 9 10 if (response != GTK_RESPONSE_ACCEPT) 11 g_signal_emit (tv, tfe_text_view_signals[OPEN_RESPONSE], 0, TFE_OPEN_RESPONSE_CANCEL); -12 else if (! G_IS_FILE (file = gtk_file_chooser_get_file (GTK_FILE_CHOOSER (dialog)))) -13 g_signal_emit (tv, tfe_text_view_signals[OPEN_RESPONSE], 0, TFE_OPEN_RESPONSE_ERROR); -14 else if (! g_file_load_contents (file, NULL, &contents, &length, NULL, &err)) { /* read error */ -15 if (G_IS_FILE (file)) -16 g_object_unref (file); -17 message_dialog = gtk_message_dialog_new (GTK_WINDOW (dialog), GTK_DIALOG_MODAL, -18 GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, -19 "%s.\n", err->message); -20 g_signal_connect (message_dialog, "response", G_CALLBACK (gtk_window_destroy), NULL); -21 gtk_widget_show (message_dialog); -22 g_error_free (err); -23 g_signal_emit (tv, tfe_text_view_signals[OPEN_RESPONSE], 0, TFE_OPEN_RESPONSE_ERROR); -24 } else { -25 gtk_text_buffer_set_text (tb, contents, length); -26 g_free (contents); -27 if (G_IS_FILE (tv->file)) -28 g_object_unref (tv->file); -29 tv->file = file; -30 gtk_text_buffer_set_modified (tb, FALSE); -31 g_signal_emit (tv, tfe_text_view_signals[OPEN_RESPONSE], 0, TFE_OPEN_RESPONSE_SUCCESS); -32 g_signal_emit (tv, tfe_text_view_signals[CHANGE_FILE], 0); -33 } -34 gtk_window_destroy (GTK_WINDOW (dialog)); -35 } -36 -37 void -38 tfe_text_view_open (TfeTextView *tv, GtkWidget *win) { -39 g_return_if_fail (TFE_IS_TEXT_VIEW (tv)); -40 g_return_if_fail (GTK_IS_WINDOW (win)); -41 -42 GtkWidget *dialog; -43 -44 dialog = gtk_file_chooser_dialog_new ("Open file", GTK_WINDOW (win), GTK_FILE_CHOOSER_ACTION_OPEN, -45 "Cancel", GTK_RESPONSE_CANCEL, -46 "Open", GTK_RESPONSE_ACCEPT, -47 NULL); -48 g_signal_connect (dialog, "response", G_CALLBACK (open_dialog_response), tv); -49 gtk_widget_show (dialog); -50 } +12 else if (! G_IS_FILE (file = gtk_file_chooser_get_file (GTK_FILE_CHOOSER (dialog)))) { +13 g_warning ("TfeTextView: gtk_file_chooser_get_file returns non GFile object.\n"); +14 g_signal_emit (tv, tfe_text_view_signals[OPEN_RESPONSE], 0, TFE_OPEN_RESPONSE_ERROR); +15 } else if (! g_file_load_contents (file, NULL, &contents, &length, NULL, &err)) { /* read error */ +16 if (G_IS_FILE (file)) +17 g_object_unref (file); +18 message_dialog = gtk_message_dialog_new (GTK_WINDOW (dialog), GTK_DIALOG_MODAL, +19 GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, +20 "%s.\n", err->message); +21 g_signal_connect (message_dialog, "response", G_CALLBACK (gtk_window_destroy), NULL); +22 gtk_widget_show (message_dialog); +23 g_error_free (err); +24 g_signal_emit (tv, tfe_text_view_signals[OPEN_RESPONSE], 0, TFE_OPEN_RESPONSE_ERROR); +25 } else { +26 gtk_text_buffer_set_text (tb, contents, length); +27 g_free (contents); +28 if (G_IS_FILE (tv->file)) +29 g_object_unref (tv->file); +30 tv->file = file; +31 gtk_text_buffer_set_modified (tb, FALSE); +32 g_signal_emit (tv, tfe_text_view_signals[OPEN_RESPONSE], 0, TFE_OPEN_RESPONSE_SUCCESS); +33 g_signal_emit (tv, tfe_text_view_signals[CHANGE_FILE], 0); +34 } +35 gtk_window_destroy (GTK_WINDOW (dialog)); +36 } +37 +38 void +39 tfe_text_view_open (TfeTextView *tv, GtkWidget *win) { +40 g_return_if_fail (TFE_IS_TEXT_VIEW (tv)); +41 g_return_if_fail (GTK_IS_WINDOW (win)); +42 +43 GtkWidget *dialog; +44 +45 dialog = gtk_file_chooser_dialog_new ("Open file", GTK_WINDOW (win), GTK_FILE_CHOOSER_ACTION_OPEN, +46 "Cancel", GTK_RESPONSE_CANCEL, +47 "Open", GTK_RESPONSE_ACCEPT, +48 NULL); +49 g_signal_connect (dialog, "response", G_CALLBACK (open_dialog_response), tv); +50 gtk_widget_show (dialog); +51 } ~~~ - 37-50: `tfe_text_view_open` function. @@ -384,8 +369,11 @@ However, in Gtk4, `gtk_dialog_run`is unavailable any more. 2 tfe_text_view_get_file (TfeTextView *tv) { 3 g_return_val_if_fail (TFE_IS_TEXT_VIEW (tv), NULL); 4 -5 return g_file_dup (tv->file); -6 } +5 if (G_IS_FILE (tv->file)) +6 return g_file_dup (tv->file); +7 else +8 return NULL; +9 } ~~~ The important thing is to duplicate `tv->file`. diff --git a/gfm/sec13.md b/gfm/sec13.md index fb27644..2c822c8 100644 --- a/gfm/sec13.md +++ b/gfm/sec13.md @@ -4,7 +4,7 @@ Up: [Readme.md](../Readme.md), Prev: [Section 12](sec12.md), Next: [Section 14] GtkNotebook is a very important object in the text file editor `tfe`. It connects the application and TfeTextView objects. -A set of functions related to GtkNotebook are declared in `tfenotebook.h`. +A set of public functions related to GtkNotebook are declared in `tfenotebook.h`. The word "tfenotebook" is used only in filenames. There's no "TfeNotebook" object. @@ -13,30 +13,34 @@ There's no "TfeNotebook" object. 2 notebook_page_save(GtkNotebook *nb); 3 4 void - 5 notebook_page_open (GtkNotebook *nb); + 5 notebook_page_close (GtkNotebook *nb); 6 7 void - 8 notebook_page_new_with_file (GtkNotebook *nb, GFile *file); + 8 notebook_page_open (GtkNotebook *nb); 9 10 void -11 notebook_page_new (GtkNotebook *nb); +11 notebook_page_new_with_file (GtkNotebook *nb, GFile *file); 12 +13 void +14 notebook_page_new (GtkNotebook *nb); +15 ~~~ This header file describes the public functions in `tfenotebook.c`. -- 10-11: The function `notebook_page_new` generates a new GtkNotebookPage and adds GtkScrolledWindow and TfeTextView to the page. -- 7-8: The function `notebook_page_new_with_file` generates a new GtkNotebookPage and adds GtkScrolledWindow and TfeTextView to the page. A file is read and inserted into GtkTextBuffer. -The GFile `file` is copied and inserted to the TfeTextView object. -- 4-5: `notebook_page_open` shows a file chooser dialog. Then, user chooses a file and the file is inserted into GtkTextBuffer. -- 1-2: `notebook_page_save` saves the contents in GtkTextBuffer into the file, which is got from the TfeTextView. +- 1-2: `notebook_page_save` saves the current page to the file of which the name specified in the tab. +If the name is `untitled` or `untitled` followed by digits, FileChooserDialog appears and a user can choose or specify a filename. +- 4-5: `notebook_page_close` closes the current page. +- 7-8: `notebook_page_open` shows a file chooser dialog and a user can choose a file. The file is inserted to a new page. +- 10-11: `notebook_page_new_with_file` generates a new page and the file give as an argument is read and inserted into the page. +- 13-14: `notebook_page_new` generates a new empty page. -You probably find that the functions above are higher level functions of +You probably find that the functions except `notebook_page_close` are higher level functions of -- `tfe_text_view_new` -- `tfe_text_view_new_with_file` -- `tef_text_view_open` - `tfe_text_view_save` +- `tef_text_view_open` +- `tfe_text_view_new_with_file` +- `tfe_text_view_new` respectively. @@ -44,7 +48,7 @@ There are two layers. One of them is `tfe_text_view ...`, which is the lower level layer. The other is `note_book ...`, which is the higher level layer. -Now let's look at each program of the functions. +Now let's look at the program of each function. ## notebook\_page\_new @@ -60,57 +64,56 @@ Now let's look at each program of the functions. 9 10 static void 11 notebook_page_build (GtkNotebook *nb, GtkWidget *tv, char *filename) { -12 GtkWidget *scr; +12 GtkWidget *scr = gtk_scrolled_window_new (); 13 GtkNotebookPage *nbp; 14 GtkWidget *lab; -15 gint i; -16 scr = gtk_scrolled_window_new (); -17 -18 gtk_text_view_set_wrap_mode (GTK_TEXT_VIEW (tv), GTK_WRAP_WORD_CHAR); -19 gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (scr), tv); -20 lab = gtk_label_new (filename); -21 i = gtk_notebook_append_page (nb, scr, lab); -22 nbp = gtk_notebook_get_page (nb, scr); -23 g_object_set (nbp, "tab-expand", TRUE, NULL); -24 gtk_notebook_set_current_page (nb, i); -25 g_signal_connect (GTK_TEXT_VIEW (tv), "change-file", G_CALLBACK (file_changed), nb); -26 } -27 -28 void -29 notebook_page_new (GtkNotebook *nb) { -30 g_return_if_fail(GTK_IS_NOTEBOOK (nb)); -31 -32 GtkWidget *tv; -33 char *filename; -34 -35 tv = tfe_text_view_new (); -36 filename = get_untitled (); -37 notebook_page_build (nb, tv, filename); -38 } +15 int i; +16 +17 gtk_text_view_set_wrap_mode (GTK_TEXT_VIEW (tv), GTK_WRAP_WORD_CHAR); +18 gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (scr), tv); +19 lab = gtk_label_new (filename); +20 i = gtk_notebook_append_page (nb, scr, lab); +21 nbp = gtk_notebook_get_page (nb, scr); +22 g_object_set (nbp, "tab-expand", TRUE, NULL); +23 gtk_notebook_set_current_page (nb, i); +24 g_signal_connect (GTK_TEXT_VIEW (tv), "change-file", G_CALLBACK (file_changed_cb), nb); +25 } +26 +27 void +28 notebook_page_new (GtkNotebook *nb) { +29 g_return_if_fail(GTK_IS_NOTEBOOK (nb)); +30 +31 GtkWidget *tv; +32 char *filename; +33 +34 tv = tfe_text_view_new (); +35 filename = get_untitled (); +36 notebook_page_build (nb, tv, filename); +37 } ~~~ -- 28-38: `notebook_page_new` function. -- 30: `g_return_if_fail` is used to check the argument. -- 35: Generates TfeTextView object. -- 36: Generates filename, which is "Untitled", "Untitled2", ... . +- 27-37: `notebook_page_new` function. +- 29: `g_return_if_fail` is used to check the argument. +- 34: Generates TfeTextView object. +- 35: Generates filename, which is "Untitled", "Untitled1", ... . - 1-8: `get_untitled` function. -- 3: Static variable `c` is initialized at the first call of this function. After that `c` keeps its value except it is changed explicitly. -- 4-7: Increases `c` by one and if it is zero then it returns "Untitled". If it is a positive integer then the it returns "Untitled\", for example, "Untitled1", "Untitled2", and so on. +- 3: Static variable `c` is initialized at the first call of this function. After that `c` keeps its value unless it is changed explicitly. +- 4-7: Increases `c` by one and if it is zero then it returns "Untitled". If it is a positive integer then it returns "Untitled\", for example, "Untitled1", "Untitled2", and so on. The function `g_strdup_printf` generates a string and it should be freed by `g_free` when it becomes useless. The caller of `get_untitled` is in charge of freeing the string. -- 37: calls `notebook_page_build` to build the contents of the page. -- 10- 26: `notebook_page_build` function. -- 16: Generates GtkScrolledWindow. -- 18: Sets the wrap mode of `tv` to GTK_WRAP_WORD_CHAR so that lines are broken between words or graphemes. -- 19: Inserts `tv` to GtkscrolledWindow as a child. -- 20-21: Generates GtkLabel, then appends it to GtkNotebookPage. -- 22-23: Sets "tab-expand" property to TRUE. +- 36: calls `notebook_page_build` to build the contents of the page. +- 10- 25: `notebook_page_build` function. +- 12: Generates GtkScrolledWindow. +- 17: Sets the wrap mode of `tv` to GTK_WRAP_WORD_CHAR so that lines are broken between words or graphemes. +- 18: Inserts `tv` to GtkscrolledWindow as a child. +- 19-20: Generates GtkLabel, then appends it to GtkNotebookPage. +- 21-22: Sets "tab-expand" property to TRUE. The function g\_object\_set sets properties on an object. The object is any object derived from GObject. In many cases, an object has its own function to set its properties, but sometimes not. In that case, use g\_object\_set to set the property. -- 24: Sets the current page of `nb` to `i` which is the number of the GtkNotebookPage above. -- 25: Connects "change-file" signal and `file_changed` handler. +- 23: Sets the current page of `nb` to the newly generated page. +- 24: Connects "change-file" signal and `file_changed_cb` handler. ## notebook\_page\_new\_with\_file @@ -139,7 +142,7 @@ The return value NULL means that an error has happened. ~~~C 1 static void - 2 open_response (TfeTextView *tv, gint response, GtkNotebook *nb) { + 2 open_response (TfeTextView *tv, int response, GtkNotebook *nb) { 3 GFile *file; 4 char *filename; 5 @@ -181,63 +184,106 @@ Such object has floating reference. You need to call `g_object_ref_sink` first. Then the floating reference is converted into an ordinary reference. Now you call `g_object_unref` to decrease the reference count by one. -- 9-11: If `tfe_text_view_get_file` returns a pointer not to point GFile, then something bad happens. +- 9-11: If `tfe_text_view_get_file` returns a pointer not to point GFile, it means that an error has happened. Sink and unref `tv`. - 12-16: Otherwise, everything is okay. Gets the filename, builds the contents of the page. -## notebook\_page\_save +## notebook\_page\_close ~~~C 1 void - 2 notebook_page_save(GtkNotebook *nb) { - 3 gint i; + 2 notebook_page_close (GtkNotebook *nb) { + 3 g_return_if_fail(GTK_IS_NOTEBOOK (nb)); + 4 + 5 GtkWidget *win; + 6 int i; + 7 + 8 if (gtk_notebook_get_n_pages (nb) == 1) { + 9 win = gtk_widget_get_ancestor (GTK_WIDGET (nb), GTK_TYPE_WINDOW); +10 gtk_window_destroy(GTK_WINDOW (win)); +11 } else { +12 i = gtk_notebook_get_current_page (nb); +13 gtk_notebook_remove_page (GTK_NOTEBOOK (nb), i); +14 } +15 } +~~~ + +This function closes the current page. +If the page is the only page the notebook has, then the function destroys the top window and quits the application. + +- 8-10: If the page is the only page the notebook has, it calls gtk\_window\_destroy to destroys the top window. +- 11-13: Otherwise, removes the current page. + +## notebook\_page\_save + +~~~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 tfe_text_view_save (TFE_TEXT_VIEW (tv)); +10 return TFE_TEXT_VIEW (tv); 11 } +12 +13 void +14 notebook_page_save (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_save (TFE_TEXT_VIEW (tv)); +21 } ~~~ -- 7-9: Get TfeTextView belongs to the current notebook page. -- 10: Call `tfe_text_view_save`. +- 13-21: `notebook_page_save`. +- 19: Gets TfeTextView belongs to the current page. +- 20: Calls `tfe_text_view_save`. +- 1-11: `get_current_textview`. +This function gets the TfeTextView object belongs to the current page. +- 7: Gets the page number of the current page. +- 8: Gets the child object `scr`, which is GtkScrolledWindow object, of the current page. +- 9-10: Gets the child object of `scr`, which is TfeTextView object, and return it. -## file\_changed handler +## file\_changed\_cb handler -The function `file_changed` is a handler connected to "change-file" signal. +The function `file_changed_cb` is a handler connected to "change-file" signal. If the file in TfeTextView is changed, it emits this signal. This handler changes the label of GtkNotebookPage. ~~~C 1 static void - 2 file_changed (TfeTextView *tv, GtkNotebook *nb) { - 3 GFile *file; - 4 char *filename; - 5 GtkWidget *scr; - 6 GtkWidget *label; + 2 file_changed_cb (TfeTextView *tv, GtkNotebook *nb) { + 3 GtkWidget *scr; + 4 GtkWidget *label; + 5 GFile *file; + 6 char *filename; 7 8 file = tfe_text_view_get_file (tv); 9 scr = gtk_widget_get_parent (GTK_WIDGET (tv)); -10 if (G_IS_FILE (file)) +10 if (G_IS_FILE (file)) { 11 filename = g_file_get_basename (file); -12 else -13 filename = get_untitled (); -14 label = gtk_label_new (filename); -15 gtk_notebook_set_tab_label (nb, scr, label); -16 g_object_unref (file); -17 g_free (filename); -18 } +12 g_object_unref (file); +13 } else +14 filename = get_untitled (); +15 label = gtk_label_new (filename); +16 gtk_notebook_set_tab_label (nb, scr, label); +17 } ~~~ - 8: Gets GFile from TfeTextView. - 9: Gets GkScrolledWindow which is the parent widget of `tv`. -- 10-13: If `file` points GFile, then assigns the filename of the GFile into `filename`. -Otherwise (file is NULL), assigns untitled string to `filename`. -- 14-15: Generates a label with the filename and inserts it into GtkNotebookPage. -- 16-17: Unrefs `file` and frees `filename`. +- 10-12: If `file` points GFile, then assigns the filename of the GFile into `filename`. +Then, unref the GFile object `file`. +- 13-14: Otherwise (file is NULL), assigns untitled string to `filename`. +- 15-16: Generates a label with the filename and inserts it into GtkNotebookPage. +The string `filename` is used in the GtkLabel object. +You mustn't free it. Up: [Readme.md](../Readme.md), Prev: [Section 12](sec12.md), Next: [Section 14](sec14.md) diff --git a/gfm/sec14.md b/gfm/sec14.md index 1dd4073..58a0863 100644 --- a/gfm/sec14.md +++ b/gfm/sec14.md @@ -3,12 +3,12 @@ Up: [Readme.md](../Readme.md), Prev: [Section 13](sec13.md), Next: [Section 15] # tfeapplication.c `tfeapplication.c` includes all the code other than `tfetxtview.c` and `tfenotebook.c`. -It does following things. +It does: - Application support, mainly handling command line arguments. -- Build widgets using ui file. -- Connect button signals and their handlers. -- Manage CSS. +- Builds widgets using ui file. +- Connects button signals and their handlers. +- Manages CSS. ## main @@ -53,9 +53,9 @@ The handler is as follows. 1 static void 2 tfe_startup (GApplication *application) { 3 GtkApplication *app = GTK_APPLICATION (application); - 4 GtkApplicationWindow *win; - 5 GtkNotebook *nb; - 6 GtkBuilder *build; + 4 GtkBuilder *build; + 5 GtkApplicationWindow *win; + 6 GtkNotebook *nb; 7 GtkButton *btno; 8 GtkButton *btnn; 9 GtkButton *btns; @@ -69,10 +69,10 @@ The handler is as follows. 17 btnn = GTK_BUTTON (gtk_builder_get_object (build, "btnn")); 18 btns = GTK_BUTTON (gtk_builder_get_object (build, "btns")); 19 btnc = GTK_BUTTON (gtk_builder_get_object (build, "btnc")); -20 g_signal_connect (btno, "clicked", G_CALLBACK (open_clicked), nb); -21 g_signal_connect (btnn, "clicked", G_CALLBACK (new_clicked), nb); -22 g_signal_connect (btns, "clicked", G_CALLBACK (save_clicked), nb); -23 g_signal_connect (btnc, "clicked", G_CALLBACK (close_clicked), nb); +20 g_signal_connect_swapped (btno, "clicked", G_CALLBACK (open_cb), nb); +21 g_signal_connect_swapped (btnn, "clicked", G_CALLBACK (new_cb), nb); +22 g_signal_connect_swapped (btns, "clicked", G_CALLBACK (save_cb), nb); +23 g_signal_connect_swapped (btnc, "clicked", G_CALLBACK (close_cb), nb); 24 g_object_unref(build); 25 26 GdkDisplay *display; @@ -119,7 +119,8 @@ If you want to set style to GtkTextView, substitute "textview" for the selector. textview {color: yellow; ...} ~~~ -Class, ID and some other things can be applied to the selector like Web CSS. Refer GTK4 API reference for further information. +Class, ID and some other things can be applied to the selector like Web CSS. +Refer [GTK4 API reference](https://gnome.pages.gitlab.gnome.org/gtk/gtk/theming.html) for further information. In line 30, the CSS is a string. @@ -128,11 +129,10 @@ textview {padding: 10px; font-family: monospace; font-size: 12pt;} ~~~ - padding is a space between the border and contents. -This space makes the text easier to read. +This space makes the textview easier to read. - font-family is a name of font. "monospace" is one of the generic family font keywords. - font-size is set to 12pt. -It is a bit large, but easy on the eyes especially for elderly people. ### GtkStyleContext, GtkCSSProvider and GdkDisplay @@ -154,6 +154,8 @@ Look at the source file of `startup` handler again. It is possible to add the provider to the context of GtkTextView instead of GdkDiplay. To do so, rewrite `tfe_text_view_new`. +First, get the GtkStyleContext object of a TfeTextView object. +Then adds the CSS provider to the context. ~~~C GtkWidget * @@ -184,46 +186,36 @@ They just generate a new GtkNotebookPage. 1 static void 2 tfe_activate (GApplication *application) { 3 GtkApplication *app = GTK_APPLICATION (application); - 4 GtkWidget *win; - 5 GtkWidget *boxv; - 6 GtkNotebook *nb; + 4 GtkWidget *win = GTK_WIDGET (gtk_application_get_active_window (app)); + 5 GtkWidget *boxv = gtk_window_get_child (GTK_WINDOW (win)); + 6 GtkNotebook *nb = GTK_NOTEBOOK (gtk_widget_get_last_child (boxv)); 7 - 8 win = GTK_WIDGET (gtk_application_get_active_window (app)); - 9 boxv = gtk_window_get_child (GTK_WINDOW (win)); -10 nb = GTK_NOTEBOOK (gtk_widget_get_last_child (boxv)); + 8 notebook_page_new (nb); + 9 gtk_widget_show (GTK_WIDGET (win)); +10 } 11 -12 notebook_page_new (nb); -13 gtk_widget_show (GTK_WIDGET (win)); -14 } -15 -16 static void -17 tfe_open (GApplication *application, GFile ** files, gint n_files, const gchar *hint) { -18 GtkApplication *app = GTK_APPLICATION (application); -19 GtkWidget *win; -20 GtkWidget *boxv; -21 GtkNotebook *nb; -22 int i; -23 -24 win = GTK_WIDGET (gtk_application_get_active_window (app)); -25 boxv = gtk_window_get_child (GTK_WINDOW (win)); -26 nb = GTK_NOTEBOOK (gtk_widget_get_last_child (boxv)); -27 -28 for (i = 0; i < n_files; i++) -29 notebook_page_new_with_file (nb, files[i]); -30 if (gtk_notebook_get_n_pages (nb) == 0) -31 notebook_page_new (nb); -32 gtk_widget_show (win); -33 } +12 static void +13 tfe_open (GApplication *application, GFile ** files, gint n_files, const gchar *hint) { +14 GtkApplication *app = GTK_APPLICATION (application); +15 GtkWidget *win = GTK_WIDGET (gtk_application_get_active_window (app)); +16 GtkWidget *boxv = gtk_window_get_child (GTK_WINDOW (win)); +17 GtkNotebook *nb = GTK_NOTEBOOK (gtk_widget_get_last_child (boxv)); +18 int i; +19 +20 for (i = 0; i < n_files; i++) +21 notebook_page_new_with_file (nb, files[i]); +22 if (gtk_notebook_get_n_pages (nb) == 0) +23 notebook_page_new (nb); +24 gtk_widget_show (win); +25 } ~~~ -- 1-14: `tfe_activate`. -- 8-10: Gets GtkNotebook object. -- 12-13: Generates a new GtkNotebookPage and show the window. -- 16-33: `tfe_open`. -- 24-26: Gets GtkNotebook object. -- 28-29: Generates GtkNotebookPage with files. -- 30-31: If opening and reading file failed and no GtkNotebookPage has generated, then it generates a empty page. -- 32: Shows the window. +- 1-11: `tfe_activate`. +- 8-10: Generates a new page and shows the window. +- 12-25: `tfe_open`. +- 20-21: Generates notebook pages with files. +- 22-23: If no page has generated, maybe because of read error, then it generates a empty page. +- 24: Shows the window. These codes have become really simple thanks to tfenotebook.c and tfetextview.c. @@ -265,43 +257,27 @@ The second instance immediately quits so shell prompt soon appears. ~~~C 1 static void - 2 open_clicked (GtkWidget *btno, GtkNotebook *nb) { + 2 open_cb (GtkNotebook *nb) { 3 notebook_page_open (nb); 4 } 5 6 static void - 7 new_clicked (GtkWidget *btnn, GtkNotebook *nb) { + 7 new_cb (GtkNotebook *nb) { 8 notebook_page_new (nb); 9 } 10 11 static void -12 save_clicked (GtkWidget *btns, GtkNotebook *nb) { +12 save_cb (GtkNotebook *nb) { 13 notebook_page_save (nb); 14 } 15 16 static void -17 close_clicked (GtkWidget *btnc, GtkNotebook *nb) { -18 GtkWidget *win; -19 GtkWidget *boxv; -20 gint i; -21 -22 if (gtk_notebook_get_n_pages (nb) == 1) { -23 boxv = gtk_widget_get_parent (GTK_WIDGET (nb)); -24 win = gtk_widget_get_parent (boxv); -25 gtk_window_destroy (GTK_WINDOW (win)); -26 } else { -27 i = gtk_notebook_get_current_page (nb); -28 gtk_notebook_remove_page (GTK_NOTEBOOK (nb), i); -29 } -30 } +17 close_cb (GtkNotebook *nb) { +18 notebook_page_close (GTK_NOTEBOOK (nb)); +19 } ~~~ -`open_clicked`, `new_clicked` and `save_clicked` just call corresponding notebook page functions. -`close_clicked` is a bit complicated. - -- 22-25: If there's only one page, we need to close the top level window and quit the application. -First, get the top level window and call `gtk_window_destroy`. -- 26-28: Otherwise, it removes the current page. +`open_cb`, `new_cb`, `save_cb` and `close_cb` just call corresponding notebook page functions. ## meson.build @@ -318,13 +294,13 @@ First, get the top level window and call `gtk_window_destroy`. 10 executable('tfe', sourcefiles, resources, dependencies: gtkdep) ~~~ -In this file, just the source file names are modified. +In this file, just the source file names are modified from the prior version. ## source files The [source files](https://github.com/ToshioCP/Gtk4-tutorial/tree/main/src/tfe5) of the text editor `tfe` will be shown in the next section. -You can download the files. +You can also download the files from the [repository](https://github.com/ToshioCP/Gtk4-tutorial). There are two options. - Use git and clone. @@ -335,6 +311,6 @@ If you use git, run the terminal and type the following. $ git clone https://github.com/ToshioCP/Gtk4-tutorial.git -The source files are under `/src/tfe5` directory. +The source files are under [`/src/tfe5`](../src/tfe5) directory. Up: [Readme.md](../Readme.md), Prev: [Section 13](sec13.md), Next: [Section 15](sec15.md) diff --git a/gfm/sec15.md b/gfm/sec15.md index 4e8571b..d88efaf 100644 --- a/gfm/sec15.md +++ b/gfm/sec15.md @@ -60,70 +60,67 @@ It is a good practice for you to add more features. ## tfe.ui ~~~xml - 1 - 2 - 3 file editor - 4 600 - 5 400 - 6 - 7 - 8 GTK_ORIENTATION_VERTICAL - 9 -10 -11 GTK_ORIENTATION_HORIZONTAL -12 -13 -14 10 -15 -16 -17 -18 -19 _New -20 TRUE + 1 + 2 + 3 + 4 file editor + 5 600 + 6 400 + 7 + 8 + 9 GTK_ORIENTATION_VERTICAL +10 +11 +12 GTK_ORIENTATION_HORIZONTAL +13 +14 +15 10 +16 +17 +18 +19 +20 New 21 22 23 24 -25 _Open -26 TRUE -27 -28 -29 -30 -31 TRUE -32 -33 -34 -35 -36 _Save -37 TRUE -38 -39 -40 -41 -42 _Close -43 TRUE -44 -45 -46 -47 -48 10 -49 -50 -51 -52 -53 -54 -55 TRUE -56 TRUE -57 TRUE -58 -59 -60 -61 -62 -63 -64 +25 Open +26 +27 +28 +29 +30 TRUE +31 +32 +33 +34 +35 Save +36 +37 +38 +39 +40 Close +41 +42 +43 +44 +45 10 +46 +47 +48 +49 +50 +51 +52 TRUE +53 TRUE +54 TRUE +55 +56 +57 +58 +59 +60 +61 ~~~ ## tfe.h @@ -138,123 +135,103 @@ It is a good practice for you to add more features. ## tfeapplication.c ~~~C - 1 #include "tfe.h" - 2 - 3 static void - 4 open_clicked (GtkWidget *btno, GtkNotebook *nb) { - 5 notebook_page_open (nb); - 6 } - 7 - 8 static void - 9 new_clicked (GtkWidget *btnn, GtkNotebook *nb) { - 10 notebook_page_new (nb); - 11 } - 12 - 13 static void - 14 save_clicked (GtkWidget *btns, GtkNotebook *nb) { - 15 notebook_page_save (nb); - 16 } - 17 - 18 static void - 19 close_clicked (GtkWidget *btnc, GtkNotebook *nb) { - 20 GtkWidget *win; - 21 GtkWidget *boxv; - 22 gint i; - 23 - 24 if (gtk_notebook_get_n_pages (nb) == 1) { - 25 boxv = gtk_widget_get_parent (GTK_WIDGET (nb)); - 26 win = gtk_widget_get_parent (boxv); - 27 gtk_window_destroy (GTK_WINDOW (win)); - 28 } else { - 29 i = gtk_notebook_get_current_page (nb); - 30 gtk_notebook_remove_page (GTK_NOTEBOOK (nb), i); - 31 } - 32 } - 33 - 34 static void - 35 tfe_activate (GApplication *application) { - 36 GtkApplication *app = GTK_APPLICATION (application); - 37 GtkWidget *win; - 38 GtkWidget *boxv; - 39 GtkNotebook *nb; - 40 - 41 win = GTK_WIDGET (gtk_application_get_active_window (app)); - 42 boxv = gtk_window_get_child (GTK_WINDOW (win)); - 43 nb = GTK_NOTEBOOK (gtk_widget_get_last_child (boxv)); - 44 - 45 notebook_page_new (nb); - 46 gtk_widget_show (GTK_WIDGET (win)); - 47 } - 48 - 49 static void - 50 tfe_open (GApplication *application, GFile ** files, gint n_files, const gchar *hint) { - 51 GtkApplication *app = GTK_APPLICATION (application); - 52 GtkWidget *win; - 53 GtkWidget *boxv; - 54 GtkNotebook *nb; - 55 int i; - 56 - 57 win = GTK_WIDGET (gtk_application_get_active_window (app)); - 58 boxv = gtk_window_get_child (GTK_WINDOW (win)); - 59 nb = GTK_NOTEBOOK (gtk_widget_get_last_child (boxv)); - 60 - 61 for (i = 0; i < n_files; i++) - 62 notebook_page_new_with_file (nb, files[i]); - 63 if (gtk_notebook_get_n_pages (nb) == 0) - 64 notebook_page_new (nb); - 65 gtk_widget_show (win); - 66 } - 67 - 68 - 69 static void - 70 tfe_startup (GApplication *application) { - 71 GtkApplication *app = GTK_APPLICATION (application); - 72 GtkApplicationWindow *win; - 73 GtkNotebook *nb; - 74 GtkBuilder *build; - 75 GtkButton *btno; - 76 GtkButton *btnn; - 77 GtkButton *btns; - 78 GtkButton *btnc; - 79 - 80 build = gtk_builder_new_from_resource ("/com/github/ToshioCP/tfe/tfe.ui"); - 81 win = GTK_APPLICATION_WINDOW (gtk_builder_get_object (build, "win")); - 82 nb = GTK_NOTEBOOK (gtk_builder_get_object (build, "nb")); - 83 gtk_window_set_application (GTK_WINDOW (win), app); - 84 btno = GTK_BUTTON (gtk_builder_get_object (build, "btno")); - 85 btnn = GTK_BUTTON (gtk_builder_get_object (build, "btnn")); - 86 btns = GTK_BUTTON (gtk_builder_get_object (build, "btns")); - 87 btnc = GTK_BUTTON (gtk_builder_get_object (build, "btnc")); - 88 g_signal_connect (btno, "clicked", G_CALLBACK (open_clicked), nb); - 89 g_signal_connect (btnn, "clicked", G_CALLBACK (new_clicked), nb); - 90 g_signal_connect (btns, "clicked", G_CALLBACK (save_clicked), nb); - 91 g_signal_connect (btnc, "clicked", G_CALLBACK (close_clicked), nb); - 92 g_object_unref(build); - 93 - 94 GdkDisplay *display; - 95 - 96 display = gtk_widget_get_display (GTK_WIDGET (win)); - 97 GtkCssProvider *provider = gtk_css_provider_new (); - 98 gtk_css_provider_load_from_data (provider, "textview {padding: 10px; font-family: monospace; font-size: 12pt;}", -1); - 99 gtk_style_context_add_provider_for_display (display, GTK_STYLE_PROVIDER (provider), GTK_STYLE_PROVIDER_PRIORITY_USER); -100 } -101 -102 int -103 main (int argc, char **argv) { -104 GtkApplication *app; -105 int stat; -106 -107 app = gtk_application_new ("com.github.ToshioCP.tfe", G_APPLICATION_HANDLES_OPEN); -108 -109 g_signal_connect (app, "startup", G_CALLBACK (tfe_startup), NULL); -110 g_signal_connect (app, "activate", G_CALLBACK (tfe_activate), NULL); -111 g_signal_connect (app, "open", G_CALLBACK (tfe_open), NULL); -112 -113 stat =g_application_run (G_APPLICATION (app), argc, argv); -114 g_object_unref (app); -115 return stat; -116 } -117 + 1 #include "tfe.h" + 2 + 3 static void + 4 open_cb (GtkNotebook *nb) { + 5 notebook_page_open (nb); + 6 } + 7 + 8 static void + 9 new_cb (GtkNotebook *nb) { +10 notebook_page_new (nb); +11 } +12 +13 static void +14 save_cb (GtkNotebook *nb) { +15 notebook_page_save (nb); +16 } +17 +18 static void +19 close_cb (GtkNotebook *nb) { +20 notebook_page_close (GTK_NOTEBOOK (nb)); +21 } +22 +23 static void +24 tfe_activate (GApplication *application) { +25 GtkApplication *app = GTK_APPLICATION (application); +26 GtkWidget *win = GTK_WIDGET (gtk_application_get_active_window (app)); +27 GtkWidget *boxv = gtk_window_get_child (GTK_WINDOW (win)); +28 GtkNotebook *nb = GTK_NOTEBOOK (gtk_widget_get_last_child (boxv)); +29 +30 notebook_page_new (nb); +31 gtk_widget_show (GTK_WIDGET (win)); +32 } +33 +34 static void +35 tfe_open (GApplication *application, GFile ** files, gint n_files, const gchar *hint) { +36 GtkApplication *app = GTK_APPLICATION (application); +37 GtkWidget *win = GTK_WIDGET (gtk_application_get_active_window (app)); +38 GtkWidget *boxv = gtk_window_get_child (GTK_WINDOW (win)); +39 GtkNotebook *nb = GTK_NOTEBOOK (gtk_widget_get_last_child (boxv)); +40 int i; +41 +42 for (i = 0; i < n_files; i++) +43 notebook_page_new_with_file (nb, files[i]); +44 if (gtk_notebook_get_n_pages (nb) == 0) +45 notebook_page_new (nb); +46 gtk_widget_show (win); +47 } +48 +49 static void +50 tfe_startup (GApplication *application) { +51 GtkApplication *app = GTK_APPLICATION (application); +52 GtkBuilder *build; +53 GtkApplicationWindow *win; +54 GtkNotebook *nb; +55 GtkButton *btno; +56 GtkButton *btnn; +57 GtkButton *btns; +58 GtkButton *btnc; +59 +60 build = gtk_builder_new_from_resource ("/com/github/ToshioCP/tfe/tfe.ui"); +61 win = GTK_APPLICATION_WINDOW (gtk_builder_get_object (build, "win")); +62 nb = GTK_NOTEBOOK (gtk_builder_get_object (build, "nb")); +63 gtk_window_set_application (GTK_WINDOW (win), app); +64 btno = GTK_BUTTON (gtk_builder_get_object (build, "btno")); +65 btnn = GTK_BUTTON (gtk_builder_get_object (build, "btnn")); +66 btns = GTK_BUTTON (gtk_builder_get_object (build, "btns")); +67 btnc = GTK_BUTTON (gtk_builder_get_object (build, "btnc")); +68 g_signal_connect_swapped (btno, "clicked", G_CALLBACK (open_cb), nb); +69 g_signal_connect_swapped (btnn, "clicked", G_CALLBACK (new_cb), nb); +70 g_signal_connect_swapped (btns, "clicked", G_CALLBACK (save_cb), nb); +71 g_signal_connect_swapped (btnc, "clicked", G_CALLBACK (close_cb), nb); +72 g_object_unref(build); +73 +74 GdkDisplay *display; +75 +76 display = gtk_widget_get_display (GTK_WIDGET (win)); +77 GtkCssProvider *provider = gtk_css_provider_new (); +78 gtk_css_provider_load_from_data (provider, "textview {padding: 10px; font-family: monospace; font-size: 12pt;}", -1); +79 gtk_style_context_add_provider_for_display (display, GTK_STYLE_PROVIDER (provider), GTK_STYLE_PROVIDER_PRIORITY_USER); +80 } +81 +82 int +83 main (int argc, char **argv) { +84 GtkApplication *app; +85 int stat; +86 +87 app = gtk_application_new ("com.github.ToshioCP.tfe", G_APPLICATION_HANDLES_OPEN); +88 +89 g_signal_connect (app, "startup", G_CALLBACK (tfe_startup), NULL); +90 g_signal_connect (app, "activate", G_CALLBACK (tfe_activate), NULL); +91 g_signal_connect (app, "open", G_CALLBACK (tfe_open), NULL); +92 +93 stat =g_application_run (G_APPLICATION (app), argc, argv); +94 g_object_unref (app); +95 return stat; +96 } +97 ~~~ ## tfenotebook.h @@ -264,14 +241,17 @@ It is a good practice for you to add more features. 2 notebook_page_save(GtkNotebook *nb); 3 4 void - 5 notebook_page_open (GtkNotebook *nb); + 5 notebook_page_close (GtkNotebook *nb); 6 7 void - 8 notebook_page_new_with_file (GtkNotebook *nb, GFile *file); + 8 notebook_page_open (GtkNotebook *nb); 9 10 void -11 notebook_page_new (GtkNotebook *nb); +11 notebook_page_new_with_file (GtkNotebook *nb, GFile *file); 12 +13 void +14 notebook_page_new (GtkNotebook *nb); +15 ~~~ ## tfenotebook.c @@ -289,111 +269,134 @@ It is a good practice for you to add more features. 10 return g_strdup_printf ("Untitled%u", c); 11 } 12 - 13 static void - 14 file_changed (TfeTextView *tv, GtkNotebook *nb) { - 15 GFile *file; - 16 char *filename; - 17 GtkWidget *scr; - 18 GtkWidget *label; - 19 - 20 file = tfe_text_view_get_file (tv); - 21 scr = gtk_widget_get_parent (GTK_WIDGET (tv)); - 22 if (G_IS_FILE (file)) - 23 filename = g_file_get_basename (file); - 24 else - 25 filename = get_untitled (); - 26 label = gtk_label_new (filename); - 27 gtk_notebook_set_tab_label (nb, scr, label); - 28 g_object_unref (file); - 29 g_free (filename); - 30 } + 13 static TfeTextView * + 14 get_current_textview (GtkNotebook *nb) { + 15 int i; + 16 GtkWidget *scr; + 17 GtkWidget *tv; + 18 + 19 i = gtk_notebook_get_current_page (nb); + 20 scr = gtk_notebook_get_nth_page (nb, i); + 21 tv = gtk_scrolled_window_get_child (GTK_SCROLLED_WINDOW (scr)); + 22 return TFE_TEXT_VIEW (tv); + 23 } + 24 + 25 static void + 26 file_changed_cb (TfeTextView *tv, GtkNotebook *nb) { + 27 GtkWidget *scr; + 28 GtkWidget *label; + 29 GFile *file; + 30 char *filename; 31 - 32 /* Save the contents in the current page */ - 33 void - 34 notebook_page_save(GtkNotebook *nb) { - 35 gint i; - 36 GtkWidget *scr; - 37 GtkWidget *tv; - 38 - 39 i = gtk_notebook_get_current_page (nb); - 40 scr = gtk_notebook_get_nth_page (nb, i); - 41 tv = gtk_scrolled_window_get_child (GTK_SCROLLED_WINDOW (scr)); - 42 tfe_text_view_save (TFE_TEXT_VIEW (tv)); - 43 } - 44 - 45 static void - 46 notebook_page_build (GtkNotebook *nb, GtkWidget *tv, char *filename) { - 47 GtkWidget *scr; - 48 GtkNotebookPage *nbp; - 49 GtkWidget *lab; - 50 gint i; - 51 scr = gtk_scrolled_window_new (); + 32 file = tfe_text_view_get_file (tv); + 33 scr = gtk_widget_get_parent (GTK_WIDGET (tv)); + 34 if (G_IS_FILE (file)) { + 35 filename = g_file_get_basename (file); + 36 g_object_unref (file); + 37 } else + 38 filename = get_untitled (); + 39 label = gtk_label_new (filename); + 40 gtk_notebook_set_tab_label (nb, scr, label); + 41 } + 42 + 43 void + 44 notebook_page_save (GtkNotebook *nb) { + 45 g_return_if_fail(GTK_IS_NOTEBOOK (nb)); + 46 + 47 TfeTextView *tv; + 48 + 49 tv = get_current_textview (nb); + 50 tfe_text_view_save (TFE_TEXT_VIEW (tv)); + 51 } 52 - 53 gtk_text_view_set_wrap_mode (GTK_TEXT_VIEW (tv), GTK_WRAP_WORD_CHAR); - 54 gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (scr), tv); - 55 lab = gtk_label_new (filename); - 56 i = gtk_notebook_append_page (nb, scr, lab); - 57 nbp = gtk_notebook_get_page (nb, scr); - 58 g_object_set (nbp, "tab-expand", TRUE, NULL); - 59 gtk_notebook_set_current_page (nb, i); - 60 g_signal_connect (GTK_TEXT_VIEW (tv), "change-file", G_CALLBACK (file_changed), nb); - 61 } - 62 - 63 static void - 64 open_response (TfeTextView *tv, gint response, GtkNotebook *nb) { - 65 GFile *file; - 66 char *filename; - 67 - 68 if (response != TFE_OPEN_RESPONSE_SUCCESS) { - 69 g_object_ref_sink (tv); - 70 g_object_unref (tv); - 71 }else if (! G_IS_FILE (file = tfe_text_view_get_file (tv))) { - 72 g_object_ref_sink (tv); - 73 g_object_unref (tv); - 74 }else { - 75 filename = g_file_get_basename (file); - 76 g_object_unref (file); - 77 notebook_page_build (nb, GTK_WIDGET (tv), filename); - 78 } - 79 } - 80 - 81 void - 82 notebook_page_open (GtkNotebook *nb) { - 83 g_return_if_fail(GTK_IS_NOTEBOOK (nb)); - 84 - 85 GtkWidget *tv; - 86 - 87 tv = tfe_text_view_new (); - 88 g_signal_connect (TFE_TEXT_VIEW (tv), "open-response", G_CALLBACK (open_response), nb); - 89 tfe_text_view_open (TFE_TEXT_VIEW (tv), gtk_widget_get_ancestor (GTK_WIDGET (nb), GTK_TYPE_WINDOW)); - 90 } - 91 - 92 void - 93 notebook_page_new_with_file (GtkNotebook *nb, GFile *file) { - 94 g_return_if_fail(GTK_IS_NOTEBOOK (nb)); - 95 g_return_if_fail(G_IS_FILE (file)); - 96 - 97 GtkWidget *tv; - 98 char *filename; - 99 -100 if ((tv = tfe_text_view_new_with_file (file)) == NULL) -101 return; /* read error */ -102 filename = g_file_get_basename (file); -103 notebook_page_build (nb, tv, filename); -104 } -105 -106 void -107 notebook_page_new (GtkNotebook *nb) { -108 g_return_if_fail(GTK_IS_NOTEBOOK (nb)); + 53 void + 54 notebook_page_close (GtkNotebook *nb) { + 55 g_return_if_fail(GTK_IS_NOTEBOOK (nb)); + 56 + 57 GtkWidget *win; + 58 int i; + 59 + 60 if (gtk_notebook_get_n_pages (nb) == 1) { + 61 win = gtk_widget_get_ancestor (GTK_WIDGET (nb), GTK_TYPE_WINDOW); + 62 gtk_window_destroy(GTK_WINDOW (win)); + 63 } else { + 64 i = gtk_notebook_get_current_page (nb); + 65 gtk_notebook_remove_page (GTK_NOTEBOOK (nb), i); + 66 } + 67 } + 68 + 69 static void + 70 notebook_page_build (GtkNotebook *nb, GtkWidget *tv, char *filename) { + 71 GtkWidget *scr = gtk_scrolled_window_new (); + 72 GtkNotebookPage *nbp; + 73 GtkWidget *lab; + 74 int i; + 75 + 76 gtk_text_view_set_wrap_mode (GTK_TEXT_VIEW (tv), GTK_WRAP_WORD_CHAR); + 77 gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (scr), tv); + 78 lab = gtk_label_new (filename); + 79 i = gtk_notebook_append_page (nb, scr, lab); + 80 nbp = gtk_notebook_get_page (nb, scr); + 81 g_object_set (nbp, "tab-expand", TRUE, NULL); + 82 gtk_notebook_set_current_page (nb, i); + 83 g_signal_connect (GTK_TEXT_VIEW (tv), "change-file", G_CALLBACK (file_changed_cb), nb); + 84 } + 85 + 86 static void + 87 open_response (TfeTextView *tv, int response, GtkNotebook *nb) { + 88 GFile *file; + 89 char *filename; + 90 + 91 if (response != TFE_OPEN_RESPONSE_SUCCESS) { + 92 g_object_ref_sink (tv); + 93 g_object_unref (tv); + 94 }else if (! G_IS_FILE (file = tfe_text_view_get_file (tv))) { + 95 g_object_ref_sink (tv); + 96 g_object_unref (tv); + 97 }else { + 98 filename = g_file_get_basename (file); + 99 g_object_unref (file); +100 notebook_page_build (nb, GTK_WIDGET (tv), filename); +101 } +102 } +103 +104 void +105 notebook_page_open (GtkNotebook *nb) { +106 g_return_if_fail(GTK_IS_NOTEBOOK (nb)); +107 +108 GtkWidget *tv; 109 -110 GtkWidget *tv; -111 char *filename; -112 -113 tv = tfe_text_view_new (); -114 filename = get_untitled (); -115 notebook_page_build (nb, tv, filename); -116 } -117 +110 tv = tfe_text_view_new (); +111 g_signal_connect (TFE_TEXT_VIEW (tv), "open-response", G_CALLBACK (open_response), nb); +112 tfe_text_view_open (TFE_TEXT_VIEW (tv), gtk_widget_get_ancestor (GTK_WIDGET (nb), GTK_TYPE_WINDOW)); +113 } +114 +115 void +116 notebook_page_new_with_file (GtkNotebook *nb, GFile *file) { +117 g_return_if_fail(GTK_IS_NOTEBOOK (nb)); +118 g_return_if_fail(G_IS_FILE (file)); +119 +120 GtkWidget *tv; +121 char *filename; +122 +123 if ((tv = tfe_text_view_new_with_file (file)) == NULL) +124 return; /* read error */ +125 filename = g_file_get_basename (file); +126 notebook_page_build (nb, tv, filename); +127 } +128 +129 void +130 notebook_page_new (GtkNotebook *nb) { +131 g_return_if_fail(GTK_IS_NOTEBOOK (nb)); +132 +133 GtkWidget *tv; +134 char *filename; +135 +136 tv = tfe_text_view_new (); +137 filename = get_untitled (); +138 notebook_page_build (nb, tv, filename); +139 } +140 ~~~ ## tfetextview.h @@ -505,157 +508,170 @@ It is a good practice for you to add more features. 64 tfe_text_view_get_file (TfeTextView *tv) { 65 g_return_val_if_fail (TFE_IS_TEXT_VIEW (tv), NULL); 66 - 67 return g_file_dup (tv->file); - 68 } - 69 - 70 static void - 71 open_dialog_response(GtkWidget *dialog, gint response, TfeTextView *tv) { - 72 GtkTextBuffer *tb = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv)); - 73 GFile *file; - 74 char *contents; - 75 gsize length; - 76 GtkWidget *message_dialog; - 77 GError *err = NULL; - 78 - 79 if (response != GTK_RESPONSE_ACCEPT) - 80 g_signal_emit (tv, tfe_text_view_signals[OPEN_RESPONSE], 0, TFE_OPEN_RESPONSE_CANCEL); - 81 else if (! G_IS_FILE (file = gtk_file_chooser_get_file (GTK_FILE_CHOOSER (dialog)))) - 82 g_signal_emit (tv, tfe_text_view_signals[OPEN_RESPONSE], 0, TFE_OPEN_RESPONSE_ERROR); - 83 else if (! g_file_load_contents (file, NULL, &contents, &length, NULL, &err)) { /* read error */ - 84 if (G_IS_FILE (file)) - 85 g_object_unref (file); - 86 message_dialog = gtk_message_dialog_new (GTK_WINDOW (dialog), GTK_DIALOG_MODAL, - 87 GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, - 88 "%s.\n", err->message); - 89 g_signal_connect (message_dialog, "response", G_CALLBACK (gtk_window_destroy), NULL); - 90 gtk_widget_show (message_dialog); - 91 g_error_free (err); - 92 g_signal_emit (tv, tfe_text_view_signals[OPEN_RESPONSE], 0, TFE_OPEN_RESPONSE_ERROR); - 93 } else { - 94 gtk_text_buffer_set_text (tb, contents, length); - 95 g_free (contents); - 96 if (G_IS_FILE (tv->file)) - 97 g_object_unref (tv->file); - 98 tv->file = file; - 99 gtk_text_buffer_set_modified (tb, FALSE); -100 g_signal_emit (tv, tfe_text_view_signals[OPEN_RESPONSE], 0, TFE_OPEN_RESPONSE_SUCCESS); -101 g_signal_emit (tv, tfe_text_view_signals[CHANGE_FILE], 0); -102 } -103 gtk_window_destroy (GTK_WINDOW (dialog)); -104 } -105 -106 void -107 tfe_text_view_open (TfeTextView *tv, GtkWidget *win) { -108 g_return_if_fail (TFE_IS_TEXT_VIEW (tv)); -109 g_return_if_fail (GTK_IS_WINDOW (win)); -110 -111 GtkWidget *dialog; -112 -113 dialog = gtk_file_chooser_dialog_new ("Open file", GTK_WINDOW (win), GTK_FILE_CHOOSER_ACTION_OPEN, -114 "Cancel", GTK_RESPONSE_CANCEL, -115 "Open", GTK_RESPONSE_ACCEPT, -116 NULL); -117 g_signal_connect (dialog, "response", G_CALLBACK (open_dialog_response), tv); -118 gtk_widget_show (dialog); -119 } -120 -121 static void -122 saveas_dialog_response (GtkWidget *dialog, gint response, TfeTextView *tv) { -123 GtkTextBuffer *tb = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv)); -124 GFile *file; -125 -126 if (response == GTK_RESPONSE_ACCEPT) { -127 file = gtk_file_chooser_get_file (GTK_FILE_CHOOSER (dialog)); -128 if (G_IS_FILE(file)) { -129 if (G_IS_FILE (tv->file)) -130 g_object_unref (tv->file); -131 tv->file = file; -132 gtk_text_buffer_set_modified (tb, TRUE); -133 g_signal_emit (tv, tfe_text_view_signals[CHANGE_FILE], 0); -134 tfe_text_view_save (TFE_TEXT_VIEW (tv)); -135 } + 67 if (G_IS_FILE (tv->file)) + 68 return g_file_dup (tv->file); + 69 else + 70 return NULL; + 71 } + 72 + 73 static gboolean + 74 save_file (GFile *file, GtkTextBuffer *tb, GtkWindow *win) { + 75 GtkTextIter start_iter; + 76 GtkTextIter end_iter; + 77 gchar *contents; + 78 GtkWidget *message_dialog; + 79 GError *err = NULL; + 80 + 81 /* This function doesn't check G_IS_FILE (file). The caller should check it. */ + 82 gtk_text_buffer_get_bounds (tb, &start_iter, &end_iter); + 83 contents = gtk_text_buffer_get_text (tb, &start_iter, &end_iter, FALSE); + 84 if (g_file_replace_contents (file, contents, strlen (contents), NULL, TRUE, G_FILE_CREATE_NONE, NULL, NULL, &err)) { + 85 gtk_text_buffer_set_modified (tb, FALSE); + 86 return TRUE; + 87 } else { + 88 message_dialog = gtk_message_dialog_new (win, GTK_DIALOG_MODAL, + 89 GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, + 90 "%s.\n", err->message); + 91 g_signal_connect (message_dialog, "response", G_CALLBACK (gtk_window_destroy), NULL); + 92 gtk_widget_show (message_dialog); + 93 g_error_free (err); + 94 return FALSE; + 95 } + 96 } + 97 + 98 static void + 99 saveas_dialog_response (GtkWidget *dialog, gint response, TfeTextView *tv) { +100 GtkTextBuffer *tb = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv)); +101 GFile *file; +102 GtkWidget *win = gtk_widget_get_ancestor (GTK_WIDGET (tv), GTK_TYPE_WINDOW); +103 +104 gtk_window_destroy (GTK_WINDOW (dialog)); +105 if (response == GTK_RESPONSE_ACCEPT) { +106 file = gtk_file_chooser_get_file (GTK_FILE_CHOOSER (dialog)); +107 if (! G_IS_FILE (file)) +108 g_warning ("TfeTextView: gtk_file_chooser_get_file returns non GFile object.\n"); +109 else { +110 save_file(file, tb, GTK_WINDOW (win)); +111 if (G_IS_FILE (tv->file)) +112 g_object_unref (tv->file); +113 tv->file = file; +114 gtk_text_buffer_set_modified (tb, FALSE); +115 g_signal_emit (tv, tfe_text_view_signals[CHANGE_FILE], 0); +116 } +117 } +118 } +119 +120 void +121 tfe_text_view_save (TfeTextView *tv) { +122 g_return_if_fail (TFE_IS_TEXT_VIEW (tv)); +123 +124 GtkTextBuffer *tb = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv)); +125 GtkWidget *win = gtk_widget_get_ancestor (GTK_WIDGET (tv), GTK_TYPE_WINDOW); +126 +127 if (! gtk_text_buffer_get_modified (tb)) +128 return; /* no need to save it */ +129 else if (tv->file == NULL) +130 tfe_text_view_saveas (tv); +131 else if (! G_IS_FILE (tv->file)) +132 g_error ("TfeTextView: The pointer in this object isn't NULL nor GFile object.\n"); +133 else { +134 if (save_file (tv->file, tb, GTK_WINDOW (win))) +135 gtk_text_buffer_set_modified (tb, FALSE); 136 } -137 gtk_window_destroy (GTK_WINDOW (dialog)); -138 } -139 -140 void -141 tfe_text_view_save (TfeTextView *tv) { -142 g_return_if_fail (TFE_IS_TEXT_VIEW (tv)); -143 -144 GtkTextBuffer *tb = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv)); -145 GtkTextIter start_iter; -146 GtkTextIter end_iter; -147 gchar *contents; -148 GtkWidget *message_dialog; -149 GtkWidget *win = gtk_widget_get_ancestor (GTK_WIDGET (tv), GTK_TYPE_WINDOW); -150 GError *err = NULL; -151 -152 if (! gtk_text_buffer_get_modified (tb)) -153 return; /* no need to save it */ -154 else if (tv->file == NULL) -155 tfe_text_view_saveas (tv); -156 else { -157 gtk_text_buffer_get_bounds (tb, &start_iter, &end_iter); -158 contents = gtk_text_buffer_get_text (tb, &start_iter, &end_iter, FALSE); -159 if (g_file_replace_contents (tv->file, contents, strlen (contents), NULL, TRUE, G_FILE_CREATE_NONE, NULL, NULL, &err)) -160 gtk_text_buffer_set_modified (tb, FALSE); -161 else { -162 /* It is possible that tv->file is broken or you don't have permission to write. */ -163 /* It is a good idea to set tv->file to NULL. */ -164 if (G_IS_FILE (tv->file)) -165 g_object_unref (tv->file); -166 tv->file =NULL; -167 g_signal_emit (tv, tfe_text_view_signals[CHANGE_FILE], 0); -168 message_dialog = gtk_message_dialog_new (GTK_WINDOW (win), GTK_DIALOG_MODAL, -169 GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, -170 "%s.\n", err->message); -171 g_signal_connect (message_dialog, "response", G_CALLBACK (gtk_window_destroy), NULL); -172 gtk_widget_show (message_dialog); -173 g_error_free (err); -174 } -175 } -176 } -177 -178 void -179 tfe_text_view_saveas (TfeTextView *tv) { -180 g_return_if_fail (TFE_IS_TEXT_VIEW (tv)); -181 -182 GtkWidget *dialog; -183 GtkWidget *win = gtk_widget_get_ancestor (GTK_WIDGET (tv), GTK_TYPE_WINDOW); -184 -185 dialog = gtk_file_chooser_dialog_new ("Save file", GTK_WINDOW (win), GTK_FILE_CHOOSER_ACTION_SAVE, -186 "Cancel", GTK_RESPONSE_CANCEL, -187 "Save", GTK_RESPONSE_ACCEPT, -188 NULL); -189 g_signal_connect (dialog, "response", G_CALLBACK (saveas_dialog_response), tv); -190 gtk_widget_show (dialog); -191 } -192 -193 GtkWidget * -194 tfe_text_view_new_with_file (GFile *file) { -195 g_return_val_if_fail (G_IS_FILE (file), NULL); -196 -197 GtkWidget *tv; -198 GtkTextBuffer *tb; -199 char *contents; -200 gsize length; -201 -202 if (! g_file_load_contents (file, NULL, &contents, &length, NULL, NULL)) /* read error */ -203 return NULL; -204 -205 tv = tfe_text_view_new(); -206 tb = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv)); -207 gtk_text_buffer_set_text (tb, contents, length); -208 g_free (contents); -209 TFE_TEXT_VIEW (tv)->file = g_file_dup (file); -210 return tv; -211 } -212 -213 GtkWidget * -214 tfe_text_view_new (void) { -215 return GTK_WIDGET (g_object_new (TFE_TYPE_TEXT_VIEW, NULL)); -216 } +137 } +138 +139 void +140 tfe_text_view_saveas (TfeTextView *tv) { +141 g_return_if_fail (TFE_IS_TEXT_VIEW (tv)); +142 +143 GtkWidget *dialog; +144 GtkWidget *win = gtk_widget_get_ancestor (GTK_WIDGET (tv), GTK_TYPE_WINDOW); +145 +146 dialog = gtk_file_chooser_dialog_new ("Save file", GTK_WINDOW (win), GTK_FILE_CHOOSER_ACTION_SAVE, +147 "Cancel", GTK_RESPONSE_CANCEL, +148 "Save", GTK_RESPONSE_ACCEPT, +149 NULL); +150 g_signal_connect (dialog, "response", G_CALLBACK (saveas_dialog_response), tv); +151 gtk_widget_show (dialog); +152 } +153 +154 GtkWidget * +155 tfe_text_view_new_with_file (GFile *file) { +156 g_return_val_if_fail (G_IS_FILE (file), NULL); +157 +158 GtkWidget *tv; +159 GtkTextBuffer *tb; +160 char *contents; +161 gsize length; +162 +163 if (! g_file_load_contents (file, NULL, &contents, &length, NULL, NULL)) /* read error */ +164 return NULL; +165 +166 tv = tfe_text_view_new(); +167 tb = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv)); +168 gtk_text_buffer_set_text (tb, contents, length); +169 g_free (contents); +170 TFE_TEXT_VIEW (tv)->file = g_file_dup (file); +171 return tv; +172 } +173 +174 static void +175 open_dialog_response(GtkWidget *dialog, gint response, TfeTextView *tv) { +176 GtkTextBuffer *tb = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv)); +177 GFile *file; +178 char *contents; +179 gsize length; +180 GtkWidget *message_dialog; +181 GError *err = NULL; +182 +183 if (response != GTK_RESPONSE_ACCEPT) +184 g_signal_emit (tv, tfe_text_view_signals[OPEN_RESPONSE], 0, TFE_OPEN_RESPONSE_CANCEL); +185 else if (! G_IS_FILE (file = gtk_file_chooser_get_file (GTK_FILE_CHOOSER (dialog)))) { +186 g_warning ("TfeTextView: gtk_file_chooser_get_file returns non GFile object.\n"); +187 g_signal_emit (tv, tfe_text_view_signals[OPEN_RESPONSE], 0, TFE_OPEN_RESPONSE_ERROR); +188 } else if (! g_file_load_contents (file, NULL, &contents, &length, NULL, &err)) { /* read error */ +189 if (G_IS_FILE (file)) +190 g_object_unref (file); +191 message_dialog = gtk_message_dialog_new (GTK_WINDOW (dialog), GTK_DIALOG_MODAL, +192 GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, +193 "%s.\n", err->message); +194 g_signal_connect (message_dialog, "response", G_CALLBACK (gtk_window_destroy), NULL); +195 gtk_widget_show (message_dialog); +196 g_error_free (err); +197 g_signal_emit (tv, tfe_text_view_signals[OPEN_RESPONSE], 0, TFE_OPEN_RESPONSE_ERROR); +198 } else { +199 gtk_text_buffer_set_text (tb, contents, length); +200 g_free (contents); +201 if (G_IS_FILE (tv->file)) +202 g_object_unref (tv->file); +203 tv->file = file; +204 gtk_text_buffer_set_modified (tb, FALSE); +205 g_signal_emit (tv, tfe_text_view_signals[OPEN_RESPONSE], 0, TFE_OPEN_RESPONSE_SUCCESS); +206 g_signal_emit (tv, tfe_text_view_signals[CHANGE_FILE], 0); +207 } +208 gtk_window_destroy (GTK_WINDOW (dialog)); +209 } +210 +211 void +212 tfe_text_view_open (TfeTextView *tv, GtkWidget *win) { +213 g_return_if_fail (TFE_IS_TEXT_VIEW (tv)); +214 g_return_if_fail (GTK_IS_WINDOW (win)); +215 +216 GtkWidget *dialog; 217 +218 dialog = gtk_file_chooser_dialog_new ("Open file", GTK_WINDOW (win), GTK_FILE_CHOOSER_ACTION_OPEN, +219 "Cancel", GTK_RESPONSE_CANCEL, +220 "Open", GTK_RESPONSE_ACCEPT, +221 NULL); +222 g_signal_connect (dialog, "response", G_CALLBACK (open_dialog_response), tv); +223 gtk_widget_show (dialog); +224 } +225 +226 GtkWidget * +227 tfe_text_view_new (void) { +228 return GTK_WIDGET (g_object_new (TFE_TYPE_TEXT_VIEW, NULL)); +229 } +230 ~~~ ## Total number of lines, words and characters @@ -663,15 +679,15 @@ It is a good practice for you to add more features. ~~~ $ LANG=C wc tfe5/meson.build tfe5/tfeapplication.c tfe5/tfe.gresource.xml tfe5/tfe.h tfe5/tfenotebook.c tfe5/tfenotebook.h tfetextview/tfetextview.c tfetextview/tfetextview.h tfe5/tfe.ui 10 17 294 tfe5/meson.build - 117 348 3576 tfe5/tfeapplication.c + 97 301 3159 tfe5/tfeapplication.c 6 9 153 tfe5/tfe.gresource.xml 4 6 87 tfe5/tfe.h - 117 325 3064 tfe5/tfenotebook.c - 12 17 196 tfe5/tfenotebook.h - 217 637 7725 tfetextview/tfetextview.c + 140 373 3580 tfe5/tfenotebook.c + 15 21 241 tfe5/tfenotebook.h + 230 686 8144 tfetextview/tfetextview.c 35 60 701 tfetextview/tfetextview.h - 64 105 2266 tfe5/tfe.ui - 582 1524 18062 total + 61 100 2073 tfe5/tfe.ui + 598 1573 18432 total ~~~ diff --git a/gfm/sec16.md b/gfm/sec16.md index 994e512..dc88936 100644 --- a/gfm/sec16.md +++ b/gfm/sec16.md @@ -58,7 +58,7 @@ Some menu items have a link to another GMenu. There are two types of links, submenu and section. GMenuItem can be inserted, appended or prepended to GMenu. -When it is inserted, all of the attribute and link values of the item are copied and used to form a new item within the menu. +When it is inserted, all of the attributes and link values of the item are copied and used to form a new item within the menu. The GMenuItem itself is not really inserted. Therefore, after the insertion, GMenuItem is useless and it should be freed. The same goes for appending or prepending. diff --git a/gfm/sec19.md b/gfm/sec19.md index d41ff82..3aefe7f 100644 --- a/gfm/sec19.md +++ b/gfm/sec19.md @@ -1,201 +1,1237 @@ Up: [Readme.md](../Readme.md), Prev: [Section 18](sec18.md), Next: [Section 20](sec20.md) -# GtkDrawingArea and Cairo +# Upgrade text file editor -If you want to draw dynamically, like an image window of gimp graphics editor, GtkDrawingArea widget is the most suitable widget. -You can draw or redraw an image in this widget freely. -It is called custom drawing. +Traditional menu structure is fine. +However, Buttons or menu items we often use are not so many. +Some mightn't be clicked at all. +Therefore, it's a good idea to put some frequently used buttons on the toolbar and put the rest of the less frequently used operations into the menu. +Such menu are often connected to GtkMenuButton. -GtkDrawingArea provides a cairo context so users can draw images by cairo functions. -In this section, I will explain: +We will restructure tfe text file editor in this section. +It will be more practical. +The buttons are changed to: -1. Cairo, but briefly. -2. GtkDrawingArea with very simple example. +- Put open, save and close buttons to the toolbar. +In addition, GtkMenuButton is added to the toolbar. +This button shows a popup menu when clicked on. +Here, popup means widely, including pull-down menu. +- Put new, save as, preference and quit items to the menu under the menu button. -## Cairo +## Signal elements in ui files -Cairo is a two dimensional graphics library. -First, you need to know surface, source, mask, destination, cairo context and transformation. +The four buttons are included in the ui file `tfe.ui`. +The difference from prior sections is signal tag. +The following is extracted from `tfe.ui` and it describes the open button. -- Surface represents an image. -It is like a canvas. -We can draw shapes and images with different colors on surfaces. -- Source pattern, or simply source, is a kind of paint, which will be transferred to destination surface by cairo functions. -- Mask is image mask used in the transference. -- Destination is a target surface. -- Cairo context manages the transference from source to destination through mask with its functions. -For example, `cairo_stroke` is a function to draw a path to the destination by the transference. -- Transformation is applied before the transfer completes. -The transformation is called affine, which is a mathematics terminology, and represented by matrix multiplication and vector addition. -Scaling, rotation, reflection, shearing and translation are examples of affine transformation. -In this section, we don't use it. -That means we only use identity transformation. -Therefore, the coordinate in source and mask is the same as the coordinate in destination. - -![Stroke a rectangle](../image/cairo.png) - -The instruction is as follows: - -1. Create a surface. -This will be a destination. -2. Create a cairo context with the surface and the surface will be the destination of the context. -3. Create a source pattern within the context. -4. Create paths, which are lines, rectangles, arcs, texts or more complicated shapes, to generate a mask. -5. Use drawing operator such as `cairo_stroke` to transfer the paint in the source to the destination. -6. Save the destination surface to a file if necessary. - -Here's a simple example code that draws a small square and save it as a png file. - -~~~C - 1 #include - 2 - 3 int - 4 main (int argc, char **argv) - 5 { - 6 cairo_surface_t *surface; - 7 cairo_t *cr; - 8 int width = 100; - 9 int height = 100; -10 -11 /* Generate surface and cairo */ -12 surface = cairo_image_surface_create (CAIRO_FORMAT_RGB24, width, height); -13 cr = cairo_create (surface); -14 -15 /* Drawing starts here. */ -16 /* Paint the background white */ -17 cairo_set_source_rgb (cr, 1.0, 1.0, 1.0); -18 cairo_paint (cr); -19 /* Draw a black rectangle */ -20 cairo_set_source_rgb (cr, 0.0, 0.0, 0.0); -21 cairo_set_line_width (cr, 2.0); -22 cairo_rectangle (cr, width/2.0 - 20.0, height/2.0 - 20.0, 40.0, 40.0); -23 cairo_stroke (cr); -24 -25 /* Write the surface to a png file and clean up cairo and surface. */ -26 cairo_surface_write_to_png (surface, "rectangle.png"); -27 cairo_destroy (cr); -28 cairo_surface_destroy (surface); -29 -30 return 0; -31 } +~~~xml + + Open + + ~~~ -- 1: Includes the header file of cairo. -- 12: `cairo_image_surface_create` creates an image surface. -`CAIRO_FORMAT_RGB24` is a constant which means that each pixel has red, green and blue data. -Each data has 8 bit quantity. -Modern displays have this type of color depth. -Width and height are pixels and given as integers. -- 13: Creates cairo context. -The surface given as an argument will be the destination of the context. -- 17: `cairo_set_source_rgb` creates a source pattern, which is a solid white paint. -The second to fourth argument is red, green and blue color depth respectively. -Their type is float and the values are between zero and one. -(0,0,0) is black and (1,1,1) is white. -- 18: `cairo_paint` copies everywhere in the source to destination. -The destination is filled with white pixels by this command. -- 20: Sets the source color to black. -- 21: `cairo_set_line_width` set the width of lines. -In this case, the line width is set to two pixels. -(It is because the transformation is identity. -If the transformation isn't identity, for example scaling with the factor three, the actual width in destination will be six (2x3=6) pixels.) -- 22: Draws a rectangle (square). -The top-left coordinate is (width/2.0-20.0, height/2.0-20.0) and the width and height have the same length 40.0. -- 23: `cairo_stroke` transfer the source to destination through the rectangle in mask. -- 26: Outputs the image to a png file `rectangle.png`. -- 27: Destroys the context. At the same time the source is destroyed. -- 28: Destroys the destination surface. - -To compile this, type the following. - - $ gcc `pkg-config --cflags cairo` cairo.c `pkg-config --libs cairo` - -![rectangle.png](../src/misc/rectangle.png) - -There are lots of documentations in [Cairo's website](https://www.cairographics.org/). -If you aren't familiar with cairo, it is strongly recommended to read the [tutorial](https://www.cairographics.org/tutorial/) in the website. - -## GtkDrawingArea - -The following is a very simple example. +Signal tag specifies the name of the signal, handler and user_data object. +They are the value of name, handler and object attribute. +Swapped attribute has the same meaning as `g_signal_connect_swapped` function. +So, the signal tag above works the same as the function below. ~~~C - 1 #include - 2 - 3 static void - 4 draw_function (GtkDrawingArea *area, cairo_t *cr, int width, int height, gpointer user_data) { - 5 cairo_set_source_rgb (cr, 1.0, 1.0, 1.0); /* whilte */ - 6 cairo_paint (cr); - 7 cairo_set_line_width (cr, 2.0); - 8 cairo_set_source_rgb (cr, 0.0, 0.0, 0.0); /* black */ - 9 cairo_rectangle (cr, width/2.0 - 20.0, height/2.0 - 20.0, 40.0, 40.0); -10 cairo_stroke (cr); +g_signal_connect_swapped (btno, "clicked", G_CALLBACK (open_cb), nb); +~~~ + +You need to compile the source file with "-WI, --export-dynamic" options. +You can achieve this by adding "export_dynamic: true" argument to executable function in `meson.build`. +And remove static class from the handler. + +~~~C +void +open_cb (GtkNotebook *nb) { + notebook_page_open (nb); +} +~~~ +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. + +## Menu and GkMenuButton + +Menus are described in `menu.ui` file. + +~~~xml + 1 + 2 + 3 + 4
+ 5 + 6 New + 7 win.new + 8 + 9 +10 Save As… +11 win.saveas +12 +13
+14
+15 +16 Preference +17 win.pref +18 +19
+20
+21 +22 Quit +23 win.close-all +24 +25
+26
+27
+~~~ + +There are four items, "New", "Saveas", "Preference" and "Quit". + +- New menu creates a new empty page. +- Saveas menu saves the current page as a new filename. +- Preference menu sets preference items. +This version of `tfe` has only font preference. +- Quit menu quits the application. + +These four menus are not used so often. +That's why they are put to the menu behind the menu button. + +The menus and the menu button are connected with `gtk_menu_button_set_menu_model` function. +The variable `btnm` below points a GtkMenuButton object. + +~~~C + 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 + +Menus are connected to actions. +Actions are defined with an array and `g_action_map_add_action_entries` function. + +~~~C + 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", quit_activated, NULL, NULL, NULL } + }; + g_action_map_add_action_entries (G_ACTION_MAP (win), win_entries, G_N_ELEMENTS (win_entries), nb); +~~~ + +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 thy necessary? +These actions are defined because of accelerators. + +Accelerators are a kind of short cut key function. +They are defined with arrays and `gtk_application_set_accels_for_action` function. + +~~~C + struct { + const char *action; + const char *accels[2]; + } action_accels[] = { + { "win.open", { "o", NULL } }, + { "win.save", { "s", NULL } }, + { "win.close", { "w", NULL } }, + { "win.new", { "n", NULL } }, + { "win.saveas", { "s", NULL } }, + { "win.close-all", { "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); +~~~ + +This code is a bit complicated. +The array `action-accels[]` is an array of structures. +The structure is: + +~~~C + 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, + +~~~C +{ "win.open", { "o", NULL } }, +~~~ + +This is the first element of the array `action_accels`. + +- The member `action` is "win.open". This specifies the action "open" belongs to the window object. +- The member `accels` is an array of two strings, "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 "o", "F2", "minus" and so on. +If you want to use symbol key like "-", use "minus" instead. +Such symbol to lower case name relation is specified in [`gdkkeysyms.h`](https://gitlab.gnome.org/GNOME/gtk/-/blob/master/gdk/gdkkeysyms.h) in the gtk4 source code. + +## Saveas handler + +TfeTextView has already had a saveas function. +So, only we need to write is the wrapper function in `tfenotebook.c`. + +~~~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 static void -14 on_activate (GApplication *app, gpointer user_data) { -15 GtkWidget *win = gtk_application_window_new (GTK_APPLICATION (app)); -16 GtkWidget *area = gtk_drawing_area_new (); -17 -18 gtk_window_set_title (GTK_WINDOW (win), "da1"); -19 /* Set initial size of width and height */ -20 gtk_drawing_area_set_content_width (GTK_DRAWING_AREA (area), 100); -21 gtk_drawing_area_set_content_height (GTK_DRAWING_AREA (area), 100); -22 gtk_drawing_area_set_draw_func (GTK_DRAWING_AREA (area), draw_function, NULL, NULL); -23 gtk_window_set_child (GTK_WINDOW (win), area); -24 -25 gtk_widget_show (win); -26 } -27 -28 int -29 main (int argc, char **argv) { -30 GtkApplication *app; -31 int stat; -32 -33 app = gtk_application_new ("com.github.ToshioCP.da1", G_APPLICATION_FLAGS_NONE); -34 g_signal_connect (app, "activate", G_CALLBACK (on_activate), NULL); -35 stat =g_application_run (G_APPLICATION (app), argc, argv); -36 g_object_unref (app); -37 return stat; -38 } -39 +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 } ~~~ -The function `main` is almost same as before. -The two functions `on_activate` and `draw_function` is important in this example. +The function `get_current_textview` is the same as before. +The function `notebook_page_saveas` simply calls `tfe_text_view_saveas`. -- 16: Generates a GtkDrawingArea object. -- 20,21: Sets the width and height of the contents of the GtkDrawingArea widget. -These width and height is the size of the destination surface of the cairo context provided by the widget. -- 22: Sets a drawing function of the widget. -GtkDrawingArea widget uses the function to draw the contents of itself whenever its necessary. -For example, when a user drag a mouse pointer and resize a top level window, GtkDrawingArea also changes the size. -Then, the whole window needs to be redrawn. +In `tfeapplication.c`, saveas handler just call `notebook_page_saveas`. -The drawing function has five parameters. +~~~C +1 static void +2 saveas_activated (GSimpleAction *action, GVariant *parameter, gpointer nb) { +3 notebook_page_saveas (GTK_NOTEBOOK (nb)); +4 } +~~~ - void drawing_function (GtkDrawingArea *drawing_area, cairo_t *cr, int width, int height, - gpointer user_data); +## Preference and alert dialog -The first parameter is the GtkDrawingArea widget which calls the drawing function. -However, you can't change any properties, for example `content-width` or `content-height`, in this function. -The second parameter is a cairo context given by the widget. -The destination surface of the context is connected to the contents of the widget. -What you draw to this surface will appear in the widget on the screen. -The third and fourth parameters are the size of the destination surface. +### Preference dialog -- 3-11: The drawing function. -- 4-5: Sets the source to be white and paint the destination white. -- 7: Sets the line width to be 2. -- 8: Sets the source to be black. -- 9: Adds a rectangle to the mask. -- 10: Draws the rectangle with black color to the destination. +Preference dialog xml definition is added to `tfe.ui`. -Compile and run it, then a window with a black rectangle (square) appears. -Try resizing the window. -The square always appears at the center of the window because the drawing function is invoked every moment the window is resized. +~~~xml + + Preferences + FALSE + TRUE + win + + + + + GTK_ORIENTATION_HORIZONTAL + 12 + 12 + 12 + 12 + 12 + + + Font: + 1 + + + + + + + + + + + +~~~ -![Square in the window](../image/da1.png) +- Preference dialog is an independent dialog. +It is not a descendant widget of the top GtkApplicationwindow `win`. +Therefore, no child tag of the dialog object. +- There are four properties of the dialog specified. +GtkDialog is a child object (not child widget) of GtkWindow, so it inherits all the properties from GtkWindow. +Title, resizable, modal and transient-for properties are inherited from GtkWindow. +Transient-for specifies a temporary parent window, which the dialog's location is based on. +- internal-child attribute is used in the child tag above.. +GtkDialog has a GtkBox child widget. +Its id is "content_area" in `gtkdialog.ui`, which is the ui file of GtkDialog in gtk4 source files. +This box is provided to users to add content widgets in it. +The tag `` is put at the top of the contents. +Then you need to specify an object tag and define its class as GtkBox and its id as content_area. +This object is defined in `gtkdialog.ui` but you need to define it again in the child tag. +- In the content area, defines GtkBox, GtkLabel and GtkFontButton. + +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 is accomplished by returning TRUE by the signal handler. + +~~~C +pref_close_cb (GtkDialog *pref, gpointer user_data) { + return TRUE; +} + +g_signal_connect (GTK_DIALOG (pref), "close-request", G_CALLBACK (pref_close_cb), NULL); +~~~ + +Generally, signal emission consists of five stages. + +1. Default handler is invoked if the signal's flag is `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`. +2. Signal handlers are invoked, unless it is connected by `g_signal_connect_after`. +3. Default handler is invoked if the signal's flag is `G_SIGNAL_RUN_LAST`. +4. Signal handlers are invoked, if it is connected by `g_signal_connect_after`. +5. Default handler is invoked if the signal's flag is `G_SIGNAL_RUN_CLEANUP`. + +In the case of "close-request" signal, the default handler's flag is `G_SIGNAL_RUN_LAST`. +The handler `pref_close_cb` is not connected by `g_signal_connect_after`. +So the number of stages are two. + +1. Signal handler `pref_close_cb` is invoked. +2. Default handler is invoked. + +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`. + +~~~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_hide (GTK_WIDGET (dialog)); + return TRUE; +} + +... ... + +static void +pref_activated (GSimpleAction *action, GVariant *parameter, gpointer nb) { + gtk_widget_show (GTK_WIDGET (pref)); +} + +... ... + +/* ----- quit application ----- */ +void +tfe_application_quit (GtkWindow *win) { + if (pref_close_request_handler_id > 0) + g_signal_handler_disconnect (pref, pref_close_request_handler_id); + if (alert_close_request_handler_id > 0) + g_signal_handler_disconnect (alert, alert_close_request_handler_id); + g_clear_object (&settings); + gtk_window_destroy (GTK_WINDOW (alert)); + gtk_window_destroy (GTK_WINDOW (pref)); + gtk_window_destroy (win); +} + +... ... + +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); + + ... ... +} +~~~ + +The function `tfe_application_quit` destroys top level windows and quits the application. +It first disconnects the handlers from the signal "close-request". + +### Alert dialog + +If a user closes a page which hasn't been saved, it is advisable to show an alert to confirm it. +Alert dialog is used in this application for such a situation. + +~~~xml + + Are you sure? + FALSE + TRUE + win + + + + + GTK_ORIENTATION_HORIZONTAL + 12 + 12 + 12 + 12 + 12 + + + dialog-warning + GTK_ICON_SIZE_LARGE + + + + + + + + + + + + + Cancel + + + + + Close + + + + btn_cancel + btn_accept + + + +~~~ + +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 icon named "dialog-warning" is something like this. + +![dialog-warning icon is like ...](../image/dialog_warning.png) + +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 by the program later. + +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. +Btn\_cancel button emits response signal with cancel response (GTK_RESPONSE_CANCEL) if it is clicked on. +Btn\_accept button 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. + +## Close and quit handlers + +If a user closes a page or quits the application without saving the contents, the application alerts. + +~~~C +static gboolean is_quit; + +... ... + +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 (close_btn_close, "Close"); + gtk_widget_show (GTK_WIDGET (alert)); + } +} + +... ... + +static void +close_activated (GSimpleAction *action, GVariant *parameter, gpointer nb) { + close_cb (GTK_NOTEBOOK (nb)); +} + +... ... + +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_hide (GTK_WIDGET (alert)); + if (response_id == GTK_RESPONSE_ACCEPT) { + if (is_quit) + tfe_application_quit (GTK_WINDOW (win)); + else + notebook_page_close (nb); + } +} + +static void +quit_activated (GSimpleAction *action, GVariant *parameter, gpointer nb) { + GtkWidget *win = gtk_widget_get_ancestor (GTK_WIDGET (nb), GTK_TYPE_WINDOW); + + is_quit = true; + if (has_saved_all (GTK_NOTEBOOK (nb))) + tfe_application_quit (GTK_WINDOW (win)); + else { + gtk_label_set_text (lb_alert, "Contents aren't saved yet.\nAre you sure to quit?"); + gtk_button_set_label (close_btn_close, "Quit"); + gtk_widget_show (GTK_WIDGET (alert)); + } +} + +static void +tfe_startup (GApplication *application) { + + ... ... + + alert = GTK_DIALOG (gtk_builder_get_object (build, "alert")); + alert_close_request_handler_id = g_signal_connect (GTK_DIALOG (alert), "close-request", G_CALLBACK (dialog_close_cb), NULL); + lb_alert = GTK_LABEL (gtk_builder_get_object (build, "lb_alert")); + btn_accept = GTK_BUTTON (gtk_builder_get_object (build, "btn_accept")); + + ... ... + +} +~~~ + +The static variable `is_quit` is true when user tries to quit the application and FAULT otherwise. +When user presses "Ctrl-w", `close_activated` handler is invoked. +It just calls `close_cb`. +When user clicks on the close button, `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 sets the text in the label and button, then shows the alert dialog. + +The response signal of the dialog is connected to the handler `alert_response_cb`. +It hides the dialog first. +Then check the response\_id. +If it is `GTK_RESPONSE_ACCEPT`, which means user clicked on the close button, then closes the current page. + +When user press "Ctrl-q" or clicked on the quit menu, then `quit_activated` handler is invoked. +The handler sets `is_quit` to true. +The function `has_saved_all` returns true if all the pages have been saved. +If it is true, it calls `tfe_application_quit` to quit the application. +Otherwise, it sets the text in the label and button, then shows the alert dialog. + +Now `alert_response_cb` works similar but it calls `tfe_application_quit`instead of `notebook_page_close`. + +The static variables `alert`, `lb_alert` and `btn_accept` are set in the startup handler. +And the signal "close-request" and `dialog_close_cb` handler are connected. + +~~~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` function. +- 10: The 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 the buffer is generated. +It is reset to false when it is replaced with a new contents from a file or it is saved to a file. +- 11-13: This function returns true if the contents of the current page has been saved and no modification has been made. +If it returns false, then the user tries to close the current page without saving the modified contents. +- 16-33: `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. + +## Notebook page tab + +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. + +~~~C +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); +} +~~~ + +When a page is built, connect "change-file" and "modified-changed" signals to `file_changed_cb` and `modified_changed_cb` handlers respectively. + +~~~C + 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 gtk_notebook_set_tab_label (GTK_NOTEBOOK (nb), scr, label); +38 } else +39 file_changed_cb (tv); +40 } +~~~ + +- 1-20: `file_changed_cb` handler. +- 9-10: If the signal emits during the page is being built, it is possible that `tv` isn't a descendant of `nb`. +That is, there's no page corresponds to `tv`. +Then, it isn't necessary to change the name of the tab because no tab exists. +- 13-15: If `file` is GFile, then it gets the filename and unrefs `file`. +- 16-17: Otherwise, `file` is probably NULL and it assigns "Untitled" related name to `filename` +- 18-19: Generates GtkLabel with `filename` and sets the tab of the page with the GtkLabel. +- 22-40: `modified_changed_cb` handler. +- 31-32: If `tv` isn't a descendant of `nb`, then nothing needs to be done. +- 33-35: If the content is modified, then it gets the text of the tab and adds asterisk at the beginning of the text. +- 36-37: Sets the tab with the asterisk prepended text. +- 38-39: Otherwise the modified bit is off. +It is because content is saved. +It calls `file_changed_cb` and resets the filename, that means it leaves out the asterisk. + +## Font + +### GtkFontButton and GtkFontChooser + +The GtkFontButton is a button which displays the current font. +It opens a font chooser dialog if a user clocked 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 inserted to the preference dialog in the initialization process. + +~~~C +static void +font_set_cb (GtkFontButton *fontbtn, gpointer user_data) { + GtkWindow *win = GTK_WINDOW (user_data); + PangoFontDescription *pango_font_desc; + + pango_font_desc = gtk_font_chooser_get_font_desc (GTK_FONT_CHOOSER (fontbtn)); + set_font_for_display_with_pango_font_desc (win, pango_font_desc); +} + +static void +tfe_startup (GApplication *application) { + + ... ... + + fontbtn = GTK_FONT_BUTTON (gtk_builder_get_object (build, "fontbtn")); + g_signal_connect (fontbtn, "font-set", G_CALLBACK (font_set_cb), win); + + ... ... + +} +~~~ + +In the startup handler, set the variable `fontbtn` to point the GtkFontButton object. +Then connect the "font-set" signal to `font_set_cb` handler. +The signal "font-set" is emitted when the user selects a font. + +GtkFontChooser is an interface implemented by GtkFontButton. +The function `gtk_font_chooser_get_font_desc` gets the PangoFontDescription of the currently-selected font. + +Another function `gtk_font_chooser_get_font' returns a font name which includes family, style, weight and size. +I thought it might be applied to tfe editor. +The font name can be used to the `font` property of GtkTextTag as it is. +But it can't be used to the CSS without converting the string to fit. +CSS is appropriate to change the font of entire text in all the buffers. +I think GtkTextTag is less appropriate. +If you know a good solution, please post it to [issue](https://github.com/ToshioCP/Gtk4-tutorial/issues) and let me know. + +It takes many codes to set the CSS from the PangoFontDescription so the task is left to the function `set_font_for_display_with_pango_font_desc`. + +### CSS and Pango + +A new file `css.c` is made for functions related to CSS. + +~~~C + 1 #include "tfe.h" + 2 + 3 void + 4 set_css_for_display (GtkWindow *win, char *css) { + 5 GdkDisplay *display; + 6 + 7 display = gtk_widget_get_display (GTK_WIDGET (win)); + 8 GtkCssProvider *provider = gtk_css_provider_new (); + 9 gtk_css_provider_load_from_data (provider, css, -1); +10 gtk_style_context_add_provider_for_display (display, GTK_STYLE_PROVIDER (provider), GTK_STYLE_PROVIDER_PRIORITY_USER); +11 } +12 +13 void +14 set_font_for_display (GtkWindow *win, const char *fontfamily, const char *fontstyle, const char *fontweight, int fontsize) { +15 char *textview_css; +16 +17 textview_css = g_strdup_printf ("textview {padding: 10px; font-family: \"%s\"; font-style: %s; font-weight: %s; font-size: %dpt;}", +18 fontfamily, fontstyle, fontweight, fontsize); +19 set_css_for_display (win, textview_css); +20 } +21 +22 void +23 set_font_for_display_with_pango_font_desc (GtkWindow *win, PangoFontDescription *pango_font_desc) { +24 int pango_style; +25 int pango_weight; +26 const char *family; +27 const char *style; +28 const char *weight; +29 int fontsize; +30 +31 family = pango_font_description_get_family (pango_font_desc); +32 pango_style = pango_font_description_get_style (pango_font_desc); +33 switch (pango_style) { +34 case PANGO_STYLE_NORMAL: +35 style = "normal"; +36 break; +37 case PANGO_STYLE_ITALIC: +38 style = "italic"; +39 break; +40 case PANGO_STYLE_OBLIQUE: +41 style = "oblique"; +42 break; +43 default: +44 style = "normal"; +45 break; +46 } +47 pango_weight = pango_font_description_get_weight (pango_font_desc); +48 switch (pango_weight) { +49 case PANGO_WEIGHT_THIN: +50 weight = "100"; +51 break; +52 case PANGO_WEIGHT_ULTRALIGHT: +53 weight = "200"; +54 break; +55 case PANGO_WEIGHT_LIGHT: +56 weight = "300"; +57 break; +58 case PANGO_WEIGHT_SEMILIGHT: +59 weight = "350"; +60 break; +61 case PANGO_WEIGHT_BOOK: +62 weight = "380"; +63 break; +64 case PANGO_WEIGHT_NORMAL: +65 weight = "400"; /* or "normal" */ +66 break; +67 case PANGO_WEIGHT_MEDIUM: +68 weight = "500"; +69 break; +70 case PANGO_WEIGHT_SEMIBOLD: +71 weight = "600"; +72 break; +73 case PANGO_WEIGHT_BOLD: +74 weight = "700"; /* or "bold" */ +75 break; +76 case PANGO_WEIGHT_ULTRABOLD: +77 weight = "800"; +78 break; +79 case PANGO_WEIGHT_HEAVY: +80 weight = "900"; +81 break; +82 case PANGO_WEIGHT_ULTRAHEAVY: +83 weight = "900"; /* In PangoWeight definition, the weight is 1000. But CSS allows the weight below 900. */ +84 break; +85 default: +86 weight = "normal"; +87 break; +88 } +89 fontsize = pango_font_description_get_size (pango_font_desc) / PANGO_SCALE; +90 set_font_for_display (win, family, style, weight, fontsize); +91 } +~~~ + +- 3-11: `set_css_for_display`. +This function sets CSS for GdkDisplay. +The content of the function is the same as the part of startup handler in the previous version of `tfeapplication.c`. +- 13-20: `set_font_for_display`. +This function sets CSS with font-family, font-style, font-weight and font-size. + - font-family is a name of font. For example, sans-serif, monospace, Helvetica and "Noto Sans" are font-family. +It is recommended to quote font family names that contain white space, digits, or punctuation characters other than hyphens. + - font-style is one of normal, italic and oblique. + - font-weight specifies the thickness of a font. +It is normal or bold. +It can be specified with a number between 100 and 900. +Normal is the same as 400. +Bold is 700. + - font-size specifies the size of a font. +Small, medium, large and 12pt are font-size. +- 17: Makes CSS text. +The function `g_strdup_printf` generates a new string with printf-like formatting. +- 22-91: `set_font_for_display_with_pango_font_desc`. +This function takes out font-family, font-style, font-weight and font-size from the PangoFontDescription object. +- 31: Gets the font-family of `pango_font_desc`. +- 32-46: Gets the font-style of `pango_font_desc`. +The functions `pango_font_description_get_style` returns a enumerated value. +- 47-88: Gets the font-weight of `pango_font_desc`. +The function `pango_font_description_get_weight` returns a enumerated value. +They corresponds to the numbers from 100 to 900. +- 89: Gets the font-size of `pango_font_desc`. +The function `pango_font_description_get_size` returns the size of a font. +The unit of this size is (1/PANGO_SCALE)pt. +If the font size is 10pt, the function returns 10*PANGO_SCALE. +PANGO_SCALE is defined as 1024. +Therefore, 10*PANGO_SCALE is 10240. +-90: calls `set_font_for_display` to set CSS for the GdkDisplay. + +## GSettings + +We want to maintain the font data after the application quits. +There are some ways to implement it. + +- Make a configuration file. +For example, a text file "~/.config/tfe/font.cfg" keeps font information. +- Use GSettings object. +The basic idea of GSettings are similar to configuration file. +Configuration information data is put into a database file. + +The coding with GSettings object is simple and easy. +However, it is a bit hard to understand the concept. +This subsection describes the concept first and then how to program. + +### GSettings schema + +GSettings schema describes a set of keys, value types and some other information. +GSettings object uses this schema and it writes/reads the value of a key to/from the right place in the database. + +- A schema has an id. +The id must be unique. +We often use the same string as application id, but schema id and application id are different. +You can use different name from application id. +Schema id is a string delimited by periods. +For example, "com.github.ToshioCP.tfe" is a correct schema id. +- A schema usually has a path. +The path is a location in the database. +Each key is stored under the path. +For example, if a key `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. +- GSettings save information as key-value style. +Key is a string begins with lower case characters followed by lower case, digit or dash `-` and ends with lower case or digit. +No consecutive dashes are allowed. +Values can be any type. +GSettings stores values as GVariant type, which may contain integer, double, boolean, string or complex types like an array. +The type of values needs to be defined in the schema. +- A default value needs to be set for each key. +- A summery and description can be set for each key. + +Schemas are described in an XML format. +For example, + +~~~xml + 1 + 2 + 3 + 4 + 5 'Monospace 12' + 6 Font + 7 The font to be used for textview. + 8 + 9 +10 +~~~ + +- 4: The type attribute is "s". +It is [GVariant format string](https://developer.gnome.org/glib/stable/gvariant-format-strings.html). +Other common types are: + - "b" gboolean + - "i" gint32. + - "d" double +Further information is in the website `GVariant format string` above. + +### gsettings + +First, let's try `gsetting` 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 +~~~ + +![gnome-calculator basic mode](../image/gnome_calculator_basic) + + +Then, change the mode to advanced and quit. + +![gnome-calculator advanced mode](../image/gnome_calculator_advanced) + +Run gsettongs and check whether the value of `button-mode` changes. + +~~~ +$ gsettings list-recursively org.gnome.calculator + +... ... + +org.gnome.calculator button-mode 'advanced' + +... ... + +~~~ + +Now we know that Gnome Calculator used gsettings and it sets `button-mode` key to "advanced" which is the current mode. +The value remains even the calculator quits. +So when the calculator is run again, it will appear as an advanced mode calculator. + +### glib-compile-schemas + +GSettings schemas are specified with 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 + 1 + 2 + 3 + 4 + 5 'Monospace 12' + 6 Font + 7 The font to be used for textview. + 8 + 9 +10 +~~~ + +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. + +- 2: The top level element is ``. +- 3: schema tag has `path` and `id` attributes. +A path determines where the settings are stored in the conceptual global tree of settings. +An id identifies the schema. +- 4: Key tag has two attributes. +Name is the name of the key. +Type us the type of the key' value and specified with [GVariant format string](https://developer.gnome.org/glib/stable/gvariant-format-strings.html). +- 5: default value of the key `font` is `Monospace 12`. +- 6: Summery and description elements describes the key. + +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` so that GSettings objet can find `gschemas.compiled`. + +~~~ +$ GSETTINGS_SCHEMA_DIR=(the directory gschemas.compiled is located):$GSETTINGS_SCHEMA_DIR (your application name) +~~~ + +This is because GSettings object searches `GSETTINGS_SCHEMA_DIR` for `gschemas.compiled`. + +GSettings object looks for this file by the following process. + +- It searches `glib-2.0/schemas` subdirectories of all the directories specified in `XDG_DATA_DIRS`. +Most common directory is `/usr/share/glib-2.0/schemas`. +- If `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. + +1. Make `.gschema.xml` file. +2. Copy it to one of the directories above. For example, `/usr/share/glib-2.0/schemas`. +3. Run `glib-compile-schemas' on the directory above. + +### Meson.build + +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. + +~~~meson +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 +~~~ + +### GSettings object and g_setting_bind + +Write gsettings related codes to `tfeapplication.c'. + +~~~C +... ... +static GSettings *settings; +... ... + +void +tfe_application_quit (GtkWindow *win) { + ... ... + g_clear_object (&settings); + ... ... +} + +static void +tfe_startup (GApplication *application) { + ... ... + settings = g_settings_new ("com.github.ToshioCP.tfe"); + g_settings_bind (settings, "font", fontbtn, "font", G_SETTINGS_BIND_DEFAULT); + ... ... +} +~~~ + +Static variable `settings` keeps a pointer to GSettings instance. +Before application quits, the application releases the GSettings instance. +The function `g_clear_object` is used. + +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. + +You need to make an effort to understand GSettings concept, but coding is very simple. +Just generate a GSettings object and bind it to a property of an object. + +## Installation + +It is a good idea to install your application in `$HOME/local/bin` directory if you have installed gtk4 under the instruction in Section 2. +Then you need to put `--prefix=$HOME/local` option to meson. + +~~~ +$ meson --prefix=$HOME/local _build +~~~ + +Add install option and set it true in executable function. + +~~~meson +executable('tfe', sourcefiles, resources, dependencies: gtkdep, export_dynamic: true, install: true) +~~~ + +Then, you can install your application by: + +~~~ +$ ninja -C _build install +~~~ + +However, you need to do one more thing. +Copy your XML file to `$HOME/local/share/glib-2.0/schemas/`, which is specified in `GSETTINGS_SCHEMA_DIR` environment variable, +and run `glib-compile-schemas` on that directory. + +~~~meson +schema_dir = get_option('prefix') / get_option('datadir') / 'glib-2.0/schemas/' +install_data('com.github.ToshioCP.tfe.gschema.xml', install_dir: schema_dir) +~~~ + +- get_option: This function returns the value of build options. +The default value of the option 'prefix' is "/usr/local", but it is "$HOME/local" because we have run meson with prefix option. +The default value of the option 'datadir' is "share". +The operator '/' connects the strings with '/' separator. +So, `$HOME/local/share/glib-2.0/schemas` is assigned to the varable `schema_dir`. +- install_data: This function installs the data to the install directory. + +Meson can runs a post compile script. + +~~~meson +meson.add_install_script('glib-compile-schemas', schema_dir) +~~~ + +This method runs 'glib-compile-schemas' with an argument `schema_dir`. + +~~~meson + 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', 'css.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 meson.add_install_script('glib-compile-schemas', schema_dir) +16 +~~~ + +Source files of `tfe` is under [src/tfe6](../src/tfe6) directory. +Copy them to your temporary directory and try to compile and install. + +~~~ +$ meson --prefix=$HOME/local _build +$ ninja -C _build +$ GSETTINGS_SCHEMA_DIR=_build:$GSETTINGS_SCHEMA_DIR _build/tfe +$ ninja -C _build install +$ tfe +$ ls $HOME/local/bin +... ... +... tfe +... ... +$ ls $HOME/local/share/glib-2.0/schemas +com.github.ToshioCP.tfe.gschema.xml +gschema.dtd +gschemas.compiled +... ... +~~~ + +![tfe6](../image/tfe6.png) Up: [Readme.md](../Readme.md), Prev: [Section 18](sec18.md), Next: [Section 20](sec20.md) diff --git a/gfm/sec2.md b/gfm/sec2.md index 0eff84c..a01cc6f 100644 --- a/gfm/sec2.md +++ b/gfm/sec2.md @@ -164,9 +164,11 @@ Modify `env.sh`. Include this file by . (dot) command before using gtk4 libraries. You may think you can add them in your `.profile`. -I think the environment variables above are necessary only when you compile gtk4 applications. -And it's not necessary except the case above and it might cause some bad things. -Therefore, I recommend you not to write them to your `.profile`. +But it's a wrong decision. +Never write them to your `.profile`. +The environment variables above are necessary only when you compile and run gtk4 applications. +Otherwise it's not necessary. +If you changed the environment variables above and run gtk3 applications, it probably causes serious damage. ## Compiling gtk4 applications diff --git a/gfm/sec20.md b/gfm/sec20.md index 54b70a5..ef82eb2 100644 --- a/gfm/sec20.md +++ b/gfm/sec20.md @@ -1,374 +1,201 @@ -Up: [Readme.md](../Readme.md), Prev: [Section 19](sec19.md) +Up: [Readme.md](../Readme.md), Prev: [Section 19](sec19.md), Next: [Section 21](sec21.md) -# Combine GtkDrawingArea and TfeTextView +# GtkDrawingArea and Cairo -Now, we will make a new application which has GtkDrawingArea and TfeTextView in it. -Its name is "color". -If you write a color in TfeTextView and click on the `run` button, then the color of GtkDrawingArea changes to the color given by you. +If you want to draw dynamically, like an image window of gimp graphics editor, GtkDrawingArea widget is the most suitable widget. +You can draw or redraw an image in this widget freely. +It is called custom drawing. -![color](../image/color.png) +GtkDrawingArea provides a cairo context so users can draw images by cairo functions. +In this section, I will explain: -The following colors are available. +1. Cairo, but briefly. +2. GtkDrawingArea with very simple example. -- white -- black -- red -- green -- blue +## Cairo -In addition the following two options are also available. +Cairo is a two dimensional graphics library. +First, you need to know surface, source, mask, destination, cairo context and transformation. -- light: Make the color of the drawing area lighter. -- dark: Make the color of the drawing area darker. +- Surface represents an image. +It is like a canvas. +We can draw shapes and images with different colors on surfaces. +- Source pattern, or simply source, is a kind of paint, which will be transferred to destination surface by cairo functions. +- Mask is image mask used in the transference. +- Destination is a target surface. +- Cairo context manages the transference from source to destination through mask with its functions. +For example, `cairo_stroke` is a function to draw a path to the destination by the transference. +- Transformation is applied before the transfer completes. +The transformation is called affine, which is a mathematics terminology, and represented by matrix multiplication and vector addition. +Scaling, rotation, reflection, shearing and translation are examples of affine transformation. +In this section, we don't use it. +That means we only use identity transformation. +Therefore, the coordinate in source and mask is the same as the coordinate in destination. -This application can only do very simple things. -However, it tells us that if we add powerful parser to it, we will be able to make it more efficient. -I want to show it to you in the later section by making a turtle graphics language like Logo program language. +![Stroke a rectangle](../image/cairo.png) -In this section, we focus on how to bind the two objects. +The instruction is as follows: -## Color.ui and color.gresource.xml +1. Create a surface. +This will be a destination. +2. Create a cairo context with the surface and the surface will be the destination of the context. +3. Create a source pattern within the context. +4. Create paths, which are lines, rectangles, arcs, texts or more complicated shapes, to generate a mask. +5. Use drawing operator such as `cairo_stroke` to transfer the paint in the source to the destination. +6. Save the destination surface to a file if necessary. -First, We need to make the ui file of the widgets. -The image in the previous subsection gives us the structure of the widgets. -Title bar, four buttons in the tool bar and two widgets textview and drawing area. -The ui file is as follows. - -~~~xml - 1 - 2 - 3 color changer - 4 600 - 5 400 - 6 - 7 - 8 GTK_ORIENTATION_VERTICAL - 9 -10 -11 GTK_ORIENTATION_HORIZONTAL -12 -13 -14 10 -15 -16 -17 -18 -19 Run -20 -21 -22 -23 -24 -25 -26 Open -27 -28 -29 -30 -31 -32 TRUE -33 -34 -35 -36 -37 Save -38 -39 -40 -41 -42 -43 Close -44 -45 -46 -47 -48 -49 10 -50 -51 -52 -53 -54 -55 -56 GTK_ORIENTATION_HORIZONTAL -57 TRUE -58 -59 -60 TRUE -61 TRUE -62 -63 -64 GTK_WRAP_WORD_CHAR -65 -66 -67 -68 -69 -70 -71 TRUE -72 TRUE -73 -74 -75 -76 -77 -78 -79 -80 -81 -~~~ - -- 9-53: This part describes the tool bar which has four buttons, `Run`, `Open`, `Save` and `Close`. -This is similar to the toolbar of tfe text editor in [Section 8](sec8.md). -There are two differences. -`Run` button replaces `New` button. -Signal element are added to each button object. -It has "name" attribute which is a signal name and "handler" attribute which is the name of its signal handler function. -Options "-WI, --export-dynamic" CFLAG is necessary when you compile the application. -You can achieve this by adding "export_dynamic: true" argument to executable function in `meson.build`. -And be careful that the handler must be defined without 'static' class. -- 54-76: Puts GtkScrolledWindow and GtkDrawingArea into GtkBox. -GtkBox has "homogeneous property with TRUE value, so the two children have the same width in the box. -TfeTextView is a child of GtkScrolledWindow. - -The xml file for the resource compiler is almost same as before. -Just substitute "color" for "tfe". - -~~~xml -1 -2 -3 -4 color.ui -5 -6 -~~~ - -## Tfetextview.h, tfetextview.c and color.h - -First two files are the same as before. -Color.h just includes tfetextview.h. +Here's a simple example code that draws a small square and save it as a png file. ~~~C -1 #include -2 -3 #include "../tfetextview/tfetextview.h" -~~~ - -## Colorapplication.c - -This is the main file. -It deals with: - -- Building widgets by GtkBuilder. -- Seting a drawing function of GtkDrawingArea. -And connecting a handler to "resize" signal on GtkDrawingArea. -- Implementing each call back functions. -Particularly, `Run` signal handler is the point in this program. - -The following is `colorapplication.c`. - -~~~C - 1 #include "color.h" - 2 - 3 static GtkWidget *win; - 4 static GtkWidget *tv; - 5 static GtkWidget *da; - 6 - 7 static cairo_surface_t *surface = NULL; - 8 - 9 static void - 10 run (void) { - 11 GtkTextBuffer *tb = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv)); - 12 GtkTextIter start_iter; - 13 GtkTextIter end_iter; - 14 char *contents; - 15 cairo_t *cr; - 16 - 17 gtk_text_buffer_get_bounds (tb, &start_iter, &end_iter); - 18 contents = gtk_text_buffer_get_text (tb, &start_iter, &end_iter, FALSE); - 19 if (surface) { - 20 cr = cairo_create (surface); - 21 if (g_strcmp0 ("red", contents) == 0) - 22 cairo_set_source_rgb (cr, 1, 0, 0); - 23 else if (g_strcmp0 ("green", contents) == 0) - 24 cairo_set_source_rgb (cr, 0, 1, 0); - 25 else if (g_strcmp0 ("blue", contents) == 0) - 26 cairo_set_source_rgb (cr, 0, 0, 1); - 27 else if (g_strcmp0 ("white", contents) == 0) - 28 cairo_set_source_rgb (cr, 1, 1, 1); - 29 else if (g_strcmp0 ("black", contents) == 0) - 30 cairo_set_source_rgb (cr, 0, 0, 0); - 31 else if (g_strcmp0 ("light", contents) == 0) - 32 cairo_set_source_rgba (cr, 1, 1, 1, 0.5); - 33 else if (g_strcmp0 ("dark", contents) == 0) - 34 cairo_set_source_rgba (cr, 0, 0, 0, 0.5); - 35 else - 36 cairo_set_source_surface (cr, surface, 0, 0); - 37 cairo_paint (cr); - 38 cairo_destroy (cr); - 39 } - 40 } - 41 - 42 void - 43 run_cb (GtkWidget *btnr) { - 44 run (); - 45 gtk_widget_queue_draw (GTK_WIDGET (da)); - 46 } - 47 - 48 void - 49 open_cb (GtkWidget *btno) { - 50 tfe_text_view_open (TFE_TEXT_VIEW (tv), win); - 51 } - 52 - 53 void - 54 save_cb (GtkWidget *btns) { - 55 tfe_text_view_save (TFE_TEXT_VIEW (tv)); - 56 } - 57 - 58 void - 59 close_cb (GtkWidget *btnc) { - 60 if (surface) - 61 cairo_surface_destroy (surface); - 62 gtk_window_destroy (GTK_WINDOW (win)); - 63 } - 64 - 65 static void - 66 resize_cb (GtkDrawingArea *drawing_area, int width, int height, gpointer user_data) { - 67 if (surface) - 68 cairo_surface_destroy (surface); - 69 surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, width, height); - 70 run (); - 71 } - 72 - 73 static void - 74 draw_func (GtkDrawingArea *drawing_area, cairo_t *cr, int width, int height, gpointer user_data) { - 75 if (surface) { - 76 cairo_set_source_surface (cr, surface, 0, 0); - 77 cairo_paint (cr); - 78 } - 79 } - 80 - 81 static void - 82 activate (GApplication *application) { - 83 gtk_widget_show (win); - 84 } - 85 - 86 static void - 87 startup (GApplication *application) { - 88 GtkApplication *app = GTK_APPLICATION (application); - 89 GtkBuilder *build; - 90 - 91 build = gtk_builder_new_from_resource ("/com/github/ToshioCP/color/color.ui"); - 92 win = GTK_WIDGET (gtk_builder_get_object (build, "win")); - 93 gtk_window_set_application (GTK_WINDOW (win), app); - 94 tv = GTK_WIDGET (gtk_builder_get_object (build, "tv")); - 95 da = GTK_WIDGET (gtk_builder_get_object (build, "da")); - 96 g_object_unref(build); - 97 g_signal_connect (GTK_DRAWING_AREA (da), "resize", G_CALLBACK (resize_cb), NULL); - 98 gtk_drawing_area_set_draw_func (GTK_DRAWING_AREA (da), draw_func, NULL, NULL); - 99 -100 GdkDisplay *display; -101 -102 display = gtk_widget_get_display (GTK_WIDGET (win)); -103 GtkCssProvider *provider = gtk_css_provider_new (); -104 gtk_css_provider_load_from_data (provider, "textview {padding: 10px; font-family: monospace; font-size: 12pt;}", -1); -105 gtk_style_context_add_provider_for_display (display, GTK_STYLE_PROVIDER (provider), GTK_STYLE_PROVIDER_PRIORITY_USER); -106 } -107 -108 int -109 main (int argc, char **argv) { -110 GtkApplication *app; -111 int stat; -112 -113 app = gtk_application_new ("com.github.ToshioCP.color", G_APPLICATION_FLAGS_NONE); -114 -115 g_signal_connect (app, "startup", G_CALLBACK (startup), NULL); -116 g_signal_connect (app, "activate", G_CALLBACK (activate), NULL); -117 -118 stat =g_application_run (G_APPLICATION (app), argc, argv); -119 g_object_unref (app); -120 return stat; -121 } -122 -~~~ - -- 108-121: The function `main` is almost same as before but there are some differences. -The application ID is "com.github.ToshioCP.color". -`G_APPLICATION_FLAGS_NONE` is specified so no open signal handler is necessary. -- 86-106: Startup handler. -- 91-96: Builds widgets. -The pointers of the top window, TfeTextView and GtkDrawingArea objects are stored to static variables `win`, `tv` and `da` respectively. -This is because these objects are often used in handlers. -They never be rewritten so they're thread safe. -- 97: connects "resize" signal and the handler. -- 98: sets the drawing function. -- 81-84: Activates handler, which just shows the widgets. -- 73-79: The drawing function. -It just copies `surface` to destination. -- 65-71: Resize handler. -Re-creates the surface to fit the width and height of the drawing area and paints by calling the function `run`. -- 58-63: Closes the handler. -It destroys `surface` if it exists. -Then it destroys the top window and quits the application. -- 48-56: Open and save handler. -They just call the corresponding functions of TfeTextView. -- 42-46: Run handler. -It calls run function to paint the surface. -After that `gtk_widget_queue_draw` is called. -This fhunction adds the widget (GtkDrawingArea) to the queue to be redrawn. -It is important to know that the drawing function is called when it is necessary. -For example, when another window is moved and uncovers part of the widget, or when the window containing it is resized. -But repaint of `surface` is not automatically notified to gtk. -Therefore, you need to call `gtk_widget_queue_draw` to redraw the widget. -- 9-40: Run function paints the surface. -First, it gets the contents of GtkTextBuffer. -Then it compares it to "red", "green" and so on. -If it matches the color, then the surface is painted the color. -If it matches "light" or "dark", then the color of the surface is lightened or darkened respectively. -Alpha channel is used. - -## Meson.build - -This file is almost same as before. -An argument "export_dynamic: true" is added to executable function. - -~~~meson - 1 project('color', 'c') + 1 #include 2 - 3 gtkdep = dependency('gtk4') - 4 - 5 gnome=import('gnome') - 6 resources = gnome.compile_resources('resources','color.gresource.xml') - 7 - 8 sourcefiles=files('colorapplication.c', '../tfetextview/tfetextview.c') - 9 -10 executable('color', sourcefiles, resources, dependencies: gtkdep, export_dynamic: true) + 3 int + 4 main (int argc, char **argv) + 5 { + 6 cairo_surface_t *surface; + 7 cairo_t *cr; + 8 int width = 100; + 9 int height = 100; +10 +11 /* Generate surface and cairo */ +12 surface = cairo_image_surface_create (CAIRO_FORMAT_RGB24, width, height); +13 cr = cairo_create (surface); +14 +15 /* Drawing starts here. */ +16 /* Paint the background white */ +17 cairo_set_source_rgb (cr, 1.0, 1.0, 1.0); +18 cairo_paint (cr); +19 /* Draw a black rectangle */ +20 cairo_set_source_rgb (cr, 0.0, 0.0, 0.0); +21 cairo_set_line_width (cr, 2.0); +22 cairo_rectangle (cr, width/2.0 - 20.0, height/2.0 - 20.0, 40.0, 40.0); +23 cairo_stroke (cr); +24 +25 /* Write the surface to a png file and clean up cairo and surface. */ +26 cairo_surface_write_to_png (surface, "rectangle.png"); +27 cairo_destroy (cr); +28 cairo_surface_destroy (surface); +29 +30 return 0; +31 } ~~~ -## Compile and execute it +- 1: Includes the header file of cairo. +- 12: `cairo_image_surface_create` creates an image surface. +`CAIRO_FORMAT_RGB24` is a constant which means that each pixel has red, green and blue data. +Each data has 8 bit quantity. +Modern displays have this type of color depth. +Width and height are pixels and given as integers. +- 13: Creates cairo context. +The surface given as an argument will be the destination of the context. +- 17: `cairo_set_source_rgb` creates a source pattern, which is a solid white paint. +The second to fourth argument is red, green and blue color depth respectively. +Their type is float and the values are between zero and one. +(0,0,0) is black and (1,1,1) is white. +- 18: `cairo_paint` copies everywhere in the source to destination. +The destination is filled with white pixels by this command. +- 20: Sets the source color to black. +- 21: `cairo_set_line_width` set the width of lines. +In this case, the line width is set to two pixels. +(It is because the transformation is identity. +If the transformation isn't identity, for example scaling with the factor three, the actual width in destination will be six (2x3=6) pixels.) +- 22: Draws a rectangle (square). +The top-left coordinate is (width/2.0-20.0, height/2.0-20.0) and the width and height have the same length 40.0. +- 23: `cairo_stroke` transfer the source to destination through the rectangle in mask. +- 26: Outputs the image to a png file `rectangle.png`. +- 27: Destroys the context. At the same time the source is destroyed. +- 28: Destroys the destination surface. -First you need to export some variables (refer to [Section 2](sec2.md)). +To compile this, type the following. - $ . env.sh + $ gcc `pkg-config --cflags cairo` cairo.c `pkg-config --libs cairo` -Then type the following to compile it. +![rectangle.png](../src/misc/rectangle.png) - $ meson _build - $ ninja -C _build +There are lots of documentations in [Cairo's website](https://www.cairographics.org/). +If you aren't familiar with cairo, it is strongly recommended to read the [tutorial](https://www.cairographics.org/tutorial/) in the website. -The application is made in `_build` directory. -Type the following to execute it. +## GtkDrawingArea - $ _build/color +The following is a very simple example. -Type "red", "green", "blue", "white", black", "light" or "dark" in the TfeTextView. -Then, click on `Run` button. -Make sure the color of GtkDrawingArea changes. +~~~C + 1 #include + 2 + 3 static void + 4 draw_function (GtkDrawingArea *area, cairo_t *cr, int width, int height, gpointer user_data) { + 5 cairo_set_source_rgb (cr, 1.0, 1.0, 1.0); /* whilte */ + 6 cairo_paint (cr); + 7 cairo_set_line_width (cr, 2.0); + 8 cairo_set_source_rgb (cr, 0.0, 0.0, 0.0); /* black */ + 9 cairo_rectangle (cr, width/2.0 - 20.0, height/2.0 - 20.0, 40.0, 40.0); +10 cairo_stroke (cr); +11 } +12 +13 static void +14 on_activate (GApplication *app, gpointer user_data) { +15 GtkWidget *win = gtk_application_window_new (GTK_APPLICATION (app)); +16 GtkWidget *area = gtk_drawing_area_new (); +17 +18 gtk_window_set_title (GTK_WINDOW (win), "da1"); +19 /* Set initial size of width and height */ +20 gtk_drawing_area_set_content_width (GTK_DRAWING_AREA (area), 100); +21 gtk_drawing_area_set_content_height (GTK_DRAWING_AREA (area), 100); +22 gtk_drawing_area_set_draw_func (GTK_DRAWING_AREA (area), draw_function, NULL, NULL); +23 gtk_window_set_child (GTK_WINDOW (win), area); +24 +25 gtk_widget_show (win); +26 } +27 +28 int +29 main (int argc, char **argv) { +30 GtkApplication *app; +31 int stat; +32 +33 app = gtk_application_new ("com.github.ToshioCP.da1", G_APPLICATION_FLAGS_NONE); +34 g_signal_connect (app, "activate", G_CALLBACK (on_activate), NULL); +35 stat =g_application_run (G_APPLICATION (app), argc, argv); +36 g_object_unref (app); +37 return stat; +38 } +39 +~~~ -In this program TfeTextView is used to change the color. -You can use buttons or menus instead of textview. -Probably it is more appropriate. -Using textview is unnatural. -It is a good practice to make such application by yourself. +The function `main` is almost same as before. +The two functions `on_activate` and `draw_function` is important in this example. -Up: [Readme.md](../Readme.md), Prev: [Section 19](sec19.md) +- 16: Generates a GtkDrawingArea object. +- 20,21: Sets the width and height of the contents of the GtkDrawingArea widget. +These width and height is the size of the destination surface of the cairo context provided by the widget. +- 22: Sets a drawing function of the widget. +GtkDrawingArea widget uses the function to draw the contents of itself whenever its necessary. +For example, when a user drag a mouse pointer and resize a top level window, GtkDrawingArea also changes the size. +Then, the whole window needs to be redrawn. + +The drawing function has five parameters. + + void drawing_function (GtkDrawingArea *drawing_area, cairo_t *cr, int width, int height, + gpointer user_data); + +The first parameter is the GtkDrawingArea widget which calls the drawing function. +However, you can't change any properties, for example `content-width` or `content-height`, in this function. +The second parameter is a cairo context given by the widget. +The destination surface of the context is connected to the contents of the widget. +What you draw to this surface will appear in the widget on the screen. +The third and fourth parameters are the size of the destination surface. + +- 3-11: The drawing function. +- 4-5: Sets the source to be white and paint the destination white. +- 7: Sets the line width to be 2. +- 8: Sets the source to be black. +- 9: Adds a rectangle to the mask. +- 10: Draws the rectangle with black color to the destination. + +Compile and run it, then a window with a black rectangle (square) appears. +Try resizing the window. +The square always appears at the center of the window because the drawing function is invoked every moment the window is resized. + +![Square in the window](../image/da1.png) + + +Up: [Readme.md](../Readme.md), Prev: [Section 19](sec19.md), Next: [Section 21](sec21.md) diff --git a/gfm/sec21.md b/gfm/sec21.md new file mode 100644 index 0000000..0993e69 --- /dev/null +++ b/gfm/sec21.md @@ -0,0 +1,373 @@ +Up: [Readme.md](../Readme.md), Prev: [Section 20](sec20.md) + +# Combine GtkDrawingArea and TfeTextView + +Now, we will make a new application which has GtkDrawingArea and TfeTextView in it. +Its name is "color". +If you write a color in TfeTextView and click on the `run` button, then the color of GtkDrawingArea changes to the color given by you. + +![color](../image/color.png) + +The following colors are available. + +- white +- black +- red +- green +- blue + +In addition the following two options are also available. + +- light: Make the color of the drawing area lighter. +- dark: Make the color of the drawing area darker. + +This application can only do very simple things. +However, it tells us that if we add powerful parser to it, we will be able to make it more efficient. +I want to show it to you in the later section by making a turtle graphics language like Logo program language. + +In this section, we focus on how to bind the two objects. + +## Color.ui and color.gresource.xml + +First, We need to make the ui file of the widgets. +The image in the previous subsection gives us the structure of the widgets. +Title bar, four buttons in the tool bar and two widgets textview and drawing area. +The ui file is as follows. + +~~~xml + 1 + 2 + 3 + 4 color changer + 5 600 + 6 400 + 7 + 8 + 9 GTK_ORIENTATION_VERTICAL +10 +11 +12 GTK_ORIENTATION_HORIZONTAL +13 +14 +15 10 +16 +17 +18 +19 +20 Run +21 +22 +23 +24 +25 +26 Open +27 +28 +29 +30 +31 +32 TRUE +33 +34 +35 +36 +37 Save +38 +39 +40 +41 +42 +43 Close +44 +45 +46 +47 +48 +49 10 +50 +51 +52 +53 +54 +55 +56 GTK_ORIENTATION_HORIZONTAL +57 TRUE +58 +59 +60 TRUE +61 TRUE +62 +63 +64 GTK_WRAP_WORD_CHAR +65 +66 +67 +68 +69 +70 +71 TRUE +72 TRUE +73 +74 +75 +76 +77 +78 +79 +80 +~~~ + +- 10-53: This part describes the tool bar which has four buttons, `Run`, `Open`, `Save` and `Close`. +This is similar to the toolbar of tfe text editor in [Section 8](sec8.md). +There are two differences. +`Run` button replaces `New` button. +A signal element is added to each button object. +It has "name" attribute which is a signal name and "handler" attribute which is the name of its signal handler function. +Options "-WI, --export-dynamic" CFLAG is necessary when you compile the application. +You can achieve this by adding "export_dynamic: true" argument to executable function in `meson.build`. +And be careful that the handler must be defined without 'static' class. +- 54-76: Puts GtkScrolledWindow and GtkDrawingArea into GtkBox. +GtkBox has "homogeneous property" with TRUE value, so the two children have the same width in the box. +TfeTextView is a child of GtkScrolledWindow. + +The xml file for the resource compiler is almost same as before. +Just substitute "color" for "tfe". + +~~~xml +1 +2 +3 +4 color.ui +5 +6 +~~~ + +## Tfetextview.h, tfetextview.c and color.h + +First two files are the same as before. +Color.h just includes tfetextview.h. + +~~~C +1 #include +2 +3 #include "../tfetextview/tfetextview.h" +~~~ + +## Colorapplication.c + +This is the main file. +It deals with: + +- Building widgets by GtkBuilder. +- Seting a drawing function of GtkDrawingArea. +And connecting a handler to "resize" signal on GtkDrawingArea. +- Implementing each call back functions. +Particularly, `Run` signal handler is the point in this program. + +The following is `colorapplication.c`. + +~~~C + 1 #include "color.h" + 2 + 3 static GtkWidget *win; + 4 static GtkWidget *tv; + 5 static GtkWidget *da; + 6 + 7 static cairo_surface_t *surface = NULL; + 8 + 9 static void + 10 run (void) { + 11 GtkTextBuffer *tb = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv)); + 12 GtkTextIter start_iter; + 13 GtkTextIter end_iter; + 14 char *contents; + 15 cairo_t *cr; + 16 + 17 gtk_text_buffer_get_bounds (tb, &start_iter, &end_iter); + 18 contents = gtk_text_buffer_get_text (tb, &start_iter, &end_iter, FALSE); + 19 if (surface) { + 20 cr = cairo_create (surface); + 21 if (g_strcmp0 ("red", contents) == 0) + 22 cairo_set_source_rgb (cr, 1, 0, 0); + 23 else if (g_strcmp0 ("green", contents) == 0) + 24 cairo_set_source_rgb (cr, 0, 1, 0); + 25 else if (g_strcmp0 ("blue", contents) == 0) + 26 cairo_set_source_rgb (cr, 0, 0, 1); + 27 else if (g_strcmp0 ("white", contents) == 0) + 28 cairo_set_source_rgb (cr, 1, 1, 1); + 29 else if (g_strcmp0 ("black", contents) == 0) + 30 cairo_set_source_rgb (cr, 0, 0, 0); + 31 else if (g_strcmp0 ("light", contents) == 0) + 32 cairo_set_source_rgba (cr, 1, 1, 1, 0.5); + 33 else if (g_strcmp0 ("dark", contents) == 0) + 34 cairo_set_source_rgba (cr, 0, 0, 0, 0.5); + 35 else + 36 cairo_set_source_surface (cr, surface, 0, 0); + 37 cairo_paint (cr); + 38 cairo_destroy (cr); + 39 } + 40 } + 41 + 42 void + 43 run_cb (GtkWidget *btnr) { + 44 run (); + 45 gtk_widget_queue_draw (GTK_WIDGET (da)); + 46 } + 47 + 48 void + 49 open_cb (GtkWidget *btno) { + 50 tfe_text_view_open (TFE_TEXT_VIEW (tv), win); + 51 } + 52 + 53 void + 54 save_cb (GtkWidget *btns) { + 55 tfe_text_view_save (TFE_TEXT_VIEW (tv)); + 56 } + 57 + 58 void + 59 close_cb (GtkWidget *btnc) { + 60 if (surface) + 61 cairo_surface_destroy (surface); + 62 gtk_window_destroy (GTK_WINDOW (win)); + 63 } + 64 + 65 static void + 66 resize_cb (GtkDrawingArea *drawing_area, int width, int height, gpointer user_data) { + 67 if (surface) + 68 cairo_surface_destroy (surface); + 69 surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, width, height); + 70 run (); + 71 } + 72 + 73 static void + 74 draw_func (GtkDrawingArea *drawing_area, cairo_t *cr, int width, int height, gpointer user_data) { + 75 if (surface) { + 76 cairo_set_source_surface (cr, surface, 0, 0); + 77 cairo_paint (cr); + 78 } + 79 } + 80 + 81 static void + 82 activate (GApplication *application) { + 83 gtk_widget_show (win); + 84 } + 85 + 86 static void + 87 startup (GApplication *application) { + 88 GtkApplication *app = GTK_APPLICATION (application); + 89 GtkBuilder *build; + 90 + 91 build = gtk_builder_new_from_resource ("/com/github/ToshioCP/color/color.ui"); + 92 win = GTK_WIDGET (gtk_builder_get_object (build, "win")); + 93 gtk_window_set_application (GTK_WINDOW (win), app); + 94 tv = GTK_WIDGET (gtk_builder_get_object (build, "tv")); + 95 da = GTK_WIDGET (gtk_builder_get_object (build, "da")); + 96 g_object_unref(build); + 97 g_signal_connect (GTK_DRAWING_AREA (da), "resize", G_CALLBACK (resize_cb), NULL); + 98 gtk_drawing_area_set_draw_func (GTK_DRAWING_AREA (da), draw_func, NULL, NULL); + 99 +100 GdkDisplay *display; +101 +102 display = gtk_widget_get_display (GTK_WIDGET (win)); +103 GtkCssProvider *provider = gtk_css_provider_new (); +104 gtk_css_provider_load_from_data (provider, "textview {padding: 10px; font-family: monospace; font-size: 12pt;}", -1); +105 gtk_style_context_add_provider_for_display (display, GTK_STYLE_PROVIDER (provider), GTK_STYLE_PROVIDER_PRIORITY_USER); +106 } +107 +108 int +109 main (int argc, char **argv) { +110 GtkApplication *app; +111 int stat; +112 +113 app = gtk_application_new ("com.github.ToshioCP.color", G_APPLICATION_FLAGS_NONE); +114 +115 g_signal_connect (app, "startup", G_CALLBACK (startup), NULL); +116 g_signal_connect (app, "activate", G_CALLBACK (activate), NULL); +117 +118 stat =g_application_run (G_APPLICATION (app), argc, argv); +119 g_object_unref (app); +120 return stat; +121 } +122 +~~~ + +- 108-121: The function `main` is almost same as before but there are some differences. +The application ID is "com.github.ToshioCP.color". +`G_APPLICATION_FLAGS_NONE` is specified so no open signal handler is necessary. +- 86-106: Startup handler. +- 91-96: Builds widgets. +The pointers of the top window, TfeTextView and GtkDrawingArea objects are stored to static variables `win`, `tv` and `da` respectively. +This is because these objects are often used in handlers. +They never be rewritten so they're thread safe. +- 97: connects "resize" signal and the handler. +- 98: sets the drawing function. +- 81-84: Activates handler, which just shows the widgets. +- 73-79: The drawing function. +It just copies `surface` to destination. +- 65-71: Resize handler. +Re-creates the surface to fit the width and height of the drawing area and paints by calling the function `run`. +- 58-63: Closes the handler. +It destroys `surface` if it exists. +Then it destroys the top window and quits the application. +- 48-56: Open and save handler. +They just call the corresponding functions of TfeTextView. +- 42-46: Run handler. +It calls run function to paint the surface. +After that `gtk_widget_queue_draw` is called. +This fhunction adds the widget (GtkDrawingArea) to the queue to be redrawn. +It is important to know that the drawing function is called when it is necessary. +For example, when another window is moved and uncovers part of the widget, or when the window containing it is resized. +But repaint of `surface` is not automatically notified to gtk. +Therefore, you need to call `gtk_widget_queue_draw` to redraw the widget. +- 9-40: Run function paints the surface. +First, it gets the contents of GtkTextBuffer. +Then it compares it to "red", "green" and so on. +If it matches the color, then the surface is painted the color. +If it matches "light" or "dark", then the color of the surface is lightened or darkened respectively. +Alpha channel is used. + +## Meson.build + +This file is almost same as before. +An argument "export_dynamic: true" is added to executable function. + +~~~meson + 1 project('color', 'c') + 2 + 3 gtkdep = dependency('gtk4') + 4 + 5 gnome=import('gnome') + 6 resources = gnome.compile_resources('resources','color.gresource.xml') + 7 + 8 sourcefiles=files('colorapplication.c', '../tfetextview/tfetextview.c') + 9 +10 executable('color', sourcefiles, resources, dependencies: gtkdep, export_dynamic: true) +~~~ + +## Compile and execute it + +First you need to export some variables (refer to [Section 2](sec2.md)). + + $ . env.sh + +Then type the following to compile it. + + $ meson _build + $ ninja -C _build + +The application is made in `_build` directory. +Type the following to execute it. + + $ _build/color + +Type "red", "green", "blue", "white", black", "light" or "dark" in the TfeTextView. +Then, click on `Run` button. +Make sure the color of GtkDrawingArea changes. + +In this program TfeTextView is used to change the color. +You can use buttons or menus instead of textview. +Probably it is more appropriate. +Using textview is unnatural. +It is a good practice to make such application by yourself. + +Up: [Readme.md](../Readme.md), Prev: [Section 20](sec20.md) diff --git a/gfm/sec8.md b/gfm/sec8.md index ccf9c97..a6d0379 100644 --- a/gfm/sec8.md +++ b/gfm/sec8.md @@ -138,65 +138,65 @@ It reduces the cumbersome work. First, let's look at the ui file `tfe3.ui` that defines a structure of the widgets. ~~~xml - 1 - 2 - 3 file editor - 4 600 - 5 400 - 6 - 7 - 8 GTK_ORIENTATION_VERTICAL - 9 -10 -11 GTK_ORIENTATION_HORIZONTAL -12 -13 -14 10 -15 -16 -17 -18 -19 New -20 -21 -22 -23 -24 Open -25 -26 -27 -28 -29 TRUE -30 -31 -32 -33 -34 Save -35 -36 -37 -38 -39 Close -40 -41 -42 -43 -44 10 -45 -46 -47 -48 -49 -50 -51 TRUE -52 TRUE -53 -54 -55 -56 -57 -58 -59 + 1 + 2 + 3 + 4 file editor + 5 600 + 6 400 + 7 + 8 + 9 GTK_ORIENTATION_VERTICAL +10 +11 +12 GTK_ORIENTATION_HORIZONTAL +13 +14 +15 10 +16 +17 +18 +19 +20 New +21 +22 +23 +24 +25 Open +26 +27 +28 +29 +30 TRUE +31 +32 +33 +34 +35 Save +36 +37 +38 +39 +40 Close +41 +42 +43 +44 +45 10 +46 +47 +48 +49 +50 +51 +52 TRUE +53 TRUE +54 +55 +56 +57 +58 +59 ~~~ This is coded with XML structure. @@ -204,18 +204,35 @@ Constructs beginning with `<` and ending with `>` are called tags. And there are two types of tags, start tag and end tag. For example, `` is a start tag and `` is an end tag. Ui file begins and ends with interface tags. -Some tags, for example, object tags can have a class and id attributes inside the start tag. +Some tags, for example, object tags can have a class and id attributes in the start tag. -- 2-5: An object with `GtkApplicationWindow` class and `win` id is defined. +- 1: The first line is XML declaration. +It specifies that the version of XML is 1.0 and the encoding is UTF-8. +Even if the line is left out, GtkBuilder builds objects from the ui file. +But ui files must use UTF-8 encoding, or GtkBuilder can't recognize it and fatal error occurs. +- 3-6: An object with `GtkApplicationWindow` class and `win` id is defined. This is the top level window. And the three properties of the window are defined. `title` property is "file editor", `default-width` property is 400 and `default-height` property is 300. -- 6: child tag means a child of the object above. +- 7: child tag means a child of the object above. For example, line 7 tells us that GtkBox object which id is "boxv" is a child of `win`. Compare this ui file and the lines 25-57 in the source code of `on_open` function. Those two describe the same structure of widgets. +You can check the ui file with `gtk4-builder-tool`. + +- `gtk4-builder-tool validate ` validates the ui file. +If the ui file includes some syntactical error, `gtk4-builder-tool` prints the error. +- `gtk4-builder-tool simplify ` simplifies the ui file and prints the result. +If `--replace` option is given, it replaces the ui file with the simplified one. +If the ui file specifies a value of property but it is default, then it will be removed. +Anf some values are simplified. +For example, "TRUE"and "FALSE" becomes "1" and "0" respectively. +However, "TRUE" or "FALSE" is better for maintenance. + +It is a good idea to check your ui file before compiling. + ## GtkBuilder GtkBuilder builds widgets based on the ui file. diff --git a/gfm/sec9.md b/gfm/sec9.md index 2179b85..93aa0ea 100644 --- a/gfm/sec9.md +++ b/gfm/sec9.md @@ -173,69 +173,7 @@ All the source files are listed below. 70 ~~~ -`tfe.ui` - -~~~xml - 1 - 2 - 3 file editor - 4 600 - 5 400 - 6 - 7 - 8 GTK_ORIENTATION_VERTICAL - 9 -10 -11 GTK_ORIENTATION_HORIZONTAL -12 -13 -14 10 -15 -16 -17 -18 -19 New -20 -21 -22 -23 -24 Open -25 -26 -27 -28 -29 TRUE -30 -31 -32 -33 -34 Save -35 -36 -37 -38 -39 Close -40 -41 -42 -43 -44 10 -45 -46 -47 -48 -49 -50 -51 TRUE -52 TRUE -53 -54 -55 -56 -57 -58 -59 -~~~ +The ui file `tfe.ui` is the same as `tfe3.ui` in the previous section. `tfe.gresource.xml` diff --git a/image/dialog_warning.png b/image/dialog_warning.png new file mode 100644 index 0000000..8622e57 Binary files /dev/null and b/image/dialog_warning.png differ diff --git a/image/dialog_warning.xcf b/image/dialog_warning.xcf new file mode 100644 index 0000000..ebfd234 Binary files /dev/null and b/image/dialog_warning.xcf differ diff --git a/image/gnome_calculator_advanced.png b/image/gnome_calculator_advanced.png new file mode 100644 index 0000000..e12ee07 Binary files /dev/null and b/image/gnome_calculator_advanced.png differ diff --git a/image/gnome_calculator_basic.png b/image/gnome_calculator_basic.png new file mode 100644 index 0000000..d0d6edc Binary files /dev/null and b/image/gnome_calculator_basic.png differ diff --git a/image/tfe6.png b/image/tfe6.png new file mode 100644 index 0000000..3133cf6 Binary files /dev/null and b/image/tfe6.png differ diff --git a/lib/lib_src2md.rb b/lib/lib_src2md.rb index 9ee35d1..d91f93e 100644 --- a/lib/lib_src2md.rb +++ b/lib/lib_src2md.rb @@ -84,9 +84,9 @@ def src2md srcmd, md, width c_file = $1 c_functions = $2.strip.split(" ") if c_file =~ /^\// # absolute path - c_file_buf = IO.readlines(c_file) + c_file_buf = File.readlines(c_file) else #relative path - c_file_buf = IO.readlines(src_dir+"/"+c_file) + c_file_buf = File.readlines(src_dir+"/"+c_file) end if c_functions.empty? # no functions are specified tmp_buf = c_file_buf @@ -96,7 +96,7 @@ def src2md srcmd, md, width c_functions.each do |c_function| from = c_file_buf.find_index { |line| line =~ /^#{c_function} *\(/ } if ! from - warn "ERROR!!! --- Didn't find #{c_function} in #{filename}. ---" + warn "ERROR in #{srcmd}: Didn't find #{c_function} in #{c_file}." break end to = from diff --git a/src/color/color.ui b/src/color/color.ui index 9417464..7c66c4c 100644 --- a/src/color/color.ui +++ b/src/color/color.ui @@ -1,3 +1,4 @@ + color changer @@ -8,68 +9,67 @@ GTK_ORIENTATION_VERTICAL - GTK_ORIENTATION_HORIZONTAL + GTK_ORIENTATION_HORIZONTAL - 10 + 10 - Run - - + Run + - Open - + Open + - TRUE + TRUE - Save - + Save + - Close - + Close + - 10 + 10 - GTK_ORIENTATION_HORIZONTAL - TRUE + GTK_ORIENTATION_HORIZONTAL + TRUE - TRUE - TRUE - - - GTK_WRAP_WORD_CHAR - - + TRUE + TRUE + + + GTK_WRAP_WORD_CHAR + + - TRUE - TRUE + TRUE + TRUE @@ -78,4 +78,3 @@ - diff --git a/src/sec10.src.md b/src/sec10.src.md index b03966a..9a0d335 100644 --- a/src/sec10.src.md +++ b/src/sec10.src.md @@ -220,7 +220,7 @@ Then C destructs itself and finally the memories allocated to C is freed. The idea above is based on an assumption that an object referred by nothing has reference count of zero. When the reference count drops to zero, the object starts its destruction process. -The destruction process is spitted into two phases: disposing and finalizing. +The destruction process is split into two phases: disposing and finalizing. In the disposing process, the object invokes the function pointed by `dispose` in its class to release all references to other objects. In the finalizing process, it invokes the function pointed by `finalize` in its class to complete the destruction process. These functions are also called handlers or methods. diff --git a/src/sec13.src.md b/src/sec13.src.md index 3c6dea2..cc6c036 100644 --- a/src/sec13.src.md +++ b/src/sec13.src.md @@ -2,7 +2,7 @@ GtkNotebook is a very important object in the text file editor `tfe`. It connects the application and TfeTextView objects. -A set of functions related to GtkNotebook are declared in `tfenotebook.h`. +A set of public functions related to GtkNotebook are declared in `tfenotebook.h`. The word "tfenotebook" is used only in filenames. There's no "TfeNotebook" object. @@ -12,18 +12,19 @@ tfe5/tfenotebook.h This header file describes the public functions in `tfenotebook.c`. -- 10-11: The function `notebook_page_new` generates a new GtkNotebookPage and adds GtkScrolledWindow and TfeTextView to the page. -- 7-8: The function `notebook_page_new_with_file` generates a new GtkNotebookPage and adds GtkScrolledWindow and TfeTextView to the page. A file is read and inserted into GtkTextBuffer. -The GFile `file` is copied and inserted to the TfeTextView object. -- 4-5: `notebook_page_open` shows a file chooser dialog. Then, user chooses a file and the file is inserted into GtkTextBuffer. -- 1-2: `notebook_page_save` saves the contents in GtkTextBuffer into the file, which is got from the TfeTextView. +- 1-2: `notebook_page_save` saves the current page to the file of which the name specified in the tab. +If the name is `untitled` or `untitled` followed by digits, FileChooserDialog appears and a user can choose or specify a filename. +- 4-5: `notebook_page_close` closes the current page. +- 7-8: `notebook_page_open` shows a file chooser dialog and a user can choose a file. The file is inserted to a new page. +- 10-11: `notebook_page_new_with_file` generates a new page and the file give as an argument is read and inserted into the page. +- 13-14: `notebook_page_new` generates a new empty page. -You probably find that the functions above are higher level functions of +You probably find that the functions except `notebook_page_close` are higher level functions of -- `tfe_text_view_new` -- `tfe_text_view_new_with_file` -- `tef_text_view_open` - `tfe_text_view_save` +- `tef_text_view_open` +- `tfe_text_view_new_with_file` +- `tfe_text_view_new` respectively. @@ -31,7 +32,7 @@ There are two layers. One of them is `tfe_text_view ...`, which is the lower level layer. The other is `note_book ...`, which is the higher level layer. -Now let's look at each program of the functions. +Now let's look at the program of each function. ## notebook\_page\_new @@ -39,28 +40,28 @@ Now let's look at each program of the functions. tfe5/tfenotebook.c get_untitled notebook_page_build notebook_page_new @@@ -- 28-38: `notebook_page_new` function. -- 30: `g_return_if_fail` is used to check the argument. -- 35: Generates TfeTextView object. -- 36: Generates filename, which is "Untitled", "Untitled2", ... . +- 27-37: `notebook_page_new` function. +- 29: `g_return_if_fail` is used to check the argument. +- 34: Generates TfeTextView object. +- 35: Generates filename, which is "Untitled", "Untitled1", ... . - 1-8: `get_untitled` function. -- 3: Static variable `c` is initialized at the first call of this function. After that `c` keeps its value except it is changed explicitly. -- 4-7: Increases `c` by one and if it is zero then it returns "Untitled". If it is a positive integer then the it returns "Untitled\", for example, "Untitled1", "Untitled2", and so on. +- 3: Static variable `c` is initialized at the first call of this function. After that `c` keeps its value unless it is changed explicitly. +- 4-7: Increases `c` by one and if it is zero then it returns "Untitled". If it is a positive integer then it returns "Untitled\", for example, "Untitled1", "Untitled2", and so on. The function `g_strdup_printf` generates a string and it should be freed by `g_free` when it becomes useless. The caller of `get_untitled` is in charge of freeing the string. -- 37: calls `notebook_page_build` to build the contents of the page. -- 10- 26: `notebook_page_build` function. -- 16: Generates GtkScrolledWindow. -- 18: Sets the wrap mode of `tv` to GTK_WRAP_WORD_CHAR so that lines are broken between words or graphemes. -- 19: Inserts `tv` to GtkscrolledWindow as a child. -- 20-21: Generates GtkLabel, then appends it to GtkNotebookPage. -- 22-23: Sets "tab-expand" property to TRUE. +- 36: calls `notebook_page_build` to build the contents of the page. +- 10- 25: `notebook_page_build` function. +- 12: Generates GtkScrolledWindow. +- 17: Sets the wrap mode of `tv` to GTK_WRAP_WORD_CHAR so that lines are broken between words or graphemes. +- 18: Inserts `tv` to GtkscrolledWindow as a child. +- 19-20: Generates GtkLabel, then appends it to GtkNotebookPage. +- 21-22: Sets "tab-expand" property to TRUE. The function g\_object\_set sets properties on an object. The object is any object derived from GObject. In many cases, an object has its own function to set its properties, but sometimes not. In that case, use g\_object\_set to set the property. -- 24: Sets the current page of `nb` to `i` which is the number of the GtkNotebookPage above. -- 25: Connects "change-file" signal and `file_changed` handler. +- 23: Sets the current page of `nb` to the newly generated page. +- 24: Connects "change-file" signal and `file_changed_cb` handler. ## notebook\_page\_new\_with\_file @@ -92,34 +93,54 @@ Such object has floating reference. You need to call `g_object_ref_sink` first. Then the floating reference is converted into an ordinary reference. Now you call `g_object_unref` to decrease the reference count by one. -- 9-11: If `tfe_text_view_get_file` returns a pointer not to point GFile, then something bad happens. +- 9-11: If `tfe_text_view_get_file` returns a pointer not to point GFile, it means that an error has happened. Sink and unref `tv`. - 12-16: Otherwise, everything is okay. Gets the filename, builds the contents of the page. +## notebook\_page\_close + +@@@include +tfe5/tfenotebook.c notebook_page_close +@@@ + +This function closes the current page. +If the page is the only page the notebook has, then the function destroys the top window and quits the application. + +- 8-10: If the page is the only page the notebook has, it calls gtk\_window\_destroy to destroys the top window. +- 11-13: Otherwise, removes the current page. + ## notebook\_page\_save @@@include -tfe5/tfenotebook.c notebook_page_save +tfe5/tfenotebook.c get_current_textview notebook_page_save @@@ -- 7-9: Get TfeTextView belongs to the current notebook page. -- 10: Call `tfe_text_view_save`. +- 13-21: `notebook_page_save`. +- 19: Gets TfeTextView belongs to the current page. +- 20: Calls `tfe_text_view_save`. +- 1-11: `get_current_textview`. +This function gets the TfeTextView object belongs to the current page. +- 7: Gets the page number of the current page. +- 8: Gets the child object `scr`, which is GtkScrolledWindow object, of the current page. +- 9-10: Gets the child object of `scr`, which is TfeTextView object, and return it. -## file\_changed handler +## file\_changed\_cb handler -The function `file_changed` is a handler connected to "change-file" signal. +The function `file_changed_cb` is a handler connected to "change-file" signal. If the file in TfeTextView is changed, it emits this signal. This handler changes the label of GtkNotebookPage. @@@include -tfe5/tfenotebook.c file_changed +tfe5/tfenotebook.c file_changed_cb @@@ - 8: Gets GFile from TfeTextView. - 9: Gets GkScrolledWindow which is the parent widget of `tv`. -- 10-13: If `file` points GFile, then assigns the filename of the GFile into `filename`. -Otherwise (file is NULL), assigns untitled string to `filename`. -- 14-15: Generates a label with the filename and inserts it into GtkNotebookPage. -- 16-17: Unrefs `file` and frees `filename`. +- 10-12: If `file` points GFile, then assigns the filename of the GFile into `filename`. +Then, unref the GFile object `file`. +- 13-14: Otherwise (file is NULL), assigns untitled string to `filename`. +- 15-16: Generates a label with the filename and inserts it into GtkNotebookPage. +The string `filename` is used in the GtkLabel object. +You mustn't free it. diff --git a/src/sec14.src.md b/src/sec14.src.md index a14ba94..c11efeb 100644 --- a/src/sec14.src.md +++ b/src/sec14.src.md @@ -1,12 +1,12 @@ # tfeapplication.c `tfeapplication.c` includes all the code other than `tfetxtview.c` and `tfenotebook.c`. -It does following things. +It does: - Application support, mainly handling command line arguments. -- Build widgets using ui file. -- Connect button signals and their handlers. -- Manage CSS. +- Builds widgets using ui file. +- Connects button signals and their handlers. +- Manages CSS. ## main @@ -72,7 +72,8 @@ If you want to set style to GtkTextView, substitute "textview" for the selector. textview {color: yellow; ...} ~~~ -Class, ID and some other things can be applied to the selector like Web CSS. Refer GTK4 API reference for further information. +Class, ID and some other things can be applied to the selector like Web CSS. +Refer [GTK4 API reference](https://gnome.pages.gitlab.gnome.org/gtk/gtk/theming.html) for further information. In line 30, the CSS is a string. @@ -81,11 +82,10 @@ textview {padding: 10px; font-family: monospace; font-size: 12pt;} ~~~ - padding is a space between the border and contents. -This space makes the text easier to read. +This space makes the textview easier to read. - font-family is a name of font. "monospace" is one of the generic family font keywords. - font-size is set to 12pt. -It is a bit large, but easy on the eyes especially for elderly people. ### GtkStyleContext, GtkCSSProvider and GdkDisplay @@ -107,6 +107,8 @@ Look at the source file of `startup` handler again. It is possible to add the provider to the context of GtkTextView instead of GdkDiplay. To do so, rewrite `tfe_text_view_new`. +First, get the GtkStyleContext object of a TfeTextView object. +Then adds the CSS provider to the context. ~~~C GtkWidget * @@ -137,14 +139,12 @@ They just generate a new GtkNotebookPage. tfe5/tfeapplication.c tfe_activate tfe_open @@@ -- 1-14: `tfe_activate`. -- 8-10: Gets GtkNotebook object. -- 12-13: Generates a new GtkNotebookPage and show the window. -- 16-33: `tfe_open`. -- 24-26: Gets GtkNotebook object. -- 28-29: Generates GtkNotebookPage with files. -- 30-31: If opening and reading file failed and no GtkNotebookPage has generated, then it generates a empty page. -- 32: Shows the window. +- 1-11: `tfe_activate`. +- 8-10: Generates a new page and shows the window. +- 12-25: `tfe_open`. +- 20-21: Generates notebook pages with files. +- 22-23: If no page has generated, maybe because of read error, then it generates a empty page. +- 24: Shows the window. These codes have become really simple thanks to tfenotebook.c and tfetextview.c. @@ -185,15 +185,10 @@ The second instance immediately quits so shell prompt soon appears. ## a series of handlers correspond to the button signals @@@include -tfe5/tfeapplication.c open_clicked new_clicked save_clicked close_clicked +tfe5/tfeapplication.c open_cb new_cb save_cb close_cb @@@ -`open_clicked`, `new_clicked` and `save_clicked` just call corresponding notebook page functions. -`close_clicked` is a bit complicated. - -- 22-25: If there's only one page, we need to close the top level window and quit the application. -First, get the top level window and call `gtk_window_destroy`. -- 26-28: Otherwise, it removes the current page. +`open_cb`, `new_cb`, `save_cb` and `close_cb` just call corresponding notebook page functions. ## meson.build @@ -201,13 +196,13 @@ First, get the top level window and call `gtk_window_destroy`. tfe5/meson.build @@@ -In this file, just the source file names are modified. +In this file, just the source file names are modified from the prior version. ## source files The [source files](https://github.com/ToshioCP/Gtk4-tutorial/tree/main/src/tfe5) of the text editor `tfe` will be shown in the next section. -You can download the files. +You can also download the files from the [repository](https://github.com/ToshioCP/Gtk4-tutorial). There are two options. - Use git and clone. @@ -218,4 +213,4 @@ If you use git, run the terminal and type the following. $ git clone https://github.com/ToshioCP/Gtk4-tutorial.git -The source files are under `/src/tfe5` directory. +The source files are under [`/src/tfe5`](tfe5) directory. diff --git a/src/sec16.src.md b/src/sec16.src.md index 79678ca..c492fb0 100644 --- a/src/sec16.src.md +++ b/src/sec16.src.md @@ -56,7 +56,7 @@ Some menu items have a link to another GMenu. There are two types of links, submenu and section. GMenuItem can be inserted, appended or prepended to GMenu. -When it is inserted, all of the attribute and link values of the item are copied and used to form a new item within the menu. +When it is inserted, all of the attributes and link values of the item are copied and used to form a new item within the menu. The GMenuItem itself is not really inserted. Therefore, after the insertion, GMenuItem is useless and it should be freed. The same goes for appending or prepending. diff --git a/src/sec19.src.md b/src/sec19.src.md index 0f31154..e0bf210 100644 --- a/src/sec19.src.md +++ b/src/sec19.src.md @@ -1,129 +1,989 @@ -# GtkDrawingArea and Cairo +# Upgrade text file editor -If you want to draw dynamically, like an image window of gimp graphics editor, GtkDrawingArea widget is the most suitable widget. -You can draw or redraw an image in this widget freely. -It is called custom drawing. +Traditional menu structure is fine. +However, Buttons or menu items we often use are not so many. +Some mightn't be clicked at all. +Therefore, it's a good idea to put some frequently used buttons on the toolbar and put the rest of the less frequently used operations into the menu. +Such menu are often connected to GtkMenuButton. -GtkDrawingArea provides a cairo context so users can draw images by cairo functions. -In this section, I will explain: +We will restructure tfe text file editor in this section. +It will be more practical. +The buttons are changed to: -1. Cairo, but briefly. -2. GtkDrawingArea with very simple example. +- Put open, save and close buttons to the toolbar. +In addition, GtkMenuButton is added to the toolbar. +This button shows a popup menu when clicked on. +Here, popup means widely, including pull-down menu. +- Put new, save as, preference and quit items to the menu under the menu button. -## Cairo +## Signal elements in ui files -Cairo is a two dimensional graphics library. -First, you need to know surface, source, mask, destination, cairo context and transformation. +The four buttons are included in the ui file `tfe.ui`. +The difference from prior sections is signal tag. +The following is extracted from `tfe.ui` and it describes the open button. -- Surface represents an image. -It is like a canvas. -We can draw shapes and images with different colors on surfaces. -- Source pattern, or simply source, is a kind of paint, which will be transferred to destination surface by cairo functions. -- Mask is image mask used in the transference. -- Destination is a target surface. -- Cairo context manages the transference from source to destination through mask with its functions. -For example, `cairo_stroke` is a function to draw a path to the destination by the transference. -- Transformation is applied before the transfer completes. -The transformation is called affine, which is a mathematics terminology, and represented by matrix multiplication and vector addition. -Scaling, rotation, reflection, shearing and translation are examples of affine transformation. -In this section, we don't use it. -That means we only use identity transformation. -Therefore, the coordinate in source and mask is the same as the coordinate in destination. +~~~xml + + Open + + +~~~ -![Stroke a rectangle](../image/cairo.png){width=9.0cm height=6.0cm} +Signal tag specifies the name of the signal, handler and user_data object. +They are the value of name, handler and object attribute. +Swapped attribute has the same meaning as `g_signal_connect_swapped` function. +So, the signal tag above works the same as the function below. -The instruction is as follows: +~~~C +g_signal_connect_swapped (btno, "clicked", G_CALLBACK (open_cb), nb); +~~~ -1. Create a surface. -This will be a destination. -2. Create a cairo context with the surface and the surface will be the destination of the context. -3. Create a source pattern within the context. -4. Create paths, which are lines, rectangles, arcs, texts or more complicated shapes, to generate a mask. -5. Use drawing operator such as `cairo_stroke` to transfer the paint in the source to the destination. -6. Save the destination surface to a file if necessary. +You need to compile the source file with "-WI, --export-dynamic" options. +You can achieve this by adding "export_dynamic: true" argument to executable function in `meson.build`. +And remove static class from the handler. -Here's a simple example code that draws a small square and save it as a png file. +~~~C +void +open_cb (GtkNotebook *nb) { + notebook_page_open (nb); +} +~~~ +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. + +## Menu and GkMenuButton + +Menus are described in `menu.ui` file. @@@include -misc/cairo.c +tfe6/menu.ui @@@ -- 1: Includes the header file of cairo. -- 12: `cairo_image_surface_create` creates an image surface. -`CAIRO_FORMAT_RGB24` is a constant which means that each pixel has red, green and blue data. -Each data has 8 bit quantity. -Modern displays have this type of color depth. -Width and height are pixels and given as integers. -- 13: Creates cairo context. -The surface given as an argument will be the destination of the context. -- 17: `cairo_set_source_rgb` creates a source pattern, which is a solid white paint. -The second to fourth argument is red, green and blue color depth respectively. -Their type is float and the values are between zero and one. -(0,0,0) is black and (1,1,1) is white. -- 18: `cairo_paint` copies everywhere in the source to destination. -The destination is filled with white pixels by this command. -- 20: Sets the source color to black. -- 21: `cairo_set_line_width` set the width of lines. -In this case, the line width is set to two pixels. -(It is because the transformation is identity. -If the transformation isn't identity, for example scaling with the factor three, the actual width in destination will be six (2x3=6) pixels.) -- 22: Draws a rectangle (square). -The top-left coordinate is (width/2.0-20.0, height/2.0-20.0) and the width and height have the same length 40.0. -- 23: `cairo_stroke` transfer the source to destination through the rectangle in mask. -- 26: Outputs the image to a png file `rectangle.png`. -- 27: Destroys the context. At the same time the source is destroyed. -- 28: Destroys the destination surface. +There are four items, "New", "Saveas", "Preference" and "Quit". -To compile this, type the following. +- New menu creates a new empty page. +- Saveas menu saves the current page as a new filename. +- Preference menu sets preference items. +This version of `tfe` has only font preference. +- Quit menu quits the application. - $ gcc `pkg-config --cflags cairo` cairo.c `pkg-config --libs cairo` +These four menus are not used so often. +That's why they are put to the menu behind the menu button. -![rectangle.png](misc/rectangle.png) +The menus and the menu button are connected with `gtk_menu_button_set_menu_model` function. +The variable `btnm` below points a GtkMenuButton object. -There are lots of documentations in [Cairo's website](https://www.cairographics.org/). -If you aren't familiar with cairo, it is strongly recommended to read the [tutorial](https://www.cairographics.org/tutorial/) in the website. +~~~C + 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); +~~~ -## GtkDrawingArea +## Actions and Accelerators -The following is a very simple example. +Menus are connected to actions. +Actions are defined with an array and `g_action_map_add_action_entries` function. + +~~~C + 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", quit_activated, NULL, NULL, NULL } + }; + g_action_map_add_action_entries (G_ACTION_MAP (win), win_entries, G_N_ELEMENTS (win_entries), nb); +~~~ + +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 thy necessary? +These actions are defined because of accelerators. + +Accelerators are a kind of short cut key function. +They are defined with arrays and `gtk_application_set_accels_for_action` function. + +~~~C + struct { + const char *action; + const char *accels[2]; + } action_accels[] = { + { "win.open", { "o", NULL } }, + { "win.save", { "s", NULL } }, + { "win.close", { "w", NULL } }, + { "win.new", { "n", NULL } }, + { "win.saveas", { "s", NULL } }, + { "win.close-all", { "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); +~~~ + +This code is a bit complicated. +The array `action-accels[]` is an array of structures. +The structure is: + +~~~C + 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, + +~~~C +{ "win.open", { "o", NULL } }, +~~~ + +This is the first element of the array `action_accels`. + +- The member `action` is "win.open". This specifies the action "open" belongs to the window object. +- The member `accels` is an array of two strings, "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 "o", "F2", "minus" and so on. +If you want to use symbol key like "-", use "minus" instead. +Such symbol to lower case name relation is specified in [`gdkkeysyms.h`](https://gitlab.gnome.org/GNOME/gtk/-/blob/master/gdk/gdkkeysyms.h) in the gtk4 source code. + +## Saveas handler + +TfeTextView has already had a saveas function. +So, only we need to write is the wrapper function in `tfenotebook.c`. @@@include -misc/da1.c +tfe6/tfenotebook.c get_current_textview notebook_page_saveas @@@ -The function `main` is almost same as before. -The two functions `on_activate` and `draw_function` is important in this example. +The function `get_current_textview` is the same as before. +The function `notebook_page_saveas` simply calls `tfe_text_view_saveas`. -- 16: Generates a GtkDrawingArea object. -- 20,21: Sets the width and height of the contents of the GtkDrawingArea widget. -These width and height is the size of the destination surface of the cairo context provided by the widget. -- 22: Sets a drawing function of the widget. -GtkDrawingArea widget uses the function to draw the contents of itself whenever its necessary. -For example, when a user drag a mouse pointer and resize a top level window, GtkDrawingArea also changes the size. -Then, the whole window needs to be redrawn. +In `tfeapplication.c`, saveas handler just call `notebook_page_saveas`. -The drawing function has five parameters. +@@@include +tfe6/tfeapplication.c saveas_activated +@@@ - void drawing_function (GtkDrawingArea *drawing_area, cairo_t *cr, int width, int height, - gpointer user_data); +## Preference and alert dialog -The first parameter is the GtkDrawingArea widget which calls the drawing function. -However, you can't change any properties, for example `content-width` or `content-height`, in this function. -The second parameter is a cairo context given by the widget. -The destination surface of the context is connected to the contents of the widget. -What you draw to this surface will appear in the widget on the screen. -The third and fourth parameters are the size of the destination surface. +### Preference dialog -- 3-11: The drawing function. -- 4-5: Sets the source to be white and paint the destination white. -- 7: Sets the line width to be 2. -- 8: Sets the source to be black. -- 9: Adds a rectangle to the mask. -- 10: Draws the rectangle with black color to the destination. +Preference dialog xml definition is added to `tfe.ui`. -Compile and run it, then a window with a black rectangle (square) appears. -Try resizing the window. -The square always appears at the center of the window because the drawing function is invoked every moment the window is resized. +~~~xml + + Preferences + FALSE + TRUE + win + + + + + GTK_ORIENTATION_HORIZONTAL + 12 + 12 + 12 + 12 + 12 + + + Font: + 1 + + + + + + + + + + + +~~~ -![Square in the window](../image/da1.png) +- Preference dialog is an independent dialog. +It is not a descendant widget of the top GtkApplicationwindow `win`. +Therefore, no child tag of the dialog object. +- There are four properties of the dialog specified. +GtkDialog is a child object (not child widget) of GtkWindow, so it inherits all the properties from GtkWindow. +Title, resizable, modal and transient-for properties are inherited from GtkWindow. +Transient-for specifies a temporary parent window, which the dialog's location is based on. +- internal-child attribute is used in the child tag above.. +GtkDialog has a GtkBox child widget. +Its id is "content_area" in `gtkdialog.ui`, which is the ui file of GtkDialog in gtk4 source files. +This box is provided to users to add content widgets in it. +The tag `` is put at the top of the contents. +Then you need to specify an object tag and define its class as GtkBox and its id as content_area. +This object is defined in `gtkdialog.ui` but you need to define it again in the child tag. +- In the content area, defines GtkBox, GtkLabel and GtkFontButton. + +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 is accomplished by returning TRUE by the signal handler. + +~~~C +pref_close_cb (GtkDialog *pref, gpointer user_data) { + return TRUE; +} + +g_signal_connect (GTK_DIALOG (pref), "close-request", G_CALLBACK (pref_close_cb), NULL); +~~~ + +Generally, signal emission consists of five stages. + +1. Default handler is invoked if the signal's flag is `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`. +2. Signal handlers are invoked, unless it is connected by `g_signal_connect_after`. +3. Default handler is invoked if the signal's flag is `G_SIGNAL_RUN_LAST`. +4. Signal handlers are invoked, if it is connected by `g_signal_connect_after`. +5. Default handler is invoked if the signal's flag is `G_SIGNAL_RUN_CLEANUP`. + +In the case of "close-request" signal, the default handler's flag is `G_SIGNAL_RUN_LAST`. +The handler `pref_close_cb` is not connected by `g_signal_connect_after`. +So the number of stages are two. + +1. Signal handler `pref_close_cb` is invoked. +2. Default handler is invoked. + +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`. + +~~~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_hide (GTK_WIDGET (dialog)); + return TRUE; +} + +... ... + +static void +pref_activated (GSimpleAction *action, GVariant *parameter, gpointer nb) { + gtk_widget_show (GTK_WIDGET (pref)); +} + +... ... + +/* ----- quit application ----- */ +void +tfe_application_quit (GtkWindow *win) { + if (pref_close_request_handler_id > 0) + g_signal_handler_disconnect (pref, pref_close_request_handler_id); + if (alert_close_request_handler_id > 0) + g_signal_handler_disconnect (alert, alert_close_request_handler_id); + g_clear_object (&settings); + gtk_window_destroy (GTK_WINDOW (alert)); + gtk_window_destroy (GTK_WINDOW (pref)); + gtk_window_destroy (win); +} + +... ... + +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); + + ... ... +} +~~~ + +The function `tfe_application_quit` destroys top level windows and quits the application. +It first disconnects the handlers from the signal "close-request". + +### Alert dialog + +If a user closes a page which hasn't been saved, it is advisable to show an alert to confirm it. +Alert dialog is used in this application for such a situation. + +~~~xml + + Are you sure? + FALSE + TRUE + win + + + + + GTK_ORIENTATION_HORIZONTAL + 12 + 12 + 12 + 12 + 12 + + + dialog-warning + GTK_ICON_SIZE_LARGE + + + + + + + + + + + + + Cancel + + + + + Close + + + + btn_cancel + btn_accept + + + +~~~ + +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 icon named "dialog-warning" is something like this. + +![dialog-warning icon is like ...](../image/dialog_warning.png){width=4.19cm height=1.62cm} + +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 by the program later. + +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. +Btn\_cancel button emits response signal with cancel response (GTK_RESPONSE_CANCEL) if it is clicked on. +Btn\_accept button 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. + +## Close and quit handlers + +If a user closes a page or quits the application without saving the contents, the application alerts. + +~~~C +static gboolean is_quit; + +... ... + +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 (close_btn_close, "Close"); + gtk_widget_show (GTK_WIDGET (alert)); + } +} + +... ... + +static void +close_activated (GSimpleAction *action, GVariant *parameter, gpointer nb) { + close_cb (GTK_NOTEBOOK (nb)); +} + +... ... + +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_hide (GTK_WIDGET (alert)); + if (response_id == GTK_RESPONSE_ACCEPT) { + if (is_quit) + tfe_application_quit (GTK_WINDOW (win)); + else + notebook_page_close (nb); + } +} + +static void +quit_activated (GSimpleAction *action, GVariant *parameter, gpointer nb) { + GtkWidget *win = gtk_widget_get_ancestor (GTK_WIDGET (nb), GTK_TYPE_WINDOW); + + is_quit = true; + if (has_saved_all (GTK_NOTEBOOK (nb))) + tfe_application_quit (GTK_WINDOW (win)); + else { + gtk_label_set_text (lb_alert, "Contents aren't saved yet.\nAre you sure to quit?"); + gtk_button_set_label (close_btn_close, "Quit"); + gtk_widget_show (GTK_WIDGET (alert)); + } +} + +static void +tfe_startup (GApplication *application) { + + ... ... + + alert = GTK_DIALOG (gtk_builder_get_object (build, "alert")); + alert_close_request_handler_id = g_signal_connect (GTK_DIALOG (alert), "close-request", G_CALLBACK (dialog_close_cb), NULL); + lb_alert = GTK_LABEL (gtk_builder_get_object (build, "lb_alert")); + btn_accept = GTK_BUTTON (gtk_builder_get_object (build, "btn_accept")); + + ... ... + +} +~~~ + +The static variable `is_quit` is true when user tries to quit the application and FAULT otherwise. +When user presses "Ctrl-w", `close_activated` handler is invoked. +It just calls `close_cb`. +When user clicks on the close button, `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 sets the text in the label and button, then shows the alert dialog. + +The response signal of the dialog is connected to the handler `alert_response_cb`. +It hides the dialog first. +Then check the response\_id. +If it is `GTK_RESPONSE_ACCEPT`, which means user clicked on the close button, then closes the current page. + +When user press "Ctrl-q" or clicked on the quit menu, then `quit_activated` handler is invoked. +The handler sets `is_quit` to true. +The function `has_saved_all` returns true if all the pages have been saved. +If it is true, it calls `tfe_application_quit` to quit the application. +Otherwise, it sets the text in the label and button, then shows the alert dialog. + +Now `alert_response_cb` works similar but it calls `tfe_application_quit`instead of `notebook_page_close`. + +The static variables `alert`, `lb_alert` and `btn_accept` are set in the startup handler. +And the signal "close-request" and `dialog_close_cb` handler are connected. + +@@@include +tfe6/tfenotebook.c has_saved has_saved_all +@@@ + +- 1-14: `has_saved` function. +- 10: The 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 the buffer is generated. +It is reset to false when it is replaced with a new contents from a file or it is saved to a file. +- 11-13: This function returns true if the contents of the current page has been saved and no modification has been made. +If it returns false, then the user tries to close the current page without saving the modified contents. +- 16-33: `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. + +## Notebook page tab + +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. + +~~~C +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); +} +~~~ + +When a page is built, connect "change-file" and "modified-changed" signals to `file_changed_cb` and `modified_changed_cb` handlers respectively. + +@@@include +tfe6/tfenotebook.c file_changed_cb modified_changed_cb +@@@ + +- 1-20: `file_changed_cb` handler. +- 9-10: If the signal emits during the page is being built, it is possible that `tv` isn't a descendant of `nb`. +That is, there's no page corresponds to `tv`. +Then, it isn't necessary to change the name of the tab because no tab exists. +- 13-15: If `file` is GFile, then it gets the filename and unrefs `file`. +- 16-17: Otherwise, `file` is probably NULL and it assigns "Untitled" related name to `filename` +- 18-19: Generates GtkLabel with `filename` and sets the tab of the page with the GtkLabel. +- 22-40: `modified_changed_cb` handler. +- 31-32: If `tv` isn't a descendant of `nb`, then nothing needs to be done. +- 33-35: If the content is modified, then it gets the text of the tab and adds asterisk at the beginning of the text. +- 36-37: Sets the tab with the asterisk prepended text. +- 38-39: Otherwise the modified bit is off. +It is because content is saved. +It calls `file_changed_cb` and resets the filename, that means it leaves out the asterisk. + +## Font + +### GtkFontButton and GtkFontChooser + +The GtkFontButton is a button which displays the current font. +It opens a font chooser dialog if a user clocked 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 inserted to the preference dialog in the initialization process. + +~~~C +static void +font_set_cb (GtkFontButton *fontbtn, gpointer user_data) { + GtkWindow *win = GTK_WINDOW (user_data); + PangoFontDescription *pango_font_desc; + + pango_font_desc = gtk_font_chooser_get_font_desc (GTK_FONT_CHOOSER (fontbtn)); + set_font_for_display_with_pango_font_desc (win, pango_font_desc); +} + +static void +tfe_startup (GApplication *application) { + + ... ... + + fontbtn = GTK_FONT_BUTTON (gtk_builder_get_object (build, "fontbtn")); + g_signal_connect (fontbtn, "font-set", G_CALLBACK (font_set_cb), win); + + ... ... + +} +~~~ + +In the startup handler, set the variable `fontbtn` to point the GtkFontButton object. +Then connect the "font-set" signal to `font_set_cb` handler. +The signal "font-set" is emitted when the user selects a font. + +GtkFontChooser is an interface implemented by GtkFontButton. +The function `gtk_font_chooser_get_font_desc` gets the PangoFontDescription of the currently-selected font. + +Another function `gtk_font_chooser_get_font' returns a font name which includes family, style, weight and size. +I thought it might be applied to tfe editor. +The font name can be used to the `font` property of GtkTextTag as it is. +But it can't be used to the CSS without converting the string to fit. +CSS is appropriate to change the font of entire text in all the buffers. +I think GtkTextTag is less appropriate. +If you know a good solution, please post it to [issue](https://github.com/ToshioCP/Gtk4-tutorial/issues) and let me know. + +It takes many codes to set the CSS from the PangoFontDescription so the task is left to the function `set_font_for_display_with_pango_font_desc`. + +### CSS and Pango + +A new file `css.c` is made for functions related to CSS. + +@@@include +tfe6/css.c +@@@ + +- 3-11: `set_css_for_display`. +This function sets CSS for GdkDisplay. +The content of the function is the same as the part of startup handler in the previous version of `tfeapplication.c`. +- 13-20: `set_font_for_display`. +This function sets CSS with font-family, font-style, font-weight and font-size. + - font-family is a name of font. For example, sans-serif, monospace, Helvetica and "Noto Sans" are font-family. +It is recommended to quote font family names that contain white space, digits, or punctuation characters other than hyphens. + - font-style is one of normal, italic and oblique. + - font-weight specifies the thickness of a font. +It is normal or bold. +It can be specified with a number between 100 and 900. +Normal is the same as 400. +Bold is 700. + - font-size specifies the size of a font. +Small, medium, large and 12pt are font-size. +- 17: Makes CSS text. +The function `g_strdup_printf` generates a new string with printf-like formatting. +- 22-91: `set_font_for_display_with_pango_font_desc`. +This function takes out font-family, font-style, font-weight and font-size from the PangoFontDescription object. +- 31: Gets the font-family of `pango_font_desc`. +- 32-46: Gets the font-style of `pango_font_desc`. +The functions `pango_font_description_get_style` returns a enumerated value. +- 47-88: Gets the font-weight of `pango_font_desc`. +The function `pango_font_description_get_weight` returns a enumerated value. +They corresponds to the numbers from 100 to 900. +- 89: Gets the font-size of `pango_font_desc`. +The function `pango_font_description_get_size` returns the size of a font. +The unit of this size is (1/PANGO_SCALE)pt. +If the font size is 10pt, the function returns 10*PANGO_SCALE. +PANGO_SCALE is defined as 1024. +Therefore, 10*PANGO_SCALE is 10240. +-90: calls `set_font_for_display` to set CSS for the GdkDisplay. + +## GSettings + +We want to maintain the font data after the application quits. +There are some ways to implement it. + +- Make a configuration file. +For example, a text file "~/.config/tfe/font.cfg" keeps font information. +- Use GSettings object. +The basic idea of GSettings are similar to configuration file. +Configuration information data is put into a database file. + +The coding with GSettings object is simple and easy. +However, it is a bit hard to understand the concept. +This subsection describes the concept first and then how to program. + +### GSettings schema + +GSettings schema describes a set of keys, value types and some other information. +GSettings object uses this schema and it writes/reads the value of a key to/from the right place in the database. + +- A schema has an id. +The id must be unique. +We often use the same string as application id, but schema id and application id are different. +You can use different name from application id. +Schema id is a string delimited by periods. +For example, "com.github.ToshioCP.tfe" is a correct schema id. +- A schema usually has a path. +The path is a location in the database. +Each key is stored under the path. +For example, if a key `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. +- GSettings save information as key-value style. +Key is a string begins with lower case characters followed by lower case, digit or dash `-` and ends with lower case or digit. +No consecutive dashes are allowed. +Values can be any type. +GSettings stores values as GVariant type, which may contain integer, double, boolean, string or complex types like an array. +The type of values needs to be defined in the schema. +- A default value needs to be set for each key. +- A summery and description can be set for each key. + +Schemas are described in an XML format. +For example, + +@@@include +tfe6/com.github.ToshioCP.tfe.gschema.xml +@@@ + +- 4: The type attribute is "s". +It is [GVariant format string](https://developer.gnome.org/glib/stable/gvariant-format-strings.html). +Other common types are: + - "b" gboolean + - "i" gint32. + - "d" double +Further information is in the website `GVariant format string` above. + +### gsettings + +First, let's try `gsetting` 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 +~~~ + +![gnome-calculator basic mode](../image/gnome_calculator_basic){width=5.34cm height=5.97cm} + + +Then, change the mode to advanced and quit. + +![gnome-calculator advanced mode](../image/gnome_calculator_advanced){width=10.74cm height=7.14cm} + +Run gsettongs and check whether the value of `button-mode` changes. + +~~~ +$ gsettings list-recursively org.gnome.calculator + +... ... + +org.gnome.calculator button-mode 'advanced' + +... ... + +~~~ + +Now we know that Gnome Calculator used gsettings and it sets `button-mode` key to "advanced" which is the current mode. +The value remains even the calculator quits. +So when the calculator is run again, it will appear as an advanced mode calculator. + +### glib-compile-schemas + +GSettings schemas are specified with XML format. +The XML schema files must have the filename extension `.gschema.xml`. +The following is the XML schema file for the application `tfe`. + +@@@include +tfe6/com.github.ToshioCP.tfe.gschema.xml +@@@ + +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. + +- 2: The top level element is ``. +- 3: schema tag has `path` and `id` attributes. +A path determines where the settings are stored in the conceptual global tree of settings. +An id identifies the schema. +- 4: Key tag has two attributes. +Name is the name of the key. +Type us the type of the key' value and specified with [GVariant format string](https://developer.gnome.org/glib/stable/gvariant-format-strings.html). +- 5: default value of the key `font` is `Monospace 12`. +- 6: Summery and description elements describes the key. + +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` so that GSettings objet can find `gschemas.compiled`. + +~~~ +$ GSETTINGS_SCHEMA_DIR=(the directory gschemas.compiled is located):$GSETTINGS_SCHEMA_DIR (your application name) +~~~ + +This is because GSettings object searches `GSETTINGS_SCHEMA_DIR` for `gschemas.compiled`. + +GSettings object looks for this file by the following process. + +- It searches `glib-2.0/schemas` subdirectories of all the directories specified in `XDG_DATA_DIRS`. +Most common directory is `/usr/share/glib-2.0/schemas`. +- If `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. + +1. Make `.gschema.xml` file. +2. Copy it to one of the directories above. For example, `/usr/share/glib-2.0/schemas`. +3. Run `glib-compile-schemas' on the directory above. + +### Meson.build + +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. + +~~~meson +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 +~~~ + +### GSettings object and g_setting_bind + +Write gsettings related codes to `tfeapplication.c'. + +~~~C +... ... +static GSettings *settings; +... ... + +void +tfe_application_quit (GtkWindow *win) { + ... ... + g_clear_object (&settings); + ... ... +} + +static void +tfe_startup (GApplication *application) { + ... ... + settings = g_settings_new ("com.github.ToshioCP.tfe"); + g_settings_bind (settings, "font", fontbtn, "font", G_SETTINGS_BIND_DEFAULT); + ... ... +} +~~~ + +Static variable `settings` keeps a pointer to GSettings instance. +Before application quits, the application releases the GSettings instance. +The function `g_clear_object` is used. + +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. + +You need to make an effort to understand GSettings concept, but coding is very simple. +Just generate a GSettings object and bind it to a property of an object. + +## Installation + +It is a good idea to install your application in `$HOME/local/bin` directory if you have installed gtk4 under the instruction in Section 2. +Then you need to put `--prefix=$HOME/local` option to meson. + +~~~ +$ meson --prefix=$HOME/local _build +~~~ + +Add install option and set it true in executable function. + +~~~meson +executable('tfe', sourcefiles, resources, dependencies: gtkdep, export_dynamic: true, install: true) +~~~ + +Then, you can install your application by: + +~~~ +$ ninja -C _build install +~~~ + +However, you need to do one more thing. +Copy your XML file to `$HOME/local/share/glib-2.0/schemas/`, which is specified in `GSETTINGS_SCHEMA_DIR` environment variable, +and run `glib-compile-schemas` on that directory. + +~~~meson +schema_dir = get_option('prefix') / get_option('datadir') / 'glib-2.0/schemas/' +install_data('com.github.ToshioCP.tfe.gschema.xml', install_dir: schema_dir) +~~~ + +- get_option: This function returns the value of build options. +The default value of the option 'prefix' is "/usr/local", but it is "$HOME/local" because we have run meson with prefix option. +The default value of the option 'datadir' is "share". +The operator '/' connects the strings with '/' separator. +So, `$HOME/local/share/glib-2.0/schemas` is assigned to the varable `schema_dir`. +- install_data: This function installs the data to the install directory. + +Meson can runs a post compile script. + +~~~meson +meson.add_install_script('glib-compile-schemas', schema_dir) +~~~ + +This method runs 'glib-compile-schemas' with an argument `schema_dir`. + +@@@include +tfe6/meson.build +@@@ + +Source files of `tfe` is under [src/tfe6](tfe6) directory. +Copy them to your temporary directory and try to compile and install. + +~~~ +$ meson --prefix=$HOME/local _build +$ ninja -C _build +$ GSETTINGS_SCHEMA_DIR=_build:$GSETTINGS_SCHEMA_DIR _build/tfe +$ ninja -C _build install +$ tfe +$ ls $HOME/local/bin +... ... +... tfe +... ... +$ ls $HOME/local/share/glib-2.0/schemas +com.github.ToshioCP.tfe.gschema.xml +gschema.dtd +gschemas.compiled +... ... +~~~ + +![tfe6](../image/tfe6.png){width=9.06cm height=6.615cm} diff --git a/src/sec2.src.md b/src/sec2.src.md index 6cc458f..161289d 100644 --- a/src/sec2.src.md +++ b/src/sec2.src.md @@ -162,9 +162,11 @@ Modify `env.sh`. Include this file by . (dot) command before using gtk4 libraries. You may think you can add them in your `.profile`. -I think the environment variables above are necessary only when you compile gtk4 applications. -And it's not necessary except the case above and it might cause some bad things. -Therefore, I recommend you not to write them to your `.profile`. +But it's a wrong decision. +Never write them to your `.profile`. +The environment variables above are necessary only when you compile and run gtk4 applications. +Otherwise it's not necessary. +If you changed the environment variables above and run gtk3 applications, it probably causes serious damage. ## Compiling gtk4 applications diff --git a/src/sec20.src.md b/src/sec20.src.md index c5ddad3..0f31154 100644 --- a/src/sec20.src.md +++ b/src/sec20.src.md @@ -1,153 +1,129 @@ -# Combine GtkDrawingArea and TfeTextView +# GtkDrawingArea and Cairo -Now, we will make a new application which has GtkDrawingArea and TfeTextView in it. -Its name is "color". -If you write a color in TfeTextView and click on the `run` button, then the color of GtkDrawingArea changes to the color given by you. +If you want to draw dynamically, like an image window of gimp graphics editor, GtkDrawingArea widget is the most suitable widget. +You can draw or redraw an image in this widget freely. +It is called custom drawing. -![color](../image/color.png){width=7.0cm height=5.13cm} +GtkDrawingArea provides a cairo context so users can draw images by cairo functions. +In this section, I will explain: -The following colors are available. +1. Cairo, but briefly. +2. GtkDrawingArea with very simple example. -- white -- black -- red -- green -- blue +## Cairo -In addition the following two options are also available. +Cairo is a two dimensional graphics library. +First, you need to know surface, source, mask, destination, cairo context and transformation. -- light: Make the color of the drawing area lighter. -- dark: Make the color of the drawing area darker. +- Surface represents an image. +It is like a canvas. +We can draw shapes and images with different colors on surfaces. +- Source pattern, or simply source, is a kind of paint, which will be transferred to destination surface by cairo functions. +- Mask is image mask used in the transference. +- Destination is a target surface. +- Cairo context manages the transference from source to destination through mask with its functions. +For example, `cairo_stroke` is a function to draw a path to the destination by the transference. +- Transformation is applied before the transfer completes. +The transformation is called affine, which is a mathematics terminology, and represented by matrix multiplication and vector addition. +Scaling, rotation, reflection, shearing and translation are examples of affine transformation. +In this section, we don't use it. +That means we only use identity transformation. +Therefore, the coordinate in source and mask is the same as the coordinate in destination. -This application can only do very simple things. -However, it tells us that if we add powerful parser to it, we will be able to make it more efficient. -I want to show it to you in the later section by making a turtle graphics language like Logo program language. +![Stroke a rectangle](../image/cairo.png){width=9.0cm height=6.0cm} -In this section, we focus on how to bind the two objects. +The instruction is as follows: -## Color.ui and color.gresource.xml +1. Create a surface. +This will be a destination. +2. Create a cairo context with the surface and the surface will be the destination of the context. +3. Create a source pattern within the context. +4. Create paths, which are lines, rectangles, arcs, texts or more complicated shapes, to generate a mask. +5. Use drawing operator such as `cairo_stroke` to transfer the paint in the source to the destination. +6. Save the destination surface to a file if necessary. -First, We need to make the ui file of the widgets. -The image in the previous subsection gives us the structure of the widgets. -Title bar, four buttons in the tool bar and two widgets textview and drawing area. -The ui file is as follows. +Here's a simple example code that draws a small square and save it as a png file. @@@include -color/color.ui +misc/cairo.c @@@ -- 9-53: This part describes the tool bar which has four buttons, `Run`, `Open`, `Save` and `Close`. -This is similar to the toolbar of tfe text editor in [Section 8](sec8.src.md). -There are two differences. -`Run` button replaces `New` button. -Signal element are added to each button object. -It has "name" attribute which is a signal name and "handler" attribute which is the name of its signal handler function. -Options "-WI, --export-dynamic" CFLAG is necessary when you compile the application. -You can achieve this by adding "export_dynamic: true" argument to executable function in `meson.build`. -And be careful that the handler must be defined without 'static' class. -- 54-76: Puts GtkScrolledWindow and GtkDrawingArea into GtkBox. -GtkBox has "homogeneous property with TRUE value, so the two children have the same width in the box. -TfeTextView is a child of GtkScrolledWindow. +- 1: Includes the header file of cairo. +- 12: `cairo_image_surface_create` creates an image surface. +`CAIRO_FORMAT_RGB24` is a constant which means that each pixel has red, green and blue data. +Each data has 8 bit quantity. +Modern displays have this type of color depth. +Width and height are pixels and given as integers. +- 13: Creates cairo context. +The surface given as an argument will be the destination of the context. +- 17: `cairo_set_source_rgb` creates a source pattern, which is a solid white paint. +The second to fourth argument is red, green and blue color depth respectively. +Their type is float and the values are between zero and one. +(0,0,0) is black and (1,1,1) is white. +- 18: `cairo_paint` copies everywhere in the source to destination. +The destination is filled with white pixels by this command. +- 20: Sets the source color to black. +- 21: `cairo_set_line_width` set the width of lines. +In this case, the line width is set to two pixels. +(It is because the transformation is identity. +If the transformation isn't identity, for example scaling with the factor three, the actual width in destination will be six (2x3=6) pixels.) +- 22: Draws a rectangle (square). +The top-left coordinate is (width/2.0-20.0, height/2.0-20.0) and the width and height have the same length 40.0. +- 23: `cairo_stroke` transfer the source to destination through the rectangle in mask. +- 26: Outputs the image to a png file `rectangle.png`. +- 27: Destroys the context. At the same time the source is destroyed. +- 28: Destroys the destination surface. -The xml file for the resource compiler is almost same as before. -Just substitute "color" for "tfe". +To compile this, type the following. + + $ gcc `pkg-config --cflags cairo` cairo.c `pkg-config --libs cairo` + +![rectangle.png](misc/rectangle.png) + +There are lots of documentations in [Cairo's website](https://www.cairographics.org/). +If you aren't familiar with cairo, it is strongly recommended to read the [tutorial](https://www.cairographics.org/tutorial/) in the website. + +## GtkDrawingArea + +The following is a very simple example. @@@include -color/color.gresource.xml +misc/da1.c @@@ -## Tfetextview.h, tfetextview.c and color.h +The function `main` is almost same as before. +The two functions `on_activate` and `draw_function` is important in this example. -First two files are the same as before. -Color.h just includes tfetextview.h. +- 16: Generates a GtkDrawingArea object. +- 20,21: Sets the width and height of the contents of the GtkDrawingArea widget. +These width and height is the size of the destination surface of the cairo context provided by the widget. +- 22: Sets a drawing function of the widget. +GtkDrawingArea widget uses the function to draw the contents of itself whenever its necessary. +For example, when a user drag a mouse pointer and resize a top level window, GtkDrawingArea also changes the size. +Then, the whole window needs to be redrawn. -@@@include -color/color.h -@@@ +The drawing function has five parameters. -## Colorapplication.c + void drawing_function (GtkDrawingArea *drawing_area, cairo_t *cr, int width, int height, + gpointer user_data); -This is the main file. -It deals with: +The first parameter is the GtkDrawingArea widget which calls the drawing function. +However, you can't change any properties, for example `content-width` or `content-height`, in this function. +The second parameter is a cairo context given by the widget. +The destination surface of the context is connected to the contents of the widget. +What you draw to this surface will appear in the widget on the screen. +The third and fourth parameters are the size of the destination surface. -- Building widgets by GtkBuilder. -- Seting a drawing function of GtkDrawingArea. -And connecting a handler to "resize" signal on GtkDrawingArea. -- Implementing each call back functions. -Particularly, `Run` signal handler is the point in this program. +- 3-11: The drawing function. +- 4-5: Sets the source to be white and paint the destination white. +- 7: Sets the line width to be 2. +- 8: Sets the source to be black. +- 9: Adds a rectangle to the mask. +- 10: Draws the rectangle with black color to the destination. -The following is `colorapplication.c`. +Compile and run it, then a window with a black rectangle (square) appears. +Try resizing the window. +The square always appears at the center of the window because the drawing function is invoked every moment the window is resized. -@@@include -color/colorapplication.c -@@@ +![Square in the window](../image/da1.png) -- 108-121: The function `main` is almost same as before but there are some differences. -The application ID is "com.github.ToshioCP.color". -`G_APPLICATION_FLAGS_NONE` is specified so no open signal handler is necessary. -- 86-106: Startup handler. -- 91-96: Builds widgets. -The pointers of the top window, TfeTextView and GtkDrawingArea objects are stored to static variables `win`, `tv` and `da` respectively. -This is because these objects are often used in handlers. -They never be rewritten so they're thread safe. -- 97: connects "resize" signal and the handler. -- 98: sets the drawing function. -- 81-84: Activates handler, which just shows the widgets. -- 73-79: The drawing function. -It just copies `surface` to destination. -- 65-71: Resize handler. -Re-creates the surface to fit the width and height of the drawing area and paints by calling the function `run`. -- 58-63: Closes the handler. -It destroys `surface` if it exists. -Then it destroys the top window and quits the application. -- 48-56: Open and save handler. -They just call the corresponding functions of TfeTextView. -- 42-46: Run handler. -It calls run function to paint the surface. -After that `gtk_widget_queue_draw` is called. -This fhunction adds the widget (GtkDrawingArea) to the queue to be redrawn. -It is important to know that the drawing function is called when it is necessary. -For example, when another window is moved and uncovers part of the widget, or when the window containing it is resized. -But repaint of `surface` is not automatically notified to gtk. -Therefore, you need to call `gtk_widget_queue_draw` to redraw the widget. -- 9-40: Run function paints the surface. -First, it gets the contents of GtkTextBuffer. -Then it compares it to "red", "green" and so on. -If it matches the color, then the surface is painted the color. -If it matches "light" or "dark", then the color of the surface is lightened or darkened respectively. -Alpha channel is used. - -## Meson.build - -This file is almost same as before. -An argument "export_dynamic: true" is added to executable function. - -@@@include -color/meson.build -@@@ - -## Compile and execute it - -First you need to export some variables (refer to [Section 2](sec2.src.md)). - - $ . env.sh - -Then type the following to compile it. - - $ meson _build - $ ninja -C _build - -The application is made in `_build` directory. -Type the following to execute it. - - $ _build/color - -Type "red", "green", "blue", "white", black", "light" or "dark" in the TfeTextView. -Then, click on `Run` button. -Make sure the color of GtkDrawingArea changes. - -In this program TfeTextView is used to change the color. -You can use buttons or menus instead of textview. -Probably it is more appropriate. -Using textview is unnatural. -It is a good practice to make such application by yourself. diff --git a/src/sec21.src.md b/src/sec21.src.md new file mode 100644 index 0000000..30f9774 --- /dev/null +++ b/src/sec21.src.md @@ -0,0 +1,153 @@ +# Combine GtkDrawingArea and TfeTextView + +Now, we will make a new application which has GtkDrawingArea and TfeTextView in it. +Its name is "color". +If you write a color in TfeTextView and click on the `run` button, then the color of GtkDrawingArea changes to the color given by you. + +![color](../image/color.png){width=7.0cm height=5.13cm} + +The following colors are available. + +- white +- black +- red +- green +- blue + +In addition the following two options are also available. + +- light: Make the color of the drawing area lighter. +- dark: Make the color of the drawing area darker. + +This application can only do very simple things. +However, it tells us that if we add powerful parser to it, we will be able to make it more efficient. +I want to show it to you in the later section by making a turtle graphics language like Logo program language. + +In this section, we focus on how to bind the two objects. + +## Color.ui and color.gresource.xml + +First, We need to make the ui file of the widgets. +The image in the previous subsection gives us the structure of the widgets. +Title bar, four buttons in the tool bar and two widgets textview and drawing area. +The ui file is as follows. + +@@@include +color/color.ui +@@@ + +- 10-53: This part describes the tool bar which has four buttons, `Run`, `Open`, `Save` and `Close`. +This is similar to the toolbar of tfe text editor in [Section 8](sec8.src.md). +There are two differences. +`Run` button replaces `New` button. +A signal element is added to each button object. +It has "name" attribute which is a signal name and "handler" attribute which is the name of its signal handler function. +Options "-WI, --export-dynamic" CFLAG is necessary when you compile the application. +You can achieve this by adding "export_dynamic: true" argument to executable function in `meson.build`. +And be careful that the handler must be defined without 'static' class. +- 54-76: Puts GtkScrolledWindow and GtkDrawingArea into GtkBox. +GtkBox has "homogeneous property" with TRUE value, so the two children have the same width in the box. +TfeTextView is a child of GtkScrolledWindow. + +The xml file for the resource compiler is almost same as before. +Just substitute "color" for "tfe". + +@@@include +color/color.gresource.xml +@@@ + +## Tfetextview.h, tfetextview.c and color.h + +First two files are the same as before. +Color.h just includes tfetextview.h. + +@@@include +color/color.h +@@@ + +## Colorapplication.c + +This is the main file. +It deals with: + +- Building widgets by GtkBuilder. +- Seting a drawing function of GtkDrawingArea. +And connecting a handler to "resize" signal on GtkDrawingArea. +- Implementing each call back functions. +Particularly, `Run` signal handler is the point in this program. + +The following is `colorapplication.c`. + +@@@include +color/colorapplication.c +@@@ + +- 108-121: The function `main` is almost same as before but there are some differences. +The application ID is "com.github.ToshioCP.color". +`G_APPLICATION_FLAGS_NONE` is specified so no open signal handler is necessary. +- 86-106: Startup handler. +- 91-96: Builds widgets. +The pointers of the top window, TfeTextView and GtkDrawingArea objects are stored to static variables `win`, `tv` and `da` respectively. +This is because these objects are often used in handlers. +They never be rewritten so they're thread safe. +- 97: connects "resize" signal and the handler. +- 98: sets the drawing function. +- 81-84: Activates handler, which just shows the widgets. +- 73-79: The drawing function. +It just copies `surface` to destination. +- 65-71: Resize handler. +Re-creates the surface to fit the width and height of the drawing area and paints by calling the function `run`. +- 58-63: Closes the handler. +It destroys `surface` if it exists. +Then it destroys the top window and quits the application. +- 48-56: Open and save handler. +They just call the corresponding functions of TfeTextView. +- 42-46: Run handler. +It calls run function to paint the surface. +After that `gtk_widget_queue_draw` is called. +This fhunction adds the widget (GtkDrawingArea) to the queue to be redrawn. +It is important to know that the drawing function is called when it is necessary. +For example, when another window is moved and uncovers part of the widget, or when the window containing it is resized. +But repaint of `surface` is not automatically notified to gtk. +Therefore, you need to call `gtk_widget_queue_draw` to redraw the widget. +- 9-40: Run function paints the surface. +First, it gets the contents of GtkTextBuffer. +Then it compares it to "red", "green" and so on. +If it matches the color, then the surface is painted the color. +If it matches "light" or "dark", then the color of the surface is lightened or darkened respectively. +Alpha channel is used. + +## Meson.build + +This file is almost same as before. +An argument "export_dynamic: true" is added to executable function. + +@@@include +color/meson.build +@@@ + +## Compile and execute it + +First you need to export some variables (refer to [Section 2](sec2.src.md)). + + $ . env.sh + +Then type the following to compile it. + + $ meson _build + $ ninja -C _build + +The application is made in `_build` directory. +Type the following to execute it. + + $ _build/color + +Type "red", "green", "blue", "white", black", "light" or "dark" in the TfeTextView. +Then, click on `Run` button. +Make sure the color of GtkDrawingArea changes. + +In this program TfeTextView is used to change the color. +You can use buttons or menus instead of textview. +Probably it is more appropriate. +Using textview is unnatural. +It is a good practice to make such application by yourself. diff --git a/src/sec8.src.md b/src/sec8.src.md index def7320..a7df680 100644 --- a/src/sec8.src.md +++ b/src/sec8.src.md @@ -59,18 +59,35 @@ Constructs beginning with `<` and ending with `>` are called tags. And there are two types of tags, start tag and end tag. For example, `` is a start tag and `` is an end tag. Ui file begins and ends with interface tags. -Some tags, for example, object tags can have a class and id attributes inside the start tag. +Some tags, for example, object tags can have a class and id attributes in the start tag. -- 2-5: An object with `GtkApplicationWindow` class and `win` id is defined. +- 1: The first line is XML declaration. +It specifies that the version of XML is 1.0 and the encoding is UTF-8. +Even if the line is left out, GtkBuilder builds objects from the ui file. +But ui files must use UTF-8 encoding, or GtkBuilder can't recognize it and fatal error occurs. +- 3-6: An object with `GtkApplicationWindow` class and `win` id is defined. This is the top level window. And the three properties of the window are defined. `title` property is "file editor", `default-width` property is 400 and `default-height` property is 300. -- 6: child tag means a child of the object above. +- 7: child tag means a child of the object above. For example, line 7 tells us that GtkBox object which id is "boxv" is a child of `win`. Compare this ui file and the lines 25-57 in the source code of `on_open` function. Those two describe the same structure of widgets. +You can check the ui file with `gtk4-builder-tool`. + +- `gtk4-builder-tool validate ` validates the ui file. +If the ui file includes some syntactical error, `gtk4-builder-tool` prints the error. +- `gtk4-builder-tool simplify ` simplifies the ui file and prints the result. +If `--replace` option is given, it replaces the ui file with the simplified one. +If the ui file specifies a value of property but it is default, then it will be removed. +Anf some values are simplified. +For example, "TRUE"and "FALSE" becomes "1" and "0" respectively. +However, "TRUE" or "FALSE" is better for maintenance. + +It is a good idea to check your ui file before compiling. + ## GtkBuilder GtkBuilder builds widgets based on the ui file. diff --git a/src/sec9.src.md b/src/sec9.src.md index 1967ac0..f453f20 100644 --- a/src/sec9.src.md +++ b/src/sec9.src.md @@ -56,11 +56,7 @@ tfe4/tfetextview.c tfe4/tfe.c @@@ -`tfe.ui` - -@@@include -tfe4/tfe.ui -@@@ +The ui file `tfe.ui` is the same as `tfe3.ui` in the previous section. `tfe.gresource.xml` diff --git a/src/tfe/tfe3.ui b/src/tfe/tfe3.ui index 39b54b9..16dc6f5 100644 --- a/src/tfe/tfe3.ui +++ b/src/tfe/tfe3.ui @@ -1,3 +1,4 @@ + file editor @@ -8,40 +9,40 @@ GTK_ORIENTATION_VERTICAL - GTK_ORIENTATION_HORIZONTAL + GTK_ORIENTATION_HORIZONTAL - 10 + 10 - New + New - Open + Open - TRUE + TRUE - Save + Save - Close + Close - 10 + 10 @@ -56,4 +57,3 @@ - diff --git a/src/tfe4/tfe.ui b/src/tfe4/tfe.ui index 39b54b9..16dc6f5 100644 --- a/src/tfe4/tfe.ui +++ b/src/tfe4/tfe.ui @@ -1,3 +1,4 @@ + file editor @@ -8,40 +9,40 @@ GTK_ORIENTATION_VERTICAL - GTK_ORIENTATION_HORIZONTAL + GTK_ORIENTATION_HORIZONTAL - 10 + 10 - New + New - Open + Open - TRUE + TRUE - Save + Save - Close + Close - 10 + 10 @@ -56,4 +57,3 @@ - diff --git a/src/tfe5/tfe.ui b/src/tfe5/tfe.ui index 15e63ff..e62e2e3 100644 --- a/src/tfe5/tfe.ui +++ b/src/tfe5/tfe.ui @@ -1,3 +1,4 @@ + file editor @@ -8,44 +9,40 @@ GTK_ORIENTATION_VERTICAL - GTK_ORIENTATION_HORIZONTAL + GTK_ORIENTATION_HORIZONTAL - 10 + 10 - _New - TRUE + New - _Open - TRUE + Open - TRUE + TRUE - _Save - TRUE + Save - _Close - TRUE + Close - 10 + 10 diff --git a/src/tfe5/tfeapplication.c b/src/tfe5/tfeapplication.c index 3eb10a6..8de78f3 100644 --- a/src/tfe5/tfeapplication.c +++ b/src/tfe5/tfeapplication.c @@ -1,46 +1,31 @@ #include "tfe.h" static void -open_clicked (GtkWidget *btno, GtkNotebook *nb) { +open_cb (GtkNotebook *nb) { notebook_page_open (nb); } static void -new_clicked (GtkWidget *btnn, GtkNotebook *nb) { +new_cb (GtkNotebook *nb) { notebook_page_new (nb); } static void -save_clicked (GtkWidget *btns, GtkNotebook *nb) { +save_cb (GtkNotebook *nb) { notebook_page_save (nb); } static void -close_clicked (GtkWidget *btnc, GtkNotebook *nb) { - GtkWidget *win; - GtkWidget *boxv; - gint i; - - if (gtk_notebook_get_n_pages (nb) == 1) { - boxv = gtk_widget_get_parent (GTK_WIDGET (nb)); - win = gtk_widget_get_parent (boxv); - gtk_window_destroy (GTK_WINDOW (win)); - } else { - i = gtk_notebook_get_current_page (nb); - gtk_notebook_remove_page (GTK_NOTEBOOK (nb), i); - } +close_cb (GtkNotebook *nb) { + notebook_page_close (GTK_NOTEBOOK (nb)); } static void tfe_activate (GApplication *application) { GtkApplication *app = GTK_APPLICATION (application); - GtkWidget *win; - GtkWidget *boxv; - GtkNotebook *nb; - - win = GTK_WIDGET (gtk_application_get_active_window (app)); - boxv = gtk_window_get_child (GTK_WINDOW (win)); - nb = GTK_NOTEBOOK (gtk_widget_get_last_child (boxv)); + GtkWidget *win = GTK_WIDGET (gtk_application_get_active_window (app)); + GtkWidget *boxv = gtk_window_get_child (GTK_WINDOW (win)); + GtkNotebook *nb = GTK_NOTEBOOK (gtk_widget_get_last_child (boxv)); notebook_page_new (nb); gtk_widget_show (GTK_WIDGET (win)); @@ -49,15 +34,11 @@ tfe_activate (GApplication *application) { static void tfe_open (GApplication *application, GFile ** files, gint n_files, const gchar *hint) { GtkApplication *app = GTK_APPLICATION (application); - GtkWidget *win; - GtkWidget *boxv; - GtkNotebook *nb; + GtkWidget *win = GTK_WIDGET (gtk_application_get_active_window (app)); + GtkWidget *boxv = gtk_window_get_child (GTK_WINDOW (win)); + GtkNotebook *nb = GTK_NOTEBOOK (gtk_widget_get_last_child (boxv)); int i; - win = GTK_WIDGET (gtk_application_get_active_window (app)); - boxv = gtk_window_get_child (GTK_WINDOW (win)); - nb = GTK_NOTEBOOK (gtk_widget_get_last_child (boxv)); - for (i = 0; i < n_files; i++) notebook_page_new_with_file (nb, files[i]); if (gtk_notebook_get_n_pages (nb) == 0) @@ -65,13 +46,12 @@ tfe_open (GApplication *application, GFile ** files, gint n_files, const gchar * gtk_widget_show (win); } - static void tfe_startup (GApplication *application) { GtkApplication *app = GTK_APPLICATION (application); + GtkBuilder *build; GtkApplicationWindow *win; GtkNotebook *nb; - GtkBuilder *build; GtkButton *btno; GtkButton *btnn; GtkButton *btns; @@ -85,10 +65,10 @@ tfe_startup (GApplication *application) { 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 (btno, "clicked", G_CALLBACK (open_clicked), nb); - g_signal_connect (btnn, "clicked", G_CALLBACK (new_clicked), nb); - g_signal_connect (btns, "clicked", G_CALLBACK (save_clicked), nb); - g_signal_connect (btnc, "clicked", G_CALLBACK (close_clicked), nb); + 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); GdkDisplay *display; diff --git a/src/tfe5/tfenotebook.c b/src/tfe5/tfenotebook.c index 4055d0b..96fe5ba 100644 --- a/src/tfe5/tfenotebook.c +++ b/src/tfe5/tfenotebook.c @@ -10,45 +10,68 @@ get_untitled () { return g_strdup_printf ("Untitled%u", c); } -static void -file_changed (TfeTextView *tv, GtkNotebook *nb) { - GFile *file; - char *filename; - GtkWidget *scr; - GtkWidget *label; - - 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); - else - filename = get_untitled (); - label = gtk_label_new (filename); - gtk_notebook_set_tab_label (nb, scr, label); - g_object_unref (file); - g_free (filename); -} - -/* Save the contents in the current page */ -void -notebook_page_save(GtkNotebook *nb) { - gint i; +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); +} + +static void +file_changed_cb (TfeTextView *tv, GtkNotebook *nb) { + GtkWidget *scr; + GtkWidget *label; + GFile *file; + char *filename; + + 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 (nb, scr, label); +} + +void +notebook_page_save (GtkNotebook *nb) { + g_return_if_fail(GTK_IS_NOTEBOOK (nb)); + + TfeTextView *tv; + + tv = get_current_textview (nb); tfe_text_view_save (TFE_TEXT_VIEW (tv)); } +void +notebook_page_close (GtkNotebook *nb) { + g_return_if_fail(GTK_IS_NOTEBOOK (nb)); + + GtkWidget *win; + int i; + + if (gtk_notebook_get_n_pages (nb) == 1) { + win = gtk_widget_get_ancestor (GTK_WIDGET (nb), GTK_TYPE_WINDOW); + gtk_window_destroy(GTK_WINDOW (win)); + } else { + i = gtk_notebook_get_current_page (nb); + gtk_notebook_remove_page (GTK_NOTEBOOK (nb), i); + } +} + static void notebook_page_build (GtkNotebook *nb, GtkWidget *tv, char *filename) { - GtkWidget *scr; + GtkWidget *scr = gtk_scrolled_window_new (); GtkNotebookPage *nbp; GtkWidget *lab; - gint i; - scr = gtk_scrolled_window_new (); + int i; gtk_text_view_set_wrap_mode (GTK_TEXT_VIEW (tv), GTK_WRAP_WORD_CHAR); gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (scr), tv); @@ -57,11 +80,11 @@ notebook_page_build (GtkNotebook *nb, GtkWidget *tv, char *filename) { nbp = gtk_notebook_get_page (nb, scr); g_object_set (nbp, "tab-expand", TRUE, NULL); gtk_notebook_set_current_page (nb, i); - g_signal_connect (GTK_TEXT_VIEW (tv), "change-file", G_CALLBACK (file_changed), nb); + g_signal_connect (GTK_TEXT_VIEW (tv), "change-file", G_CALLBACK (file_changed_cb), nb); } static void -open_response (TfeTextView *tv, gint response, GtkNotebook *nb) { +open_response (TfeTextView *tv, int response, GtkNotebook *nb) { GFile *file; char *filename; diff --git a/src/tfe5/tfenotebook.h b/src/tfe5/tfenotebook.h index ab30489..cff881b 100644 --- a/src/tfe5/tfenotebook.h +++ b/src/tfe5/tfenotebook.h @@ -1,6 +1,9 @@ void notebook_page_save(GtkNotebook *nb); +void +notebook_page_close (GtkNotebook *nb); + void notebook_page_open (GtkNotebook *nb); diff --git a/src/tfe6/com.github.ToshioCP.tfe.gschema.xml b/src/tfe6/com.github.ToshioCP.tfe.gschema.xml new file mode 100644 index 0000000..90f26ca --- /dev/null +++ b/src/tfe6/com.github.ToshioCP.tfe.gschema.xml @@ -0,0 +1,10 @@ + + + + + 'Monospace 12' + Font + The font to be used for textview. + + + diff --git a/src/tfe6/css.c b/src/tfe6/css.c new file mode 100644 index 0000000..864b570 --- /dev/null +++ b/src/tfe6/css.c @@ -0,0 +1,91 @@ +#include "tfe.h" + +void +set_css_for_display (GtkWindow *win, char *css) { + GdkDisplay *display; + + display = gtk_widget_get_display (GTK_WIDGET (win)); + GtkCssProvider *provider = gtk_css_provider_new (); + gtk_css_provider_load_from_data (provider, css, -1); + gtk_style_context_add_provider_for_display (display, GTK_STYLE_PROVIDER (provider), GTK_STYLE_PROVIDER_PRIORITY_USER); +} + +void +set_font_for_display (GtkWindow *win, const char *fontfamily, const char *fontstyle, const char *fontweight, int fontsize) { + char *textview_css; + + textview_css = g_strdup_printf ("textview {padding: 10px; font-family: \"%s\"; font-style: %s; font-weight: %s; font-size: %dpt;}", + fontfamily, fontstyle, fontweight, fontsize); + set_css_for_display (win, textview_css); +} + +void +set_font_for_display_with_pango_font_desc (GtkWindow *win, PangoFontDescription *pango_font_desc) { + int pango_style; + int pango_weight; + const char *family; + const char *style; + const char *weight; + int fontsize; + + family = pango_font_description_get_family (pango_font_desc); + pango_style = pango_font_description_get_style (pango_font_desc); + switch (pango_style) { + case PANGO_STYLE_NORMAL: + style = "normal"; + break; + case PANGO_STYLE_ITALIC: + style = "italic"; + break; + case PANGO_STYLE_OBLIQUE: + style = "oblique"; + break; + default: + style = "normal"; + break; + } + pango_weight = pango_font_description_get_weight (pango_font_desc); + switch (pango_weight) { + case PANGO_WEIGHT_THIN: + weight = "100"; + break; + case PANGO_WEIGHT_ULTRALIGHT: + weight = "200"; + break; + case PANGO_WEIGHT_LIGHT: + weight = "300"; + break; + case PANGO_WEIGHT_SEMILIGHT: + weight = "350"; + break; + case PANGO_WEIGHT_BOOK: + weight = "380"; + break; + case PANGO_WEIGHT_NORMAL: + weight = "400"; /* or "normal" */ + break; + case PANGO_WEIGHT_MEDIUM: + weight = "500"; + break; + case PANGO_WEIGHT_SEMIBOLD: + weight = "600"; + break; + case PANGO_WEIGHT_BOLD: + weight = "700"; /* or "bold" */ + break; + case PANGO_WEIGHT_ULTRABOLD: + weight = "800"; + break; + case PANGO_WEIGHT_HEAVY: + weight = "900"; + break; + case PANGO_WEIGHT_ULTRAHEAVY: + weight = "900"; /* In PangoWeight definition, the weight is 1000. But CSS allows the weight below 900. */ + break; + default: + weight = "normal"; + break; + } + fontsize = pango_font_description_get_size (pango_font_desc) / PANGO_SCALE; + set_font_for_display (win, family, style, weight, fontsize); +} diff --git a/src/tfe6/css.h b/src/tfe6/css.h new file mode 100644 index 0000000..ec376bc --- /dev/null +++ b/src/tfe6/css.h @@ -0,0 +1,11 @@ +/* css.h */ + +void +set_css (GtkWindow *win, char *css); + +void +set_font_for_display (GtkWindow *win, const char *fontfamily, const char *fontstyle, const char *fontweight, int size); + +void +set_font_for_display_with_pango_font_desc (GtkWindow *win, PangoFontDescription *pango_font_desc); + diff --git a/src/tfe6/menu.ui b/src/tfe6/menu.ui new file mode 100755 index 0000000..20477ce --- /dev/null +++ b/src/tfe6/menu.ui @@ -0,0 +1,27 @@ + + + +
+ + New + win.new + + + Save As… + win.saveas + +
+
+ + Preference + win.pref + +
+
+ + Quit + win.close-all + +
+
+
diff --git a/src/tfe6/meson.build b/src/tfe6/meson.build new file mode 100644 index 0000000..6fe44fc --- /dev/null +++ b/src/tfe6/meson.build @@ -0,0 +1,16 @@ +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', 'css.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) +meson.add_install_script('glib-compile-schemas', schema_dir) + diff --git a/src/tfe6/tfe.gresource.xml b/src/tfe6/tfe.gresource.xml new file mode 100644 index 0000000..4f88c31 --- /dev/null +++ b/src/tfe6/tfe.gresource.xml @@ -0,0 +1,7 @@ + + + + tfe.ui + menu.ui + + diff --git a/src/tfe6/tfe.h b/src/tfe6/tfe.h new file mode 100644 index 0000000..90e2710 --- /dev/null +++ b/src/tfe6/tfe.h @@ -0,0 +1,11 @@ +/* tfe.h */ + +#include + +#include "../tfetextview/tfetextview.h" +#include "tfenotebook.h" +#include "css.h" + +void +tfe_application_quit (GtkWindow *win); + diff --git a/src/tfe6/tfe.ui b/src/tfe6/tfe.ui new file mode 100644 index 0000000..a023997 --- /dev/null +++ b/src/tfe6/tfe.ui @@ -0,0 +1,140 @@ + + + + file editor + 600 + 400 + + + GTK_ORIENTATION_VERTICAL + + + GTK_ORIENTATION_HORIZONTAL + + + 10 + + + + + Open + + + + + + Save + + + + + + TRUE + + + + + Close + + + + + + down + start + + + + + 10 + + + + + + + TRUE + TRUE + TRUE + + + + + + + Preferences + FALSE + TRUE + win + + + + + GTK_ORIENTATION_HORIZONTAL + 12 + 12 + 12 + 12 + 12 + + + Font: + 1 + + + + + + + + + + + + + Are you sure? + FALSE + TRUE + win + + + + + GTK_ORIENTATION_HORIZONTAL + 12 + 12 + 12 + 12 + 12 + + + dialog-warning + GTK_ICON_SIZE_LARGE + + + + + + + + + + + + + Cancel + + + + + Close + + + + btn_cancel + btn_accept + + + + + diff --git a/src/tfe6/tfeapplication.c b/src/tfe6/tfeapplication.c new file mode 100644 index 0000000..dcd612d --- /dev/null +++ b/src/tfe6/tfeapplication.c @@ -0,0 +1,230 @@ +#include "tfe.h" + +static GtkDialog *pref; +static GtkFontButton *fontbtn; +static GSettings *settings; +static GtkDialog *alert; +static GtkLabel *lb_alert; +static GtkButton *btn_accept; + +static gulong pref_close_request_handler_id = 0; +static gulong alert_close_request_handler_id = 0; +static gboolean is_quit; + +/* ----- button handlers ----- */ +void +open_cb (GtkNotebook *nb) { + notebook_page_open (nb); +} + +void +save_cb (GtkNotebook *nb) { + notebook_page_save (nb); +} + +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_widget_show (GTK_WIDGET (alert)); + } +} + +/* ----- menu or accelerator => action => handlers ----- */ +static void +open_activated (GSimpleAction *action, GVariant *parameter, gpointer nb) { + open_cb (GTK_NOTEBOOK (nb)); +} + +static void +save_activated (GSimpleAction *action, GVariant *parameter, gpointer nb) { + save_cb (GTK_NOTEBOOK (nb)); +} + +static void +close_activated (GSimpleAction *action, GVariant *parameter, gpointer nb) { + close_cb (GTK_NOTEBOOK (nb)); +} + +static void +new_activated (GSimpleAction *action, GVariant *parameter, gpointer nb) { + notebook_page_new (GTK_NOTEBOOK (nb)); +} + +static void +saveas_activated (GSimpleAction *action, GVariant *parameter, gpointer nb) { + notebook_page_saveas (GTK_NOTEBOOK (nb)); +} + +static gboolean +dialog_close_cb (GtkDialog *dialog, gpointer user_data) { + gtk_widget_hide (GTK_WIDGET (dialog)); + return TRUE; +} + +static void +font_set_cb (GtkFontButton *fontbtn, gpointer user_data) { + GtkWindow *win = GTK_WINDOW (user_data); + PangoFontDescription *pango_font_desc; + + pango_font_desc = gtk_font_chooser_get_font_desc (GTK_FONT_CHOOSER (fontbtn)); + set_font_for_display_with_pango_font_desc (win, pango_font_desc); +} + +static void +pref_activated (GSimpleAction *action, GVariant *parameter, gpointer nb) { + gtk_widget_show (GTK_WIDGET (pref)); +} + +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_hide (GTK_WIDGET (alert)); + if (response_id == GTK_RESPONSE_ACCEPT) { + if (is_quit) + tfe_application_quit (GTK_WINDOW (win)); + else + notebook_page_close (nb); + } +} + +static void +quit_activated (GSimpleAction *action, GVariant *parameter, gpointer nb) { + GtkWidget *win = gtk_widget_get_ancestor (GTK_WIDGET (nb), GTK_TYPE_WINDOW); + + is_quit = true; + if (has_saved_all (GTK_NOTEBOOK (nb))) + tfe_application_quit (GTK_WINDOW (win)); + 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_widget_show (GTK_WIDGET (alert)); + } +} + +/* ----- quit application ----- */ +void +tfe_application_quit (GtkWindow *win) { + if (pref_close_request_handler_id > 0) + g_signal_handler_disconnect (pref, pref_close_request_handler_id); + if (alert_close_request_handler_id > 0) + g_signal_handler_disconnect (alert, alert_close_request_handler_id); + g_clear_object (&settings); + gtk_window_destroy (GTK_WINDOW (alert)); + gtk_window_destroy (GTK_WINDOW (pref)); + gtk_window_destroy (win); +} + +/* ----- activate, open, startup handlers ----- */ +static void +tfe_activate (GApplication *application) { + GtkApplication *app = GTK_APPLICATION (application); + GtkWidget *win = GTK_WIDGET (gtk_application_get_active_window (app)); + GtkWidget *boxv = gtk_window_get_child (GTK_WINDOW (win)); + GtkNotebook *nb = GTK_NOTEBOOK (gtk_widget_get_last_child (boxv)); + + notebook_page_new (nb); + gtk_widget_show (GTK_WIDGET (win)); +} + +static void +tfe_open (GApplication *application, GFile ** files, gint n_files, const gchar *hint) { + GtkApplication *app = GTK_APPLICATION (application); + GtkWidget *win = GTK_WIDGET (gtk_application_get_active_window (app)); + GtkWidget *boxv = gtk_window_get_child (GTK_WINDOW (win)); + GtkNotebook *nb = GTK_NOTEBOOK (gtk_widget_get_last_child (boxv)); + int i; + + for (i = 0; i < n_files; i++) + notebook_page_new_with_file (nb, files[i]); + if (gtk_notebook_get_n_pages (nb) == 0) + notebook_page_new (nb); + gtk_widget_show (win); +} + +static void +tfe_startup (GApplication *application) { + GtkApplication *app = GTK_APPLICATION (application); + GtkBuilder *build; + GtkApplicationWindow *win; + GtkNotebook *nb; + GtkMenuButton *btnm; + GMenuModel *menu; + int i; + + build = gtk_builder_new_from_resource ("/com/github/ToshioCP/tfe/tfe.ui"); + win = GTK_APPLICATION_WINDOW (gtk_builder_get_object (build, "win")); + gtk_window_set_application (GTK_WINDOW (win), app); + nb = GTK_NOTEBOOK (gtk_builder_get_object (build, "nb")); + btnm = GTK_MENU_BUTTON (gtk_builder_get_object (build, "btnm")); + 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); + fontbtn = GTK_FONT_BUTTON (gtk_builder_get_object (build, "fontbtn")); + g_signal_connect (fontbtn, "font-set", G_CALLBACK (font_set_cb), win); + settings = g_settings_new ("com.github.ToshioCP.tfe"); + g_settings_bind (settings, "font", fontbtn, "font", G_SETTINGS_BIND_DEFAULT); + alert = GTK_DIALOG (gtk_builder_get_object (build, "alert")); + alert_close_request_handler_id = g_signal_connect (GTK_DIALOG (alert), "close-request", G_CALLBACK (dialog_close_cb), NULL); + lb_alert = GTK_LABEL (gtk_builder_get_object (build, "lb_alert")); + btn_accept = GTK_BUTTON (gtk_builder_get_object (build, "btn_accept")); + g_object_unref(build); + + 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); + g_object_unref(build); + +/* ----- action ----- */ + 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", quit_activated, NULL, NULL, NULL } + }; + g_action_map_add_action_entries (G_ACTION_MAP (win), win_entries, G_N_ELEMENTS (win_entries), nb); + +/* ----- accelerator ----- */ + struct { + const char *action; + const char *accels[2]; + } action_accels[] = { + { "win.open", { "o", NULL } }, + { "win.save", { "s", NULL } }, + { "win.close", { "w", NULL } }, + { "win.new", { "n", NULL } }, + { "win.saveas", { "s", NULL } }, + { "win.close-all", { "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); + + font_set_cb (fontbtn, GTK_WINDOW (win)); +} + +/* ----- main ----- */ +int +main (int argc, char **argv) { + GtkApplication *app; + int stat; + + app = gtk_application_new ("com.github.ToshioCP.tfe", G_APPLICATION_HANDLES_OPEN); + + g_signal_connect (app, "startup", G_CALLBACK (tfe_startup), NULL); + g_signal_connect (app, "activate", G_CALLBACK (tfe_activate), NULL); + g_signal_connect (app, "open", G_CALLBACK (tfe_open), NULL); + + stat =g_application_run (G_APPLICATION (app), argc, argv); + g_object_unref (app); + return stat; +} + diff --git a/src/tfe6/tfenotebook.c b/src/tfe6/tfenotebook.c new file mode 100644 index 0000000..b9838c6 --- /dev/null +++ b/src/tfe6/tfenotebook.c @@ -0,0 +1,210 @@ +#include "tfe.h" + +/* The returned string should be freed with g_free() when no longer needed. */ +static gchar* +get_untitled () { + static int c = -1; + if (++c == 0) + return g_strdup_printf("Untitled"); + else + return g_strdup_printf ("Untitled%u", 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); +} + +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); + gtk_notebook_set_tab_label (GTK_NOTEBOOK (nb), scr, label); + } else + file_changed_cb (tv); +} + +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; +} + +void +notebook_page_save (GtkNotebook *nb) { + g_return_if_fail(GTK_IS_NOTEBOOK (nb)); + + TfeTextView *tv; + + tv = get_current_textview (nb); + tfe_text_view_save (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)); +} + +void +notebook_page_close (GtkNotebook *nb) { + g_return_if_fail(GTK_IS_NOTEBOOK (nb)); + + GtkWidget *win; + int i; + + if (gtk_notebook_get_n_pages (nb) == 1) { + win = gtk_widget_get_ancestor (GTK_WIDGET (nb), GTK_TYPE_WINDOW); + tfe_application_quit (GTK_WINDOW (win)); + } else { + i = gtk_notebook_get_current_page (nb); + gtk_notebook_remove_page (GTK_NOTEBOOK (nb), i); + } +} + +static void +notebook_page_build (GtkNotebook *nb, GtkWidget *tv, char *filename) { + GtkWidget *scr = gtk_scrolled_window_new (); + GtkTextBuffer *tb = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv)); + GtkNotebookPage *nbp; + GtkWidget *lab; + int i; + + gtk_text_view_set_wrap_mode (GTK_TEXT_VIEW (tv), GTK_WRAP_WORD_CHAR); + gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (scr), tv); + lab = gtk_label_new (filename); + 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); + 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); +} + +static void +open_response (TfeTextView *tv, int response, GtkNotebook *nb) { + GFile *file; + char *filename; + + if (response != TFE_OPEN_RESPONSE_SUCCESS) { + g_object_ref_sink (tv); + g_object_unref (tv); + }else if (! G_IS_FILE (file = tfe_text_view_get_file (tv))) { + g_object_ref_sink (tv); + g_object_unref (tv); + }else { + filename = g_file_get_basename (file); + g_object_unref (file); + notebook_page_build (nb, GTK_WIDGET (tv), filename); + } +} + +void +notebook_page_open (GtkNotebook *nb) { + g_return_if_fail(GTK_IS_NOTEBOOK (nb)); + + GtkWidget *tv; + + tv = tfe_text_view_new (); + g_signal_connect (TFE_TEXT_VIEW (tv), "open-response", G_CALLBACK (open_response), nb); + tfe_text_view_open (TFE_TEXT_VIEW (tv), gtk_widget_get_ancestor (GTK_WIDGET (nb), GTK_TYPE_WINDOW)); +} + +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)); + + GtkWidget *tv; + char *filename; + + if ((tv = tfe_text_view_new_with_file (file)) == NULL) + return; /* read error */ + filename = g_file_get_basename (file); + notebook_page_build (nb, tv, filename); +} + +void +notebook_page_new (GtkNotebook *nb) { + g_return_if_fail(GTK_IS_NOTEBOOK (nb)); + + GtkWidget *tv; + char *filename; + + tv = tfe_text_view_new (); + filename = get_untitled (); + notebook_page_build (nb, tv, filename); +} + diff --git a/src/tfe6/tfenotebook.h b/src/tfe6/tfenotebook.h new file mode 100644 index 0000000..979687e --- /dev/null +++ b/src/tfe6/tfenotebook.h @@ -0,0 +1,26 @@ +/* tfenotbook.h */ + +gboolean +has_saved (GtkNotebook *nb); + +gboolean +has_saved_all (GtkNotebook *nb); + +void +notebook_page_save(GtkNotebook *nb); + +void +notebook_page_saveas(GtkNotebook *nb); + +void +notebook_page_close (GtkNotebook *nb); + +void +notebook_page_open (GtkNotebook *nb); + +void +notebook_page_new_with_file (GtkNotebook *nb, GFile *file); + +void +notebook_page_new (GtkNotebook *nb); + diff --git a/src/tfetextview/tfetextview.c b/src/tfetextview/tfetextview.c index ecf86e0..964945d 100644 --- a/src/tfetextview/tfetextview.c +++ b/src/tfetextview/tfetextview.c @@ -64,77 +64,57 @@ GFile * tfe_text_view_get_file (TfeTextView *tv) { g_return_val_if_fail (TFE_IS_TEXT_VIEW (tv), NULL); - return g_file_dup (tv->file); + if (G_IS_FILE (tv->file)) + return g_file_dup (tv->file); + else + return NULL; } -static void -open_dialog_response(GtkWidget *dialog, gint response, TfeTextView *tv) { - GtkTextBuffer *tb = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv)); - GFile *file; - char *contents; - gsize length; +static gboolean +save_file (GFile *file, GtkTextBuffer *tb, GtkWindow *win) { + GtkTextIter start_iter; + GtkTextIter end_iter; + gchar *contents; GtkWidget *message_dialog; GError *err = NULL; - if (response != GTK_RESPONSE_ACCEPT) - g_signal_emit (tv, tfe_text_view_signals[OPEN_RESPONSE], 0, TFE_OPEN_RESPONSE_CANCEL); - else if (! G_IS_FILE (file = gtk_file_chooser_get_file (GTK_FILE_CHOOSER (dialog)))) - g_signal_emit (tv, tfe_text_view_signals[OPEN_RESPONSE], 0, TFE_OPEN_RESPONSE_ERROR); - else if (! g_file_load_contents (file, NULL, &contents, &length, NULL, &err)) { /* read error */ - if (G_IS_FILE (file)) - g_object_unref (file); - message_dialog = gtk_message_dialog_new (GTK_WINDOW (dialog), GTK_DIALOG_MODAL, + /* This function doesn't check G_IS_FILE (file). The caller should check it. */ + 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)) { + gtk_text_buffer_set_modified (tb, FALSE); + return TRUE; + } else { + message_dialog = gtk_message_dialog_new (win, GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, "%s.\n", err->message); g_signal_connect (message_dialog, "response", G_CALLBACK (gtk_window_destroy), NULL); gtk_widget_show (message_dialog); g_error_free (err); - g_signal_emit (tv, tfe_text_view_signals[OPEN_RESPONSE], 0, TFE_OPEN_RESPONSE_ERROR); - } else { - gtk_text_buffer_set_text (tb, contents, length); - g_free (contents); - if (G_IS_FILE (tv->file)) - g_object_unref (tv->file); - tv->file = file; - gtk_text_buffer_set_modified (tb, FALSE); - g_signal_emit (tv, tfe_text_view_signals[OPEN_RESPONSE], 0, TFE_OPEN_RESPONSE_SUCCESS); - g_signal_emit (tv, tfe_text_view_signals[CHANGE_FILE], 0); + return FALSE; } - gtk_window_destroy (GTK_WINDOW (dialog)); -} - -void -tfe_text_view_open (TfeTextView *tv, GtkWidget *win) { - g_return_if_fail (TFE_IS_TEXT_VIEW (tv)); - g_return_if_fail (GTK_IS_WINDOW (win)); - - GtkWidget *dialog; - - dialog = gtk_file_chooser_dialog_new ("Open file", GTK_WINDOW (win), GTK_FILE_CHOOSER_ACTION_OPEN, - "Cancel", GTK_RESPONSE_CANCEL, - "Open", GTK_RESPONSE_ACCEPT, - NULL); - g_signal_connect (dialog, "response", G_CALLBACK (open_dialog_response), tv); - gtk_widget_show (dialog); } static void saveas_dialog_response (GtkWidget *dialog, gint response, TfeTextView *tv) { GtkTextBuffer *tb = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv)); GFile *file; + GtkWidget *win = gtk_widget_get_ancestor (GTK_WIDGET (tv), GTK_TYPE_WINDOW); + gtk_window_destroy (GTK_WINDOW (dialog)); if (response == GTK_RESPONSE_ACCEPT) { file = gtk_file_chooser_get_file (GTK_FILE_CHOOSER (dialog)); - if (G_IS_FILE(file)) { + if (! G_IS_FILE (file)) + g_warning ("TfeTextView: gtk_file_chooser_get_file returns non GFile object.\n"); + else { + save_file(file, tb, GTK_WINDOW (win)); if (G_IS_FILE (tv->file)) g_object_unref (tv->file); tv->file = file; - gtk_text_buffer_set_modified (tb, TRUE); + gtk_text_buffer_set_modified (tb, FALSE); g_signal_emit (tv, tfe_text_view_signals[CHANGE_FILE], 0); - tfe_text_view_save (TFE_TEXT_VIEW (tv)); } } - gtk_window_destroy (GTK_WINDOW (dialog)); } void @@ -142,36 +122,17 @@ tfe_text_view_save (TfeTextView *tv) { g_return_if_fail (TFE_IS_TEXT_VIEW (tv)); GtkTextBuffer *tb = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv)); - GtkTextIter start_iter; - GtkTextIter end_iter; - gchar *contents; - GtkWidget *message_dialog; GtkWidget *win = gtk_widget_get_ancestor (GTK_WIDGET (tv), GTK_TYPE_WINDOW); - GError *err = NULL; if (! gtk_text_buffer_get_modified (tb)) return; /* no need to save it */ else if (tv->file == NULL) tfe_text_view_saveas (tv); + else if (! G_IS_FILE (tv->file)) + g_error ("TfeTextView: The pointer in this object isn't NULL nor GFile object.\n"); else { - 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 (tv->file, contents, strlen (contents), NULL, TRUE, G_FILE_CREATE_NONE, NULL, NULL, &err)) + if (save_file (tv->file, tb, GTK_WINDOW (win))) gtk_text_buffer_set_modified (tb, FALSE); - else { -/* It is possible that tv->file is broken or you don't have permission to write. */ -/* It is a good idea to set tv->file to NULL. */ - if (G_IS_FILE (tv->file)) - g_object_unref (tv->file); - tv->file =NULL; - g_signal_emit (tv, tfe_text_view_signals[CHANGE_FILE], 0); - message_dialog = gtk_message_dialog_new (GTK_WINDOW (win), GTK_DIALOG_MODAL, - GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, - "%s.\n", err->message); - g_signal_connect (message_dialog, "response", G_CALLBACK (gtk_window_destroy), NULL); - gtk_widget_show (message_dialog); - g_error_free (err); - } } } @@ -210,6 +171,58 @@ tfe_text_view_new_with_file (GFile *file) { return tv; } +static void +open_dialog_response(GtkWidget *dialog, gint response, TfeTextView *tv) { + GtkTextBuffer *tb = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv)); + GFile *file; + char *contents; + gsize length; + GtkWidget *message_dialog; + GError *err = NULL; + + if (response != GTK_RESPONSE_ACCEPT) + g_signal_emit (tv, tfe_text_view_signals[OPEN_RESPONSE], 0, TFE_OPEN_RESPONSE_CANCEL); + else if (! G_IS_FILE (file = gtk_file_chooser_get_file (GTK_FILE_CHOOSER (dialog)))) { + g_warning ("TfeTextView: gtk_file_chooser_get_file returns non GFile object.\n"); + g_signal_emit (tv, tfe_text_view_signals[OPEN_RESPONSE], 0, TFE_OPEN_RESPONSE_ERROR); + } else if (! g_file_load_contents (file, NULL, &contents, &length, NULL, &err)) { /* read error */ + if (G_IS_FILE (file)) + g_object_unref (file); + message_dialog = gtk_message_dialog_new (GTK_WINDOW (dialog), GTK_DIALOG_MODAL, + GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, + "%s.\n", err->message); + g_signal_connect (message_dialog, "response", G_CALLBACK (gtk_window_destroy), NULL); + gtk_widget_show (message_dialog); + g_error_free (err); + g_signal_emit (tv, tfe_text_view_signals[OPEN_RESPONSE], 0, TFE_OPEN_RESPONSE_ERROR); + } else { + gtk_text_buffer_set_text (tb, contents, length); + g_free (contents); + if (G_IS_FILE (tv->file)) + g_object_unref (tv->file); + tv->file = file; + gtk_text_buffer_set_modified (tb, FALSE); + g_signal_emit (tv, tfe_text_view_signals[OPEN_RESPONSE], 0, TFE_OPEN_RESPONSE_SUCCESS); + g_signal_emit (tv, tfe_text_view_signals[CHANGE_FILE], 0); + } + gtk_window_destroy (GTK_WINDOW (dialog)); +} + +void +tfe_text_view_open (TfeTextView *tv, GtkWidget *win) { + g_return_if_fail (TFE_IS_TEXT_VIEW (tv)); + g_return_if_fail (GTK_IS_WINDOW (win)); + + GtkWidget *dialog; + + dialog = gtk_file_chooser_dialog_new ("Open file", GTK_WINDOW (win), GTK_FILE_CHOOSER_ACTION_OPEN, + "Cancel", GTK_RESPONSE_CANCEL, + "Open", GTK_RESPONSE_ACCEPT, + NULL); + g_signal_connect (dialog, "response", G_CALLBACK (open_dialog_response), tv); + gtk_widget_show (dialog); +} + GtkWidget * tfe_text_view_new (void) { return GTK_WIDGET (g_object_new (TFE_TYPE_TEXT_VIEW, NULL));