diff --git a/.gitignore b/.gitignore index 7dcdc05..2cd67e7 100755 --- a/.gitignore +++ b/.gitignore @@ -28,7 +28,6 @@ src/list5/_build src/expression/_build src/column/_build latex/* -html/* # backup file *~ diff --git a/Rakefile b/Rakefile index 454bd5a..d8a4f89 100644 --- a/Rakefile +++ b/Rakefile @@ -13,14 +13,16 @@ otherfiles = ["src/turtle/turtle_doc.src.md", srcfiles = secfiles + otherfiles abstract = Src_file.new "src/abstract.src.md" +# docs is a directory for html files. +html_dir = 'docs' mdfiles = srcfiles.map {|file| "gfm/" + file.to_md} -htmlfiles = srcfiles.map {|file| "html/" + file.to_html} +htmlfiles = srcfiles.map {|file| "#{html_dir}/" + file.to_html} sectexfiles = secfiles.map {|file| "latex/" + file.to_tex} othertexfiles = otherfiles.map {|file| "latex/" + file.to_tex} texfiles = sectexfiles + othertexfiles abstract_tex = "latex/"+abstract.to_tex -["gfm", "html", "latex"].each{|d| Dir.mkdir(d) unless Dir.exist?(d)} +["gfm", html_dir, "latex"].each{|d| Dir.mkdir(d) unless Dir.exist?(d)} CLEAN.append(*mdfiles) CLEAN << "Readme.md" @@ -69,10 +71,10 @@ pair(srcfiles, mdfiles).each do |src, dst, i| end end -task html: %w[html/index.html] + htmlfiles +task html: %W[#{html_dir}/index.html] + htmlfiles -file "html/index.html" => [abstract] + secfiles do - abstract_md = "html/#{abstract.to_md}" +file "#{html_dir}/index.html" => [abstract] + secfiles do + abstract_md = "#{html_dir}/#{abstract.to_md}" src2md abstract, abstract_md, "html" buf = ["# Gtk4 Tutorial for beginners\n", "\n"]\ + File.readlines(abstract_md)\ @@ -82,15 +84,15 @@ file "html/index.html" => [abstract] + secfiles do h = File.open(secfile.path){|file| file.readline}.sub(/^#* */,"").chomp buf << "1. [#{h}](#{secfile.to_html})\n" end - File.write("html/index.md", buf.join) - sh "pandoc -o html/index.html html/index.md" - File.delete "html/index.md" - add_head_tail_html "html/index.html" + File.write("#{html_dir}/index.md", buf.join) + sh "pandoc -o #{html_dir}/index.html #{html_dir}/index.md" + File.delete "#{html_dir}/index.md" + add_head_tail_html "#{html_dir}/index.html" end pair(srcfiles, htmlfiles).each do |src, dst, i| file dst => [src] + src.c_files do - html_md = "html/#{src.to_md}" + html_md = "#{html_dir}/#{src.to_md}" src2md src, html_md, "html" if src.instance_of? Sec_file if secfiles.size == 1 diff --git a/docs/Readme_for_developers.html b/docs/Readme_for_developers.html new file mode 100644 index 0000000..6e26c4c --- /dev/null +++ b/docs/Readme_for_developers.html @@ -0,0 +1,368 @@ + + + + + + + Gtk4 tutorial for beginners + + + +

How to build Gtk4 Tutorial

+

Quick start guide

+
    +
  1. You need linux operationg system, ruby, rake, pandoc and latex system.
  2. +
  3. download this repository and uncompress the files.
  4. +
  5. change your current directory to the top directory of the source files.
  6. +
  7. type rake html to create html files. The files are created under html directory.
  8. +
  9. type rake pdf to create pdf file. The file is created under latex directory.
  10. +
+

Prerequisites

+ +

Github flavored markdown

+

When you see gtk4_tutorial github page, you’ll find the contents of Readme.md below the list of files. This file is written in markdown language. A markdown file has .md suffix.

+

There are several kinds of markdown language. Readme.md uses ‘github flavored markdown’, which is often shortened as GFM. Markdown files in the gfm directory also written in GFM. If you are not familiar with it, refer to the page github flavor markdown spec.

+

Pandoc’s markdown

+

This tutorial also uses another markdown – pandoc’s markdown. Pandoc is a converter between markdown, html, latex, word docx and so on. This type of markdown is used to convert markdown to html and latex.

+

Src.md file

+

Src.md file has “.src.md” suffix. The syntax of .src.md file is similar to markdown but it has a special command which isn’t included in markdown syntax. It is @@@ command. The command starts with a line that begins with “@@@” and it ends with a line “@@@”. For example,

+
@@@include
+tfeapplication.c
+@@@
+

There are four types of @@@ command.

+

@@@include

+

This type of @@@ command starts with a line “@@@include”.

+
@@@include
+tfeapplication.c
+@@@
+

This command replaces itself with the text read from the C source files surrounded by @@@include and @@@. If a function list follows the filename, only the functions are read. If no function list is given, the command can read any text file other than C source file.

+
@@@include
+tfeapplication.c main startup
+@@@
+

The command above is replaced by the contents of main and startup functions in tfeapplication.c.

+
@@@include
+lib_src2md.rb
+@@@
+

This command is replaced by the contents of lib_src2md.rb which is a ruby script (not C file).

+

The inserted text is converted to fence code block. Fence code block begins with ~~~ and ends with ~~~. The contents are displayed verbatim. ~~~ is look like a fence so the block is called “fence code block”.

+

If the target markdown is GFM, then an info string follows the beginning fence. The following example shows that the @@@ command includes a C source file sample.c.

+
$ cat src/sample.c
+int
+main (int argc, char **argv) {
+  ... ...
+}
+$cat src/sample.src.md
+  ... ...
+@@@include -N
+sample.c
+@@@
+  ... ...
+$ ruby src2md.rb src/sample.src.md gfm/sample.md
+$ cat gfm/sample.md
+  ... ...
+~~~C
+int
+main (int argc, char **argv) {
+  ... ...
+}
+~~~
+  ... ...
+

Info strings are usually languages like C, ruby, xml and so on. This string is decided with the filename extension.

+ +

The supported language is written in line 290 and 291 in lib/lib_src2md.rb.

+

A line number is inserted at the top of each line in the code block. If you don’t want to insert it, give “-N” option to @@@include command.

+

Options:

+ +

The following shows that line numbers are inserted at the beginning of lines.

+
$cat src/sample.src.md
+  ... ...
+@@@include
+sample.c
+@@@
+  ... ...
+$ ruby src2md.rb src/sample.src.md gfm/sample.md
+$ cat gfm/sample.md
+  ... ...
+~~~C
+ 1 int
+ 2 main (int argc, char **argv) {
+  ... ...
+14 }
+~~~
+  ... ...
+

If the target markdown is an intermediate file to html, then another type of info string follows the beginning fence. If @@@include command doesn’t have -N option, then the generated markdown is:

+
~~~{.C .numberLines}
+int
+main (int argc, char **argv) {
+  ... ...
+}
+~~~
+

The info string .C specifies C language. The info string .numberLines is a class of the pandoc markdown. If the class is given, pandoc generates CSS to insert line numbers to the source code in the html file. That’s why the fence code block in the markdown doesn’t have line numbers, which is different from gfm markdown. If -N option is given, then the info string is {.C} only.

+

If the target markdown is an intermediate file to latex, then the same info string follows the beginning fence.

+
~~~{.C .numberLines}
+int
+main (int argc, char **argv) {
+  ... ...
+}
+~~~
+

Rake uses pandoc with –listings option when it converts markdown to latex. The generated latex file uses listings package to list source files instead of verbatim environment. The markdwon above is converted to the following latex source file.

+
\begin{lstlisting}[language=C, numbers=left]
+int
+main (int argc, char **argv) {
+  ... ...
+}
+\end{lstlisting}
+

Listing package can color or emphasize keywords, strings, comments and directives. But it doesn’t analyze the syntax or token of the language, so the kind of emphasis target is limited.

+

@@@include command have two advantages.

+
    +
  1. Less typing.
  2. +
  3. You don’t need to modify your .src.md file, even if the C source file is modified.
  4. +
+

@@@shell

+

This type of @@@ command starts with a line begins with “@@@shell”.

+
@@@shell
+shell command
+ ... ...
+@@@
+

This command replaces itself with:

+ +

For example,

+
@@@shell
+wc Rakefile
+@@@
+

This is converted to:

+
~~~
+$ wc Rakefile
+164  475 4971 Rakefile
+~~~
+

@@@if series

+

This type of @@@ command starts with a line begins with “@@@if”, and followed by “@@@elif”, “@@@else” or “@@@end”. This command is similar to “#if”, “#elif”, #else" and “#endif” directives in C preprocessor. For example,

+
@@@if gfm
+Refer to  [tfetextview API reference](../src/tfetextview_doc.md)
+@@@elif html
+Refer to  [tfetextview API reference](../src/tfetextview_doc.html)
+@@@elif latex
+Refer to tfetextview API reference in appendix.
+@@@end
+

@@@if and @@@elif have conditions. They are gfm, html or latex so far.

+ +

Other type of conditions may be available in the future version.

+

The code analyzing @@@if series command is rather complicated. It is based on the state diagram below.

+
+
state diagram
+
+

@@@table

+

This type of @@@ command starts with a line begins with “@@@table”. The contents of this command is a table of GFM or pandoc’s markdown. The command makes a table easy to read. For example, a text file sample.md has a table like this:

+
Price list
+
+@@@table
+|item|price|
+|:---:|:---:|
+|mouse|$10|
+|PC|$500|
+@@@
+

The command changes this into:

+
Price list
+
+|item |price|
+|:---:|:---:|
+|mouse| $10 |
+| PC  |$500 |
+

This command just changes the appearance of the table. There’s no influence on html/latex files that is converted from the markdown.

+

A script mktbl.rb supports this command. If you run the script like this:

+
$ ruby mktbl.rb sample.md
+

Then, the appearance of the table will be changed The script also makes a backup file sample.md.bak.

+

The task of the script seems easy, but the program is not so simple. The script mktbl.rb uses a library lib/lib_src2md.rb

+

@@@commands are effective in the whole text. This means you can’t stop the @@@commands. But sometimes you want to show the commands literally like this document. One solution is to add four blanks at the top of the line. Then @@@commands are not effective because @@@commands must be at the top of the line.

+

Conversion

+

The @@@ commands above (@@@include, @@@shell, @@@if series and @@@table) are carried out by a method src2md, which is in the file lib/lib_src2md.rb. This method converts src.md file into md file. In addition, some other conversions are made by src2md method.

+ +

The order of the conversions are:

+
    +
  1. @@@if
  2. +
  3. @@@table
  4. +
  5. @@@include
  6. +
  7. @@@shell
  8. +
  9. others
  10. +
+

There is src2md.rb file in the top directory of this repository. It just invokes the method src2md. In the same way, the method is called in the action in Rakefile.

+

Directory structure

+

There are six directories under gtk4_tutorial directory. They are gfm, src, image, html, latex and lib. Three directories gfm, html and latex are the destination directories for GFM, html and latex files respectively. It is possible that these three directories don’t exist before the conversion.

+ +

Src directory and the top directory

+

Src directory contains .src.md files and C-related source files. The top directory, which is gtk_tutorial directory, contains Rakefile, src2md.rb and some other files. When Readme.md is generated, it will be located at the top directory. Readme.md has title, abstract, table of contents with links to GFM files.

+

Rakefile describes how to convert .src.md files into GFM files. Rake carries out the conversion according to the Rakefile.

+

The name of files in src directory

+

Each file in src directory is an abstract, sections of the whole document and other .src.md files. An abstract.src.md contains the abstract of this tutorial. Each section filename is “sec”, number of the section and “.src.md” suffix. For example, “sec1.src.md”, “sec5.src.md” or “sec12.src.md”. They are the files correspond to section 1, section 5 and section 12 respectively.

+

C source file directory

+

Most of .src.md files have @@@include commands and they include C source files. Such C source files are located in the subdirectories of src directory.

+

Those C files have been compiled and tested. When you compile source files, some auxiliary files and a target file like a.out are created. Or _build directory is made when meson and ninja is used when compiling. Those files are not tracked by git because they are specified in .gitignore.

+

The name of the subdirectories should be independent of section names. It is because of renumbering, which will be explained in the next subsection.

+

Renumbering

+

Sometimes you might want to insert a section. For example, you want to insert it between section 4 and section 5. You can make a temporary section 4.5, that is a rational number between 4 and 5. However, section numbers are usually integer so section 4.5 must be changed to section 5. And the numbers of the following sections must be increased by one.

+

This renumbering is done by a method renum! of the class Sec_files. The method and class is written in lib/lib_sec_file.rb.

+ +

Rakefile

+

Rakefile is a similar file to Makefile but controlled by rake, which is a make-like program written in ruby. Rakefile in this tutorial has the following tasks.

+ +

Rake does renumbering before the tasks above.

+

Generate GFM markdown files

+

Markdown files (GFM) are generated by rake.

+
$ rake
+

This command generates Readme.md with src/abstract.src.md and titles of .src.md files. At the same time, it converts each .src.md file into GFM file under gfm directory. Navigation lines are added at the top and bottom of each markdown section file.

+

You can describe width and height of images in .src.md files. For example,

+
![sample image](../image/sample_image.png)
+

The size between left brace and right brace is used in latex file and it is not fit to GFM syntax. So the size is removed in the conversion.

+

If a .src.md file has relative URL link, it will be changed by conversion. Because .src.md files are located under src directory and GFM files are located under gfm directory, base URL of GFM files is different from base URL of .src.md files. For example, [src/sample.c](../src/sample.c) is translated to [src/sample.c](../src/sample.c).

+

If a link points another .src.md file, then the target filename will be changed to .md file. For example, [Section 5](sec5.html) is translated to [Section 5](../src/sec5.md).

+

If you want to clean the directory, that means remove all the generated markdown files, type rake clean.

+
$ rake clean
+

Sometimes this is necessary before generating GFM files.

+
$ rake clean
+$ rake
+

For example, if you append a new section and other files are still the same as before, rake clean is necessary. Because the navigation of the previous section of the newly added section needs to be updated. If you don’t do rake clean, then it won’t be updated because the the timestamp of .md file in gfm is newer than the one of .src.md file. In this case, using touch to the previous section .src.md also works to update the file.

+

If you see the github repository (ToshioCP/Gtk4-tutorial), Readme.md is shown below the code. And Readme.md includes links to each markdown files. The repository not only stores source files but also shows the whole tutorial.

+

Generate html files

+

Src.md files can be translated to html files. You need pandoc to do this. Most linux distribution has pandoc package. Refer to your distribution document to install it.

+

Type rake html to generate html files.

+
$ rake html
+

First, it generates pandoc’s markdown files under html directory. Then, pandoc converts them to html files. The width and height of image files are removed.

+

index.html is the top html file. If you want to clean html directory, type rake cleanhtml

+
$ rake cleanhtml
+

Every html file has stylesheet in its header. This is created by lib/lib_add_head_tail_html.rb. This script has a sample markdown code and convert it with pandoc and -s option. Pandoc generates a html file with header. The script extracts the header and use it for html files. You can customize the style by modifying lib/lib_add_head_tail_html.rb.

+

html directory contains all the necessary html files. So if you want to upload the html files to your own web site, just upload the files in the html directory.

+

Generate a pdf file

+

You need pandoc to convert markdown files into latex source files.

+

Type rake pdf to generate latex files and finally make a pdf file.

+
$ rake pdf
+

First, it generates pandoc’s markdown files under latex directory. Then, pandoc converts them into latex files. Links to files or directories are removed because latex doesn’t support them. However, links to full URL and image files are kept. Image size is set with the size between the left brace and right brace.

+
![sample image](../image/sample_image.png)
+

You need to specify appropriate width and height. It is almost 0.015 x pixels cm. For example, if the width of an image is 400 pixels, the width in a latex file will be almost 6cm.

+

A file main.tex is the root file of all the generated latex files. It has \input commands, which inserts each section file, between \begin{document} and \end{document}. It also has \input, which inserts helper.tex, in the preamble. Two files main.tex and helper.tex are created by lib/lib_gen_main_tex.rb. It has a sample markdown code and converts it witn pandoc -s. Then, it extracts the preamble in the generated file and puts it into helper.tex. You can customize helper.tex by modifying lib/lib_gen_main_tex.rb.

+

Finally, lualatex compiles the main.tex into a pdf file.

+

If you want to clean latex directory, type rake cleanlatex

+
$ rake cleanlatex
+

This removes all the latex source files and a pdf file.

+ + diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000..ddb9561 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,141 @@ + + + + + + + Gtk4 tutorial for beginners + + + +

Gtk4 Tutorial for beginners

+

The github page of this tutorial is also available. Click here.

+

Contents of this Repository

+

This tutorial illustrates how to write C programs with the Gtk4 library. It focuses on beginners so the contents are limited to the basics. The table of contents is at the end of this abstract.

+ +

The latest version of the tutorial is located at Gtk4-tutorial github repository. You can read it from there directly without having to download anything.

+

Gtk4 Documentation

+

Please refer to Gtk API Reference and Gnome Developer Documentation Website for further information.

+

These websites are newly opened lately (Aug/2021). The old documentation is located at Gtk Reference Manual and Gnome Developer Center. The new website is in progress at present, so you might need to refer to the old version.

+

If you want to know about GObject and the type system, please refer to GObject tutorial. The GObject details are easy to understand and also necessary to know when writing Gtk4 programs.

+

Contribution

+

This tutorial is under development and unstable. Even though the codes of the examples have been tested on Gtk4 version 4.0, bugs may still exist. If you find any bugs, errors or mistakes in the tutorial and C examples, please let me know. You can post it to github issues. You can also post corrected files as a commit to pull request. When you make corrections, correct the source files, which are under the ‘src’ directory, then run rake to create to create the output file. The GFM files under the ‘gfm’ directory are automatically updated.

+

If you have a question, feel free to post it as an issue. All questions are helpful and will make this tutorial get better.

+

How to get a HTML or PDF version

+

If you want to get a HTML or PDF version, you can make them with rake, which is a ruby version of make. Type rake html for HTML. Type rake pdf for PDF. There is a documentation (“How to build Gtk4 Tutorial”) that describes how to make them.

+

Table of contents

+
    +
  1. Prerequisite and License
  2. +
  3. Installation of Gtk4 to Linux distributions
  4. +
  5. GtkApplication and GtkApplicationWindow
  6. +
  7. Widgets (1)
  8. +
  9. Widgets (2)
  10. +
  11. String and memory management
  12. +
  13. Widgets (3)
  14. +
  15. Defining a Child object
  16. +
  17. The User Interface (UI) file and GtkBuilder
  18. +
  19. Build system
  20. +
  21. Initialization and destruction of instances
  22. +
  23. Signals
  24. +
  25. Functions in TfeTextView
  26. +
  27. Functions in GtkNotebook
  28. +
  29. tfeapplication.c
  30. +
  31. tfe5 source files
  32. +
  33. Menu and action
  34. +
  35. Stateful action
  36. +
  37. Ui file for menu and action entries
  38. +
  39. GtkMenuButton, accelerators, font, pango and gsettings
  40. +
  41. Template XML and composite widget
  42. +
  43. GtkDrawingArea and Cairo
  44. +
  45. Periodic Events
  46. +
  47. Combine GtkDrawingArea and TfeTextView
  48. +
  49. Tiny turtle graphics interpreter
  50. +
  51. GtkListView
  52. +
  53. GtkGridView and activate signal
  54. +
  55. GtkExpression
  56. +
  57. GtkColumnView
  58. +
+ + diff --git a/docs/sec1.html b/docs/sec1.html new file mode 100644 index 0000000..c9fe9a2 --- /dev/null +++ b/docs/sec1.html @@ -0,0 +1,114 @@ + + + + + + + Gtk4 tutorial for beginners + + + +

Up: index.html, Next: Section 2

+

Prerequisite and License

+

Prerequisite

+

Gtk4 on a Linux OS

+

This tutorial is about Gtk4 libraries. It is originally used on Linux with C compiler, but now it is used more widely, on Windows and MacOS, with Vala, Python and so on. However, this tutorial describes only C programs on Linux.

+

If you want to try the examples in the tutorial, you need:

+ +

Ruby and rake for making the document

+

This repository includes Ruby programs. They are used to make Markdown files, HTML files, Latex files and a PDF file.

+

You need:

+ +

License

+

Copyright (C) 2020 ToshioCP (Toshio Sekiya)

+

Gtk4 tutorial repository contains the tutorial document and software such as converters, generators and controllers. All of them make up the ‘Gtk4 tutorial’ package. This package is simply called ‘Gtk4 tutorial’ in the following description. ‘Gtk4 tutorial’ is free; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License or, at your option, any later version.

+

‘Gtk4 tutorial’ is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

+

Up: index.html, Next: Section 2

+ + diff --git a/docs/sec10.html b/docs/sec10.html new file mode 100644 index 0000000..3d0eab4 --- /dev/null +++ b/docs/sec10.html @@ -0,0 +1,362 @@ + + + + + + + Gtk4 tutorial for beginners + + + +

Up: index.html, Prev: Section 9, Next: Section 11

+

Build system

+

What do we need to think about to manage big source files?

+

We’ve compiled a small editor so far. But Some bad signs are beginning to appear.

+ +

These ideas are useful to manage big source files.

+

Divide a C source file into two parts.

+

When you divide C source file into several parts, each file should contain only one thing. For example, our source has two things, the definition of TfeTextView and functions related to GtkApplication and GtkApplicationWindow. It is a good idea to separate them into two files, tfetextview.c and tfe.c.

+ +

Now we have three source files, tfetextview.c, tfe.c and tfe3.ui. The 3 of tfe3.ui is like a version number. Managing version with filenames is one possible idea but it may make bothersome problem. You need to rewrite filename in each version and it affects to contents of source files that refer to filenames. So, we should take 3 away from the filename.

+

In tfe.c the function tfe_text_view_new is invoked to create a TfeTextView instance. But it is defined in tfetextview.c, not tfe.c. The lack of the declaration (not definition) of tfe_text_view_new makes error when tfe.c is compiled. The declaration is necessary in tfe.c. Those public information is usually written in header files. It has .h suffix like tfetextview.h And header files are included by C source files. For example, tfetextview.h is included by tfe.c.

+

All the source files are listed below.

+

tfetextview.h

+
#include <gtk/gtk.h>
+
+#define TFE_TYPE_TEXT_VIEW tfe_text_view_get_type ()
+G_DECLARE_FINAL_TYPE (TfeTextView, tfe_text_view, TFE, TEXT_VIEW, GtkTextView)
+
+void
+tfe_text_view_set_file (TfeTextView *tv, GFile *f);
+
+GFile *
+tfe_text_view_get_file (TfeTextView *tv);
+
+GtkWidget *
+tfe_text_view_new (void);
+

tfetextview.c

+
#include <gtk/gtk.h>
+#include "tfetextview.h"
+
+struct _TfeTextView
+{
+  GtkTextView parent;
+  GFile *file;
+};
+
+G_DEFINE_TYPE (TfeTextView, tfe_text_view, GTK_TYPE_TEXT_VIEW);
+
+static void
+tfe_text_view_init (TfeTextView *tv) {
+}
+
+static void
+tfe_text_view_class_init (TfeTextViewClass *class) {
+}
+
+void
+tfe_text_view_set_file (TfeTextView *tv, GFile *f) {
+  tv -> file = f;
+}
+
+GFile *
+tfe_text_view_get_file (TfeTextView *tv) {
+  return tv -> file;
+}
+
+GtkWidget *
+tfe_text_view_new (void) {
+  return GTK_WIDGET (g_object_new (TFE_TYPE_TEXT_VIEW, NULL));
+}
+

tfe.c

+
#include <gtk/gtk.h>
+#include "tfetextview.h"
+
+static void
+app_activate (GApplication *app, gpointer user_data) {
+  g_print ("You need a filename argument.\n");
+}
+
+static void
+app_open (GApplication *app, GFile ** files, gint n_files, gchar *hint, gpointer user_data) {
+  GtkWidget *win;
+  GtkWidget *nb;
+  GtkWidget *lab;
+  GtkNotebookPage *nbp;
+  GtkWidget *scr;
+  GtkWidget *tv;
+  GtkTextBuffer *tb;
+  char *contents;
+  gsize length;
+  char *filename;
+  int i;
+  GtkBuilder *build;
+
+  build = gtk_builder_new_from_resource ("/com/github/ToshioCP/tfe3/tfe.ui");
+  win = GTK_WIDGET (gtk_builder_get_object (build, "win"));
+  gtk_window_set_application (GTK_WINDOW (win), GTK_APPLICATION (app));
+  nb = GTK_WIDGET (gtk_builder_get_object (build, "nb"));
+  g_object_unref (build);
+  for (i = 0; i < n_files; i++) {
+    if (g_file_load_contents (files[i], NULL, &contents, &length, NULL, NULL)) {
+      scr = gtk_scrolled_window_new ();
+      tv = tfe_text_view_new ();
+      tb = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv));
+      gtk_text_view_set_wrap_mode (GTK_TEXT_VIEW (tv), GTK_WRAP_WORD_CHAR);
+      gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (scr), tv);
+
+      tfe_text_view_set_file (TFE_TEXT_VIEW (tv),  g_file_dup (files[i]));
+      gtk_text_buffer_set_text (tb, contents, length);
+      g_free (contents);
+      filename = g_file_get_basename (files[i]);
+      lab = gtk_label_new (filename);
+      gtk_notebook_append_page (GTK_NOTEBOOK (nb), scr, lab);
+      nbp = gtk_notebook_get_page (GTK_NOTEBOOK (nb), scr);
+      g_object_set (nbp, "tab-expand", TRUE, NULL);
+      g_free (filename);
+    } else if ((filename = g_file_get_path (files[i])) != NULL) {
+        g_print ("No such file: %s.\n", filename);
+        g_free (filename);
+    } else
+        g_print ("No valid file is given\n");
+  }
+  if (gtk_notebook_get_n_pages (GTK_NOTEBOOK (nb)) > 0) {
+    gtk_widget_show (win);
+  } else
+    gtk_window_destroy (GTK_WINDOW (win));
+}
+
+int
+main (int argc, char **argv) {
+  GtkApplication *app;
+  int stat;
+
+  app = gtk_application_new ("com.github.ToshioCP.tfe", G_APPLICATION_HANDLES_OPEN);
+  g_signal_connect (app, "activate", G_CALLBACK (app_activate), NULL);
+  g_signal_connect (app, "open", G_CALLBACK (app_open), NULL);
+  stat =g_application_run (G_APPLICATION (app), argc, argv);
+  g_object_unref (app);
+  return stat;
+}
+

The ui file tfe.ui is the same as tfe3.ui in the previous section.

+

tfe.gresource.xml

+
<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/com/github/ToshioCP/tfe3">
+    <file>tfe.ui</file>
+  </gresource>
+</gresources>
+

Make

+

Dividing a file makes it easy to maintain source files. But now we are faced with a new problem. The building step increases.

+ +

Now build tool is necessary to manage it. Make is one of the build tools. It was created in 1976. It is an old and widely used program.

+

Make analyzes Makefile and executes compilers. All instructions are written in Makefile.

+
sample.o: sample.c
+    gcc -o sample.o sample.c
+

The sample of Malefile above consists of three elements, sample.o, sample.c and gcc -o sample.o sample.c.

+ +

The rule is:

+

If a prerequisite modified later than a target, then make executes the recipe.

+

In the example above, if sample.c is modified after the generation of sample.o, then make executes gcc and compile sample.c into sample.o. If the modification time of sample.c is older then the generation of sample.o, then no compiling is necessary, so make does nothing.

+

The Makefile for tfe is as follows.

+
all: tfe
+
+tfe: tfe.o tfetextview.o resources.o
+    gcc -o tfe tfe.o tfetextview.o resources.o `pkg-config --libs gtk4`
+
+tfe.o: tfe.c tfetextview.h
+    gcc -c -o tfe.o `pkg-config --cflags gtk4` tfe.c
+tfetextview.o: tfetextview.c tfetextview.h
+    gcc -c -o tfetextview.o `pkg-config --cflags gtk4` tfetextview.c
+resources.o: resources.c
+    gcc -c -o resources.o `pkg-config --cflags gtk4` resources.c
+
+resources.c: tfe.gresource.xml tfe.ui
+    glib-compile-resources tfe.gresource.xml --target=resources.c --generate-source
+
+.Phony: clean
+
+clean:
+    rm -f tfe tfe.o tfetextview.o resources.o resources.c
+

You only need to type make.

+
$ make
+gcc -c -o tfe.o `pkg-config --cflags gtk4` tfe.c
+gcc -c -o tfetextview.o `pkg-config --cflags gtk4` tfetextview.c
+glib-compile-resources tfe.gresource.xml --target=resources.c --generate-source
+gcc -c -o resources.o `pkg-config --cflags gtk4` resources.c
+gcc -o tfe tfe.o tfetextview.o resources.o `pkg-config --libs gtk4`
+

I used only very basic rules to write this Makefile. There are many more convenient methods to make it more compact. But it will be long to explain it. So I want to finish explaining make and move on to the next topic.

+

Rake

+

Rake is a similar program to make. It is written in Ruby code. If you don’t use Ruby, you don’t need to read this subsection. However, Ruby is really sophisticated and recommendable script language.

+ +

Rake has task and file task, which is similar to target, prerequisite and recipe in make.

+
require 'rake/clean'
+
+targetfile = "tfe"
+srcfiles = FileList["tfe.c", "tfetextview.c", "resources.c"]
+rscfile = srcfiles[2]
+objfiles = srcfiles.gsub(/.c$/, '.o')
+
+CLEAN.include(targetfile, objfiles, rscfile)
+
+task default: targetfile
+
+file targetfile => objfiles do |t|
+  sh "gcc -o #{t.name} #{t.prerequisites.join(' ')} `pkg-config --libs gtk4`"
+end
+
+objfiles.each do |obj|
+  src = obj.gsub(/.o$/,'.c')
+  file obj => src do |t|
+    sh "gcc -c -o #{t.name} `pkg-config --cflags gtk4` #{t.source}"
+  end
+end
+
+file rscfile => ["tfe.gresource.xml", "tfe.ui"] do |t|
+  sh "glib-compile-resources #{t.prerequisites[0]} --target=#{t.name} --generate-source"
+end
+

The contents of the Rakefile is almost same as the Makefile in the previous subsection.

+ +

Rakefile might seem to be difficult for beginners. But, you can use any Ruby syntax in Rakefile, so it is really flexible. If you practice Ruby and Rakefile, it will be highly productive tools.

+

Meson and ninja

+

Meson is one of the most popular building tool despite the developing version. And ninja is similar to make but much faster than make. Several years ago, most of the C developers used autotools and make. But now the situation has changed. Many developers are using meson and ninja now.

+

To use meson, you first need to write meson.build file.

+
project('tfe', 'c')
+
+gtkdep = dependency('gtk4')
+
+gnome=import('gnome')
+resources = gnome.compile_resources('resources','tfe.gresource.xml')
+
+sourcefiles=files('tfe.c', 'tfetextview.c')
+
+executable('tfe', sourcefiles, resources, dependencies: gtkdep)
+ +

Now run meson and ninja.

+
$ meson _build
+$ ninja -C _build
+

Then, the executable file tfe is generated under the directory _build.

+
$ _build/tfe tfe.c tfetextview.c
+

Then the window appears. And two notebook pages are in the window. One notebook is tfe.c and the other is tfetextview.c.

+

I’ve shown you three build tools. I think meson and ninja is the best choice for the present.

+

We divided a file into some categorized files and used a build tool. This method is used by many developers.

+

Up: index.html, Prev: Section 9, Next: Section 11

+ + diff --git a/docs/sec11.html b/docs/sec11.html new file mode 100644 index 0000000..70c009d --- /dev/null +++ b/docs/sec11.html @@ -0,0 +1,405 @@ + + + + + + + Gtk4 tutorial for beginners + + + +

Up: index.html, Prev: Section 10, Next: Section 12

+

Initialization and destruction of instances

+

A new version of the text file editor (tfe) will be made in this section and the following four sections. It is tfe5. There are many changes from the prior version. All the sources are listed in Section 16. They are located in two directories, src/tfe5 and src/tfetextview.

+

Encapsulation

+

We’ve divided C source file into two parts. But it is not enough in terms of encapsulation.

+ +

However, first of all, I’d like to focus on the object TfeTextView. It is a child object of GtkTextView and has a new member file in it. The important thing is to manage the Gfile object pointed by file.

+ +

You need to know at least class, instance and signals before thinking about them. I will explain them in this section and the next section. After that I will explain:

+ +

GObject and its children

+

GObject and its children are objects, which have both class and instance. First, think about instance of objects. Instance is structured memory. THe structure is described as C language structure. The following is a structure of TfeTextView.

+
/* This typedef statement is automatically generated by the macro G_DECLARE_FINAL_TYPE */
+typedef struct _TfeTextView TfeTextView;
+
+struct _TfeTextView {
+  GtkTextView parent;
+  GFile *file;
+};
+

The members of the structure are:

+ +

Notice the program above is the declaration of the structure, not the definition. So, no memories are allocated at this moment. They are to be allocated when tfe_text_view_new function is invoked. The memory allocated with tfe_text_view_new is an instance of TfeTextView object. Therefore, There can be multiple TfeTextView instances if tfe_text_view_new is called multiple times.

+

You can find the declaration of the ancestors of TfeTextView in the source files of GTK and GLib. The following is extracts from the source files (not exactly the same).

+
typedef struct _GObject GObject;
+typedef struct _GObject GInitiallyUnowned;
+struct  _GObject
+{
+  GTypeInstance  g_type_instance;
+  volatile guint ref_count;
+  GData         *qdata;
+};
+
+typedef struct _GtkWidget GtkWidget;
+struct _GtkWidget
+{
+  GInitiallyUnowned parent_instance;
+  GtkWidgetPrivate *priv;
+};
+
+typedef struct _GtkTextView GtkTextView;
+struct _GtkTextView
+{
+  GtkWidget parent_instance;
+  GtkTextViewPrivate *priv;
+};
+

In each structure, its parent is declared at the top of the members. So, every ancestors is included in the child instance. This is very important. It guarantees a child widget to inherit all the features from ancestors. The structure of TfeTextView is like the following diagram.

+
+
The structure of the instance TfeTextView
+
+

Initialization of a TfeTextView instance

+

The function tfe_text_view_new creates a new TfeTextView instance.

+
GtkWidget *
+tfe_text_view_new (void) {
+  return GTK_WIDGET (g_object_new (TFE_TYPE_TEXT_VIEW, NULL));
+}
+

When this function is involed, a TfeTextView instance is created and initialized. The initialization process is as follows.

+
    +
  1. Initializes GObject (GInitiallyUnowned) part in TfeTextView instance.
  2. +
  3. Initializes GtkWidget part in TfeTextView instance.
  4. +
  5. Initializes GtkTextView part in TfeTextView instance.
  6. +
  7. Initializes TfeTextView part in TfeTextView instance.
  8. +
+

The step one through three is done by g_object_init, gtk_widget_init and gtk_text_view_init. They are called by the system automatically and you don’t need to care about them. Step four is done by the function tfe_text_view_init in tfetextview.c.

+
static void
+tfe_text_view_init (TfeTextView *tv) {
+  tv->file = NULL;
+}
+

This function just initializes tv->file to be NULL.

+

Functions and Classes

+

In Gtk, all objects derived from GObject have class and instance (except abstract object). An instance is memory of C structure, which is described in the previous two subsections. Each object can have more than one instance. Those instances have the same structure. An instance just keeps status of the instance. Therefore, it is insufficient to define its behavior. We need at least two things. One is functions and the other is class.

+

You’ve already seen many functions. For example, tfe_text_view_new is a function to create a TfeTextView instance. These functions are similar to public object methods in object oriented languages such as Java or Ruby. Functions are public, which means that they are expected to be used by other objects.

+

Class comprises mainly pointers to functions. Those functions are used by the object itself or its descendant objects. For example, GObject class is declared in gobject.h in GLib source files.

+
typedef struct _GObjectClass             GObjectClass;
+typedef struct _GObjectClass             GInitiallyUnownedClass;
+
+struct  _GObjectClass
+{
+  GTypeClass   g_type_class;
+  /*< private >*/
+  GSList      *construct_properties;
+  /*< public >*/
+  /* seldom overridden */
+  GObject*   (*constructor)     (GType                  type,
+                                 guint                  n_construct_properties,
+                                 GObjectConstructParam *construct_properties);
+  /* overridable methods */
+  void       (*set_property)    (GObject        *object,
+                                 guint           property_id,
+                                 const GValue   *value,
+                                 GParamSpec     *pspec);
+  void       (*get_property)    (GObject        *object,
+                                 guint           property_id,
+                                 GValue         *value,
+                                 GParamSpec     *pspec);
+  void       (*dispose)         (GObject        *object);
+  void       (*finalize)        (GObject        *object);
+  /* seldom overridden */
+  void       (*dispatch_properties_changed) (GObject      *object,
+                                             guint         n_pspecs,
+                                             GParamSpec  **pspecs);
+  /* signals */
+  void      (*notify)           (GObject    *object,
+                                 GParamSpec *pspec);
+  /* called when done constructing */
+  void      (*constructed)      (GObject    *object);
+  /*< private >*/
+  gsize  flags;
+  /* padding */
+  gpointer pdummy[6];
+};
+

I’d like to explain some of the members. There’s a pointer to the function dispose in line 23.

+
void (*dispose) (GObject *object);
+

The declaration is a bit complicated. The asterisk before the identifier dispose means pointer. So, the pointer dispose points to a function which has one parameter, which points a GObject structure, and returns no value. In the same way, line 24 says finalize is a pointer to the function which has one parameter, which points a GObject structure, and returns no value.

+
void (*finalize) (GObject *object);
+

Look at the declaration of _GObjectClass so that you would find that most of the members are pointers to functions.

+ +

These functions are called class methods. The methods are open to its descendants. But not open to the objects which are not the descendants.

+

TfeTextView class

+

TfeTextView class is a structure and it includes all its ancestors’ class in it.

+
typedef _TfeTextView TfeTextView;
+struct _TfeTextView {
+  GtkTextView parent;
+  GFile *file;
+};
+

TfeTextView structure has GtkTextView type as the first member. In the same way, GtkTextView has its parent type (GtkWidget) as the first member. GtkWidget has its parent type (GtkInitiallyUnowned) as the first member. The structure of GtkInitiallyUnowned is the same as GObject. Therefore, TFeTextView includes GObject, GtkWidget and GtkTextView in itself.

+
GObject -- GInitiallyUnowned -- GtkWidget -- GtkTextView -- TfeTextView
+

The following is extracts from the source files (not exactly the same).

+
struct _GtkWidgetClass
+{
+  GInitiallyUnownedClass parent_class;
+
+  /*< public >*/
+
+  /* basics */
+  void (* show)                (GtkWidget        *widget);
+  void (* hide)                (GtkWidget        *widget);
+  void (* map)                 (GtkWidget        *widget);
+  void (* unmap)               (GtkWidget        *widget);
+  void (* realize)             (GtkWidget        *widget);
+  void (* unrealize)           (GtkWidget        *widget);
+  void (* root)                (GtkWidget        *widget);
+  void (* unroot)              (GtkWidget        *widget);
+  void (* size_allocate)       (GtkWidget           *widget,
+                                int                  width,
+                                int                  height,
+                                int                  baseline);
+  void (* state_flags_changed) (GtkWidget        *widget,
+                                GtkStateFlags     previous_state_flags);
+  void (* direction_changed)   (GtkWidget        *widget,
+                                GtkTextDirection  previous_direction);
+
+  /* size requests */
+  GtkSizeRequestMode (* get_request_mode)               (GtkWidget      *widget);
+  void              (* measure) (GtkWidget      *widget,
+                                 GtkOrientation  orientation,
+                                 int             for_size,
+                                 int            *minimum,
+                                 int            *natural,
+                                 int            *minimum_baseline,
+                                 int            *natural_baseline);
+
+  /* Mnemonics */
+  gboolean (* mnemonic_activate)        (GtkWidget           *widget,
+                                         gboolean             group_cycling);
+
+  /* explicit focus */
+  gboolean (* grab_focus)               (GtkWidget           *widget);
+  gboolean (* focus)                    (GtkWidget           *widget,
+                                         GtkDirectionType     direction);
+  void     (* set_focus_child)          (GtkWidget           *widget,
+                                         GtkWidget           *child);
+
+  /* keyboard navigation */
+  void     (* move_focus)               (GtkWidget           *widget,
+                                         GtkDirectionType     direction);
+  gboolean (* keynav_failed)            (GtkWidget           *widget,
+                                         GtkDirectionType     direction);
+
+  gboolean     (* query_tooltip)      (GtkWidget  *widget,
+                                       int         x,
+                                       int         y,
+                                       gboolean    keyboard_tooltip,
+                                       GtkTooltip *tooltip);
+
+  void         (* compute_expand)     (GtkWidget  *widget,
+                                       gboolean   *hexpand_p,
+                                       gboolean   *vexpand_p);
+
+  void         (* css_changed)                 (GtkWidget            *widget,
+                                                GtkCssStyleChange    *change);
+
+  void         (* system_setting_changed)      (GtkWidget            *widget,
+                                                GtkSystemSetting      settings);
+
+  void         (* snapshot)                    (GtkWidget            *widget,
+                                                GtkSnapshot          *snapshot);
+
+  gboolean     (* contains)                    (GtkWidget *widget,
+                                                double     x,
+                                                double     y);
+
+  /*< private >*/
+
+  GtkWidgetClassPrivate *priv;
+
+  gpointer padding[8];
+};
+
+struct _GtkTextViewClass
+{
+  GtkWidgetClass parent_class;
+
+  /*< public >*/
+
+  void (* move_cursor)           (GtkTextView      *text_view,
+                                  GtkMovementStep   step,
+                                  int               count,
+                                  gboolean          extend_selection);
+  void (* set_anchor)            (GtkTextView      *text_view);
+  void (* insert_at_cursor)      (GtkTextView      *text_view,
+                                  const char       *str);
+  void (* delete_from_cursor)    (GtkTextView      *text_view,
+                                  GtkDeleteType     type,
+                                  int               count);
+  void (* backspace)             (GtkTextView      *text_view);
+  void (* cut_clipboard)         (GtkTextView      *text_view);
+  void (* copy_clipboard)        (GtkTextView      *text_view);
+  void (* paste_clipboard)       (GtkTextView      *text_view);
+  void (* toggle_overwrite)      (GtkTextView      *text_view);
+  GtkTextBuffer * (* create_buffer) (GtkTextView   *text_view);
+  void (* snapshot_layer)        (GtkTextView      *text_view,
+                                  GtkTextViewLayer  layer,
+                                  GtkSnapshot      *snapshot);
+  gboolean (* extend_selection)  (GtkTextView            *text_view,
+                                  GtkTextExtendSelection  granularity,
+                                  const GtkTextIter      *location,
+                                  GtkTextIter            *start,
+                                  GtkTextIter            *end);
+  void (* insert_emoji)          (GtkTextView      *text_view);
+
+  /*< private >*/
+
+  gpointer padding[8];
+};
+
+/* The following definition is generated by the macro G_DECLARE_FINAL_TYPE */
+typedef struct {
+  GtkTextView parent_class;
+} TfeTextViewClass;
+ +

TfeTextViewClass includes its ancestors’ class in it. It is illustrated in the following diagram.

+
+
The structure of TfeTextView Class
+
+

Destruction of TfeTextView

+

Every Object derived from GObject has a reference count. If an object A refers to an object B, then A keeps a pointer to B in A and at the same time increases the reference count of B by one with the function g_object_ref (B). If A doesn’t need B any longer, then A discards the pointer to B (usually it is done by assigning NULL to the pointer) and decreases the reference count of B by one with the function g_object_unref (B).

+

If two objects A and B refer to C, then the reference count of C is two. If A no longer needs C, A discards the pointer to C and decreases the reference count in C by one. Now the reference count of C is one. In the same way, if B no longer needs C, B discards the pointer to C and decreases the reference count in C by one. At this moment, no object refers to C and the reference count of C is zero. This means C is no longer useful. Then C destructs itself and finally the memories allocated to C is freed.

+
+
Reference count of B
+
+

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 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. For example, dispose handler or dispose method.

+

In the destruction process of TfeTextView, the reference count of widgets related to TfeTextView is automatically decreased. But GFile pointed by tv->file needs to decrease its reference count by one. You must write the code in the dispose handler tfe_text_view_dispose.

+
static void
+tfe_text_view_dispose (GObject *gobject) {
+  TfeTextView *tv = TFE_TEXT_VIEW (gobject);
+
+  if (G_IS_FILE (tv->file))
+    g_clear_object (&tv->file);
+
+  G_OBJECT_CLASS (tfe_text_view_parent_class)->dispose (gobject);
+}
+ +

In the disposing process, the object uses the pointer in its class to call the handler. Therefore, tfe_text_view_dispose needs to be registered in the class when the TfeTextView class is initialized. The function tfe_text_view_class_init is the class initialization function and it is declared in the replacement produced by G_DEFINE_TYPE macro.

+
static void
+tfe_text_view_class_init (TfeTextViewClass *class) {
+  GObjectClass *object_class = G_OBJECT_CLASS (class);
+
+  object_class->dispose = tfe_text_view_dispose;
+
+}
+

Each ancestors’ class has been created before TfeTextViewClass is created. Therefore, there are four classes and each class has a pointer to each dispose handler. Look at the following diagram. There are four classes – GObjectClass (GInitiallyUnownedClass), GtkWidgetClass, GtkTextViewClass and TfeTextViewClass. Each class has its own dispose handler – dh1, dh2, dh3 and tfe_text_view_dispose.

+
+
dispose handlers
+
+

Now, look at the tfe_text_view_dispose program above. It first releases the reference to GFile object pointed by tv->file. Then it invokes its parent’s dispose handler in line 8.

+
G_OBJECT_CLASS (tfe_text_view_parent_class)->dispose (gobject);
+

tfe_text_view_parent_class,which is made by G_DEFINE_TYPE macro, is a pointer that points the parent object class. Therefore, G_OBJECT_CLASS (tfe_text_view_parent_class)->dispose points the handler dh3 in the diagram above. And gobject is a pointer to TfeTextView instance which is casted as a GObject instance. dh3 releases all the references to objects in the GtkTextView part (it is actually the private area pointed by prev) in TfeTextView instance. After that, dh3 calls dh2, and dh2 calls dh1. Finally all the references are released.

+

Up: index.html, Prev: Section 10, Next: Section 12

+ + diff --git a/docs/sec12.html b/docs/sec12.html new file mode 100644 index 0000000..89d8c31 --- /dev/null +++ b/docs/sec12.html @@ -0,0 +1,206 @@ + + + + + + + Gtk4 tutorial for beginners + + + +

Up: index.html, Prev: Section 11, Next: Section 13

+

Signals

+

Signals

+

In Gtk programming, each object is encapsulated. And it is not recommended to use global variables because they tend to make the program complicated. So, we need something to communicate between objects. There are two ways to do so.

+ +

The caller of the function or the handler connected to the signal is usually out of the object. One of the difference between these two is that the object is active or passive. In functions the object passively responds to the caller. In signals the object actively sends a signal to the handler.

+

GObject signals are registered, connected and emitted.

+
    +
  1. Signals are registered with the object type on which they are emitted. The registration is done usually when the object class is initialized.
  2. +
  3. Signals are connected to handlers by g_connect_signal or its family functions. The connection is usually done out of the object.
  4. +
  5. When Signals are emitted, the connected handlers are invoked. Signal is emitted on the instance of the object.
  6. +
+

Signal registration

+

In TfeTextView, two signals are registered.

+ +

A static variable or array is used to store the signal ID. A static array is used to register two or more signals.

+
enum {
+  CHANGE_FILE,
+  OPEN_RESPONSE,
+  NUMBER_OF_SIGNALS
+};
+
+static guint tfe_text_view_signals[NUMBER_OF_SIGNALS];
+

Signals are registered in the class initialization function.

+
static void
+tfe_text_view_class_init (TfeTextViewClass *class) {
+  GObjectClass *object_class = G_OBJECT_CLASS (class);
+
+  object_class->dispose = tfe_text_view_dispose;
+  tfe_text_view_signals[CHANGE_FILE] = g_signal_new ("change-file",
+                                 G_TYPE_FROM_CLASS (class),
+                                 G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS,
+                                 0 /* class offset */,
+                                 NULL /* accumulator */,
+                                 NULL /* accumulator data */,
+                                 NULL /* C marshaller */,
+                                 G_TYPE_NONE /* return_type */,
+                                 0     /* n_params */
+                                 );
+  tfe_text_view_signals[OPEN_RESPONSE] = g_signal_new ("open-response",
+                                 G_TYPE_FROM_CLASS (class),
+                                 G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS,
+                                 0 /* class offset */,
+                                 NULL /* accumulator */,
+                                 NULL /* accumulator data */,
+                                 NULL /* C marshaller */,
+                                 G_TYPE_NONE /* return_type */,
+                                 1     /* n_params */,
+                                 G_TYPE_INT
+                                 );
+}
+ +

The handlers are declared as follows.

+
/* "change-file" signal handler */
+void
+user_function (TfeTextView *tv,
+               gpointer user_data)
+
+/* "open-response" signal handler */
+void
+user_function (TfeTextView *tv,
+               TfeTextViewOpenResponseType response-id,
+               gpointer user_data)
+ +

The values of the parameter is defined in tfetextview.h because they are public.

+
/* "open-response" signal response */
+enum
+{
+  TFE_OPEN_RESPONSE_SUCCESS,
+  TFE_OPEN_RESPONSE_CANCEL,
+  TFE_OPEN_RESPONSE_ERROR
+};
+ +

Signal connection

+

A signal and a handler are connected by the function g_signal_connect. There are some similar functions like g_signal_connect_after, g_signal_connect_swapped and so on. However, g_signal_connect is the most common. The signals “change-file” is connected to a callback function out of the TfeTextView object. In the same way, the signals “open-response” is connected to a callback function out of the TfeTextView object. Those callback functions are defined by users.

+

In the program tfe, callback functions are defined in tfenotebook.c. And their names are file_changed and open_response. They will be explained later.

+
g_signal_connect (GTK_TEXT_VIEW (tv), "change-file", G_CALLBACK (file_changed), nb);
+
+g_signal_connect (TFE_TEXT_VIEW (tv), "open-response", G_CALLBACK (open_response), nb);
+

Signal emission

+

Signals are emitted on an instance. The type of the instance is the second argument of g_signal_new. The relationship between the signal and object type is determined when the signal is registered.

+

A function g_signal_emit is used to emit the signal. The following lines are extracted from tfetextview.c. Each line comes from a different line.

+
g_signal_emit (tv, tfe_text_view_signals[CHANGE_FILE], 0);
+g_signal_emit (tv, tfe_text_view_signals[OPEN_RESPONSE], 0, TFE_OPEN_RESPONSE_SUCCESS);
+g_signal_emit (tv, tfe_text_view_signals[OPEN_RESPONSE], 0, TFE_OPEN_RESPONSE_CANCEL);
+g_signal_emit (tv, tfe_text_view_signals[OPEN_RESPONSE], 0, TFE_OPEN_RESPONSE_ERROR);
+ +

Up: index.html, Prev: Section 11, Next: Section 13

+ + diff --git a/docs/sec13.html b/docs/sec13.html new file mode 100644 index 0000000..be54660 --- /dev/null +++ b/docs/sec13.html @@ -0,0 +1,402 @@ + + + + + + + Gtk4 tutorial for beginners + + + +

Up: index.html, Prev: Section 12, Next: Section 14

+

Functions in TfeTextView

+

In this section I will explain functions in TfeTextView object.

+

tfe.h and tfetextview.h

+

tfe.h is a top header file and it includes gtk.h and all the header files. C source files tfeapplication.c and tfenotebook.c include tfe.h at the beginning.

+
#include <gtk/gtk.h>
+
+#include "../tfetextview/tfetextview.h"
+#include "tfenotebook.h"
+

../tfetextview/tfetextview.h is a header file which describes the public functions in tfetextview.c.

+
#ifndef __TFE_TEXT_VIEW_H__
+#define __TFE_TEXT_VIEW_H__
+
+#include <gtk/gtk.h>
+
+#define TFE_TYPE_TEXT_VIEW tfe_text_view_get_type ()
+G_DECLARE_FINAL_TYPE (TfeTextView, tfe_text_view, TFE, TEXT_VIEW, GtkTextView)
+
+/* "open-response" signal response */
+enum TfeTextViewOpenResponseType
+{
+  TFE_OPEN_RESPONSE_SUCCESS,
+  TFE_OPEN_RESPONSE_CANCEL,
+  TFE_OPEN_RESPONSE_ERROR
+};
+
+GFile *
+tfe_text_view_get_file (TfeTextView *tv);
+
+void
+tfe_text_view_open (TfeTextView *tv, GtkWindow *win);
+
+void
+tfe_text_view_save (TfeTextView *tv);
+
+void
+tfe_text_view_saveas (TfeTextView *tv);
+
+GtkWidget *
+tfe_text_view_new_with_file (GFile *file);
+
+GtkWidget *
+tfe_text_view_new (void);
+
+#endif /* __TFE_TEXT_VIEW_H__ */
+ +

Functions to create TfeTextView instances

+

A TfeTextView instance is created with tfe_text_view_new or tfe_text_view_new_with_file.

+
GtkWidget *tfe_text_view_new (void);
+

tfe_text_view_new just creates a new TfeTextView instance and returns the pointer to the new instance.

+
GtkWidget *tfe_text_view_new_with_file (GFile *file);
+

tfe_text_view_new_with_file is given a Gfile object as an argument and it loads the file into the GtkTextBuffer instance, then returns the pointer to the new instance. If an error occurs during the creation process, NULL is returned.

+

Each function is defined as follows.

+
GtkWidget *
+tfe_text_view_new_with_file (GFile *file) {
+  g_return_val_if_fail (G_IS_FILE (file), NULL);
+
+  GtkWidget *tv;
+  GtkTextBuffer *tb;
+  char *contents;
+  gsize length;
+
+  if (! g_file_load_contents (file, NULL, &contents, &length, NULL, NULL)) /* read error */
+    return NULL;
+
+  if ((tv = tfe_text_view_new()) != NULL) {
+    tb = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv));
+    gtk_text_buffer_set_text (tb, contents, length);
+    TFE_TEXT_VIEW (tv)->file = g_file_dup (file);
+    gtk_text_buffer_set_modified (tb, FALSE);
+  }
+  g_free (contents);
+  return tv;
+}
+
+GtkWidget *
+tfe_text_view_new (void) {
+  return GTK_WIDGET (g_object_new (TFE_TYPE_TEXT_VIEW, NULL));
+}
+ +

Save and saveas functions

+

Save and saveas functions write the contents in the GtkTextBuffer to a file.

+
void tfe_text_view_save (TfeTextView *tv)
+

The function tfe_text_view_save writes the contents in the GtkTextBuffer to a file specified by tv->file. If tv->file is NULL, then it shows GtkFileChooserDialog and prompts the user to choose a file to save. Then it saves the contents to the file and sets tv->file to point the GFile instance for the file.

+
void tfe_text_view_saveas (TfeTextView *tv)
+

The function saveas uses GtkFileChooserDialog and prompts the user to select a existed file or specify a new file to save. Then, the function changes tv->file and save the contents to the specified file. If an error occurs, it is shown to the user through the message dialog. The error is managed only in the TfeTextView and no information is notified to the caller.

+
static gboolean
+save_file (GFile *file, GtkTextBuffer *tb, GtkWindow *win) {
+  GtkTextIter start_iter;
+  GtkTextIter end_iter;
+  gchar *contents;
+  gboolean stat;
+  GtkWidget *message_dialog;
+  GError *err = NULL;
+
+  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);
+    stat = 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);
+    stat = FALSE;
+  }
+  g_free (contents);
+  return stat;
+}
+
+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);
+
+  if (response == GTK_RESPONSE_ACCEPT) {
+    file = gtk_file_chooser_get_file (GTK_FILE_CHOOSER (dialog));
+    if (! G_IS_FILE (file))
+      g_warning ("TfeTextView: gtk_file_chooser_get_file returns non GFile.\n");
+    else if (save_file(file, tb, GTK_WINDOW (win))) {
+      if (G_IS_FILE (tv->file))
+        g_object_unref (tv->file);
+      tv->file = file;
+      g_signal_emit (tv, tfe_text_view_signals[CHANGE_FILE], 0);
+    } else
+      g_object_unref (file);
+  }
+  gtk_window_destroy (GTK_WINDOW (dialog));
+}
+
+void
+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));
+  GtkWidget *win = gtk_widget_get_ancestor (GTK_WIDGET (tv), GTK_TYPE_WINDOW);
+
+  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 tv->file isn't NULL nor GFile.\n");
+  else
+    save_file (tv->file, tb, GTK_WINDOW (win));
+}
+
+void
+tfe_text_view_saveas (TfeTextView *tv) {
+  g_return_if_fail (TFE_IS_TEXT_VIEW (tv));
+
+  GtkWidget *dialog;
+  GtkWidget *win = gtk_widget_get_ancestor (GTK_WIDGET (tv), GTK_TYPE_WINDOW);
+
+  dialog = gtk_file_chooser_dialog_new ("Save file", GTK_WINDOW (win), GTK_FILE_CHOOSER_ACTION_SAVE,
+                                      "Cancel", GTK_RESPONSE_CANCEL,
+                                      "Save", GTK_RESPONSE_ACCEPT,
+                                      NULL);
+  g_signal_connect (dialog, "response", G_CALLBACK (saveas_dialog_response), tv);
+  gtk_widget_show (dialog);
+}
+ +
+
Saveas process
+
+

When you use GtkFileChooserDialog, you need to divide the program into two parts. One is a function which creates GtkFileChooserDialog and the other is a signal handler. The function just creates and shows GtkFileChooserDialog. The rest is done by the handler. It gets Gfile from GtkFileChooserDialog and saves the buffer to the file by calling save_file.

+

Open function

+

Open function shows GtkFileChooserDialog to users and prompts them to choose a file. Then it reads the file and puts the text into GtkTextBuffer.

+
void tfe_text_view_open (TfeTextView *tv, GtkWindow *win);
+

The parameter win is the top-level window. It will be a transient parent window of GtkFileChooserDialog when the dialog is created. This allows window managers to keep the dialog on top of the parent window, or center the dialog over the parent window. It is possible to give no parent window to the dialog. However, it is encouraged to give a parent window to dialog. This function might be called just after tv has been created. In that case, tv has not been incorporated into the widget hierarchy. Therefore it is impossible to get the top-level window from tv. That’s why the function needs win parameter.

+

This function is usually called when the buffer of tv is empty. However, even if the buffer is not empty, tfe_text_view_open doesn’t treat it as an error. If you want to revert the buffer, calling this function is appropriate. Otherwise probably bad things will happen.

+
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.\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 */
+    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, GtkWindow *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", 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);
+}
+ +

Now let’s think about the whole process between the caller and TfeTextView. It is shown in the following diagram and you would think that it is really complicated. Because signal is the only way for GtkFileChooserDialog to communicate with others. In Gtk3, gtk_dialog_run function is available. It simplifies the process. However, in Gtk4, gtk_dialog_run is unavailable any more.

+
+
Caller and TfeTextView
+
+
    +
  1. A caller gets a pointer tv to a TfeTextView instance by calling tfe_text_view_new.
  2. +
  3. The caller connects the handler (left bottom in the diagram) and the signal “open-response”.
  4. +
  5. It calls tfe_text_view_open to prompt the user to select a file from GtkFileChooserDialog.
  6. +
  7. The dialog emits a signal and it invokes the handler open_dialog_response.
  8. +
  9. The handler reads the file and inserts the text into GtkTextBuffer and emits a signal to inform the status as a response code.
  10. +
  11. The handler out of the TfeTextView receives the signal.
  12. +
+

Getting Gfile

+

gtk_text_view_get_file is a simple function shown as follows.

+
GFile *
+tfe_text_view_get_file (TfeTextView *tv) {
+  g_return_val_if_fail (TFE_IS_TEXT_VIEW (tv), NULL);
+
+  if (G_IS_FILE (tv->file))
+    return g_file_dup (tv->file);
+  else
+    return NULL;
+}
+

The important thing is to duplicate tv->file. Otherwise, if the caller frees the GFile object, tv->file is no more guaranteed to point the GFile. Another reason to use g_file_dup is that GFile isn’t thread-safe. If you use GFile in the different thread, the duplication is necessary. See Gio API Reference, g_file_dup.

+

The API document and source file of tfetextview.c

+

Refer API document of TfeTextView. Its original markdown file is under the directory src/tfetextview.

+

All the source files are listed in Section 16. You can find them under src/tfe5 and src/tfetextview directories.

+

Up: index.html, Prev: Section 12, Next: Section 14

+ + diff --git a/docs/sec14.html b/docs/sec14.html new file mode 100644 index 0000000..f788552 --- /dev/null +++ b/docs/sec14.html @@ -0,0 +1,318 @@ + + + + + + + Gtk4 tutorial for beginners + + + +

Up: index.html, Prev: Section 13, Next: Section 15

+

Functions in GtkNotebook

+

GtkNotebook is a very important object in the text file editor tfe. It connects the application and TfeTextView objects. A set of public functions are declared in tfenotebook.h. The word “tfenotebook” is used only in filenames. There’s no “TfeNotebook” object.

+
void
+notebook_page_save(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);
+

This header file describes the public functions in tfenotebook.c.

+ +

You probably find that the functions except notebook_page_close are higher level functions of

+ +

respectively.

+

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 the program of each function.

+

notebook_page_new

+
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 void
+notebook_page_build (GtkNotebook *nb, GtkWidget *tv, char *filename) {
+  GtkWidget *scr = gtk_scrolled_window_new ();
+  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), nb);
+}
+
+void
+notebook_page_new (GtkNotebook *nb) {
+  g_return_if_fail(GTK_IS_NOTEBOOK (nb));
+
+  GtkWidget *tv;
+  char *filename;
+
+  if ((tv = tfe_text_view_new ()) == NULL)
+    return;
+  filename = get_untitled ();
+  notebook_page_build (nb, tv, filename);
+}
+ +

notebook_page_new_with_file

+
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);
+}
+ +

notebook_page_open

+
static void
+open_response (TfeTextView *tv, int response, GtkNotebook *nb) {
+  GFile *file;
+  char *filename;
+
+  if (response != TFE_OPEN_RESPONSE_SUCCESS || ! 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;
+
+  if ((tv = tfe_text_view_new ()) == NULL)
+    return;
+  g_signal_connect (TFE_TEXT_VIEW (tv), "open-response", G_CALLBACK (open_response), nb);
+  tfe_text_view_open (TFE_TEXT_VIEW (tv), GTK_WINDOW (gtk_widget_get_ancestor (GTK_WIDGET (nb), GTK_TYPE_WINDOW)));
+}
+ +

All the widgets are derived from GInitiallyUnowned. When an instance of GInitiallyUnowned or its descendant is created, the instance has a floating reference. The function g_object_ref_sink converts the floating reference into an ordinary reference. If the instance doesn’t have a floating reference, g_object_ref_sink simply increases the reference count by one. On the other hand, when an instance of GObject (not GInitiallyUnowned) is created, no floating reference is given. And the instance has a normal reference count instead of floating reference.

+

If you use g_object_unref to an instance that has a floating reference, you need to convert the floating reference to a normal reference in advance. See GObject Reference Manual for further information.

+

notebook_page_close

+
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);
+  }
+}
+

This function closes the current page. If the page is the only page the notebook has, then the function destroys the top-level window and quits the application.

+ +

notebook_page_save

+
static TfeTextView *
+get_current_textview (GtkNotebook *nb) {
+  int i;
+  GtkWidget *scr;
+  GtkWidget *tv;
+
+  i = gtk_notebook_get_current_page (nb);
+  scr = gtk_notebook_get_nth_page (nb, i);
+  tv = gtk_scrolled_window_get_child (GTK_SCROLLED_WINDOW (scr));
+  return TFE_TEXT_VIEW (tv);
+}
+
+void
+notebook_page_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));
+}
+ +

file_changed_cb handler

+

The function file_changed_cb is a handler connected to “change-file” signal. If a file in a TfeTextView instance is changed, it emits this signal. This handler changes the label of GtkNotebookPage.

+
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);
+  g_free (filename);
+  gtk_notebook_set_tab_label (nb, scr, label);
+}
+ +

Up: index.html, Prev: Section 13, Next: Section 15

+ + diff --git a/docs/sec15.html b/docs/sec15.html new file mode 100644 index 0000000..590d78f --- /dev/null +++ b/docs/sec15.html @@ -0,0 +1,306 @@ + + + + + + + Gtk4 tutorial for beginners + + + +

Up: index.html, Prev: Section 14, Next: Section 16

+

tfeapplication.c

+

tfeapplication.c includes all the code other than tfetxtview.c and tfenotebook.c. It does:

+ +

main

+

The function main is the first invoked function in C language. It connects the command line given by the user and Gtk application.

+
#define APPLICATION_ID "com.github.ToshioCP.tfe"
+
+int
+main (int argc, char **argv) {
+  GtkApplication *app;
+  int stat;
+
+  app = gtk_application_new (APPLICATION_ID, G_APPLICATION_HANDLES_OPEN);
+
+  g_signal_connect (app, "startup", G_CALLBACK (app_startup), NULL);
+  g_signal_connect (app, "activate", G_CALLBACK (app_activate), NULL);
+  g_signal_connect (app, "open", G_CALLBACK (app_open), NULL);
+
+  stat =g_application_run (G_APPLICATION (app), argc, argv);
+  g_object_unref (app);
+  return stat;
+}
+ +

startup signal handler

+

Startup signal is emitted just after the GtkApplication instance is initialized. What the signal handler needs to do is initialization of the application.

+ +

The handler is as follows.

+
static void
+app_startup (GApplication *application) {
+  GtkApplication *app = GTK_APPLICATION (application);
+  GtkBuilder *build;
+  GtkApplicationWindow *win;
+  GtkNotebook *nb;
+  GtkButton *btno;
+  GtkButton *btnn;
+  GtkButton *btns;
+  GtkButton *btnc;
+
+  build = gtk_builder_new_from_resource ("/com/github/ToshioCP/tfe/tfe.ui");
+  win = GTK_APPLICATION_WINDOW (gtk_builder_get_object (build, "win"));
+  nb = GTK_NOTEBOOK (gtk_builder_get_object (build, "nb"));
+  gtk_window_set_application (GTK_WINDOW (win), app);
+  btno = GTK_BUTTON (gtk_builder_get_object (build, "btno"));
+  btnn = GTK_BUTTON (gtk_builder_get_object (build, "btnn"));
+  btns = GTK_BUTTON (gtk_builder_get_object (build, "btns"));
+  btnc = GTK_BUTTON (gtk_builder_get_object (build, "btnc"));
+  g_signal_connect_swapped (btno, "clicked", G_CALLBACK (open_cb), nb);
+  g_signal_connect_swapped (btnn, "clicked", G_CALLBACK (new_cb), nb);
+  g_signal_connect_swapped (btns, "clicked", G_CALLBACK (save_cb), nb);
+  g_signal_connect_swapped (btnc, "clicked", G_CALLBACK (close_cb), nb);
+  g_object_unref(build);
+
+GdkDisplay *display;
+
+  display = gtk_widget_get_display (GTK_WIDGET (win));
+  GtkCssProvider *provider = gtk_css_provider_new ();
+  gtk_css_provider_load_from_data (provider, "textview {padding: 10px; font-family: monospace; font-size: 12pt;}", -1);
+  gtk_style_context_add_provider_for_display (display, GTK_STYLE_PROVIDER (provider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
+}
+ +

CSS in Gtk

+

CSS is an abbreviation of Cascading Style Sheet. It is originally used with HTML to describe the presentation semantics of a document. You might have found that the widgets in Gtk is similar to a window in a browser. It implies that CSS can also be applied to Gtk windowing system.

+

CSS nodes, selectors

+

The syntax of CSS is as follows.

+
selector { color: yellow; padding-top: 10px; ...}
+

Every widget has CSS node. For example GtkTextView has textview node. 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 to Gtk4 API Reference, CSS in Gtk for further information.

+

In line 30, the CSS is a string.

+
textview {padding: 10px; font-family: monospace; font-size: 12pt;}
+ +

GtkStyleContext, GtkCSSProvider and GdkDisplay

+

GtkStyleContext is an object that stores styling information affecting a widget. Each widget is connected to the corresponding GtkStyleContext. You can get the context by gtk_widget_get_style_context.

+

GtkCssProvider is an object which parses CSS in order to style widgets.

+

To apply your CSS to widgets, you need to add GtkStyleProvider (the interface of GtkCSSProvider) to GtkStyleContext. However, instead, you can add it to GdkDisplay of the window (usually top-level window).

+

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.

+
GtkWidget *
+tfe_text_view_new (void) {
+  GtkWidget *tv;
+
+  tv = gtk_widget_new (TFE_TYPE_TEXT_VIEW, NULL);
+
+  GtkStyleContext *context;
+
+  context = gtk_widget_get_style_context (GTK_WIDGET (tv));
+  GtkCssProvider *provider = gtk_css_provider_new ();
+  gtk_css_provider_load_from_data (provider, "textview {padding: 10px; font-family: monospace; font-size: 12pt;}", -1);
+  gtk_style_context_add_provider (context, GTK_STYLE_PROVIDER (provider), GTK_STYLE_PROVIDER_PRIORITY_USER);
+
+  return tv;
+}
+

CSS in the context takes precedence over CSS in the display.

+

activate and open handler

+

The handler of “activate” and “open” signal are app_activate and app_open respectively. They just create a new GtkNotebookPage.

+
static void
+app_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
+app_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);
+}
+ +

These codes have become really simple thanks to tfenotebook.c and tfetextview.c.

+

Primary instance

+

Only one GApplication instance can be run at a time per session. The session is a bit difficult concept and also platform-dependent, but roughly speaking, it corresponds to a graphical desktop login. When you use your PC, you probably login first, then your desktop appears until you log off. This is the session.

+

However, Linux is multi process OS and you can run two or more instances of the same application. Isn’t it a contradiction?

+

When first instance is launched, then it registers itself with its application ID (for example, com.github.ToshioCP.tfe). Just after the registration, startup signal is emitted, then activate or open signal is emitted and the instance’s main loop runs. I wrote “startup signal is emitted just after the application instance is initialized” in the prior subsection. More precisely, it is emitted just after the registration.

+

If another instance which has the same application ID is invoked, it also tries to register itself. Because this is the second instance, the registration of the ID has already done, so it fails. Because of the failure startup signal isn’t emitted. After that, activate or open signal is emitted in the primary instance, not the second instance. The primary instance receives the signal and its handler is invoked. On the other hand, the second instance doesn’t receive the signal and it immediately quits.

+

Try to run two instances in a row.

+
$ ./_build/tfe &
+[1] 84453
+$ ./build/tfe tfeapplication.c
+$
+

First, the primary instance opens a window. Then, after the second instance is run, a new notebook page with the contents of tfeapplication.c appears in the primary instance’s window. This is because the open signal is emitted in the primary instance. The second instance immediately quits so shell prompt soon appears.

+

a series of handlers correspond to the button signals

+
static void
+open_cb (GtkNotebook *nb) {
+  notebook_page_open (nb);
+}
+
+static void
+new_cb (GtkNotebook *nb) {
+  notebook_page_new (nb);
+}
+
+static void
+save_cb (GtkNotebook *nb) {
+  notebook_page_save (nb);
+}
+
+static void
+close_cb (GtkNotebook *nb) {
+  notebook_page_close (GTK_NOTEBOOK (nb));
+}
+

open_cb, new_cb, save_cb and close_cb just call corresponding notebook page functions.

+

meson.build

+
project('tfe', 'c')
+
+gtkdep = dependency('gtk4')
+
+gnome=import('gnome')
+resources = gnome.compile_resources('resources','tfe.gresource.xml')
+
+sourcefiles=files('tfeapplication.c', 'tfenotebook.c', '../tfetextview/tfetextview.c')
+
+executable('tfe', sourcefiles, resources, dependencies: gtkdep)
+

In this file, just the source file names are modified from the prior version.

+

source files

+

The source files of the text editor tfe will be shown in the next section.

+

You can also download the files from the repository. There are two options.

+ +

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.

+

Up: index.html, Prev: Section 14, Next: Section 16

+ + diff --git a/docs/sec16.html b/docs/sec16.html new file mode 100644 index 0000000..c7008e5 --- /dev/null +++ b/docs/sec16.html @@ -0,0 +1,730 @@ + + + + + + + Gtk4 tutorial for beginners + + + +

Up: index.html, Prev: Section 15, Next: Section 17

+

tfe5 source files

+

How to compile and execute the text editor ‘tfe’.

+

First, source files are shown in the later subsections. How to download them is written at the end of the previous section.

+

The following is the instruction of compilation and execution.

+ +
$ . env.sh
+ +

Then the window appears. There are four buttons, New, Open, Save and Close.

+ +

This is a very simple editor. It is a good practice for you to add more features.

+

meson.build

+
project('tfe', 'c')
+
+gtkdep = dependency('gtk4')
+
+gnome=import('gnome')
+resources = gnome.compile_resources('resources','tfe.gresource.xml')
+
+sourcefiles=files('tfeapplication.c', 'tfenotebook.c', '../tfetextview/tfetextview.c')
+
+executable('tfe', sourcefiles, resources, dependencies: gtkdep)
+

tfe.gresource.xml

+
<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/com/github/ToshioCP/tfe">
+    <file>tfe.ui</file>
+  </gresource>
+</gresources>
+

tfe.ui

+
<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <object class="GtkApplicationWindow" id="win">
+    <property name="title">file editor</property>
+    <property name="default-width">600</property>
+    <property name="default-height">400</property>
+    <child>
+      <object class="GtkBox" id="boxv">
+        <property name="orientation">GTK_ORIENTATION_VERTICAL</property>
+        <child>
+          <object class="GtkBox" id="boxh">
+            <property name="orientation">GTK_ORIENTATION_HORIZONTAL</property>
+            <child>
+              <object class="GtkLabel" id="dmy1">
+                <property name="width-chars">10</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkButton" id="btnn">
+                <property name="label">New</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkButton" id="btno">
+                <property name="label">Open</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkLabel" id="dmy2">
+                <property name="hexpand">TRUE</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkButton" id="btns">
+                <property name="label">Save</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkButton" id="btnc">
+                <property name="label">Close</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkLabel" id="dmy3">
+                <property name="width-chars">10</property>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="GtkNotebook" id="nb">
+            <property name="scrollable">TRUE</property>
+            <property name="hexpand">TRUE</property>
+            <property name="vexpand">TRUE</property>
+          </object>
+        </child>
+      </object>
+    </child>
+  </object>
+</interface>
+

tfe.h

+
#include <gtk/gtk.h>
+
+#include "../tfetextview/tfetextview.h"
+#include "tfenotebook.h"
+

tfeapplication.c

+
#include "tfe.h"
+
+static void
+open_cb (GtkNotebook *nb) {
+  notebook_page_open (nb);
+}
+
+static void
+new_cb (GtkNotebook *nb) {
+  notebook_page_new (nb);
+}
+
+static void
+save_cb (GtkNotebook *nb) {
+  notebook_page_save (nb);
+}
+
+static void
+close_cb (GtkNotebook *nb) {
+  notebook_page_close (GTK_NOTEBOOK (nb));
+}
+
+static void
+app_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
+app_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
+app_startup (GApplication *application) {
+  GtkApplication *app = GTK_APPLICATION (application);
+  GtkBuilder *build;
+  GtkApplicationWindow *win;
+  GtkNotebook *nb;
+  GtkButton *btno;
+  GtkButton *btnn;
+  GtkButton *btns;
+  GtkButton *btnc;
+
+  build = gtk_builder_new_from_resource ("/com/github/ToshioCP/tfe/tfe.ui");
+  win = GTK_APPLICATION_WINDOW (gtk_builder_get_object (build, "win"));
+  nb = GTK_NOTEBOOK (gtk_builder_get_object (build, "nb"));
+  gtk_window_set_application (GTK_WINDOW (win), app);
+  btno = GTK_BUTTON (gtk_builder_get_object (build, "btno"));
+  btnn = GTK_BUTTON (gtk_builder_get_object (build, "btnn"));
+  btns = GTK_BUTTON (gtk_builder_get_object (build, "btns"));
+  btnc = GTK_BUTTON (gtk_builder_get_object (build, "btnc"));
+  g_signal_connect_swapped (btno, "clicked", G_CALLBACK (open_cb), nb);
+  g_signal_connect_swapped (btnn, "clicked", G_CALLBACK (new_cb), nb);
+  g_signal_connect_swapped (btns, "clicked", G_CALLBACK (save_cb), nb);
+  g_signal_connect_swapped (btnc, "clicked", G_CALLBACK (close_cb), nb);
+  g_object_unref(build);
+
+GdkDisplay *display;
+
+  display = gtk_widget_get_display (GTK_WIDGET (win));
+  GtkCssProvider *provider = gtk_css_provider_new ();
+  gtk_css_provider_load_from_data (provider, "textview {padding: 10px; font-family: monospace; font-size: 12pt;}", -1);
+  gtk_style_context_add_provider_for_display (display, GTK_STYLE_PROVIDER (provider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
+}
+
+#define APPLICATION_ID "com.github.ToshioCP.tfe"
+
+int
+main (int argc, char **argv) {
+  GtkApplication *app;
+  int stat;
+
+  app = gtk_application_new (APPLICATION_ID, G_APPLICATION_HANDLES_OPEN);
+
+  g_signal_connect (app, "startup", G_CALLBACK (app_startup), NULL);
+  g_signal_connect (app, "activate", G_CALLBACK (app_activate), NULL);
+  g_signal_connect (app, "open", G_CALLBACK (app_open), NULL);
+
+  stat =g_application_run (G_APPLICATION (app), argc, argv);
+  g_object_unref (app);
+  return stat;
+}
+

tfenotebook.h

+
void
+notebook_page_save(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);
+

tfenotebook.c

+
#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, 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);
+  g_free (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 = gtk_scrolled_window_new ();
+  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), nb);
+}
+
+static void
+open_response (TfeTextView *tv, int response, GtkNotebook *nb) {
+  GFile *file;
+  char *filename;
+
+  if (response != TFE_OPEN_RESPONSE_SUCCESS || ! 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;
+
+  if ((tv = tfe_text_view_new ()) == NULL)
+    return;
+  g_signal_connect (TFE_TEXT_VIEW (tv), "open-response", G_CALLBACK (open_response), nb);
+  tfe_text_view_open (TFE_TEXT_VIEW (tv), GTK_WINDOW (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;
+
+  if ((tv = tfe_text_view_new ()) == NULL)
+    return;
+  filename = get_untitled ();
+  notebook_page_build (nb, tv, filename);
+}
+

tfetextview.h

+
#ifndef __TFE_TEXT_VIEW_H__
+#define __TFE_TEXT_VIEW_H__
+
+#include <gtk/gtk.h>
+
+#define TFE_TYPE_TEXT_VIEW tfe_text_view_get_type ()
+G_DECLARE_FINAL_TYPE (TfeTextView, tfe_text_view, TFE, TEXT_VIEW, GtkTextView)
+
+/* "open-response" signal response */
+enum TfeTextViewOpenResponseType
+{
+  TFE_OPEN_RESPONSE_SUCCESS,
+  TFE_OPEN_RESPONSE_CANCEL,
+  TFE_OPEN_RESPONSE_ERROR
+};
+
+GFile *
+tfe_text_view_get_file (TfeTextView *tv);
+
+void
+tfe_text_view_open (TfeTextView *tv, GtkWindow *win);
+
+void
+tfe_text_view_save (TfeTextView *tv);
+
+void
+tfe_text_view_saveas (TfeTextView *tv);
+
+GtkWidget *
+tfe_text_view_new_with_file (GFile *file);
+
+GtkWidget *
+tfe_text_view_new (void);
+
+#endif /* __TFE_TEXT_VIEW_H__ */
+

tfetextview.c

+
#include <string.h>
+#include "tfetextview.h"
+
+struct _TfeTextView {
+  GtkTextView parent;
+  GFile *file;
+};
+
+G_DEFINE_TYPE (TfeTextView, tfe_text_view, GTK_TYPE_TEXT_VIEW);
+
+enum {
+  CHANGE_FILE,
+  OPEN_RESPONSE,
+  NUMBER_OF_SIGNALS
+};
+
+static guint tfe_text_view_signals[NUMBER_OF_SIGNALS];
+
+static void
+tfe_text_view_dispose (GObject *gobject) {
+  TfeTextView *tv = TFE_TEXT_VIEW (gobject);
+
+  if (G_IS_FILE (tv->file))
+    g_clear_object (&tv->file);
+
+  G_OBJECT_CLASS (tfe_text_view_parent_class)->dispose (gobject);
+}
+
+static void
+tfe_text_view_init (TfeTextView *tv) {
+  tv->file = NULL;
+}
+
+static void
+tfe_text_view_class_init (TfeTextViewClass *class) {
+  GObjectClass *object_class = G_OBJECT_CLASS (class);
+
+  object_class->dispose = tfe_text_view_dispose;
+  tfe_text_view_signals[CHANGE_FILE] = g_signal_new ("change-file",
+                                 G_TYPE_FROM_CLASS (class),
+                                 G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS,
+                                 0 /* class offset */,
+                                 NULL /* accumulator */,
+                                 NULL /* accumulator data */,
+                                 NULL /* C marshaller */,
+                                 G_TYPE_NONE /* return_type */,
+                                 0     /* n_params */
+                                 );
+  tfe_text_view_signals[OPEN_RESPONSE] = g_signal_new ("open-response",
+                                 G_TYPE_FROM_CLASS (class),
+                                 G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS,
+                                 0 /* class offset */,
+                                 NULL /* accumulator */,
+                                 NULL /* accumulator data */,
+                                 NULL /* C marshaller */,
+                                 G_TYPE_NONE /* return_type */,
+                                 1     /* n_params */,
+                                 G_TYPE_INT
+                                 );
+}
+
+GFile *
+tfe_text_view_get_file (TfeTextView *tv) {
+  g_return_val_if_fail (TFE_IS_TEXT_VIEW (tv), NULL);
+
+  if (G_IS_FILE (tv->file))
+    return g_file_dup (tv->file);
+  else
+    return NULL;
+}
+
+static gboolean
+save_file (GFile *file, GtkTextBuffer *tb, GtkWindow *win) {
+  GtkTextIter start_iter;
+  GtkTextIter end_iter;
+  gchar *contents;
+  gboolean stat;
+  GtkWidget *message_dialog;
+  GError *err = NULL;
+
+  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);
+    stat = 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);
+    stat = FALSE;
+  }
+  g_free (contents);
+  return stat;
+}
+
+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);
+
+  if (response == GTK_RESPONSE_ACCEPT) {
+    file = gtk_file_chooser_get_file (GTK_FILE_CHOOSER (dialog));
+    if (! G_IS_FILE (file))
+      g_warning ("TfeTextView: gtk_file_chooser_get_file returns non GFile.\n");
+    else if (save_file(file, tb, GTK_WINDOW (win))) {
+      if (G_IS_FILE (tv->file))
+        g_object_unref (tv->file);
+      tv->file = file;
+      g_signal_emit (tv, tfe_text_view_signals[CHANGE_FILE], 0);
+    } else
+      g_object_unref (file);
+  }
+  gtk_window_destroy (GTK_WINDOW (dialog));
+}
+
+void
+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));
+  GtkWidget *win = gtk_widget_get_ancestor (GTK_WIDGET (tv), GTK_TYPE_WINDOW);
+
+  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 tv->file isn't NULL nor GFile.\n");
+  else
+    save_file (tv->file, tb, GTK_WINDOW (win));
+}
+
+void
+tfe_text_view_saveas (TfeTextView *tv) {
+  g_return_if_fail (TFE_IS_TEXT_VIEW (tv));
+
+  GtkWidget *dialog;
+  GtkWidget *win = gtk_widget_get_ancestor (GTK_WIDGET (tv), GTK_TYPE_WINDOW);
+
+  dialog = gtk_file_chooser_dialog_new ("Save file", GTK_WINDOW (win), GTK_FILE_CHOOSER_ACTION_SAVE,
+                                      "Cancel", GTK_RESPONSE_CANCEL,
+                                      "Save", GTK_RESPONSE_ACCEPT,
+                                      NULL);
+  g_signal_connect (dialog, "response", G_CALLBACK (saveas_dialog_response), tv);
+  gtk_widget_show (dialog);
+}
+
+GtkWidget *
+tfe_text_view_new_with_file (GFile *file) {
+  g_return_val_if_fail (G_IS_FILE (file), NULL);
+
+  GtkWidget *tv;
+  GtkTextBuffer *tb;
+  char *contents;
+  gsize length;
+
+  if (! g_file_load_contents (file, NULL, &contents, &length, NULL, NULL)) /* read error */
+    return NULL;
+
+  if ((tv = tfe_text_view_new()) != NULL) {
+    tb = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv));
+    gtk_text_buffer_set_text (tb, contents, length);
+    TFE_TEXT_VIEW (tv)->file = g_file_dup (file);
+    gtk_text_buffer_set_modified (tb, FALSE);
+  }
+  g_free (contents);
+  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.\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 */
+    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, GtkWindow *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", 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));
+}
+

Total number of lines, words and characters

+
$ 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
+   99   304  3205 tfe5/tfeapplication.c
+    6     9   153 tfe5/tfe.gresource.xml
+    4     6    87 tfe5/tfe.h
+  140   378  3601 tfe5/tfenotebook.c
+   15    21   241 tfe5/tfenotebook.h
+  229   671  8017 tfetextview/tfetextview.c
+   35    60   701 tfetextview/tfetextview.h
+   61   100  2073 tfe5/tfe.ui
+  599  1566 18372 total
+

Up: index.html, Prev: Section 15, Next: Section 17

+ + diff --git a/docs/sec17.html b/docs/sec17.html new file mode 100644 index 0000000..e2997fd --- /dev/null +++ b/docs/sec17.html @@ -0,0 +1,230 @@ + + + + + + + Gtk4 tutorial for beginners + + + +

Up: index.html, Prev: Section 16, Next: Section 18

+

Menu and action

+ +

Users often use menus to tell a command to the computer. It is like this:

+
+
Menu
+
+

Now let’s analyze the menu above. There are two types of object.

+ +
+
Menu structure
+
+ +

Menus can build a complicated structure thanks to the links of menu items.

+

GMenuModel, GMenu and GMenuItem

+

GMenuModel is an abstract object which represents a menu. GMenu is a simple implementation of GMenuModel and a child object of GMenuModel.

+
GObject -- GMenuModel -- GMenu
+

Because GMenuModel is an abstract object, it isn’t instantiatable. Therefore, it doesn’t have any functions to create its instance. If you want to create a menu, use g_menu_new to create a GMenu instance. GMenu inherits all the functions of GMenuModel because of the child object.

+

GMenuItem is an object directly derived from GObject. GMenuItem and Gmenu (or GMenuModel) don’t have a parent-child relationship.

+
GObject -- GMenuModel -- GMenu
+GObject -- GMenuItem
+

GMenuItem has attributes. One of the attributes is label. For example, there is a menu item which has “Edit” label in the first diagram in this section. “Cut”, “Copy”, “Paste” and “Select All” are also the labels of the menu items. Other attributes will be explained later.

+

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 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.

+

The following code shows how to append GMenuItem to GMenu.

+
GMenu *menu = g_menu_new ();
+GMenuItem *menu_item_quit = g_menu_item_new ("Quit", "app.quit");
+g_menu_append_item (menu, menu_item_quit);
+g_object_unref (menu_item_quit);
+ +

One of the attributes of menu items is an action. This attribute points an action object.

+

There are two action objects, GSimpleAction and GPropertyAction. GSimpleAction is often used. And it is used with a menu item. Only GSimpleAction is described in this section.

+

An action corresponds to a menu item will be activated when the menu item is clicked. Then the action emits an activate signal.

+
    +
  1. menu item is clicked.
  2. +
  3. The corresponding action is activated.
  4. +
  5. The action emits a signal.
  6. +
  7. The connected handler is invoked.
  8. +
+

The following code is an example.

+
static void
+quit_activated(GSimpleAction *action, GVariant *parameter, gpointer app) { ... ... ...}
+
+GSimpleAction *act_quit = g_simple_action_new ("quit", NULL);
+g_action_map_add_action (G_ACTION_MAP (app), G_ACTION (act_quit));
+g_signal_connect (act_quit, "activate", G_CALLBACK (quit_activated), app);
+GMenuItem *menu_item_quit = g_menu_item_new ("Quit", "app.quit");
+
    +
  1. menu_item_quit is a menu item. It has a label “Quit” and is connected to an action “app.quit”. “app” is a prefix and “quit” is a name of the action. The prefix “app” means that the action belongs to a GtkApplication instance. If the menu is clicked, then the corresponding action “quit” which belongs to the GtkApplication will be activated.
  2. +
  3. act_quit is an action. It has a name “quit”. The function g_simple_action_new creates a stateless action. So, act_quit is stateless. The meaning of stateless will be explained later. The argument NULL means that the action doesn’t have an parameter. Most of the actions are stateless and have no parameter.
  4. +
  5. The action act_quit is added to the GtkApplication instance with g_action_map_add_action. When act_quit is activated, it will emit “activate” signal.
  6. +
  7. “activate” signal of the action is connected to the handler quit_activated. So, if the action is activated, the handler will be invoked.
  8. +
+

Simple example

+

The following is a simple example of menus and actions.

+
#include <gtk/gtk.h>
+
+static void
+quit_activated(GSimpleAction *action, GVariant *parameter, gpointer user_data) {
+  GApplication *app = G_APPLICATION (user_data);
+
+  g_application_quit (app);
+}
+
+static void
+app_activate (GApplication *app, gpointer user_data) {
+  GtkWidget *win = gtk_application_window_new (GTK_APPLICATION (app));
+  gtk_window_set_title (GTK_WINDOW (win), "menu1");
+  gtk_window_set_default_size (GTK_WINDOW (win), 400, 300);
+
+  GSimpleAction *act_quit = g_simple_action_new ("quit", NULL);
+  g_action_map_add_action (G_ACTION_MAP (app), G_ACTION (act_quit));
+  g_signal_connect (act_quit, "activate", G_CALLBACK (quit_activated), app);
+
+  GMenu *menubar = g_menu_new ();
+  GMenuItem *menu_item_menu = g_menu_item_new ("Menu", NULL);
+  GMenu *menu = g_menu_new ();
+  GMenuItem *menu_item_quit = g_menu_item_new ("Quit", "app.quit");
+  g_menu_append_item (menu, menu_item_quit);
+  g_object_unref (menu_item_quit);
+  g_menu_item_set_submenu (menu_item_menu, G_MENU_MODEL (menu));
+  g_menu_append_item (menubar, menu_item_menu);
+  g_object_unref (menu_item_menu);
+
+  gtk_application_set_menubar (GTK_APPLICATION (app), G_MENU_MODEL (menubar));
+  gtk_application_window_set_show_menubar (GTK_APPLICATION_WINDOW (win), TRUE);
+  gtk_window_present (GTK_WINDOW (win));
+/*  gtk_widget_show (win); is also OKay instead of gtk_window_present. */
+}
+
+#define APPLICATION_ID "com.github.ToshioCP.menu1"
+
+int
+main (int argc, char **argv) {
+  GtkApplication *app;
+  int stat;
+
+  app = gtk_application_new (APPLICATION_ID, G_APPLICATION_FLAGS_NONE);
+  g_signal_connect (app, "activate", G_CALLBACK (app_activate), NULL);
+
+  stat =g_application_run (G_APPLICATION (app), argc, argv);
+  g_object_unref (app);
+  return stat;
+}
+ +
+
menu and action
+
+
+
Screenshot of menu1
+
+

Up: index.html, Prev: Section 16, Next: Section 18

+ + diff --git a/docs/sec18.html b/docs/sec18.html new file mode 100644 index 0000000..f2b8611 --- /dev/null +++ b/docs/sec18.html @@ -0,0 +1,328 @@ + + + + + + + Gtk4 tutorial for beginners + + + +

Up: index.html, Prev: Section 17, Next: Section 19

+

Stateful action

+

Some actions have states. The typical values of states is boolean or string. However, other types of states are possible if you want.

+

There’s an example menu2_int16.c in the src/men directory. It behaves the same as menu2.c. But it uses gint16 type of states instead of string type.

+

Actions which have states are called stateful.

+

Stateful action without a parameter

+

Some menus are called toggle menu. For example, fullscreen menu has a state which has two values – fullscreen and non-fullscreen. The value of the state is changed every time the menu is clicked. An action corresponds to the fullscreen menu also have a state. Its value is TRUE or FALSE and it is called boolean value. TRUE corresponds to fullscreen and FALSE to non-fullscreen.

+

The following is an example code to implement a fullscreen menu except the signal handler. The signal handler will be described after the explanation of this code.

+
static void
+app_activate (GApplication *app, gpointer user_data) {
+  ... ... ...
+  GSimpleAction *act_fullscreen = g_simple_action_new_stateful ("fullscreen",
+                                  NULL, g_variant_new_boolean (FALSE));
+  GMenuItem *menu_item_fullscreen = g_menu_item_new ("Full Screen", "win.fullscreen");
+  g_signal_connect (act_fullscreen, "change-state", G_CALLBACK (fullscreen_changed), win);
+  ... ... ...
+}
+ +

The following is the “change-state” signal handler.

+
static void
+fullscreen_changed(GSimpleAction *action, GVariant *value, gpointer win) {
+  if (g_variant_get_boolean (value))
+    gtk_window_maximize (GTK_WINDOW (win));
+  else
+    gtk_window_unmaximize (GTK_WINDOW (win));
+  g_simple_action_set_state (action, value);
+}
+ +

You can use “activate” signal instead of “change-state” signal, or both signals. But the way above is the simplest and the best.

+

GVariant

+

GVarient can contain boolean, string or other type values. For example, the following program assigns TRUE to value whose type is GVariant.

+
GVariant *value = g_variant_new_boolean (TRUE);
+

Another example is:

+
GVariant *value2 = g_variant_new_string ("Hello");
+

value2 is a GVariant and it has a string type value “Hello”. GVariant can contain other types like int16, int32, int64, double and so on.

+

If you want to get the original value, use g_variant_get series functions. For example, you can get the boolean value by g_variant_get_boolean.

+
gboolean bool = g_variant_get_boolean (value);
+

Because value has been created as a boolean type GVariant and TRUE value, bool equals TRUE. In the same way, you can get a string from value2

+
const char *str = g_variant_get_string (value2, NULL);
+

The second parameter is a pointer to gsize type variable (gsize is defined as unsigned long). If it isn’t NULL, then the length of the string will be set by the function. If it is NULL, nothing happens. The returned string str can’t be changed.

+

Stateful action with a parameter

+

Another example of stateful actions is an action corresponds to color select menus. For example, there are three menus and each menu has red, green or blue color respectively. They determine the background color of a certain widget. One action is connected to the three menus. The action has a state which values are “red”, “green” and “blue”. The values are string. Those colors are given to the signal handler as a parameter.

+
static void
+app_activate (GApplication *app, gpointer user_data) {
+  ... ... ...
+  GSimpleAction *act_color = g_simple_action_new_stateful ("color",
+                     g_variant_type_new("s"), g_variant_new_string ("red"));
+  GMenuItem *menu_item_red = g_menu_item_new ("Red", "win.color::red");
+  GMenuItem *menu_item_green = g_menu_item_new ("Green", "win.color::green");
+  GMenuItem *menu_item_blue = g_menu_item_new ("Blue", "win.color::blue");
+  g_signal_connect (act_color, "activate", G_CALLBACK (color_activated), win);
+  ... ... ...
+}
+ +

The following is the “activate” signal handler.

+
static void
+color_activated(GSimpleAction *action, GVariant *parameter, gpointer win) {
+  char *color = g_strdup_printf ("label#lb {background-color: %s;}",
+                                   g_variant_get_string (parameter, NULL));
+  gtk_css_provider_load_from_data (provider, color, -1);
+  g_free (color);
+  g_action_change_state (G_ACTION (action), parameter);
+}
+ +

Note: If you have set a “change-state” signal handler, g_action_change_state will emit “change-state” signal instead of calling g_simple_action_set_state.

+

GVariantType

+

GVariantType gives a type of GVariant. GVariant can contain many kinds of types. And the type often needs to be recognized at runtime. GVariantType provides such functionality.

+

GVariantType is created with a string which expresses a type.

+ +

The following program is a simple example. It finally outputs the string “s”.

+
#include <glib.h>
+
+int
+main (int argc, char **argv) {
+  GVariantType *vtype = g_variant_type_new ("s");
+  const char *type_string = g_variant_type_peek_string (vtype);
+  g_print ("%s\n",type_string);
+}
+ +

Example code

+

The following code includes stateful actions above. This program has menus like this:

+
+
menu2
+
+ +

The code is as follows.

+
#include <gtk/gtk.h>
+
+static GtkCssProvider *provider;
+
+static void
+fullscreen_changed(GSimpleAction *action, GVariant *value, gpointer win) {
+  if (g_variant_get_boolean (value))
+    gtk_window_maximize (GTK_WINDOW (win));
+  else
+    gtk_window_unmaximize (GTK_WINDOW (win));
+  g_simple_action_set_state (action, value);
+}
+
+static void
+color_activated(GSimpleAction *action, GVariant *parameter, gpointer win) {
+  char *color = g_strdup_printf ("label#lb {background-color: %s;}", g_variant_get_string (parameter, NULL));
+  gtk_css_provider_load_from_data (provider, color, -1);
+  g_free (color);
+  g_action_change_state (G_ACTION (action), parameter);
+}
+
+static void
+quit_activated(GSimpleAction *action, GVariant *parameter, gpointer app)
+{
+  g_application_quit (G_APPLICATION(app));
+}
+
+static void
+app_activate (GApplication *app, gpointer user_data) {
+  GtkWidget *win = gtk_application_window_new (GTK_APPLICATION (app));
+  gtk_window_set_title (GTK_WINDOW (win), "menu2");
+  gtk_window_set_default_size (GTK_WINDOW (win), 400, 300);
+
+  GtkWidget *lb = gtk_label_new (NULL);
+  gtk_widget_set_name (lb, "lb"); /* the name is used by CSS Selector */
+  gtk_window_set_child (GTK_WINDOW (win), lb);
+
+  GSimpleAction *act_fullscreen
+    = g_simple_action_new_stateful ("fullscreen", NULL, g_variant_new_boolean (FALSE));
+  GSimpleAction *act_color
+    = g_simple_action_new_stateful ("color", g_variant_type_new("s"), g_variant_new_string ("red"));
+  GSimpleAction *act_quit
+    = g_simple_action_new ("quit", NULL);
+
+  GMenu *menubar = g_menu_new ();
+  GMenu *menu = g_menu_new ();
+  GMenu *section1 = g_menu_new ();
+  GMenu *section2 = g_menu_new ();
+  GMenu *section3 = g_menu_new ();
+  GMenuItem *menu_item_fullscreen = g_menu_item_new ("Full Screen", "win.fullscreen");
+  GMenuItem *menu_item_red = g_menu_item_new ("Red", "win.color::red");
+  GMenuItem *menu_item_green = g_menu_item_new ("Green", "win.color::green");
+  GMenuItem *menu_item_blue = g_menu_item_new ("Blue", "win.color::blue");
+  GMenuItem *menu_item_quit = g_menu_item_new ("Quit", "app.quit");
+
+  g_signal_connect (act_fullscreen, "change-state", G_CALLBACK (fullscreen_changed), win);
+  g_signal_connect (act_color, "activate", G_CALLBACK (color_activated), win);
+  g_signal_connect (act_quit, "activate", G_CALLBACK (quit_activated), app);
+  g_action_map_add_action (G_ACTION_MAP (win), G_ACTION (act_fullscreen));
+  g_action_map_add_action (G_ACTION_MAP (win), G_ACTION (act_color));
+  g_action_map_add_action (G_ACTION_MAP (app), G_ACTION (act_quit));
+
+  g_menu_append_item (section1, menu_item_fullscreen);
+  g_menu_append_item (section2, menu_item_red);
+  g_menu_append_item (section2, menu_item_green);
+  g_menu_append_item (section2, menu_item_blue);
+  g_menu_append_item (section3, menu_item_quit);
+  g_object_unref (menu_item_red);
+  g_object_unref (menu_item_green);
+  g_object_unref (menu_item_blue);
+  g_object_unref (menu_item_fullscreen);
+  g_object_unref (menu_item_quit);
+
+  g_menu_append_section (menu, NULL, G_MENU_MODEL (section1));
+  g_menu_append_section (menu, "Color", G_MENU_MODEL (section2));
+  g_menu_append_section (menu, NULL, G_MENU_MODEL (section3));
+  g_menu_append_submenu (menubar, "Menu", G_MENU_MODEL (menu));
+
+  gtk_application_set_menubar (GTK_APPLICATION (app), G_MENU_MODEL (menubar));
+  gtk_application_window_set_show_menubar (GTK_APPLICATION_WINDOW (win), TRUE);
+
+/*  GtkCssProvider *provider = gtk_css_provider_new ();*/
+  provider = gtk_css_provider_new ();
+  GdkDisplay *display = gtk_widget_get_display (GTK_WIDGET (win));
+  gtk_css_provider_load_from_data (provider, "label#lb {background-color: red;}", -1);
+  gtk_style_context_add_provider_for_display (display, GTK_STYLE_PROVIDER (provider),
+                                              GTK_STYLE_PROVIDER_PRIORITY_USER);
+
+/*  gtk_widget_show (win);*/
+  gtk_window_present (GTK_WINDOW (win));
+}
+
+#define APPLICATION_ID "com.github.ToshioCP.menu2"
+
+int
+main (int argc, char **argv) {
+  GtkApplication *app;
+  int stat;
+
+  app = gtk_application_new (APPLICATION_ID, G_APPLICATION_FLAGS_NONE);
+  g_signal_connect (app, "activate", G_CALLBACK (app_activate), NULL);
+
+  stat =g_application_run (G_APPLICATION (app), argc, argv);
+  g_object_unref (app);
+  return stat;
+}
+ +

Up: index.html, Prev: Section 17, Next: Section 19

+ + diff --git a/docs/sec19.html b/docs/sec19.html new file mode 100644 index 0000000..7a03c13 --- /dev/null +++ b/docs/sec19.html @@ -0,0 +1,383 @@ + + + + + + + Gtk4 tutorial for beginners + + + +

Up: index.html, Prev: Section 18, Next: Section 20

+

Ui file for menu and action entries

+

Ui file for menu

+

You might have thought that building menus is really bothersome. Yes, the program was complicated and it needs lots of time to code it. The situation is similar to building widgets. When we built widgets, using ui file was a good way to avoid such complicated coding. The same goes for menus.

+

The ui file for menus has interface, menu tags. The file starts and ends with interface tag.

+
<interface>
+  <menu id="menubar">
+  </menu>
+</interface>
+

menu tag corresponds to GMenu object. id attribute defines the name of the object. It will be referred by GtkBuilder.

+
<submenu>
+  <attribute name="label">File</attribute>
+    <item>
+      <attribute name="label">New</attribute>
+      <attribute name="action">win.new</attribute>
+    </item>
+</submenu>
+

item tag corresponds to item in GMenu which has the same structure as GMenuItem. The item above has a label attribute. Its value is “New”. The item also has an action attribute and its value is “win.new”. “win” is a prefix and “new” is an action name. submenu tag corresponds to both GMenuItem and GMenu. The GMenuItem has a link to GMenu.

+

The ui file above can be described as follows.

+
<item>
+  <attribute name="label">File</attribute>
+    <link name="submenu">
+      <item>
+        <attribute name="label">New</attribute>
+        <attribute name="action">win.new</attribute>
+      </item>
+    </link>
+</item>
+

link tag expresses the link to submenu. And at the same time it also expresses the submenu itself. This file illustrates the relationship between the menus and items better than the prior ui file. But submenu tag is simple and easy to understand. So, we usually prefer the former ui file style.

+

The following is a screenshot of the sample program in this section. Its name is menu3.

+
+
menu3
+
+

The following is the ui file of the menu in menu3.

+
<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <menu id="menubar">
+    <submenu>
+      <attribute name="label">File</attribute>
+      <section>
+        <item>
+          <attribute name="label">New</attribute>
+          <attribute name="action">win.new</attribute>
+        </item>
+        <item>
+          <attribute name="label">Open</attribute>
+          <attribute name="action">win.open</attribute>
+        </item>
+      </section>
+      <section>
+        <item>
+          <attribute name="label">Save</attribute>
+          <attribute name="action">win.save</attribute>
+        </item>
+        <item>
+          <attribute name="label">Save As…</attribute>
+          <attribute name="action">win.saveas</attribute>
+        </item>
+      </section>
+      <section>
+        <item>
+          <attribute name="label">Close</attribute>
+          <attribute name="action">win.close</attribute>
+        </item>
+      </section>
+      <section>
+        <item>
+          <attribute name="label">Quit</attribute>
+          <attribute name="action">app.quit</attribute>
+        </item>
+      </section>
+    </submenu>
+    <submenu>
+      <attribute name="label">Edit</attribute>
+      <section>
+        <item>
+          <attribute name="label">Cut</attribute>
+          <attribute name="action">win.cut</attribute>
+        </item>
+        <item>
+          <attribute name="label">Copy</attribute>
+          <attribute name="action">win.copy</attribute>
+        </item>
+        <item>
+          <attribute name="label">Paste</attribute>
+          <attribute name="action">win.paste</attribute>
+        </item>
+      </section>
+      <section>
+        <item>
+          <attribute name="label">Select All</attribute>
+          <attribute name="action">win.selectall</attribute>
+        </item>
+      </section>
+    </submenu>
+    <submenu>
+      <attribute name="label">View</attribute>
+      <section>
+        <item>
+          <attribute name="label">Full Screen</attribute>
+          <attribute name="action">win.fullscreen</attribute>
+        </item>
+      </section>
+    </submenu>
+  </menu>
+</interface>
+

The ui file is converted to the resource by the resource compiler glib-compile-resouces with xml file below.

+
<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/com/github/ToshioCP/menu3">
+    <file>menu3.ui</file>
+  </gresource>
+</gresources>
+

GtkBuilder builds menus from the resource.

+
GtkBuilder *builder = gtk_builder_new_from_resource ("/com/github/ToshioCP/menu3/menu3.ui");
+GMenuModel *menubar = G_MENU_MODEL (gtk_builder_get_object (builder, "menubar"));
+
+gtk_application_set_menubar (GTK_APPLICATION (app), menubar);
+g_object_unref (builder);
+

It is important that builder is unreferred after the GMenuModel menubar is inserted to the application. If you do it before setting, bad thing will happen – your computer might freeze.

+

Action entry

+

The coding for building actions and signal handlers is bothersome work as well. Therefore, it should be automated. You can implement them easily with GActionEntry structure and g_action_map_add_action_entries function.

+

GActionEntry contains action name, signal handlers, parameter and state.

+
typedef struct _GActionEntry GActionEntry;
+
+struct _GActionEntry
+{
+  /* action name */
+  const gchar *name;
+  /* activate handler */
+  void (* activate) (GSimpleAction *action, GVariant *parameter, gpointer user_data);
+  /* the type of the parameter given as a single GVariant type string */
+  const gchar *parameter_type;
+  /* initial state given in GVariant text format */
+  const gchar *state;
+  /* change-state handler */
+  void (* change_state) (GSimpleAction *action, GVariant *value, gpointer user_data);
+  /*< private >*/
+  gsize padding[3];
+};
+

For example, the actions in the previous section are:

+
{ "fullscreen", NULL, NULL, "false", fullscreen_changed }
+{ "color", color_activated, "s", "red", NULL }
+{ "quit", quit_activated, NULL, NULL, NULL },
+

And g_action_map_add_action_entries does all the process instead of the functions you have needed.

+
const GActionEntry app_entries[] = {
+  { "quit", quit_activated, NULL, NULL, NULL }
+};
+g_action_map_add_action_entries (G_ACTION_MAP (app), app_entries,
+                                 G_N_ELEMENTS (app_entries), app);
+

The code above does:

+ +

The same goes for the other actions.

+
const GActionEntry win_entries[] = {
+  { "fullscreen", NULL, NULL, "false", fullscreen_changed },
+  { "color", color_activated, "s", "red", NULL }
+};
+g_action_map_add_action_entries (G_ACTION_MAP (win), win_entries,
+                                 G_N_ELEMENTS (win_entries), win);
+

The code above does:

+ +

Example code

+

The C source code of menu3 and meson.build is as follows.

+
#include <gtk/gtk.h>
+
+static void
+new_activated (GSimpleAction *action, GVariant *parameter, gpointer win) {
+}
+
+static void
+open_activated (GSimpleAction *action, GVariant *parameter, gpointer win) {
+}
+
+static void
+save_activated (GSimpleAction *action, GVariant *parameter, gpointer win) {
+}
+
+static void
+saveas_activated (GSimpleAction *action, GVariant *parameter, gpointer win) {
+}
+
+static void
+close_activated (GSimpleAction *action, GVariant *parameter, gpointer win) {
+}
+
+static void
+cut_activated (GSimpleAction *action, GVariant *parameter, gpointer win) {
+}
+
+static void
+copy_activated (GSimpleAction *action, GVariant *parameter, gpointer win) {
+}
+
+static void
+paste_activated (GSimpleAction *action, GVariant *parameter, gpointer win) {
+}
+
+static void
+selectall_activated (GSimpleAction *action, GVariant *parameter, gpointer win) {
+}
+
+static void
+fullscreen_changed (GSimpleAction *action, GVariant *state, gpointer win) {
+  if (g_variant_get_boolean (state))
+    gtk_window_maximize (GTK_WINDOW (win));
+  else
+    gtk_window_unmaximize (GTK_WINDOW (win));
+  g_simple_action_set_state (action, state);
+}
+
+static void
+quit_activated (GSimpleAction *action, GVariant *parameter, gpointer app)
+{
+  g_application_quit (G_APPLICATION(app));
+}
+
+static void
+app_activate (GApplication *app, gpointer user_data) {
+  GtkWidget *win = gtk_application_window_new (GTK_APPLICATION (app));
+
+  const GActionEntry win_entries[] = {
+    { "new", new_activated, NULL, NULL, NULL },
+    { "open", open_activated, NULL, NULL, NULL },
+    { "save", save_activated, NULL, NULL, NULL },
+    { "saveas", saveas_activated, NULL, NULL, NULL },
+    { "close", close_activated, NULL, NULL, NULL },
+    { "cut", cut_activated, NULL, NULL, NULL },
+    { "copy", copy_activated, NULL, NULL, NULL },
+    { "paste", paste_activated, NULL, NULL, NULL },
+    { "selectall", selectall_activated, NULL, NULL, NULL },
+    { "fullscreen", NULL, NULL, "false", fullscreen_changed }
+  };
+  g_action_map_add_action_entries (G_ACTION_MAP (win), win_entries, G_N_ELEMENTS (win_entries), win);
+
+  gtk_application_window_set_show_menubar (GTK_APPLICATION_WINDOW (win), TRUE);
+
+  gtk_window_set_title (GTK_WINDOW (win), "menu3");
+  gtk_window_set_default_size (GTK_WINDOW (win), 400, 300);
+  gtk_widget_show (win);
+}
+
+static void
+app_startup (GApplication *app, gpointer user_data) {
+  GtkBuilder *builder = gtk_builder_new_from_resource ("/com/github/ToshioCP/menu3/menu3.ui");
+  GMenuModel *menubar = G_MENU_MODEL (gtk_builder_get_object (builder, "menubar"));
+
+  gtk_application_set_menubar (GTK_APPLICATION (app), menubar);
+  g_object_unref (builder);
+
+  const GActionEntry app_entries[] = {
+    { "quit", quit_activated, NULL, NULL, NULL }
+  };
+  g_action_map_add_action_entries (G_ACTION_MAP (app), app_entries, G_N_ELEMENTS (app_entries), app);
+}
+
+#define APPLICATION_ID "com.github.ToshioCP.menu3"
+
+int
+main (int argc, char **argv) {
+  GtkApplication *app;
+  int stat;
+
+  app = gtk_application_new (APPLICATION_ID, G_APPLICATION_FLAGS_NONE);
+  g_signal_connect (app, "startup", G_CALLBACK (app_startup), NULL);
+  g_signal_connect (app, "activate", G_CALLBACK (app_activate), NULL);
+
+  stat =g_application_run (G_APPLICATION (app), argc, argv);
+  g_object_unref (app);
+  return stat;
+}
+

meson.build

+
project('menu3', 'c')
+
+gtkdep = dependency('gtk4')
+
+gnome=import('gnome')
+resources = gnome.compile_resources('resources','menu3.gresource.xml')
+
+sourcefiles=files('menu3.c')
+
+executable('menu3', sourcefiles, resources, dependencies: gtkdep)
+

Up: index.html, Prev: Section 18, Next: Section 20

+ + diff --git a/docs/sec2.html b/docs/sec2.html new file mode 100644 index 0000000..51330f6 --- /dev/null +++ b/docs/sec2.html @@ -0,0 +1,307 @@ + + + + + + + Gtk4 tutorial for beginners + + + +

Up: index.html, Prev: Section 1, Next: Section 3

+

Installation of Gtk4 to Linux distributions

+

This section describes how to install Gtk4 into Linux distributions.

+

This tutorial including this section is without any warranty. If you install Gtk4 to your computer, do it at your own risk.

+

This section is written on 12/August/2021. “At present” means 12/August/2021 in this section.

+

There are three possible way to install Gtk4.

+ +

Installation from the distribution packages

+

The first way is easy to install. It is a recommended way. I’ve installed Gtk4 packages in Ubuntu 21.04.

+
$ sudo apt-get install libgtk-4-bin libgtk-4-common libgtk-4-dev libgtk-4-doc
+

Arch, Debian and Fedora are also possible. See Installing GTK from packages. If you’ve installed Gtk4 from the packages, you don’t need to read the rest of this section.

+

Installation from the source file

+

If your operating system doesn’t have Gtk4 packages, you need to build it from the source. Or, if you want the latest version of Gtk4, you also need to build it from the source. At present, the version of Gtk4 on Ubuntu 21.04 is 4.0.3.

+

I installed Gtk4 in January 2021. So, the following is old information, especially for the version of each software. For the latest information, see Gtk API Reference, Building GTK.

+

Prerequisites for Gtk4 installation

+ +

Installation target

+

I installed Gtk4 under the directory $HOME/local. This is a private user area.

+

If you want to install it in the system area, /opt/gtk4 is one of good choices. Gtk API Reference, Building GTK gives an installation example to /opt/gtk4.

+

Don’t install it to /usr/local which is the default. It is used by Ubuntu applications, which are not build on Gtk4. Therefore, the risk is high and probably bad things will happen. Actually I did it and I needed to reinstall Ubuntu.

+

Installation to Ubuntu 20.10

+

Most of the necessary libraries are included by Ubuntu 20.10. Therefore, they can be installed with apt-get command. You don’t need to install them from the source tarballs. You can skip the subsections below about prerequisite library installation (Glib, Pango, Gdk-pixbuf and Gtk-doc).

+

Glib installation

+

If your Ubuntu is 20.04LTS, you need to install prerequisite libraries from the tarballs. Check the version of your library and if it is lower than the necessary version, install it from the source.

+

For example,

+
$ pkg-config --modversion glib-2.0
+2.64.6
+

The necessary version is 2.66.0 or higher. Therefore, the example above shows that you need to install Glib.

+

I installed 2.67.1 which was the latest version at that time (January 2021). Download Glib source files from the repository, then decompress and extract files.

+
$ wget https://download.gnome.org/sources/glib/2.67/glib-2.67.1.tar.xz
+$ tar -Jxf glib-2.67.1.tar.xz
+

Some packages are required to build Glib. You can find them if you run meson.

+
$ meson --prefix $HOME/local _build
+

Use apt-get and install the prerequisites. For example,

+
$ sudo apt-get install -y  libpcre2-dev libffi-dev
+

After that, compile Glib.

+
$ rm -rf _build
+$ meson --prefix $HOME/local _build
+$ ninja -C _build
+$ ninja -C _build install
+

Set several environment variables so that the Glib libraries installed can be used by build tools. Make a text file below and save it as env.sh

+
# compiler
+CPPFLAGS="-I$HOME/local/include"
+LDFLAGS="-L$HOME/local/lib"
+PKG_CONFIG_PATH="$HOME/local/lib/pkgconfig:$HOME/local/lib/x86_64-linux-gnu/pkgconfig"
+export CPPFLAGS LDFLAGS PKG_CONFIG_PATH
+# linker
+LD_LIBRARY_PATH="$HOME/local/lib/x86_64-linux-gnu/"
+PATH="$HOME/local/bin:$PATH"
+export LD_LIBRARY_PATH PATH
+# gsetting
+export GSETTINGS_SCHEMA_DIR=$HOME/local/share/glib-2.0/schemas
+

Then, use . (dot) or source command to include these commands to the current bash.

+
$ . env.sh
+

or

+
$ source env.sh
+

This command carries out the commands in env.sh and changes the environment variables above in the current shell.

+

Pango installation

+

Download and untar.

+
$ wget https://download.gnome.org/sources/pango/1.48/pango-1.48.0.tar.xz
+$ tar -Jxf pango-1.48.0.tar.xz
+

Try meson and check the required packages. Install all the prerequisites. Then, compile and install Pango.

+
$ meson --prefix $HOME/local _build
+$ ninja -C _build
+$ ninja -C _build install
+

It installs Pango-1.0.gir under $HOME/local/share/gir-1.0. If you installed Pango without --prefix option, then it would be located at /usr/local/share/gir-1.0. This directory (/usr/local/share) is used by applications. They find the directory by the environment variable XDG_DATA_DIRS. It is a text file which keep the list of ‘share’ directories like /usr/share, usr/local/share and so on. Now $HOME/local/share needs to be added to XDG_DATA_DIRS, or error will occur in the later compilation.

+
$ export XDG_DATA_DIRS=$HOME/local/share:$XDG_DATA_DIRS
+

Gdk-pixbuf and Gtk-doc installation

+

Download and untar.

+
$ wget https://download.gnome.org/sources/gdk-pixbuf/2.42/gdk-pixbuf-2.42.2.tar.xz
+$ tar -Jxf gdk-pixbuf-2.42.2.tar.xz
+$ wget https://download.gnome.org/sources/gtk-doc/1.33/gtk-doc-1.33.1.tar.xz
+$ tar -Jxf gtk-doc-1.33.1.tar.xz
+

Same as before, install prerequisite packages, then compile and install them.

+

The installation of Gtk-doc put gtk-doc.pc under $HOME/local/share/pkgconfig. This file is used by pkg-config, which is one of the build tools. The directory needs to be added to the environment variable PKG_CONFIG_PATH

+
$ export PKG_CONFIG_PATH="$HOME/local/share/pkgconfig:$PKG_CONFIG_PATH"
+

Gtk4 installation

+

If you want the latest development version of Gtk4, use git and clone the repository.

+
$ git clone https://gitlab.gnome.org/GNOME/gtk.git
+

If you want a stable version of Gtk4, then download it from Gnome source website. The latest version is 4.3.1 (13/June/2021).

+

Compile and install it.

+
$ meson --prefix $HOME/local _build
+$ ninja -C _build
+$ ninja -C _build install
+

If you want to know more information, refer to Gtk4 API Reference, Building GTK.

+

Modify env.sh

+

Because environment variables disappear when you log out, you need to add them again. Modify env.sh.

+
# compiler
+CPPFLAGS="-I$HOME/local/include"
+LDFLAGS="-L$HOME/local/lib"
+PKG_CONFIG_PATH="$HOME/local/lib/pkgconfig:$HOME/local/lib/x86_64-linux-gnu/pkgconfig:
+$HOME/local/share/pkgconfig"
+export CPPFLAGS LDFLAGS PKG_CONFIG_PATH
+# linker
+LD_LIBRARY_PATH="$HOME/local/lib/x86_64-linux-gnu/"
+PATH="$HOME/local/bin:$PATH"
+export LD_LIBRARY_PATH PATH
+# gir
+XDG_DATA_DIRS=$HOME/local/share:$XDG_DATA_DIRS
+export XDG_DATA_DIRS
+# gsetting
+export GSETTINGS_SCHEMA_DIR=$HOME/local/share/glib-2.0/schemas
+# girepository-1.0
+export GI_TYPELIB_PATH=$HOME/local/lib/x86_64-linux-gnu/girepository-1.0
+

Include this file by . (dot) command before using Gtk4 libraries.

+

You may think you can add them in 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

+

Before you compile Gtk4 applications, define environment variables above.

+
$ . env.sh
+

After that you can compile them without anything. For example, to compile sample.c, type the following.

+
$ gcc `pkg-config --cflags gtk4` sample.c `pkg-config --libs gtk4`
+

To know how to compile Gtk4 applications, refer to the section 3 (GtkApplication and GtkApplicationWindow) and after.

+

Installing Fedora 34 with gnome-boxes

+

The last part of this section is about Gnome40. Gnome 40 is a new version of Gnome desktop system. And Gtk4 is installed in the distribution. See Gnome 40 website first.

+

However, Gnome40 is not necessary to compile and run Gtk4 applications.

+

There are only three choices at present.

+ +

I’ve tried installing Fedora 34 with gnome-boxes.

+

There are two ways to install them.

+ +

I’ve chosen the second way. I’ve tried installing Fedora 34 with gnome-boxes. My OS was Ubuntu 21.04 at that time. Gnome-boxes creates a virtual machine in Ubuntu and Fedora will be installed to that virtual machine.

+

The instruction is as follows.

+
    +
  1. Download Fedora 34 iso file. There is an link at the end of Gnome 40 website.
  2. +
  3. Install gnome-boxes with apt-get command.
  4. +
+
$ sudo apt-get install gnome-boxes
+
    +
  1. Run gnome-boxes.
  2. +
  3. Click on + button on the top left corner and launch a box creation wizard by clicking Create a Virtual Machine .... Then a dialog appears. Click on Operationg System Image File and select the iso file you have downloaded.
  4. +
  5. Then, the installer of Fedora is executed. Follow the instructions by the installer. At the end of the installation, the installer instructs to reboot the system. Click on the right of the title bar and select reboot or shutdown.
  6. +
  7. Your display is back to the initial window of gnome-boxes, but there is a button Fedora 34 Workstation on the upper left of the window. Click on the button then Fedora will be executed.
  8. +
  9. A setup dialog appears. Setup Fedora according to the wizard.
  10. +
+

Now you can use Fedora. It includes Gtk4 libraries already. But you need to install the Gtk4 development package. Use dnf to install gtk4.x86_64 package.

+
$ sudo dnf install gtk4.x86_64
+

Test for compiling a Gtk4 application

+

You can test the Gtk4 development package by compiling files which are based on Gtk4. I’ve tried compiling tfe text editor, which is written in section 21.

+
    +
  1. Run Firefox.
  2. +
  3. Open this website (Gtk4-Tutorial).
  4. +
  5. Click on the green button labeled Code.
  6. +
  7. Select Download ZIP and download the codes from the repository.
  8. +
  9. Unzip the file.
  10. +
  11. Change your current directory to src/tfe7.
  12. +
  13. Compile it.
  14. +
+
$ meson _build
+bash: meson: command not found...
+Install package 'meson' to provide command 'meson'? [N/y] y
+
+ * Waiting in queue...
+The following packages have to be installed:
+ meson-0.56.2-2.fc34.noarch    High productivity build system
+ ninja-build-1.10.2-2.fc34.x86_64    Small build system with a focus on speed
+ vim-filesystem-2:8.2.2787-1.fc34.noarch    VIM filesystem layout
+Proceed with changes? [N/y] y
+
+... ...
+... ...
+
+The Meson build system
+Version: 0.56.2
+
+... ...
+... ...
+
+Project name: tfe
+Project version: undefined
+C compiler for the host machine: cc (gcc 11.0.0 "cc (GCC) 11.0.0 20210210 (Red Hat 11.0.0-0)")
+C linker for the host machine: cc ld.bfd 2.35.1-38
+Host machine cpu family: x86_64
+Host machine cpu: x86_64
+Found pkg-config: /usr/bin/pkg-config (1.7.3)
+Run-time dependency gtk4 found: YES 4.2.0
+Found pkg-config: /usr/bin/pkg-config (1.7.3)
+Program glib-compile-resources found: YES (/usr/bin/glib-compile-resources)
+Program glib-compile-schemas found: YES (/usr/bin/glib-compile-schemas)
+Program glib-compile-schemas found: YES (/usr/bin/glib-compile-schemas)
+Build targets in project: 4
+ 
+Found ninja-1.10.2 at /usr/bin/ninja
+ 
+$ ninja -C _build
+ninja: Entering directory `_build'
+[12/12] Linking target tfe
+
+$ ninja -C _build install
+ninja: Entering directory `_build'
+[0/1] Installing files.
+Installing tfe to /usr/local/bin
+Installation failed due to insufficient permissions.
+Attempting to use polkit to gain elevated privileges...
+Installing tfe to /usr/local/bin
+Installing /home/<username>/Gtk4-tutorial-main/src/tfe7/com.github.ToshioCP.tfe.gschema.xml to /usr/local/share/glib-2.0/schemas
+Running custom install script '/usr/bin/glib-compile-schemas /usr/local/share/glib-2.0/schemas/'
+
    +
  1. Execute it.
  2. +
+
$ tfe
+

Then, the window of tfe text editor appears. The compilation and execution have succeeded.

+

Up: index.html, Prev: Section 1, Next: Section 3

+ + diff --git a/docs/sec20.html b/docs/sec20.html new file mode 100644 index 0000000..557937a --- /dev/null +++ b/docs/sec20.html @@ -0,0 +1,938 @@ + + + + + + + Gtk4 tutorial for beginners + + + +

Up: index.html, Prev: Section 19, Next: Section 21

+

GtkMenuButton, accelerators, font, pango and gsettings

+

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.

+

We will restructure tfe text file editor in this section. It will be more practical. The buttons are changed to:

+ +

Signal elements in ui files

+

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.

+
<object class="GtkButton" id="btno">
+  <property name="label">Open</property>
+  <signal name="clicked" handler="open_cb" swapped="TRUE" object="nb"></signal>
+</object>
+

Signal tag specifies the name of the signal, handler and user_data object. They are the value of name, handler and object attributes. Swapped attribute has the same meaning as g_signal_connect_swapped function. So, the signal tag above works the same as the function below.

+
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.

+
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.

+ +

Menus are described in menu.ui file.

+
<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <menu id="menu">
+    <section>
+      <item>
+        <attribute name="label">New</attribute>
+        <attribute name="action">win.new</attribute>
+      </item>
+      <item>
+        <attribute name="label">Save As…</attribute>
+        <attribute name="action">win.saveas</attribute>
+      </item>
+    </section>
+    <section>
+      <item>
+        <attribute name="label">Preference</attribute>
+        <attribute name="action">win.pref</attribute>
+      </item>
+    </section>
+    <section>
+      <item>
+        <attribute name="label">Quit</attribute>
+        <attribute name="action">win.close-all</attribute>
+      </item>
+    </section>
+  </menu>
+</interface>
+

There are four items, “New”, “Saveas”, “Preference” and “Quit”.

+ +

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.

+
  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.

+
  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.

+
  struct {
+    const char *action;
+    const char *accels[2];
+  } action_accels[] = {
+    { "win.open", { "<Control>o", NULL } },
+    { "win.save", { "<Control>s", NULL } },
+    { "win.close", { "<Control>w", NULL } },
+    { "win.new", { "<Control>n", NULL } },
+    { "win.saveas", { "<Shift><Control>s", NULL } },
+    { "win.close-all", { "<Control>q", NULL } },
+  };
+
+  for (i = 0; i < G_N_ELEMENTS(action_accels); i++)
+    gtk_application_set_accels_for_action(GTK_APPLICATION(app), action_accels[i].action, action_accels[i].accels);
+

This code is a bit complicated. The array action-accels[] is an array of structures. The structure is:

+
  struct {
+    const char *action;
+    const char *accels[2];
+  }
+

The member action is a string. The member accels is an array of two strings. For example,

+
{ "win.open", { "<Control>o", NULL } },
+

This is the first element of the array action_accels.

+ +

Saveas handler

+

TfeTextView has already had a saveas function. So, only we need to write is the wrapper function in tfenotebook.c.

+
static TfeTextView *
+get_current_textview (GtkNotebook *nb) {
+  int i;
+  GtkWidget *scr;
+  GtkWidget *tv;
+
+  i = gtk_notebook_get_current_page (nb);
+  scr = gtk_notebook_get_nth_page (nb, i);
+  tv = gtk_scrolled_window_get_child (GTK_SCROLLED_WINDOW (scr));
+  return TFE_TEXT_VIEW (tv);
+}
+
+void
+notebook_page_saveas (GtkNotebook *nb) {
+  g_return_if_fail(GTK_IS_NOTEBOOK (nb));
+
+  TfeTextView *tv;
+
+  tv = get_current_textview (nb);
+  tfe_text_view_saveas (TFE_TEXT_VIEW (tv));
+}
+

The function get_current_textview is the same as before. The function notebook_page_saveas simply calls tfe_text_view_saveas.

+

In tfeapplication.c, saveas handler just call notebook_page_saveas.

+
static void
+saveas_activated (GSimpleAction *action, GVariant *parameter, gpointer user_data) {
+  GtkNotebook *nb = GTK_NOTEBOOK (user_data);
+  notebook_page_saveas (nb);
+}
+

Preference and alert dialog

+

Preference dialog

+

Preference dialog xml definition is added to tfe.ui.

+
<object class="GtkDialog" id="pref">
+  <property name="title">Preferences</property>
+  <property name="resizable">FALSE</property>
+  <property name="modal">TRUE</property>
+  <property name="transient-for">win</property>
+  <child internal-child="content_area">
+    <object class="GtkBox" id="content_area">
+      <child>
+        <object class="GtkBox" id="pref_boxh">
+          <property name="orientation">GTK_ORIENTATION_HORIZONTAL</property>
+          <property name="spacing">12</property>
+          <property name="margin-start">12</property>
+          <property name="margin-end">12</property>
+          <property name="margin-top">12</property>
+          <property name="margin-bottom">12</property>
+          <child>
+            <object class="GtkLabel" id="fontlabel">
+              <property name="label">Font:</property>
+              <property name="xalign">1</property>
+            </object>
+          </child>
+          <child>
+            <object class="GtkFontButton" id="fontbtn">
+            </object>
+          </child>
+        </object>
+      </child>
+    </object>
+  </child>
+</object>
+ +

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.

+
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_connectseries 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. +
  3. Signal handlers are invoked, unless it is connected by g_signal_connect_after.
  4. +
  5. Default handler is invoked if the signal’s flag is G_SIGNAL_RUN_LAST.
  6. +
  7. Signal handlers are invoked, if it is connected by g_signal_connect_after.
  8. +
  9. Default handler is invoked if the signal’s flag is G_SIGNAL_RUN_CLEANUP.
  10. +
+

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. +
  3. Default handler is invoked.
  4. +
+

And If the user signal handler returns TRUE, then other handlers will be stopped being invoked. Therefore, the program above prevents the invocation of the default handler and stop the closing process of the dialog.

+

The following codes are extracted from tfeapplication.c.

+
static gulong pref_close_request_handler_id = 0;
+static gulong alert_close_request_handler_id = 0;
+
+... ...
+
+static gboolean
+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.

+
  <object class="GtkDialog" id="alert">
+    <property name="title">Are you sure?</property>
+    <property name="resizable">FALSE</property>
+    <property name="modal">TRUE</property>
+    <property name="transient-for">win</property>
+    <child internal-child="content_area">
+      <object class="GtkBox">
+        <child>
+          <object class="GtkBox">
+            <property name="orientation">GTK_ORIENTATION_HORIZONTAL</property>
+            <property name="spacing">12</property>
+            <property name="margin-start">12</property>
+            <property name="margin-end">12</property>
+            <property name="margin-top">12</property>
+            <property name="margin-bottom">12</property>
+            <child>
+              <object class="GtkImage">
+                <property name="icon-name">dialog-warning</property>
+                <property name="icon-size">GTK_ICON_SIZE_LARGE</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkLabel" id="lb_alert">
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+    <child type="action">
+      <object class="GtkButton" id="btn_cancel">
+        <property name="label">Cancel</property>
+      </object>
+    </child>
+    <child type="action">
+      <object class="GtkButton" id="btn_accept">
+        <property name="label">Close</property>
+      </object>
+    </child>
+    <action-widgets>
+      <action-widget response="cancel" default="true">btn_cancel</action-widget>
+      <action-widget response="accept">btn_accept</action-widget>
+    </action-widgets>
+    <signal name="response" handler="alert_response_cb" swapped="NO" object="nb"></signal>
+  </object>
+

This ui file describes the alert dialog. Some part are the same as preference dialog. There are two objects in the content area, GtkImage and GtkLabel.

+

GtkImage shows an image. The image can comes from files, resources, icon theme and so on. The image above displays an icon from the current icon theme. You can see icons in the theme by gtk4-icon-browser.

+
$ gtk4-icon-browser
+

The icon named “dialog-warning” is something like this.

+
+
dialog-warning icon is like …
+
+

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.

+
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 user_data) {
+  GtkNotebook *nb = GTK_NOTEBOOK (user_data);
+  close_cb (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 user_data) {
+  GtkNotebook *nb = GTK_NOTEBOOK (user_data);
+  GtkWidget *win = gtk_widget_get_ancestor (GTK_WIDGET (nb), GTK_TYPE_WINDOW);
+
+  is_quit = true;
+  if (has_saved_all (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));
+  }
+}
+
+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 false 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 message of the dialog and the label of the 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 checks the response_id. If it is GTK_RESPONSE_ACCEPT, which means user clicked on the close button, then it closes the current page. Otherwise it does nothing.

+

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 message of the dialog and the label of the button, then shows the alert dialog.

+

If the user clicked on the buttons on the alert dialog, alert_resoponse_cb is invoked. It hides the dialog and checks the response_id. If it is GTK_RESPONSE_ACCEPT, which means user clicked on the quit button, then it calls tfe_application_quit to quit the application. Otherwise it does nothing.

+

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.

+
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;
+}
+ +

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.

+
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.

+
static void
+file_changed_cb (TfeTextView *tv) {
+  GtkWidget *nb =  gtk_widget_get_ancestor (GTK_WIDGET (tv), GTK_TYPE_NOTEBOOK);
+  GtkWidget *scr;
+  GtkWidget *label;
+  GFile *file;
+  char *filename;
+
+  if (! GTK_IS_NOTEBOOK (nb)) /* tv not connected to nb yet */
+    return;
+  file = tfe_text_view_get_file (tv);
+  scr = gtk_widget_get_parent (GTK_WIDGET (tv));
+  if (G_IS_FILE (file)) {
+    filename = g_file_get_basename (file);
+    g_object_unref (file);
+  } else
+    filename = get_untitled ();
+  label = gtk_label_new (filename);
+  gtk_notebook_set_tab_label (GTK_NOTEBOOK (nb), scr, label);
+}
+
+static void
+modified_changed_cb (GtkTextBuffer *tb, gpointer user_data) {
+  TfeTextView *tv = TFE_TEXT_VIEW (user_data);
+  GtkWidget *scr = gtk_widget_get_parent (GTK_WIDGET (tv));
+  GtkWidget *nb =  gtk_widget_get_ancestor (GTK_WIDGET (tv), GTK_TYPE_NOTEBOOK);
+  GtkWidget *label;
+  const char *filename;
+  char *text;
+
+  if (! GTK_IS_NOTEBOOK (nb)) /* tv not connected to nb yet */
+    return;
+  else if (gtk_text_buffer_get_modified (tb)) {
+    filename = gtk_notebook_get_tab_label_text (GTK_NOTEBOOK (nb), scr);
+    text = g_strdup_printf ("*%s", filename);
+    label = gtk_label_new (text);
+    g_free (text);
+    gtk_notebook_set_tab_label (GTK_NOTEBOOK (nb), scr, label);
+  } else
+    file_changed_cb (tv);
+}
+ +

Font

+

GtkFontButton and GtkFontChooser

+

The GtkFontButton is a button which displays the current font. It opens a font chooser dialog if a user clicked on the button. A user can change the font (family, style, weight and size) with the dialog. Then the button keeps the new font and displays it.

+

The button and its signal “font-set” is initialized in the application startup process.

+
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 able to 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 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 "tfe.h"
+
+void
+set_css_for_display (GtkWindow *win, const 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);
+  g_free (textview_css);
+} 
+
+void
+set_font_for_display_with_pango_font_desc (GtkWindow *win, PangoFontDescription *pango_font_desc) {
+  PangoStyle pango_style;
+  PangoWeight 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);
+}
+ +

For further information, see Pango API Reference.

+

GSettings

+

We want to maintain the font data after the application quits. There are some ways to implement it.

+ +

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 it.

+

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.

+ +

Schemas are described in an XML format. For example,

+
<?xml version="1.0" encoding="UTF-8"?>
+<schemalist>
+  <schema path="/com/github/ToshioCP/tfe/" id="com.github.ToshioCP.tfe">
+    <key name="font" type="s">
+      <default>'Monospace 12'</default>
+      <summary>Font</summary>
+      <description>The font to be used for textview.</description>
+    </key>
+  </schema>
+</schemalist>
+ +

Further information is in Glib API Reference, VarientType.

+

gsettings

+

First, let’s try gsettings application. It is a configuration tool for GSettings.

+
$ gsettings help
+Usage:
+  gsettings --version
+  gsettings [--schemadir SCHEMADIR] COMMAND [ARGS?]
+
+Commands:
+  help                      Show this information
+  list-schemas              List installed schemas
+  list-relocatable-schemas  List relocatable schemas
+  list-keys                 List keys in a schema
+  list-children             List children of a schema
+  list-recursively          List keys and values, recursively
+  range                     Queries the range of a key
+  describe                  Queries the description of a key
+  get                       Get the value of a key
+  set                       Set the value of a key
+  reset                     Reset the value of a key
+  reset-recursively         Reset all values in a given schema
+  writable                  Check if a key is writable
+  monitor                   Watch for changes
+
+Use "gsettings help COMMAND" to get detailed help.
+

List schemas.

+
$ gsettings list-schemas
+org.gnome.rhythmbox.podcast
+ca.desrt.dconf-editor.Demo.Empty
+org.gnome.gedit.preferences.ui
+org.gnome.evolution-data-server.calendar
+org.gnome.rhythmbox.plugins.generic-player
+
+... ...
+
+

Each line is an id of a schema. Each schema has a key-value configuration data. You can see them with list-recursively command. Let’s look at the keys and values of org.gnome.calculator schema.

+
$ gsettings list-recursively org.gnome.calculator
+org.gnome.calculator source-currency ''
+org.gnome.calculator source-units 'degree'
+org.gnome.calculator button-mode 'basic'
+org.gnome.calculator target-currency ''
+org.gnome.calculator base 10
+org.gnome.calculator angle-units 'degrees'
+org.gnome.calculator word-size 64
+org.gnome.calculator accuracy 9
+org.gnome.calculator show-thousands false
+org.gnome.calculator window-position (122, 77)
+org.gnome.calculator refresh-interval 604800
+org.gnome.calculator target-units 'radian'
+org.gnome.calculator precision 2000
+org.gnome.calculator number-format 'automatic'
+org.gnome.calculator show-zeroes false
+

This schema is used by Gnome Calculator. Run the calculator and change the mode, then check the schema again.

+
$ gnome-calculator
+
+
gnome-calculator basic mode
+
+

Then, change the mode to advanced and quit.

+
+
gnome-calculator advanced mode
+
+

Run gsettings 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 has set button-mode key to “advanced”. 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 an XML format. The XML schema files must have the filename extension .gschema.xml. The following is the XML schema file for the application tfe.

+
<?xml version="1.0" encoding="UTF-8"?>
+<schemalist>
+  <schema path="/com/github/ToshioCP/tfe/" id="com.github.ToshioCP.tfe">
+    <key name="font" type="s">
+      <default>'Monospace 12'</default>
+      <summary>Font</summary>
+      <description>The font to be used for textview.</description>
+    </key>
+  </schema>
+</schemalist>
+

The filename is “com.github.ToshioCP.tfe.gschema.xml”. Schema XML filenames are usually the schema id followed by “.gschema.xml” suffix. You can use different name from schema id, but it is not recommended.

+ +

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.

+ +

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. +
  3. Copy it to one of the directories above. For example, /usr/local/share/glib-2.0/schemas.
  4. +
  5. Run glib-compile-schemas on the directory above.
  6. +
+

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.

+
gnome.compile_schemas(build_by_default: true, depend_files: 'com.github.ToshioCP.tfe.gschema.xml')
+ +

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_settings_bind

+

Write gsettings related codes to `tfeapplication.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 create 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 from the source (See Section 2). Then you need to put --prefix=$HOME/local option to meson like this.

+
$ meson --prefix=$HOME/local _build
+

If you’ve installed Gtk4 from the distribution package, --prefix option isn’t necessary. You just install tfe to the default bin directory like /usr/local/bin.

+

Modify meson.build and add install option and set it true in executable function.

+
executable('tfe', sourcefiles, resources, dependencies: gtkdep, export_dynamic: true, install: true)
+

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.

+
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 can run a post compile script.

+
meson.add_install_script('glib-compile-schemas', schema_dir)
+

This method runs ‘glib-compile-schemas’ with an argument schema_dir. The following is meson.build.

+
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)
+

Source files of tfe is under 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
+... ...
+

The screenshot is as follows.

+
+
tfe6
+
+

Up: index.html, Prev: Section 19, Next: Section 21

+ + diff --git a/docs/sec21.html b/docs/sec21.html new file mode 100644 index 0000000..339caba --- /dev/null +++ b/docs/sec21.html @@ -0,0 +1,771 @@ + + + + + + + Gtk4 tutorial for beginners + + + +

Up: index.html, Prev: Section 20, Next: Section 22

+

Template XML and composite widget

+

The tfe program in the previous section is not so good because many things are crammed into tfepplication.c. Many static variables in tfepplication.c shows that.

+
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;
+

Generally, if there are many global or static variables in the program, it is not a good program. Such programs are difficult to maintain.

+

The file tfeapplication.c should be divided into several files.

+ +

The preference dialog is defined by a ui file. And it has GtkBox, GtkLabel and GtkFontButton in it. Such widget is called composite widget. Composite widget is a child object (not child widget) of a widget. For example, the preference composite widget is a child object of GtkDialog. Composite widget can be built from template XML. Next subsection shows how to build a preference dialog.

+

Preference dialog

+

First, write a template XML file.

+
<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="TfePref" parent="GtkDialog">
+    <property name="title">Preferences</property>
+    <property name="resizable">FALSE</property>
+    <property name="modal">TRUE</property>
+    <child internal-child="content_area">
+      <object class="GtkBox" id="content_area">
+        <child>
+          <object class="GtkBox" id="pref_boxh">
+            <property name="orientation">GTK_ORIENTATION_HORIZONTAL</property>
+            <property name="spacing">12</property>
+            <property name="margin-start">12</property>
+            <property name="margin-end">12</property>
+            <property name="margin-top">12</property>
+            <property name="margin-bottom">12</property>
+            <child>
+              <object class="GtkLabel" id="fontlabel">
+                <property name="label">Font:</property>
+                <property name="xalign">1</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkFontButton" id="fontbtn">
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
+ +

Other lines are the same as before. The object TfePref is defined in tfepref.h and tfepref.c.

+
#ifndef __TFE_PREF_H__
+#define __TFE_PREF_H__
+
+#include <gtk/gtk.h>
+
+#define TFE_TYPE_PREF tfe_pref_get_type ()
+G_DECLARE_FINAL_TYPE (TfePref, tfe_pref, TFE, PREF, GtkDialog)
+
+GtkWidget *
+tfe_pref_new (GtkWindow *win);
+
+#endif /* __TFE_PREF_H__ */
+ +
#include "tfepref.h"
+
+struct _TfePref
+{
+  GtkDialog parent;
+  GSettings *settings;
+  GtkFontButton *fontbtn;
+};
+
+G_DEFINE_TYPE (TfePref, tfe_pref, GTK_TYPE_DIALOG);
+
+static void
+tfe_pref_dispose (GObject *gobject) {
+  TfePref *pref = TFE_PREF (gobject);
+
+  g_clear_object (&pref->settings);
+  G_OBJECT_CLASS (tfe_pref_parent_class)->dispose (gobject);
+}
+
+static void
+tfe_pref_init (TfePref *pref) {
+  gtk_widget_init_template (GTK_WIDGET (pref));
+  pref->settings = g_settings_new ("com.github.ToshioCP.tfe");
+  g_settings_bind (pref->settings, "font", pref->fontbtn, "font", G_SETTINGS_BIND_DEFAULT);
+}
+
+static void
+tfe_pref_class_init (TfePrefClass *class) {
+  GObjectClass *object_class = G_OBJECT_CLASS (class);
+
+  object_class->dispose = tfe_pref_dispose;
+  gtk_widget_class_set_template_from_resource (GTK_WIDGET_CLASS (class), "/com/github/ToshioCP/tfe/tfepref.ui");
+  gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (class), TfePref, fontbtn);
+}
+
+GtkWidget *
+tfe_pref_new (GtkWindow *win) {
+  return GTK_WIDGET (g_object_new (TFE_TYPE_PREF, "transient-for", win, NULL));
+}
+ +

Now, It is very simple to use this dialog. A caller just creates this object and shows it.

+
TfePref *pref;
+pref = tfe_pref_new (win) /* win is the top-level window */
+gtk_widget_show (GTK_WINDOW (win));
+

This instance is automatically destroyed when a user clicks on the close button. That’s all. If you want to show the dialog again, just create and show it.

+

Alert dialog

+

It is almost same as preference dialog.

+

Its XML file is:

+
<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="TfeAlert" parent="GtkDialog">
+    <property name="title">Are you sure?</property>
+    <property name="resizable">FALSE</property>
+    <property name="modal">TRUE</property>
+    <child internal-child="content_area">
+      <object class="GtkBox">
+        <child>
+          <object class="GtkBox">
+            <property name="orientation">GTK_ORIENTATION_HORIZONTAL</property>
+            <property name="spacing">12</property>
+            <property name="margin-start">12</property>
+            <property name="margin-end">12</property>
+            <property name="margin-top">12</property>
+            <property name="margin-bottom">12</property>
+            <child>
+              <object class="GtkImage">
+                <property name="icon-name">dialog-warning</property>
+                <property name="icon-size">GTK_ICON_SIZE_LARGE</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkLabel" id="lb_alert">
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+    <child type="action">
+      <object class="GtkButton" id="btn_cancel">
+        <property name="label">Cancel</property>
+      </object>
+    </child>
+    <child type="action">
+      <object class="GtkButton" id="btn_accept">
+        <property name="label">Close</property>
+      </object>
+    </child>
+    <action-widgets>
+      <action-widget response="cancel" default="true">btn_cancel</action-widget>
+      <action-widget response="accept">btn_accept</action-widget>
+    </action-widgets>
+  </template>
+</interface>
+

The header file is:

+
#ifndef __TFE_ALERT_H__
+#define __TFE_ALERT_H__
+
+#include <gtk/gtk.h>
+
+#define TFE_TYPE_ALERT tfe_alert_get_type ()
+G_DECLARE_FINAL_TYPE (TfeAlert, tfe_alert, TFE, ALERT, GtkDialog)
+
+void
+tfe_alert_set_message (TfeAlert *alert, const char *message);
+
+void
+tfe_alert_set_button_label (TfeAlert *alert, const char *label);
+
+GtkWidget *
+tfe_alert_new (GtkWindow *win);
+
+#endif /* __TFE_ALERT_H__ */
+

There are three public functions. The functions tfe_alert_set_message and tfe_alert_set_button_label sets the label and button name of the alert dialog. For example, if you want to show an alert that the user tries to close without saving the content, set them like:

+
tfe_alert_set_message (alert, "Are you really close without saving?"); /* alert points to a TfeAlert instance */
+tfe_alert_set_button_label (alert, "Close");
+

The function tfe_alert_new creates a TfeAlert dialog.

+

The C source file is:

+
#include "tfealert.h"
+
+struct _TfeAlert
+{
+  GtkDialog parent;
+  GtkLabel *lb_alert;
+  GtkButton *btn_accept;
+};
+
+G_DEFINE_TYPE (TfeAlert, tfe_alert, GTK_TYPE_DIALOG);
+
+void
+tfe_alert_set_message (TfeAlert *alert, const char *message) {
+  gtk_label_set_text (alert->lb_alert, message);
+}
+
+void
+tfe_alert_set_button_label (TfeAlert *alert, const char *label) {
+  gtk_button_set_label (alert->btn_accept, label);
+}
+
+static void
+tfe_alert_init (TfeAlert *alert) {
+  gtk_widget_init_template (GTK_WIDGET (alert));
+}
+
+static void
+tfe_alert_class_init (TfeAlertClass *class) {
+  gtk_widget_class_set_template_from_resource (GTK_WIDGET_CLASS (class), "/com/github/ToshioCP/tfe/tfealert.ui");
+  gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (class), TfeAlert, lb_alert);
+  gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (class), TfeAlert, btn_accept);
+}
+
+GtkWidget *
+tfe_alert_new (GtkWindow *win) {
+  return GTK_WIDGET (g_object_new (TFE_TYPE_ALERT, "transient-for", win, NULL));
+}
+

The program is almost same as tfepref.c.

+

The instruction how to use this object is as follows.

+
    +
  1. Write a “response” signal handler.
  2. +
  3. Create a TfeAlert object.
  4. +
  5. Connect “response” signal to a handler
  6. +
  7. Show the dialog
  8. +
  9. In the signal handler, do something with regard to the response-id. Then destroy the dialog.
  10. +
+

Top-level window

+

In the same way, create a child object of GtkApplicationWindow. The object name is “TfeWindow”.

+
<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="TfeWindow" parent="GtkApplicationWindow">
+    <property name="title">file editor</property>
+    <property name="default-width">600</property>
+    <property name="default-height">400</property>
+    <child>
+      <object class="GtkBox" id="boxv">
+        <property name="orientation">GTK_ORIENTATION_VERTICAL</property>
+        <child>
+          <object class="GtkBox" id="boxh">
+            <property name="orientation">GTK_ORIENTATION_HORIZONTAL</property>
+            <child>
+              <object class="GtkLabel" id="dmy1">
+                <property name="width-chars">10</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkButton" id="btno">
+                <property name="label">Open</property>
+                <property name="action-name">win.open</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkButton" id="btns">
+                <property name="label">Save</property>
+                <property name="action-name">win.save</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkLabel" id="dmy2">
+                <property name="hexpand">TRUE</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkButton" id="btnc">
+                <property name="label">Close</property>
+                <property name="action-name">win.close</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkMenuButton" id="btnm">
+                <property name="direction">down</property>
+                <property name="halign">start</property>
+                <property name="icon-name">open-menu-symbolic</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkLabel" id="dmy3">
+                <property name="width-chars">10</property>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="GtkNotebook" id="nb">
+            <property name="scrollable">TRUE</property>
+            <property name="hexpand">TRUE</property>
+            <property name="vexpand">TRUE</property>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
+

This XML file is almost same as before except template tag and “action-name” property in buttons.

+

GtkButton implements GtkActionable interface, which has “action-name” property. If this property is set, GtkButton activates the action when it is clicked. For example, if an open button is clicked, “win.open” action will be activated and open_activated handler will be invoked.

+

This action is also used by “<Control>o” accelerator (See the source code of tfewindow.c below). If you use “clicked” signal for the button, you need its signal handler. Then, there are two handlers:

+ +

These two handlers do almost same thing. It is inefficient. Connecting buttons to actions is a good way to reduce unnecessary codes.

+
#ifndef __TFE_WINDOW_H__
+#define __TFE_WINDOW_H__
+
+#include <gtk/gtk.h>
+
+#define TFE_TYPE_WINDOW tfe_window_get_type ()
+G_DECLARE_FINAL_TYPE (TfeWindow, tfe_window, TFE, WINDOW, GtkApplicationWindow)
+
+void
+tfe_window_notebook_page_new (TfeWindow *win);
+
+void
+tfe_window_notebook_page_new_with_files (TfeWindow *win, GFile **files, int n_files);
+
+GtkWidget *
+tfe_window_new (GtkApplication *app);
+
+#endif /* __TFE_WINDOW_H__ */
+

There are three public functions. The function tfe_window_notebook_page_new creates a new notebook page. This is a wrapper function for notebook_page_new. It is called by GtkApplication object. The function tfe_window_notebook_page_new_with_files creates notebook pages with a contents read from the given files. The function tfe_window_new creates a TfeWindow instance.

+
#include "tfewindow.h"
+#include "tfenotebook.h"
+#include "tfepref.h"
+#include "tfealert.h"
+#include "css.h"
+
+struct _TfeWindow {
+  GtkApplicationWindow parent;
+  GtkMenuButton *btnm;
+  GtkNotebook *nb;
+  GSettings *settings;
+  gboolean is_quit;
+};
+
+G_DEFINE_TYPE (TfeWindow, tfe_window, GTK_TYPE_APPLICATION_WINDOW);
+
+/* alert response signal handler */
+static void
+alert_response_cb (GtkDialog *alert, int response_id, gpointer user_data) {
+  TfeWindow *win = TFE_WINDOW (user_data);
+
+  if (response_id == GTK_RESPONSE_ACCEPT) {
+    if (win->is_quit)
+      gtk_window_destroy(GTK_WINDOW (win));
+    else
+      notebook_page_close (win->nb);
+  }
+  gtk_window_destroy (GTK_WINDOW (alert));
+}
+
+/* ----- action activated handlers ----- */
+static void
+open_activated (GSimpleAction *action, GVariant *parameter, gpointer user_data) {
+  TfeWindow *win = TFE_WINDOW (user_data);
+
+  notebook_page_open (GTK_NOTEBOOK (win->nb));
+}
+
+static void
+save_activated (GSimpleAction *action, GVariant *parameter, gpointer user_data) {
+  TfeWindow *win = TFE_WINDOW (user_data);
+
+  notebook_page_save (GTK_NOTEBOOK (win->nb));
+}
+
+static void
+close_activated (GSimpleAction *action, GVariant *parameter, gpointer user_data) {
+  TfeWindow *win = TFE_WINDOW (user_data);
+  TfeAlert *alert;
+
+  if (has_saved (win->nb))
+    notebook_page_close (win->nb);
+  else {
+    win->is_quit = false;
+    alert = TFE_ALERT (tfe_alert_new (GTK_WINDOW (win)));
+    tfe_alert_set_message (alert, "Contents aren't saved yet.\nAre you sure to close?");
+    tfe_alert_set_button_label (alert, "Close");
+    g_signal_connect (GTK_DIALOG (alert), "response", G_CALLBACK (alert_response_cb), win);
+    gtk_widget_show (GTK_WIDGET (alert));
+  }
+}
+
+static void
+new_activated (GSimpleAction *action, GVariant *parameter, gpointer user_data) {
+  TfeWindow *win = TFE_WINDOW (user_data);
+
+  notebook_page_new (GTK_NOTEBOOK (win->nb));
+}
+
+static void
+saveas_activated (GSimpleAction *action, GVariant *parameter, gpointer user_data) {
+  TfeWindow *win = TFE_WINDOW (user_data);
+
+  notebook_page_saveas (GTK_NOTEBOOK (win->nb));
+}
+
+static void
+pref_activated (GSimpleAction *action, GVariant *parameter, gpointer user_data) {
+  TfeWindow *win = TFE_WINDOW (user_data);
+  GtkWidget *pref;
+
+  pref = tfe_pref_new (GTK_WINDOW (win));
+  gtk_widget_show (pref);
+}
+
+static void
+quit_activated (GSimpleAction *action, GVariant *parameter, gpointer user_data) {
+  TfeWindow *win = TFE_WINDOW (user_data);
+
+  TfeAlert *alert;
+
+  if (has_saved_all (GTK_NOTEBOOK (win->nb)))
+    gtk_window_destroy (GTK_WINDOW (win));
+  else {
+    win->is_quit = true;
+    alert = TFE_ALERT (tfe_alert_new (GTK_WINDOW (win)));
+    tfe_alert_set_message (alert, "Contents aren't saved yet.\nAre you sure to quit?");
+    tfe_alert_set_button_label (alert, "Quit");
+    g_signal_connect (GTK_DIALOG (alert), "response", G_CALLBACK (alert_response_cb), win);
+    gtk_widget_show (GTK_WIDGET (alert));
+  }
+}
+
+/* gsettings changed::font signal handler */
+static void
+changed_font_cb (GSettings *settings, char *key, gpointer user_data) {
+  GtkWindow *win = GTK_WINDOW (user_data); 
+  char *font;
+  PangoFontDescription *pango_font_desc;
+
+  font = g_settings_get_string (settings, "font");
+  pango_font_desc = pango_font_description_from_string (font);
+  g_free (font);
+  set_font_for_display_with_pango_font_desc (win, pango_font_desc);
+}
+
+/* --- public functions --- */
+
+void
+tfe_window_notebook_page_new (TfeWindow *win) {
+  notebook_page_new (win->nb);
+}
+
+void
+tfe_window_notebook_page_new_with_files (TfeWindow *win, GFile **files, int n_files) {
+  int i;
+
+  for (i = 0; i < n_files; i++)
+    notebook_page_new_with_file (win->nb, files[i]);
+  if (gtk_notebook_get_n_pages (win->nb) == 0)
+    notebook_page_new (win->nb);
+}
+
+/* --- TfeWindow object construction/destruction --- */ 
+static void
+tfe_window_dispose (GObject *gobject) {
+  TfeWindow *window = TFE_WINDOW (gobject);
+
+  g_clear_object (&window->settings);
+  G_OBJECT_CLASS (tfe_window_parent_class)->dispose (gobject);
+}
+
+static void
+tfe_window_init (TfeWindow *win) {
+  GtkBuilder *build;
+  GMenuModel *menu;
+
+  gtk_widget_init_template (GTK_WIDGET (win));
+
+  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 (win->btnm, menu);
+  g_object_unref(build);
+
+  win->settings = g_settings_new ("com.github.ToshioCP.tfe");
+  g_signal_connect (win->settings, "changed::font", G_CALLBACK (changed_font_cb), win);
+
+/* ----- 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), win);
+
+  changed_font_cb(win->settings, "font", win);
+}
+
+static void
+tfe_window_class_init (TfeWindowClass *class) {
+  GObjectClass *object_class = G_OBJECT_CLASS (class);
+
+  object_class->dispose = tfe_window_dispose;
+  gtk_widget_class_set_template_from_resource (GTK_WIDGET_CLASS (class), "/com/github/ToshioCP/tfe/tfewindow.ui");
+  gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (class), TfeWindow, btnm);
+  gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (class), TfeWindow, nb);
+}
+
+GtkWidget *
+tfe_window_new (GtkApplication *app) {
+  return GTK_WIDGET (g_object_new (TFE_TYPE_WINDOW, "application", app, NULL));
+}
+ +

TfeApplication

+

The file tfeapplication.c is now very simple.

+
#include "tfewindow.h"
+
+/* ----- activate, open, startup handlers ----- */
+static void
+app_activate (GApplication *application) {
+  GtkApplication *app = GTK_APPLICATION (application);
+  GtkWidget *win = GTK_WIDGET (gtk_application_get_active_window (app));
+
+  tfe_window_notebook_page_new (TFE_WINDOW (win));
+  gtk_widget_show (GTK_WIDGET (win));
+}
+
+static void
+app_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));
+
+  tfe_window_notebook_page_new_with_files (TFE_WINDOW (win), files, n_files);
+  gtk_widget_show (win);
+}
+
+static void
+app_startup (GApplication *application) {
+  GtkApplication *app = GTK_APPLICATION (application);
+  int i;
+
+  tfe_window_new (app);
+
+/* ----- accelerator ----- */ 
+  struct {
+    const char *action;
+    const char *accels[2];
+  } action_accels[] = {
+    { "win.open", { "<Control>o", NULL } },
+    { "win.save", { "<Control>s", NULL } },
+    { "win.close", { "<Control>w", NULL } },
+    { "win.new", { "<Control>n", NULL } },
+    { "win.saveas", { "<Shift><Control>s", NULL } },
+    { "win.close-all", { "<Control>q", NULL } },
+  };
+
+  for (i = 0; i < G_N_ELEMENTS(action_accels); i++)
+    gtk_application_set_accels_for_action(GTK_APPLICATION(app), action_accels[i].action, action_accels[i].accels);
+}
+
+/* ----- main ----- */
+
+#define APPLICATION_ID "com.github.ToshioCP.tfe"
+
+int
+main (int argc, char **argv) {
+  GtkApplication *app;
+  int stat;
+
+  app = gtk_application_new (APPLICATION_ID, G_APPLICATION_HANDLES_OPEN);
+
+  g_signal_connect (app, "startup", G_CALLBACK (app_startup), NULL);
+  g_signal_connect (app, "activate", G_CALLBACK (app_activate), NULL);
+  g_signal_connect (app, "open", G_CALLBACK (app_open), NULL);
+
+  stat =g_application_run (G_APPLICATION (app), argc, argv);
+  g_object_unref (app);
+  return stat;
+}
+ +

Other files

+

Resource XML file.

+
<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/com/github/ToshioCP/tfe">
+    <file>tfewindow.ui</file>
+    <file>tfepref.ui</file>
+    <file>tfealert.ui</file>
+    <file>menu.ui</file>
+  </gresource>
+</gresources>
+

GSchema XML file

+
<?xml version="1.0" encoding="UTF-8"?>
+<schemalist>
+  <schema path="/com/github/ToshioCP/tfe/" id="com.github.ToshioCP.tfe">
+    <key name="font" type="s">
+      <default>'Monospace 12'</default>
+      <summary>Font</summary>
+      <description>The font to be used for textview.</description>
+    </key>
+  </schema>
+</schemalist>
+

Meson.build

+
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', 'tfewindow.c', 'tfenotebook.c', 'tfepref.c', 'tfealert.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)
+

Compilation and installation.

+

If you build Gtk4 from the source, use --prefix option.

+
$ meson --prefix=$HOME/local _build
+$ ninja -C _build
+$ ninja -C _build install
+

If you install Gtk4 from the distribution packages, you don’t need the prefix option. Maybe you need root privilege to install it.

+
$ meson _build
+$ ninja -C _build
+$ ninja -C _build install  # or 'sudo ninja -C _build install'
+

Source files are in src/tfe7 directory.

+

We made a very small text editor. You can add features to this editor. When you add a new feature, care about the structure of the program. Maybe you need to divide a file into several files like this section. It isn’t good to put many things into one file. And it is important to think about the relationship between source files and widget structures. It is appropriate that they correspond to each other in many cases.

+

Up: index.html, Prev: Section 20, Next: Section 22

+ + diff --git a/docs/sec22.html b/docs/sec22.html new file mode 100644 index 0000000..c119cb3 --- /dev/null +++ b/docs/sec22.html @@ -0,0 +1,248 @@ + + + + + + + Gtk4 tutorial for beginners + + + +

Up: index.html, Prev: Section 21, Next: Section 23

+

GtkDrawingArea and Cairo

+

If you want to draw dynamically on the screen, like an image window of gimp graphics editor, the GtkDrawingArea widget is the most suitable widget. You can freely draw or redraw an image in this widget. This is called custom drawing.

+

GtkDrawingArea provides a cairo drawing context so users can draw images by using cairo functions. In this section, I will explain:

+
    +
  1. Cairo, but only briefly; and
  2. +
  3. GtkDrawingArea, with a very simple example.
  4. +
+

Cairo

+

Cairo is a set of two dimensional graphical drawing functions (or graphics library). There is a lot of documentation on Cairo’s website. If you aren’t familiar with Cairo, it is worth reading their tutorial.

+

The following is a gentle introduction to the Cairo library and how to use it. Firstly, in order to use Cairo you need to know about surfaces, sources, masks, destinations, cairo context and transformations.

+ +
+
Stroke a rectangle
+
+

The instruction is as follows:

+
    +
  1. Create a surface. This will be the destination.
  2. +
  3. Create a cairo context with the surface, the surface will be the destination of the context.
  4. +
  5. Create a source pattern within the context.
  6. +
  7. Create paths, which are lines, rectangles, arcs, texts or more complicated shapes in the mask.
  8. +
  9. Use a drawing operator such as cairo_stroke to transfer the paint in the source to the destination.
  10. +
  11. Save the destination surface to a file if necessary.
  12. +
+

Here’s a simple example program that draws a small square and saves it as a png file.

+
#include <cairo.h>
+
+int
+main (int argc, char **argv)
+{
+  cairo_surface_t *surface;
+  cairo_t *cr;
+  int width = 100;
+  int height = 100;
+  int square_size = 40.0;
+
+  /* Create surface and cairo */
+  surface = cairo_image_surface_create (CAIRO_FORMAT_RGB24, width, height);
+  cr = cairo_create (surface);
+
+  /* Drawing starts here. */
+  /* Paint the background white */
+  cairo_set_source_rgb (cr, 1.0, 1.0, 1.0);
+  cairo_paint (cr);
+  /* Draw a black rectangle */
+  cairo_set_source_rgb (cr, 0.0, 0.0, 0.0);
+  cairo_set_line_width (cr, 2.0);
+  cairo_rectangle (cr,
+                   width/2.0 - square_size/2,
+                   height/2.0 - square_size/2,
+                   square_size,
+                   square_size);
+  cairo_stroke (cr);
+
+  /* Write the surface to a png file and clean up cairo and surface. */
+  cairo_surface_write_to_png (surface, "rectangle.png");
+  cairo_destroy (cr);
+  cairo_surface_destroy (surface);
+
+  return 0;
+}
+ +

To compile this, type the following.

+
$ gcc `pkg-config --cflags cairo` cairo.c `pkg-config --libs cairo`
+
+
rectangle.png
+
+

See the Cairo’s website for more details.

+

GtkDrawingArea

+

The following is a very simple example.

+
#include <gtk/gtk.h>
+
+static void
+draw_function (GtkDrawingArea *area, cairo_t *cr, int width, int height, gpointer user_data) {
+  int square_size = 40.0;
+
+  cairo_set_source_rgb (cr, 1.0, 1.0, 1.0); /* white */
+  cairo_paint (cr);
+  cairo_set_line_width (cr, 2.0);
+  cairo_set_source_rgb (cr, 0.0, 0.0, 0.0); /* black */
+  cairo_rectangle (cr,
+                   width/2.0 - square_size/2,
+                   height/2.0 - square_size/2,
+                   square_size,
+                   square_size);
+  cairo_stroke (cr);
+}
+
+static void
+app_activate (GApplication *app, gpointer user_data) {
+  GtkWidget *win = gtk_application_window_new (GTK_APPLICATION (app));
+  GtkWidget *area = gtk_drawing_area_new ();
+
+  gtk_window_set_title (GTK_WINDOW (win), "da1");
+  gtk_drawing_area_set_draw_func (GTK_DRAWING_AREA (area), draw_function, NULL, NULL);
+  gtk_window_set_child (GTK_WINDOW (win), area);
+
+  gtk_widget_show (win);
+}
+
+#define APPLICATION_ID "com.github.ToshioCP.da1"
+
+int
+main (int argc, char **argv) {
+  GtkApplication *app;
+  int stat;
+
+  app = gtk_application_new (APPLICATION_ID, G_APPLICATION_FLAGS_NONE);
+  g_signal_connect (app, "activate", G_CALLBACK (app_activate), NULL);
+  stat =g_application_run (G_APPLICATION (app), argc, argv);
+  g_object_unref (app);
+  return stat;
+}
+

The function main is almost same as before. The two functions app_activate and draw_function are important in this example.

+ +

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. 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. Now, look at the program example again.

+ +

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 each time the window is resized.

+
+
Square in the window
+
+

Up: index.html, Prev: Section 21, Next: Section 23

+ + diff --git a/docs/sec23.html b/docs/sec23.html new file mode 100644 index 0000000..e7ff9b5 --- /dev/null +++ b/docs/sec23.html @@ -0,0 +1,440 @@ + + + + + + + Gtk4 tutorial for beginners + + + +

Up: index.html, Prev: Section 22, Next: Section 24

+

Periodic Events

+

This chapter was written by Paul Schulz paul@mawsonlakes.org.

+

How do we create an animation?

+

In this section we will continue to build on our previous work. We will create an analog clock application. By adding a function which periodically redraws GtkDrawingArea, the clock will be able to continuously display the time.

+

The application uses a compiled in ‘resource’ file, so if the GTK4 libraries and their dependencies are installed and available, the application will run from anywhere.

+

The program also makes use of some standard mathematical and time handling functions.

+

The clocks mechanics were taken from a Cairo drawing example, using gtkmm4, which can be found here.

+

The complete code is at the end.

+

Drawing the clock face, hour, minute and second hands

+

The draw_clock() function does all the work. See the in-file comments for an explanation of how the Cairo drawing works.

+

For a detailed reference of what each of the Cairo functions does see the cairo_t reference.

+
static void
+draw_clock (GtkDrawingArea *area, cairo_t *cr, int width, int height, gpointer user_data) {
+
+    // Scale to unit square and translate (0, 0) to be (0.5, 0.5), i.e.
+    // the center of the window
+    cairo_scale(cr, width, height);
+    cairo_translate(cr, 0.5, 0.5);
+
+    // Set the line width and save the cairo drawing state.
+    cairo_set_line_width(cr, m_line_width);
+    cairo_save(cr);
+
+    // Set the background to a slightly transparent green.
+    cairo_set_source_rgba(cr, 0.337, 0.612, 0.117, 0.9);   // green
+    cairo_paint(cr);
+
+    // Resore back to precious drawing state and draw the circular path
+    // representing the clockface. Save this state (including the path) so we
+    // can reuse it.
+    cairo_restore(cr);
+    cairo_arc(cr, 0.0, 0.0, m_radius, 0.0, 2.0 * M_PI);
+    cairo_save(cr);
+
+    // Fill the clockface with white
+    cairo_set_source_rgba(cr, 1.0, 1.0, 1.0, 0.8);
+    cairo_fill_preserve(cr);
+    // Restore the path, paint the outside of the clock face.
+    cairo_restore(cr);
+    cairo_stroke_preserve(cr);
+    // Set the 'clip region' to the inside of the path (fill region).
+    cairo_clip(cr);
+
+    // Clock ticks
+    for (int i = 0; i < 12; i++)
+    {
+        // Major tick size
+        double inset = 0.05;
+
+        // Save the graphics state, restore after drawing tick to maintain pen
+        // size
+        cairo_save(cr);
+        cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND);
+
+        // Minor ticks are shorter, and narrower.
+        if(i % 3 != 0)
+        {
+            inset *= 0.8;
+            cairo_set_line_width(cr, 0.03);
+        }
+
+        // Draw tick mark
+        cairo_move_to(
+            cr,
+            (m_radius - inset) * cos (i * M_PI / 6.0),
+            (m_radius - inset) * sin (i * M_PI / 6.0));
+        cairo_line_to(
+            cr,
+            m_radius * cos (i * M_PI / 6.0),
+            m_radius * sin (i * M_PI / 6.0));
+        cairo_stroke(cr);
+        cairo_restore(cr); /* stack-pen-size */
+    }
+
+    // Draw the analog hands
+
+    // Get the current Unix time, convert to the local time and break into time
+    // structure to read various time parts.
+    time_t rawtime;
+    time(&rawtime);
+    struct tm * timeinfo = localtime (&rawtime);
+
+    // Calculate the angles of the hands of our clock
+    double hours   = timeinfo->tm_hour * M_PI / 6.0;
+    double minutes = timeinfo->tm_min * M_PI / 30.0;
+    double seconds = timeinfo->tm_sec * M_PI / 30.0;
+
+    // Save the graphics state
+    cairo_save(cr);
+    cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND);
+
+    cairo_save(cr);
+
+    // Draw the seconds hand
+    cairo_set_line_width(cr, m_line_width / 3.0);
+    cairo_set_source_rgba(cr, 0.7, 0.7, 0.7, 0.8);   // gray
+    cairo_move_to(cr, 0.0, 0.0);
+    cairo_line_to(cr,
+                  sin(seconds) * (m_radius * 0.9),
+                  -cos(seconds) * (m_radius * 0.9));
+    cairo_stroke(cr);
+    cairo_restore(cr);
+
+    // Draw the minutes hand
+    cairo_set_source_rgba(cr, 0.117, 0.337, 0.612, 0.9);   // blue
+    cairo_move_to(cr, 0, 0);
+    cairo_line_to(cr,
+                  sin(minutes + seconds / 60) * (m_radius * 0.8),
+                  -cos(minutes + seconds / 60) * (m_radius * 0.8));
+    cairo_stroke(cr);
+
+    // draw the hours hand
+    cairo_set_source_rgba(cr, 0.337, 0.612, 0.117, 0.9);   // green
+    cairo_move_to(cr, 0.0, 0.0);
+    cairo_line_to(cr,
+                  sin(hours + minutes / 12.0) * (m_radius * 0.5),
+                  -cos(hours + minutes / 12.0) * (m_radius * 0.5));
+    cairo_stroke(cr);
+    cairo_restore(cr);
+
+    // Draw a little dot in the middle
+    cairo_arc(cr, 0.0, 0.0, m_line_width / 3.0, 0.0, 2.0 * M_PI);
+    cairo_fill(cr);
+}
+

In order for the clock to be drawn, the drawing function draw_clock() needs to be registered with GTK4. This is done in the app_activate() function (on line 24).

+

Whenever the application needs to redraw the GtkDrawingArea, it will now call draw_clock().

+

There is still a problem though. In order to animate the clock we need to also tell the application that the clock needs to be redrawn every second. This process starts by registering (on the next line, line 15) a timeout function with g_timeout_add() that will wakeup and run another function time_handler, every second (or 1000ms).

+
static void
+app_activate (GApplication *app, gpointer user_data) {
+    GtkWidget *win;
+    GtkWidget *clock;
+    GtkBuilder *build;
+
+    build = gtk_builder_new_from_resource ("/com/github/ToshioCP/tfc/tfc.ui");
+    win = GTK_WIDGET (gtk_builder_get_object (build, "win"));
+    gtk_window_set_application (GTK_WINDOW (win), GTK_APPLICATION (app));
+
+    clock = GTK_WIDGET (gtk_builder_get_object (build, "clock"));
+    g_object_unref(build);
+
+    gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA (clock), draw_clock, NULL, NULL);
+    g_timeout_add(1000, (GSourceFunc) time_handler, (gpointer) clock);
+    gtk_widget_show(win);
+
+}
+

Our time_handler() function is very simple, as it just calls gtk_widget_queue_draw() which schedules a redraw of the widget.

+
gboolean
+time_handler(GtkWidget* widget) {
+    gtk_widget_queue_draw(widget);
+
+    return TRUE;
+}
+

.. and that is all there is to it. If you compile and run the example you will get a ticking analog clock.

+

If you get this working, you can try modifying some of the code in draw_clock() to tweak the application (such as change the color or size and length of the hands) or even add text, or create a digital clock.

+

The Complete code

+

You can find the source files in the tfc directory. it can be compiled with ./comp tfc.

+

tfc.c

+
#include <gtk/gtk.h>
+#include <math.h>
+#include <time.h>
+
+float m_radius     = 0.42;
+float m_line_width = 0.05;
+
+static void
+draw_clock (GtkDrawingArea *area, cairo_t *cr, int width, int height, gpointer user_data) {
+
+    // Scale to unit square and translate (0, 0) to be (0.5, 0.5), i.e.
+    // the center of the window
+    cairo_scale(cr, width, height);
+    cairo_translate(cr, 0.5, 0.5);
+
+    // Set the line width and save the cairo drawing state.
+    cairo_set_line_width(cr, m_line_width);
+    cairo_save(cr);
+
+    // Set the background to a slightly transparent green.
+    cairo_set_source_rgba(cr, 0.337, 0.612, 0.117, 0.9);   // green
+    cairo_paint(cr);
+
+    // Resore back to precious drawing state and draw the circular path
+    // representing the clockface. Save this state (including the path) so we
+    // can reuse it.
+    cairo_restore(cr);
+    cairo_arc(cr, 0.0, 0.0, m_radius, 0.0, 2.0 * M_PI);
+    cairo_save(cr);
+
+    // Fill the clockface with white
+    cairo_set_source_rgba(cr, 1.0, 1.0, 1.0, 0.8);
+    cairo_fill_preserve(cr);
+    // Restore the path, paint the outside of the clock face.
+    cairo_restore(cr);
+    cairo_stroke_preserve(cr);
+    // Set the 'clip region' to the inside of the path (fill region).
+    cairo_clip(cr);
+
+    // Clock ticks
+    for (int i = 0; i < 12; i++)
+    {
+        // Major tick size
+        double inset = 0.05;
+
+        // Save the graphics state, restore after drawing tick to maintain pen
+        // size
+        cairo_save(cr);
+        cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND);
+
+        // Minor ticks are shorter, and narrower.
+        if(i % 3 != 0)
+        {
+            inset *= 0.8;
+            cairo_set_line_width(cr, 0.03);
+        }
+
+        // Draw tick mark
+        cairo_move_to(
+            cr,
+            (m_radius - inset) * cos (i * M_PI / 6.0),
+            (m_radius - inset) * sin (i * M_PI / 6.0));
+        cairo_line_to(
+            cr,
+            m_radius * cos (i * M_PI / 6.0),
+            m_radius * sin (i * M_PI / 6.0));
+        cairo_stroke(cr);
+        cairo_restore(cr); /* stack-pen-size */
+    }
+
+    // Draw the analog hands
+
+    // Get the current Unix time, convert to the local time and break into time
+    // structure to read various time parts.
+    time_t rawtime;
+    time(&rawtime);
+    struct tm * timeinfo = localtime (&rawtime);
+
+    // Calculate the angles of the hands of our clock
+    double hours   = timeinfo->tm_hour * M_PI / 6.0;
+    double minutes = timeinfo->tm_min * M_PI / 30.0;
+    double seconds = timeinfo->tm_sec * M_PI / 30.0;
+
+    // Save the graphics state
+    cairo_save(cr);
+    cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND);
+
+    cairo_save(cr);
+
+    // Draw the seconds hand
+    cairo_set_line_width(cr, m_line_width / 3.0);
+    cairo_set_source_rgba(cr, 0.7, 0.7, 0.7, 0.8);   // gray
+    cairo_move_to(cr, 0.0, 0.0);
+    cairo_line_to(cr,
+                  sin(seconds) * (m_radius * 0.9),
+                  -cos(seconds) * (m_radius * 0.9));
+    cairo_stroke(cr);
+    cairo_restore(cr);
+
+    // Draw the minutes hand
+    cairo_set_source_rgba(cr, 0.117, 0.337, 0.612, 0.9);   // blue
+    cairo_move_to(cr, 0, 0);
+    cairo_line_to(cr,
+                  sin(minutes + seconds / 60) * (m_radius * 0.8),
+                  -cos(minutes + seconds / 60) * (m_radius * 0.8));
+    cairo_stroke(cr);
+
+    // draw the hours hand
+    cairo_set_source_rgba(cr, 0.337, 0.612, 0.117, 0.9);   // green
+    cairo_move_to(cr, 0.0, 0.0);
+    cairo_line_to(cr,
+                  sin(hours + minutes / 12.0) * (m_radius * 0.5),
+                  -cos(hours + minutes / 12.0) * (m_radius * 0.5));
+    cairo_stroke(cr);
+    cairo_restore(cr);
+
+    // Draw a little dot in the middle
+    cairo_arc(cr, 0.0, 0.0, m_line_width / 3.0, 0.0, 2.0 * M_PI);
+    cairo_fill(cr);
+}
+
+
+gboolean
+time_handler(GtkWidget* widget) {
+    gtk_widget_queue_draw(widget);
+
+    return TRUE;
+}
+
+
+static void
+app_activate (GApplication *app, gpointer user_data) {
+    GtkWidget *win;
+    GtkWidget *clock;
+    GtkBuilder *build;
+
+    build = gtk_builder_new_from_resource ("/com/github/ToshioCP/tfc/tfc.ui");
+    win = GTK_WIDGET (gtk_builder_get_object (build, "win"));
+    gtk_window_set_application (GTK_WINDOW (win), GTK_APPLICATION (app));
+
+    clock = GTK_WIDGET (gtk_builder_get_object (build, "clock"));
+    g_object_unref(build);
+
+    gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA (clock), draw_clock, NULL, NULL);
+    g_timeout_add(1000, (GSourceFunc) time_handler, (gpointer) clock);
+    gtk_widget_show(win);
+
+}
+
+static void
+app_open (GApplication *app, GFile **files, gint n_files, gchar *hint, gpointer user_data) {
+    app_activate(app,user_data);
+}
+
+int
+main (int argc, char **argv) {
+    GtkApplication *app;
+    int stat;
+
+    app = gtk_application_new ("com.github.ToshioCP.tfc", G_APPLICATION_HANDLES_OPEN);
+    g_signal_connect (app, "activate", G_CALLBACK (app_activate), NULL);
+    g_signal_connect (app, "open", G_CALLBACK (app_open), NULL);
+    stat = g_application_run (G_APPLICATION (app), argc, argv);
+    g_object_unref (app);
+    return stat;
+}
+

tfc.ui

+
<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <object class="GtkApplicationWindow" id="win">
+    <property name="title">Clock</property>
+    <property name="default-width">200</property>
+    <property name="default-height">200</property>
+    <child>
+      <object class="GtkDrawingArea" id="clock">
+        <property name="hexpand">TRUE</property>
+        <property name="vexpand">TRUE</property>
+      </object>
+    </child>
+  </object>
+</interface>
+

tfc.gresource.xml

+
<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/com/github/ToshioCP/tfc">
+    <file>tfc.ui</file>
+  </gresource>
+</gresources>
+

comp

+
glib-compile-resources $1.gresource.xml --target=$1.gresource.c --generate-source
+gcc `pkg-config --cflags gtk4` $1.gresource.c $1.c `pkg-config --libs gtk4` -lm
+

Up: index.html, Prev: Section 22, Next: Section 24

+ + diff --git a/docs/sec24.html b/docs/sec24.html new file mode 100644 index 0000000..0f5ce4c --- /dev/null +++ b/docs/sec24.html @@ -0,0 +1,378 @@ + + + + + + + Gtk4 tutorial for beginners + + + +

Up: index.html, Prev: Section 23, Next: Section 25

+

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 name of a color in TfeTextView and click on the run button, then the color of GtkDrawingArea changes to the color given by you.

+
+
color
+
+

The following colors are available.

+ +

In addition the following two options are also available.

+ +

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 version="1.0" encoding="UTF-8"?>
+<interface>
+  <object class="GtkApplicationWindow" id="win">
+    <property name="title">color changer</property>
+    <property name="default-width">600</property>
+    <property name="default-height">400</property>
+    <child>
+      <object class="GtkBox" id="boxv">
+        <property name="orientation">GTK_ORIENTATION_VERTICAL</property>
+        <child>
+          <object class="GtkBox" id="boxh1">
+            <property name="orientation">GTK_ORIENTATION_HORIZONTAL</property>
+            <child>
+              <object class="GtkLabel" id="dmy1">
+                <property name="width-chars">10</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkButton" id="btnr">
+                <property name="label">Run</property>
+                <signal name="clicked" handler="run_cb"></signal>
+              </object>
+            </child>
+            <child>
+              <object class="GtkButton" id="btno">
+                <property name="label">Open</property>
+                <signal name="clicked" handler="open_cb"></signal>
+              </object>
+            </child>
+            <child>
+              <object class="GtkLabel" id="dmy2">
+                <property name="hexpand">TRUE</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkButton" id="btns">
+                <property name="label">Save</property>
+                <signal name="clicked" handler="save_cb"></signal>
+              </object>
+            </child>
+            <child>
+              <object class="GtkButton" id="btnc">
+                <property name="label">Close</property>
+                <signal name="clicked" handler="close_cb"></signal>
+              </object>
+            </child>
+            <child>
+              <object class="GtkLabel" id="dmy3">
+                <property name="width-chars">10</property>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="GtkBox" id="boxh2">
+            <property name="orientation">GTK_ORIENTATION_HORIZONTAL</property>
+            <property name="homogeneous">TRUE</property>
+            <child>
+              <object class="GtkScrolledWindow" id="scr">
+                <property name="hexpand">TRUE</property>
+                <property name="vexpand">TRUE</property>
+                <child>
+                  <object class="TfeTextView" id="tv">
+                    <property name="wrap-mode">GTK_WRAP_WORD_CHAR</property>
+                  </object>
+                </child>
+              </object>
+            </child>
+            <child>
+              <object class="GtkDrawingArea" id="da">
+                <property name="hexpand">TRUE</property>
+                <property name="vexpand">TRUE</property>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </object>
+</interface>
+ +

The xml file for the resource compiler is almost same as before. Just substitute “color” for “tfe”.

+
<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/com/github/ToshioCP/color">
+    <file>color.ui</file>
+  </gresource>
+</gresources>
+

Tfetextview.h, tfetextview.c and color.h

+

First two files are the same as before. Color.h just includes tfetextview.h.

+
#include <gtk/gtk.h>
+
+#include "../tfetextview/tfetextview.h"
+

Colorapplication.c

+

This is the main file. It deals with:

+ +

The following is colorapplication.c.

+
#include "color.h"
+
+static GtkWidget *win;
+static GtkWidget *tv;
+static GtkWidget *da;
+
+static cairo_surface_t *surface = NULL;
+
+static void
+run (void) {
+  GtkTextBuffer *tb = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv));
+  GtkTextIter start_iter;
+  GtkTextIter end_iter;
+  char *contents;
+  cairo_t *cr;
+
+  gtk_text_buffer_get_bounds (tb, &start_iter, &end_iter);
+  contents = gtk_text_buffer_get_text (tb, &start_iter, &end_iter, FALSE);
+  if (surface) {
+    cr = cairo_create (surface);
+    if (g_strcmp0 ("red", contents) == 0)
+      cairo_set_source_rgb (cr, 1, 0, 0);
+    else if (g_strcmp0 ("green", contents) == 0)
+      cairo_set_source_rgb (cr, 0, 1, 0);
+    else if (g_strcmp0 ("blue", contents) == 0)
+      cairo_set_source_rgb (cr, 0, 0, 1);
+    else if (g_strcmp0 ("white", contents) == 0)
+      cairo_set_source_rgb (cr, 1, 1, 1);
+    else if (g_strcmp0 ("black", contents) == 0)
+      cairo_set_source_rgb (cr, 0, 0, 0);
+    else if (g_strcmp0 ("light", contents) == 0)
+      cairo_set_source_rgba (cr, 1, 1, 1, 0.5);
+    else if (g_strcmp0 ("dark", contents) == 0)
+      cairo_set_source_rgba (cr, 0, 0, 0, 0.5);
+    else
+      cairo_set_source_surface (cr, surface, 0, 0);
+    cairo_paint (cr);
+    cairo_destroy (cr);
+  }
+  g_free (contents);
+}
+
+void
+run_cb (GtkWidget *btnr) {
+  run ();
+  gtk_widget_queue_draw (GTK_WIDGET (da));
+}
+
+void
+open_cb (GtkWidget *btno) {
+  tfe_text_view_open (TFE_TEXT_VIEW (tv), GTK_WINDOW (win));
+}
+
+void
+save_cb (GtkWidget *btns) {
+  tfe_text_view_save (TFE_TEXT_VIEW (tv));
+}
+
+void
+close_cb (GtkWidget *btnc) {
+  if (surface)
+    cairo_surface_destroy (surface);
+  gtk_window_destroy (GTK_WINDOW (win));
+}
+
+static void
+resize_cb (GtkDrawingArea *drawing_area, int width, int height, gpointer user_data) {
+  if (surface)
+    cairo_surface_destroy (surface);
+  surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, width, height);
+  run ();
+}
+
+static void
+draw_func (GtkDrawingArea *drawing_area, cairo_t *cr, int width, int height, gpointer user_data) {
+  if (surface) {
+    cairo_set_source_surface (cr, surface, 0, 0);
+    cairo_paint (cr);
+  }
+}
+
+static void
+app_activate (GApplication *application) {
+  gtk_widget_show (win);
+}
+
+static void
+app_startup (GApplication *application) {
+  GtkApplication *app = GTK_APPLICATION (application);
+  GtkBuilder *build;
+
+  build = gtk_builder_new_from_resource ("/com/github/ToshioCP/color/color.ui");
+  win = GTK_WIDGET (gtk_builder_get_object (build, "win"));
+  gtk_window_set_application (GTK_WINDOW (win), app);
+  tv = GTK_WIDGET (gtk_builder_get_object (build, "tv"));
+  da = GTK_WIDGET (gtk_builder_get_object (build, "da"));
+  g_object_unref(build);
+  g_signal_connect (GTK_DRAWING_AREA (da), "resize", G_CALLBACK (resize_cb), NULL);
+  gtk_drawing_area_set_draw_func (GTK_DRAWING_AREA (da), draw_func, NULL, NULL);
+
+GdkDisplay *display;
+
+  display = gtk_widget_get_display (GTK_WIDGET (win));
+  GtkCssProvider *provider = gtk_css_provider_new ();
+  gtk_css_provider_load_from_data (provider, "textview {padding: 10px; font-family: monospace; font-size: 12pt;}", -1);
+  gtk_style_context_add_provider_for_display (display, GTK_STYLE_PROVIDER (provider), GTK_STYLE_PROVIDER_PRIORITY_USER);
+}
+
+#define APPLICATION_ID "com.github.ToshioCP.color"
+
+int
+main (int argc, char **argv) {
+  GtkApplication *app;
+  int stat;
+
+  app = gtk_application_new (APPLICATION_ID, G_APPLICATION_FLAGS_NONE);
+
+  g_signal_connect (app, "startup", G_CALLBACK (app_startup), NULL);
+  g_signal_connect (app, "activate", G_CALLBACK (app_activate), NULL);
+
+  stat =g_application_run (G_APPLICATION (app), argc, argv);
+  g_object_unref (app);
+  return stat;
+}
+ +

Meson.build

+

This file is almost same as before. An argument “export_dynamic: true” is added to executable function.

+
project('color', 'c')
+
+gtkdep = dependency('gtk4')
+
+gnome=import('gnome')
+resources = gnome.compile_resources('resources','color.gresource.xml')
+
+sourcefiles=files('colorapplication.c', '../tfetextview/tfetextview.c')
+
+executable('color', sourcefiles, resources, dependencies: gtkdep, export_dynamic: true)
+

Compile and execute it

+

First you need to export some variables (refer to Section 2) if you’ve installed Gtk4 from the source. If you’ve installed Gtk4 from the distribution packages, you don’t need to do this.

+
$ . 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: index.html, Prev: Section 23, Next: Section 25

+ + diff --git a/docs/sec25.html b/docs/sec25.html new file mode 100644 index 0000000..a948dd9 --- /dev/null +++ b/docs/sec25.html @@ -0,0 +1,1996 @@ + + + + + + + Gtk4 tutorial for beginners + + + +

Up: index.html, Prev: Section 24, Next: Section 26

+

Tiny turtle graphics interpreter

+

A program turtle is an example with the combination of TfeTextView and GtkDrawingArea objects. It is a very small interpreter but it provides a tool to draw fractal curves. The following diagram is a Koch curve, which is a famous example of fractal curves.

+
+
Koch curve
+
+

This program uses flex and bison. Flex is a lexical analyzer. Bison is a parser generator. These two programs are similar to lex and yacc which are proprietary software developed in Bell Laboratory. However, flex and bison are open source software. I will write about how to use those software, but they are not topics about gtk. So, readers can skip that part of this sections.

+

How to use turtle

+

The documentation of turtle is here. I’ll show you a simple example.

+
fc (1,0,0) # Foreground color is red, rgb = (1,0,0).
+pd         # Pen down.
+fd 100     # Go forward by 100 pixels.
+tr 90      # Turn right by 90 degrees.
+fd 100
+tr 90
+fd 100
+tr 90
+fd 100
+tr 90
+
    +
  1. Compile and install turtle (See the documentation above). Then, run turtle.
  2. +
  3. Type the program above in the editor (left part of the window).
  4. +
  5. Click on the Run button, then a red square appears on the right part of the window. The side of the square is 100 pixels long.
  6. +
+

In the same way, you can draw other curves. The documentation above shows some fractal curves such as tree, snow and square-koch. The source code in turtle language is located at src/turtle/example directory. You can read these files into turtle editor by clicking on the Open button.

+

Combination of TfeTextView and GtkDrawingArea objects

+

Turtle uses TfeTextView and GtkDrawingArea. It is similar to color program in the previous section.

+
    +
  1. A user inputs/reads a turtle program into the buffer in the TfeTextView instance.
  2. +
  3. The user clicks on the “Run” button.
  4. +
  5. The parser reads the program and generates tree-structured data.
  6. +
  7. The interpriter reads the data and executes it step by step. And it draws shapes on a surface. The surface is different from the surface of the GtkDrawingArea widget.
  8. +
  9. The widget is added to the queue. It will be redrawn with the drawing function. The function just copies the surface, which is drawn by the interpreter, into the surface of the GtkDrawingArea.
  10. +
+
+
Parser, interpreter and drawing function
+
+

The body of the interpreter is written with flex and bison. The codes are not thread safe. So the handler of “clicked” signal of the Run button prevents from reentering.

+
void
+run_cb (GtkWidget *btnr) {
+  GtkTextBuffer *tb = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv));
+  GtkTextIter start_iter;
+  GtkTextIter end_iter;
+  char *contents;
+  int stat;
+  static gboolean busy = FALSE;
+
+  /* yyparse() and run() are NOT thread safe. */
+  /* The variable busy avoids reentry. */
+  if (busy)
+    return;
+  busy = TRUE;
+  gtk_text_buffer_get_bounds (tb, &start_iter, &end_iter);
+  contents = gtk_text_buffer_get_text (tb, &start_iter, &end_iter, FALSE);
+  if (surface) {
+    init_flex (contents);
+    stat = yyparse ();
+    if (stat == 0) /* No error */ {
+      run ();
+    }
+    finalize_flex ();
+  }
+  g_free (contents);
+  gtk_widget_queue_draw (GTK_WIDGET (da));
+  busy = FALSE;
+}
+
+static void
+resize_cb (GtkDrawingArea *drawing_area, int width, int height, gpointer user_data) {
+  if (surface)
+    cairo_surface_destroy (surface);
+  surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, width, height);
+}
+ +

Other part of turtleapplication.c is almost same as the codes of colorapplication.c in the previous section. The codes of turtleapplication.c is in the turtle directory.

+

What does the interpreter do?

+

Suppose that the turtle runs with the following program.

+
distance = 100
+fd distance*2
+

The turtle recognizes the program above and works as follows.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
token kindyylval.IDyylval.NUM
1IDdistance
2=
3NUM100
4FD
5IDdistance
6*
7NUM2
+ +
+
turtle parser tree
+
+ +

Actual turtle program is more complicated than the example above. However, what turtle does is basically the same. Interpretation consists of three parts.

+ +

Compilation flow

+

The source files are:

+ +

The compilation process is a bit complicated.

+
    +
  1. glib-compile-resources compiles turtle.ui to resources.c according to turtle.gresource.xml. It also generates resources.h.
  2. +
  3. bison compiles turtle.y to turtle_parser.c and generates turtle_parser.h
  4. +
  5. flex compiles turtle.lex to turtle_lex.c.
  6. +
  7. gcc compiles application.c, resources.c, turtle_parser.c and turtle_lex.c with turtle.h, turtle_lex.h, resources.h and turtle_parser.h. It generates an executable file turtle.
  8. +
+
+
compile process
+
+

Meson controls the process and the instruction is described in meson.build.

+
project('turtle', 'c')
+
+compiler = meson.get_compiler('c')
+mathdep = compiler.find_library('m', required : true)
+
+gtkdep = dependency('gtk4')
+
+gnome=import('gnome')
+resources = gnome.compile_resources('resources','turtle.gresource.xml')
+
+flex = find_program('flex')
+bison = find_program('bison')
+turtleparser = custom_target('turtleparser', input: 'turtle.y', output: ['turtle_parser.c', 'turtle_parser.h'], command: [bison, '-d', '-o', 'turtle_parser.c', '@INPUT@'])
+turtlelexer = custom_target('turtlelexer', input: 'turtle.lex', output: 'turtle_lex.c', command: [flex, '-o', '@OUTPUT@', '@INPUT@'])
+
+sourcefiles=files('turtleapplication.c', '../tfetextview/tfetextview.c')
+
+executable('turtle', sourcefiles, resources, turtleparser, turtlelexer, turtleparser[1], dependencies: [mathdep, gtkdep], export_dynamic: true, install: true)
+ +

Turtle.lex

+

What does flex do?

+

Flex creates lexical analyzer from flex source file. Flex source file is a text file. Its syntactic rule will be explained later. Generated lexical analyzer is a C source file. It is also called scanner. It reads a text file, which is a source file of a program language, and gets variable names, numbers and symbols. Suppose here is a turtle source file.

+
fc (1,0,0) # Foreground color is red, rgb = (1,0,0).
+pd         # Pen down.
+distance = 100
+angle = 90
+fd distance    # Go forward by distance (100) pixels.
+tr angle     # Turn right by angle (90) degrees.
+

The content of the text file is separated into fc, (, 1 and so on. The words fc, pd, distance, angle, tr, 1, 0, 100 and 90 are called tokens. The characters ‘(’ (left parenthesis), ‘,’ (comma), ‘)’ (right parenthesis) and ‘=’ (equal sign) are called symbols. ( Sometimes those symbols called tokens, too.)

+

Flex reads turtle.lex and generates the C source file of a scanner. The file turtle.lex specifies tokens, symbols and the behavior which corresponds to each token or symbol. Turtle.lex isn’t a big program.

+
%top{
+#include <string.h>
+#include <stdlib.h>
+#include "turtle.h"
+
+  static int nline = 1;
+  static int ncolumn = 1;
+  static void get_location (char *text);
+
+  /* Dinamically allocated memories are added to the single list. They will be freed in the finalize function. */
+  extern GSList *list;
+}
+
+%option noyywrap
+
+REAL_NUMBER (0|[1-9][0-9]*)(\.[0-9]+)?
+IDENTIFIER [a-zA-Z][a-zA-Z0-9]*
+%%
+  /* rules */
+#.*               ; /* comment. Be careful. Dot symbol (.) matches any character but new line. */
+[ ]               ncolumn++;
+\t                ncolumn += 8; /* assume that tab is 8 spaces. */
+\n                nline++; ncolumn = 1;
+  /* reserved keywords */
+pu                get_location (yytext); return PU; /* pen up */
+pd                get_location (yytext); return PD; /* pen down */
+pw                get_location (yytext); return PW; /* pen width = line width */
+fd                get_location (yytext); return FD; /* forward */
+tr                get_location (yytext); return TR; /* turn right */
+bc                get_location (yytext); return BC; /* background color */
+fc                get_location (yytext); return FC; /* foreground color */
+dp                get_location (yytext); return DP; /* define procedure */
+if                get_location (yytext); return IF; /* if statement */
+rt                get_location (yytext); return RT; /* return statement */
+rs                get_location (yytext); return RS; /* reset the status */
+  /* constant */
+{REAL_NUMBER}     get_location (yytext); yylval.NUM = atof (yytext); return NUM;
+  /* identifier */
+{IDENTIFIER}      { get_location (yytext); yylval.ID = g_strdup(yytext);
+                    list = g_slist_prepend (list, yylval.ID);
+                    return ID;
+                  }
+"="               get_location (yytext); return '=';
+">"               get_location (yytext); return '>';
+"<"               get_location (yytext); return '<';
+"+"               get_location (yytext); return '+';
+"-"               get_location (yytext); return '-';
+"*"               get_location (yytext); return '*';
+"/"               get_location (yytext); return '/';
+"("               get_location (yytext); return '(';
+")"               get_location (yytext); return ')';
+"{"               get_location (yytext); return '{';
+"}"               get_location (yytext); return '}';
+","               get_location (yytext); return ',';
+.                 ncolumn++;             return YYUNDEF;
+%%
+
+static void
+get_location (char *text) {
+  yylloc.first_line = yylloc.last_line = nline;
+  yylloc.first_column = ncolumn;
+  yylloc.last_column = (ncolumn += strlen(text)) - 1;
+}
+
+static YY_BUFFER_STATE state;
+
+void
+init_flex (const char *text) {
+  state = yy_scan_string (text);
+}
+
+void
+finalize_flex (void) {
+  yy_delete_buffer (state);
+}
+

The file consists of three sections which are separated by “%%” (line 18 and 56). They are definitions, rules and user code sections.

+

Definitions section

+ +

Rules section

+

This section is the most important part. Rules consist of patterns and actions. The patterns are regular expressions or names surrounded by braces. The names must be defined in the definitions section. The definition of the regular expression is written in the flex documentation.

+

For example, line 37 is a rule.

+ +

{REAL_NUMBER} is defined in the 16th line, so it expands to (0|[1-9][0-9]*)(\.[0-9]+)?. This regular expression matches numbers like 0, 12 and 1.5. If the input is a number, it matches the pattern in line 37. Then the matched text is assigned to yytext and corresponding action is executed. A function get_location changes the location variables. It assigns atof (yytext), which is double sized number converted from yytext, to yylval.NUM and return NUM. NUM is an integer defined by turtle.y.

+

The scanner generated by flex and C compiler has yylex function. If yylex is called and the input is “123.4”, then it works as follows.

+
    +
  1. A string “123.4” matches {REAL_NUMBER}.
  2. +
  3. Update the location variable ncolumn and yyllocwith get_location.
  4. +
  5. atof converts the string “123.4” to double type number 123.4.
  6. +
  7. It is assigned to yylval.NUM.
  8. +
  9. yylex returns NUM to the caller.
  10. +
+

Then the caller knows the input is NUM (number), and its value is 123.4.

+ +

User code section

+

This section is just copied to C source file.

+ +

Turtle.y

+

Turtle.y has more than 800 lines so it is difficult to explain all the source code. So I will explain the key points and leave out other less important parts.

+

What does bison do?

+

Bison creates C source file from bison source file. Bison source file is a text file. A parser analyzes a program source code according to its grammar. Suppose here is a turtle source file.

+
fc (1,0,0) # Foreground color is red, rgb = (1,0,0).
+pd         # Pen down.
+distance = 100
+angle = 90
+fd distance    # Go forward by distance (100) pixels.
+tr angle     # Turn right by angle (90) degrees.
+

The parser calls yylex to get a token. The token consists of its type (token kind) and value (semantic value). So, the parser gets items in the following table whenever it calls yylex.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
token kindyylval.IDyylval.NUM
1FC
2(
3NUM1.0
4,
5NUM0.0
6,
7NUM0.0
8)
9PD
10IDdistance
11=
12NUM100.0
13IDangle
14=
15NUM90.0
16FD
17IDdistance
18TR
19IDangle
+

Bison source code specifies the grammar rules of turtle language. For example, fc (1,0,0) is called primary procedure. A procedure is like a void type function in C source code. It doesn’t return any values. Programmers can define their own procedures. On the other hand, fc is a built-in procedure. Such procedures are called primary procedures. It is described in bison source code like:

+
primary_procedure: FC '(' expression ',' expression ',' expression ')';
+expression: ID | NUM;
+

This means:

+ +

The description above is called BNF (Backus-Naur form). More precisely, it is similar to BNF.

+

The first line is:

+
FC '(' NUM ',' NUM ',' NUM ')';
+

The parser analyzes the turtle source code and if the input matches the definition above, the parser recognizes it as a primary procedure.

+

The grammar of turtle is described in the document. The following is an extract from the document.

+
program:
+  statement
+| program statement
+;
+
+statement:
+  primary_procedure
+| procedure_definition
+;
+
+primary_procedure:
+  PU
+| PD
+| PW expression
+| FD expression
+| TR expression
+| BC '(' expression ',' expression ',' expression ')'
+| FC '(' expression ',' expression ',' expression ')'
+| ID '=' expression
+| IF '(' expression ')' '{' primary_procedure_list '}'
+| RT
+| RS
+| ID '(' ')'
+| ID '(' argument_list ')'
+;
+
+procedure_definition:
+  DP ID '('  ')' '{' primary_procedure_list '}'
+| DP ID '(' parameter_list ')' '{' primary_procedure_list '}'
+;
+
+parameter_list:
+  ID
+| parameter_list ',' ID
+;
+
+argument_list:
+  expression
+| argument_list ',' expression
+;
+
+primary_procedure_list:
+  primary_procedure
+| primary_procedure_list primary_procedure
+;
+
+expression:
+  expression '=' expression
+| expression '>' expression
+| expression '<' expression
+| expression '+' expression
+| expression '-' expression
+| expression '*' expression
+| expression '/' expression
+| '-' expression %prec UMINUS
+| '(' expression ')'
+| ID
+| NUM
+;
+

The grammar rule defines program first.

+ +

The definition is recursive.

+ +

You can find that a list of statements is program like this.

+

program and statement aren’t tokens. They don’t appear in the input. They are called non terminal symbols. On the other hand, tokens are called terminal symbols. The word “token” used here has wide meaning, it includes tokens and symbols which appear in the input. Non terminal symbols are often shortened to nterm.

+

Let’s analyze the program above as bison does.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
token kindyylval.IDyylval.NUMparseS/R
1FCFCS
2(FC(S
3NUM1.0FC(NUMS
FC(expressionR
4,FC(expression,S
5NUM0.0FC(expression,NUMS
FC(expression,expressionR
6,FC(expression,expression,S
7NUM0.0FC(expression,expression,NUMS
FC(expression,expression,expressionR
8)FC(expression,expression,expression)S
primary_procedureR
statementR
programR
9PDprogram PDS
program primary_procedureR
program statementR
programR
10IDdistanceprogram IDS
11=program ID=S
12NUM100.0program ID=NUMS
program ID=expressionR
program primary_procedureR
program statementR
programR
13IDangleprogram IDS
14=program ID=S
15NUM90.0program ID=NUMS
program ID=expressionR
program primary_procedureR
program statementR
programR
16FDprogram FDS
17IDdistanceprogram FD IDS
program FD expressionR
program primary_procedureR
program statementR
programR
18TRprogram TRS
19IDangleprogram TR IDS
program TR expressionR
program primary_procedureR
program statementR
programR
+

The right most column shows shift/reduce. Shift is appending an input to the buffer. Reduce is substituting a higher nterm for the pattern in the buffer. For example, NUM is replaced by expression in the forth row. This substitution is “reduce”.

+

Bison repeats shift and reduction until the end of the input. If the result is reduced to program, the input is syntactically valid. Bison executes an action whenever reduction occurs. Actions build a tree. The tree is analyzed and executed by runtime routine later.

+

Bison source files are called bison grammar files. A bison grammar file consists of four sections, prologue, declarations, rules and epilogue. The format is as follows.

+
%{
+prologue
+%}
+declarations
+%%
+rules
+%%
+epilogue
+

Prologue

+

Prologue section consists of C codes and the codes are copied to the parser implementation file. You can use %code directives to qualify the prologue and identifies the purpose explicitly. The following is an extract from turtle.y.

+
%code top{
+  #include <stdarg.h>
+  #include <setjmp.h>
+  #include <math.h>
+  #include "turtle.h"
+
+  /* error reporting */
+  static void yyerror (char const *s) { /* for syntax error */
+    g_print ("%s from line %d, column %d to line %d, column %d\n",s, yylloc.first_line, yylloc.first_column, yylloc.last_line, yylloc.last_column);
+  }
+  /* Node type */
+  enum {
+    N_PU,
+    N_PD,
+    N_PW,
+ ... ... ...
+  };
+}
+

The directive %code top copies its contents to the top of the parser implementation file. It usually includes #include directives, declarations of functions and definitions of constants. A function yyerror reports a syntax error and is called by the parser. Node type identifies a node in the tree.

+

Another directive %code requires copies its contents to both the parser implementation file and header file. The header file is read by the scanner C source file and other files.

+
%code requires {
+  int yylex (void);
+  int yyparse (void);
+  void run (void);
+
+  /* semantic value type */
+  typedef struct _node_t node_t;
+  struct _node_t {
+    int type;
+    union {
+      struct {
+        node_t *child1, *child2, *child3;
+      } child;
+      char *name;
+      double value;
+    } content;
+  };
+}
+ +
/* Value type.  */
+#if ! defined YYSTYPE && ! defined YYSTYPE_IS_DECLARED
+union YYSTYPE
+{
+  char * ID;                               /* ID  */
+  double NUM;                              /* NUM  */
+  node_t * program;                        /* program  */
+  node_t * statement;                      /* statement  */
+  node_t * primary_procedure;              /* primary_procedure  */
+  node_t * primary_procedure_list;         /* primary_procedure_list  */
+  node_t * procedure_definition;           /* procedure_definition  */
+  node_t * parameter_list;                 /* parameter_list  */
+  node_t * argument_list;                  /* argument_list  */
+  node_t * expression;                     /* expression  */
+};
+

Other useful macros and declarations are put into the %code directive.

+
%code {
+/* The following macro is convenient to get the member of the node. */
+  #define child1(n) (n)->content.child.child1
+  #define child2(n) (n)->content.child.child2
+  #define child3(n) (n)->content.child.child3
+  #define name(n) (n)->content.name
+  #define value(n) (n)->content.value
+
+  /* start of nodes */
+  static node_t *node_top = NULL;
+  /* functions to generate trees */
+  static node_t *tree1 (int type, node_t *child1, node_t *child2, node_t *child3);
+  static node_t *tree2 (int type, double value);
+  static node_t *tree3 (int type, char *name);
+}
+

Bison declarations

+

Bison declarations defines terminal and non-terminal symbols. It also specifies some directives.

+
%locations
+%define api.value.type union /* YYSTYPE, the type of semantic values, is union of following types */
+ /* key words */
+%token PU
+%token PD
+%token PW
+%token FD
+%token TR
+%token BC
+%token FC
+%token DP
+%token IF
+%token RT
+%token RS
+ /* constant */
+%token <double> NUM
+ /* identirier */
+%token <char *> ID
+ /* non terminal symbol */
+%nterm <node_t *> program
+%nterm <node_t *> statement
+%nterm <node_t *> primary_procedure
+%nterm <node_t *> primary_procedure_list
+%nterm <node_t *> procedure_definition
+%nterm <node_t *> parameter_list
+%nterm <node_t *> argument_list
+%nterm <node_t *> expression
+ /* logical relation symbol */
+%left '=' '<' '>'
+ /* arithmetic symbol */
+%left '+' '-'
+%left '*' '/'
+%precedence UMINUS /* unary minus */
+

%locations directive inserts the location structure into the header file. It is like this.

+
typedef struct YYLTYPE YYLTYPE;
+struct YYLTYPE
+{
+  int first_line;
+  int first_column;
+  int last_line;
+  int last_column;
+};
+

This type is shared by the scanner file and the parser implementation file. The error report function yyerror uses it so that it can inform the location that error occurs.

+

%define api.value.type union generates semantic value type with tokens and nterms and inserts it to the header file. The inserted part is shown in the previous subsection as the extracts that shows the value type (YYSTYPE).

+

%token and %nterm directives define tokens and non terminal symbols respectively.

+
%token PU
+... ...
+%token <double> NUM
+

These directives define a token PU and NUM. The values of token kinds PU and NUM are defined as an enumeration constant in the header file.

+
  enum yytokentype
+  {
+  ... ... ...
+    PU = 258,                      /* PU  */
+  ... ... ...
+    NUM = 269,                     /* NUM  */
+  ... ... ...
+  };
+  typedef enum yytokentype yytoken_kind_t;
+

In addition, the type of the semantic value of NUM is defined as double in the header file because of <double> tag.

+
union YYSTYPE
+{
+  char * ID;                               /* ID  */
+  double NUM;                              /* NUM  */
+  ... ...
+}
+

All the nterm symbols have the same type * node_t of the semantic value.

+

%left and %precedence directives define the precedence of operation symbols.

+
 /* logical relation symbol */
+%left '=' '<' '>'
+ /* arithmetic symbol */
+%left '+' '-'
+%left '*' '/'
+%precedence UMINUS /* unary minus */
+

%left directive defines the following symbols as left-associated operators. If an operator + is left-associated, then

+
A + B + C = (A + B) + C
+

That is, the calculation is carried out the left operator first, then the right operator. If an operator * is right-associated, then:

+
A * B * C = A * (B * C)
+

The definition above decides the behavior of the parser. Addition and multiplication hold associative law so the result of (A+B)+C and A+(B+C) are equal in terms of mathematics. However, the parser will be confused if left (or right) associativity is not specified.

+

%left and %precedence directives show the precedence of operators. Later declared operators have higher precedence than former declared ones. The declaration above says, for example,

+
v=w+z*5+7 is the same as v=((w+(z*5))+7)
+

Be careful. The operator = above is an assignment. Assignment is not expression in turtle language. It is primary_procedure. But if = appears in an expression, it is a logical operater, not an assignment. The logical equal ‘=’ usually used in the conditional expression, for example, in if statement.

+

Grammar rules

+

Grammar rules section defines the syntactic grammar of the language. It is similar to BNF form.

+
result: components { action };
+ +

The following is a part of the grammar rule in turtle.y.

+
program:
+  statement { node_top = $$ = $1; }
+;
+statement:
+  primary_procedure
+;
+primary_procedure:
+  FD expression    { $$ = tree1 (N_FD, $2, NULL, NULL); }
+;
+expression:
+  NUM   { $$ = tree2 (N_NUM, $1); }
+;
+ +
node --+-- type
+       +-- union contents
+                    +---struct {node_t *child1, *child2, *child3;};
+                    +---char *name
+                    +---double value
+ +

Suppose the parser reads the following program.

+
fd 100
+

What does the parser do?

+
    +
  1. The parser recognizes the input is FD. Maybe it is the start of primary_procedure, but parser needs to read the next token.
  2. +
  3. yylex returns the token kind NUM and sets yylval.NUM to 100.0 (the type is double). The parser reduces NUM to expression. At the same time, it sets the semantic value of the expression to point a new node. The node has an type N_NUM and a semantic value 100.0.
  4. +
  5. After the reduction, the buffer has FD and expression. The parser reduces it to primary_procedure. And it sets the semantic value of the primary_procedure to point a new node. The node has an type N_FD and its member child1 points the node of expression, whose type is N_NUM.
  6. +
  7. The parser reduces primary_procedure to statement. The semantic value of statement is the same as the one of primary_procedure, which points to the node N_FD.
  8. +
  9. The parser reduces statement to program. The semantic value of statement is assigned to the one of program and the static variable node_top.
  10. +
  11. Finally node_top points the node N_FD and the node N_FD points the node N_NUM.
  12. +
+
+
tree
+
+

The following is the grammar rule extracted from turtle.y. The rules there are based on the same idea above. I don’t want to explain the whole rules below. Please look into each line carefully so that you will understand all the rules and actions.

+
program:
+  statement { node_top = $$ = $1; }
+| program statement {
+        node_top = $$ = tree1 (N_program, $1, $2, NULL);
+        }
+;
+
+statement:
+  primary_procedure
+| procedure_definition
+;
+
+primary_procedure:
+  PU    { $$ = tree1 (N_PU, NULL, NULL, NULL); }
+| PD    { $$ = tree1 (N_PD, NULL, NULL, NULL); }
+| PW expression    { $$ = tree1 (N_PW, $2, NULL, NULL); }
+| FD expression    { $$ = tree1 (N_FD, $2, NULL, NULL); }
+| TR expression    { $$ = tree1 (N_TR, $2, NULL, NULL); }
+| BC '(' expression ',' expression ',' expression ')' { $$ = tree1 (N_BC, $3, $5, $7); }
+| FC '(' expression ',' expression ',' expression ')' { $$ = tree1 (N_FC, $3, $5, $7); }
+ /* assignment */
+| ID '=' expression   { $$ = tree1 (N_ASSIGN, tree3 (N_ID, $1), $3, NULL); }
+ /* control flow */
+| IF '(' expression ')' '{' primary_procedure_list '}' { $$ = tree1 (N_IF, $3, $6, NULL); }
+| RT    { $$ = tree1 (N_RT, NULL, NULL, NULL); }
+| RS    { $$ = tree1 (N_RS, NULL, NULL, NULL); }
+ /* user defined procedure call */
+| ID '(' ')'  { $$ = tree1 (N_procedure_call, tree3 (N_ID, $1), NULL, NULL); }
+| ID '(' argument_list ')'  { $$ = tree1 (N_procedure_call, tree3 (N_ID, $1), $3, NULL); }
+;
+
+procedure_definition:
+  DP ID '('  ')' '{' primary_procedure_list '}'  {
+         $$ = tree1 (N_procedure_definition, tree3 (N_ID, $2), NULL, $6);
+        }
+| DP ID '(' parameter_list ')' '{' primary_procedure_list '}'  {
+         $$ = tree1 (N_procedure_definition, tree3 (N_ID, $2), $4, $7);
+        }
+;
+
+parameter_list:
+  ID { $$ = tree3 (N_ID, $1); }
+| parameter_list ',' ID  { $$ = tree1 (N_parameter_list, $1, tree3 (N_ID, $3), NULL); }
+;
+
+argument_list:
+  expression
+| argument_list ',' expression { $$ = tree1 (N_argument_list, $1, $3, NULL); }
+;
+
+primary_procedure_list:
+  primary_procedure
+| primary_procedure_list primary_procedure {
+         $$ = tree1 (N_primary_procedure_list, $1, $2, NULL);
+        }
+;
+
+expression:
+  expression '=' expression { $$ = tree1 (N_EQ, $1, $3, NULL); }
+| expression '>' expression { $$ = tree1 (N_GT, $1, $3, NULL); }
+| expression '<' expression { $$ = tree1 (N_LT, $1, $3, NULL); }
+| expression '+' expression { $$ = tree1 (N_ADD, $1, $3, NULL); }
+| expression '-' expression { $$ = tree1 (N_SUB, $1, $3, NULL); }
+| expression '*' expression { $$ = tree1 (N_MUL, $1, $3, NULL); }
+| expression '/' expression { $$ = tree1 (N_DIV, $1, $3, NULL); }
+| '-' expression %prec UMINUS { $$ = tree1 (N_UMINUS, $2, NULL, NULL); }
+| '(' expression ')' { $$ = $2; }
+| ID    { $$ = tree3 (N_ID, $1); }
+| NUM   { $$ = tree2 (N_NUM, $1); }
+;
+

Epilogue

+

The epilogue is written in C language and copied to the parser implementation file. Generally, you can put anything into the epilogue. In the case of turtle interpreter, the runtime routine and some other functions are in the epilogue.

+

Functions to create tree nodes

+

There are three functions, tree1, tree2 and tree3.

+ +

Each function gets memories first and build a node on them. The memories are inserted to the list. They will be freed when runtime routine finishes.

+

The three functions are called in the actions in the rules section.

+
/* Dynamically allocated memories are added to the single list. They will be freed in the finalize function. */
+GSList *list = NULL;
+
+node_t *
+tree1 (int type, node_t *child1, node_t *child2, node_t *child3) {
+  node_t *new_node;
+
+  list = g_slist_prepend (list, g_malloc (sizeof (node_t)));
+  new_node = (node_t *) list->data;
+  new_node->type = type;
+  child1(new_node) = child1;
+  child2(new_node) = child2;
+  child3(new_node) = child3;
+  return new_node;
+}
+
+node_t *
+tree2 (int type, double value) {
+  node_t *new_node;
+
+  list = g_slist_prepend (list, g_malloc (sizeof (node_t)));
+  new_node = (node_t *) list->data;
+  new_node->type = type;
+  value(new_node) = value;
+  return new_node;
+}
+
+node_t *
+tree3 (int type, char *name) {
+  node_t *new_node;
+
+  list = g_slist_prepend (list, g_malloc (sizeof (node_t)));
+  new_node = (node_t *) list->data;
+  new_node->type = type;
+  name(new_node) = name;
+  return new_node;
+}
+

Symbol table

+

Variables and user defined procedures are registered in a symbol table. This table is a C array. It should be replaced by more appropriate data structure with memory allocation in the future version

+ +

Therefore the table has the following fields.

+ +
#define MAX_TABLE_SIZE 100
+enum {
+  PROC,
+  VAR
+};
+
+typedef union _object_t object_t;
+union _object_t {
+  node_t *node;
+  double value;
+};
+ 
+struct {
+  int type;
+  char *name;
+  object_t object;
+} table[MAX_TABLE_SIZE];
+int tp;
+
+void
+init_table (void) {
+  tp = 0;
+}
+

init_table initializes the table. This must be called before any registrations.

+

There are five functions to access the table,

+ +
int
+tbl_lookup (int type, char *name) {
+  int i;
+
+  if (tp == 0)
+    return -1;
+  for (i=0; i<tp; ++i)
+    if (type == table[i].type && strcmp(name, table[i].name) == 0)
+      return i;
+  return -1;
+}
+
+void
+tbl_install (int type, char *name, object_t object) {
+  if (tp >= MAX_TABLE_SIZE)
+    runtime_error ("Symbol table overflow.\n");
+  else if (tbl_lookup (type, name) >= 0)
+    runtime_error ("Name %s is already registered.\n", name);
+  else {
+    table[tp].type = type;
+    table[tp].name = name;
+    if (type == PROC)
+      table[tp++].object.node = object.node;
+    else
+      table[tp++].object.value = object.value;
+  }
+}
+
+void
+proc_install (char *name, node_t *node) {
+  object_t object;
+  object.node = node;
+  tbl_install (PROC, name, object);
+}
+
+void
+var_install (char *name, double value) {
+  object_t object;
+  object.value = value;
+  tbl_install (VAR, name, object);
+}
+
+void
+var_replace (char *name, double value) {
+  int i;
+  if ((i = tbl_lookup (VAR, name)) >= 0)
+    table[i].object.value = value;
+  else
+    var_install (name, value);
+}
+
+node_t *
+proc_lookup (char *name) {
+  int i;
+  if ((i = tbl_lookup (PROC, name)) < 0)
+    return NULL;
+  else
+    return table[i].object.node;
+}
+
+gboolean
+var_lookup (char *name, double *value) {
+  int i;
+  if ((i = tbl_lookup (VAR, name)) < 0)
+    return FALSE;
+  else {
+    *value = table[i].object.value;
+    return TRUE;
+  }
+}
+

Stack for parameters and arguments

+

Stack is a last-in first-out data structure. It is shortened to LIFO. Turtle uses a stack to keep parameters and arguments. They are like auto class variables in C language. They are pushed to the stack whenever the procedure is called. LIFO structure is useful for recursive calls.

+

Each element of the stack has name and value.

+
#define MAX_STACK_SIZE 500
+struct {
+  char *name;
+  double value;
+} stack[MAX_STACK_SIZE];
+int sp, sp_biggest;
+
+void
+init_stack (void) {
+  sp = sp_biggest = 0;
+}
+

sp is a stack pointer. It is an index of the array stack and it always points an element of the array to store the next data. sp_biggest is the biggest number assigned to sp. We can know the amount of elements used in the array during the runtime. The purpose of the variable is to find appropriate MAX_STACK_SIZE. It will be unnecessary in the future version if the stack is implemented with better data structure and memory allocation.

+

The runtime routine push data to the stack when it executes a node of a procedure call. (The type of the node is N_procedure_call.)

+
dp drawline (angle, distance) { ... ... ... }
+drawline (90, 100)
+ +

The following diagram shows the structure of the stack. First, procedure 1 is called. The procedure has two parameters. In the procedure 1, another procedure procedure 2, which has one parameter, is called. And in the procedure 2, procedure 3, which has three parameters, is called.

+

Programs push data to a stack from a low address memory to a high address memory. In the following diagram, the lowest address is at the top and the highest address is at the bottom. That is the order of the address. However, “the top of the stack” is the last pushed data and “the bottom of the stack” is the first pushed data. Therefore, “the top of the stack” is the bottom of the rectangle in the diagram and “the bottom of the stack” is the top of the rectangle.

+
+
Stack
+
+

There are four functions to access the stack.

+ +
void
+stack_push (char *name, double value) {
+  if (sp >= MAX_STACK_SIZE)
+    runtime_error ("Stack overflow.\n");
+  else {
+    stack[sp].name = name;
+    stack[sp++].value = value;
+    sp_biggest = sp > sp_biggest ? sp : sp_biggest;
+  }
+}
+
+int
+stack_search (char *name) {
+  int depth, i;
+
+  if (sp == 0)
+    return -1;
+  depth = (int) stack[sp-1].value;
+  if (depth + 1 > sp) /* something strange */
+    runtime_error ("Stack error.\n");
+  for (i=0; i<depth; ++i)
+    if (strcmp(name, stack[sp-(i+2)].name) == 0) {
+      return sp-(i+2);
+    }
+  return -1;
+}
+
+gboolean
+stack_lookup (char *name, double *value) {
+  int i;
+
+  if ((i = stack_search (name)) < 0)
+    return FALSE;
+  else {
+    *value = stack[i].value;
+    return TRUE;
+  }
+}
+
+gboolean
+stack_replace (char *name, double value) {
+  int i;
+
+  if ((i = stack_search (name)) < 0)
+    return FALSE;
+  else {
+    stack[i].value = value;
+    return TRUE;
+  }
+}
+
+void
+stack_return(void) {
+  int depth;
+
+  if (sp <= 0)
+    return;
+  depth = (int) stack[sp-1].value;
+  if (depth + 1 > sp) /* something strange */
+    runtime_error ("Stack error.\n");
+  sp -= depth + 1;
+}
+

Surface and cairo

+

A global variable surface is shared by turtleapplication.c and turtle.y. It is initialized in turtleapplication.c.

+

The runtime routine has its own cairo context. This is different from the cairo of GtkDrawingArea. Runtime routine draws a shape on the surface with the cairo context. After runtime routine returns to run_cb, run_cb adds the GtkDrawingArea widget to the queue to redraw. When the widget is redraw,the drawing function draw_func is called. It copies the surface to the surface in the GtkDrawingArea object.

+

turtle.y has two functions init_cairo and destroy_cairo.

+ +

Turtle has its own coordinate. The origin is at the center of the surface, and positive direction of x and y axes are right and up respectively. But surfaces have its own coordinate. Its origin is at the top-left corner of the surface and positive direction of x and y are right and down respectively. A plane with the turtle’s coordinate is called user space, which is the same as cairo’s user space. A plane with the surface’s coordinate is called device space.

+

Cairo provides a transformation which is an affine transformation. It transforms a user-space coordinate (x, y) into a device-space coordinate (z, w).

+
+
transformation
+
+

init_cairo gets the width and height of the surface (See the program below).

+ +

You can determine a, b, c, d, p and q by substituting the numbers above for x, y, z and w in the equation above. The solution of the simultaneous equations is:

+
a = 1, b = 0, c = 0, d = -1, p = width/2, q = height/2
+

Cairo provides a structure cairo_matrix_t. init_cairo uses it and sets the cairo transformation (See the program below). Once the matrix is set, the transformation always performs whenever cairo_stroke function is invoked.

+
/* status of the surface */
+static gboolean pen = TRUE;
+static double angle = 90.0; /* angle starts from x axis and measured counterclockwise */
+                   /* Initially facing to the north */
+static double cur_x = 0.0;
+static double cur_y = 0.0;
+static double line_width = 2.0;
+
+struct color {
+  double red;
+  double green;
+  double blue;
+};
+static struct color bc = {0.95, 0.95, 0.95}; /* white */ 
+static struct color fc = {0.0, 0.0, 0.0}; /* black */ 
+
+/* cairo */
+static cairo_t *cr;
+gboolean
+init_cairo (void) {
+  int width, height;
+  cairo_matrix_t matrix;
+
+  pen = TRUE;
+  angle = 90.0;
+  cur_x = 0.0;
+  cur_y = 0.0;
+  line_width = 2.0;
+  bc.red = 0.95; bc.green = 0.95; bc.blue = 0.95;
+  fc.red = 0.0; fc.green = 0.0; fc.blue = 0.0;
+
+  if (surface) {
+    width = cairo_image_surface_get_width (surface);
+    height = cairo_image_surface_get_height (surface);
+    matrix.xx = 1.0; matrix.xy = 0.0; matrix.x0 = (double) width / 2.0;
+    matrix.yx = 0.0; matrix.yy = -1.0; matrix.y0 = (double) height / 2.0;
+
+    cr = cairo_create (surface);
+    cairo_transform (cr, &matrix);
+    cairo_set_source_rgb (cr, bc.red, bc.green, bc.blue);
+    cairo_paint (cr);
+    cairo_set_source_rgb (cr, fc.red, fc.green, fc.blue);
+    cairo_move_to (cr, cur_x, cur_y);
+    return TRUE;
+  } else
+    return FALSE;
+}
+
+void
+destroy_cairo () {
+  cairo_destroy (cr);
+}
+

Eval function

+

A function eval evaluates an expression and returns the value of the expression. It calls itself recursively. For example, if the node is N_ADD, then:

+
    +
  1. Calls eval(child1(node)) and gets the value1.
  2. +
  3. Calls eval(child2(node)) and gets the value2.
  4. +
  5. Returns value1+value2.
  6. +
+

This is performed by a macro calc defined in the sixth line in the following program.

+
double
+eval (node_t *node) {
+double value = 0.0;
+  if (node == NULL)
+    runtime_error ("No expression to evaluate.\n");
+#define calc(op) eval (child1(node)) op eval (child2(node))
+  switch (node->type) {
+    case N_EQ:
+      value = (double) calc(==);
+      break;
+    case N_GT:
+      value = (double) calc(>);
+      break;
+    case N_LT:
+      value = (double) calc(<);
+      break;
+    case N_ADD:
+      value = calc(+);
+      break;
+    case N_SUB:
+      value = calc(-);
+      break;
+    case N_MUL:
+      value = calc(*);
+      break;
+    case N_DIV:
+      if (eval (child2(node)) == 0.0)
+        runtime_error ("Division by zerp.\n");
+      else
+        value = calc(/);
+      break;
+    case N_UMINUS:
+      value = -(eval (child1(node)));
+      break;
+    case N_ID:
+      if (! (stack_lookup (name(node), &value)) && ! var_lookup (name(node), &value) )
+        runtime_error ("Variable %s not defined.\n", name(node));
+      break;
+    case N_NUM:
+      value = value(node);
+      break;
+    default: 
+      runtime_error ("Illegal expression.\n");
+  }
+  return value;
+}
+

Execute function

+

Primary procedures and procedure definitions are analyzed and executed by the function execute. It doesn’t return any values. It calls itself recursively. The process of N_RT and N_procedure_call is complicated. It will explained after the following program. Other parts are not so difficult. Read the program below carefully so that you will understand the process.

+
/* procedure - return status */
+static int proc_level = 0;
+static int ret_level = 0;
+
+void
+execute (node_t *node) {
+  double d, x, y;
+  char *name;
+  int n, i;
+
+  if (node == NULL)
+    runtime_error ("Node is NULL.\n");
+  if (proc_level > ret_level)
+    return;
+  switch (node->type) {
+    case N_program:
+      execute (child1(node));
+      execute (child2(node));
+      break;
+    case N_PU:
+      pen = FALSE;
+      break;
+    case N_PD:
+      pen = TRUE;
+      break;
+    case N_PW:
+      line_width = eval (child1(node)); /* line width */
+      break;
+    case N_FD:
+      d = eval (child1(node)); /* distance */
+      x = d * cos (angle*M_PI/180);
+      y = d * sin (angle*M_PI/180);
+      /* initialize the current point = start point of the line */
+      cairo_move_to (cr, cur_x, cur_y);
+      cur_x += x;
+      cur_y += y;
+      cairo_set_line_width (cr, line_width);
+      cairo_set_source_rgb (cr, fc.red, fc.green, fc.blue);
+      if (pen)
+        cairo_line_to (cr, cur_x, cur_y);
+      else
+        cairo_move_to (cr, cur_x, cur_y);
+      cairo_stroke (cr);
+      break;
+    case N_TR:
+      angle -= eval (child1(node));
+      for (; angle < 0; angle += 360.0);
+      for (; angle>360; angle -= 360.0);
+      break;
+    case N_BC:
+      bc.red = eval (child1(node));
+      bc.green = eval (child2(node));
+      bc.blue = eval (child3(node));
+#define fixcolor(c)  c = c < 0 ? 0 : (c > 1 ? 1 : c)
+      fixcolor (bc.red);
+      fixcolor (bc.green);
+      fixcolor (bc.blue);
+      /* clear the shapes and set the background color */
+      cairo_set_source_rgb (cr, bc.red, bc.green, bc.blue);
+      cairo_paint (cr);
+      break;
+    case N_FC:
+      fc.red = eval (child1(node));
+      fc.green = eval (child2(node));
+      fc.blue = eval (child3(node));
+      fixcolor (fc.red);
+      fixcolor (fc.green);
+      fixcolor (fc.blue);
+      break;
+    case N_ASSIGN:
+      name = name(child1(node));
+      d = eval (child2(node));
+      if (! stack_replace (name, d)) /* First, tries to replace the value in the stack (parameter).*/
+        var_replace (name, d); /* If the above fails, tries to replace the value in the table. If the variable isn't in the table, installs it, */
+      break;
+    case N_IF:
+      if (eval (child1(node)))
+        execute (child2(node));
+      break;
+    case N_RT:
+      ret_level--;
+      break;
+    case N_RS:
+      pen = TRUE;
+      angle = 90.0;
+      cur_x = 0.0;
+      cur_y = 0.0;
+      line_width = 2.0;
+      fc.red = 0.0; fc.green = 0.0; fc.blue = 0.0;
+      /* To change background color, use bc. */
+      break;
+    case N_procedure_call:
+      name = name(child1(node));
+node_t *proc = proc_lookup (name);
+      if (! proc)
+        runtime_error ("Procedure %s not defined.\n", name);
+      if (strcmp (name, name(child1(proc))) != 0)
+        runtime_error ("Unexpected error. Procedure %s is called, but invoked procedure is %s.\n", name, name(child1(proc)));
+/* make tuples (parameter (name), argument (value)) and push them to the stack */
+node_t *param_list;
+node_t *arg_list;
+      param_list = child2(proc);
+      arg_list = child2(node);
+      if (param_list == NULL) {
+        if (arg_list == NULL) {
+          stack_push (NULL, 0.0); /* number of argument == 0 */
+        } else
+          runtime_error ("Procedure %s has different number of argument and parameter.\n", name);
+      }else {
+/* Don't change the stack until finish evaluating the arguments. */
+#define TEMP_STACK_SIZE 20
+        char *temp_param[TEMP_STACK_SIZE];
+        double temp_arg[TEMP_STACK_SIZE];
+        n = 0;
+        for (; param_list->type == N_parameter_list; param_list = child1(param_list)) {
+          if (arg_list->type != N_argument_list)
+            runtime_error ("Procedure %s has different number of argument and parameter.\n", name);
+          if (n >= TEMP_STACK_SIZE)
+            runtime_error ("Too many parameters. the number must be %d or less.\n", TEMP_STACK_SIZE);
+          temp_param[n] = name(child2(param_list));
+          temp_arg[n] = eval (child2(arg_list));
+          arg_list = child1(arg_list);
+          ++n;
+        }
+        if (param_list->type == N_ID && arg_list -> type != N_argument_list) {
+          temp_param[n] = name(param_list);
+          temp_arg[n] = eval (arg_list);
+          if (++n >= TEMP_STACK_SIZE)
+            runtime_error ("Too many parameters. the number must be %d or less.\n", TEMP_STACK_SIZE);
+          temp_param[n] = NULL;
+          temp_arg[n] = (double) n;
+          ++n;
+        } else
+          runtime_error ("Unexpected error.\n");
+        for (i = 0; i < n; ++i)
+          stack_push (temp_param[i], temp_arg[i]);
+      }
+      ret_level = ++proc_level;
+      execute (child3(proc));
+      ret_level = --proc_level;
+      stack_return ();
+      break;
+    case N_procedure_definition:
+      name = name(child1(node));
+      proc_install (name, node);
+      break;
+    case N_primary_procedure_list:
+      execute (child1(node));
+      execute (child2(node));
+      break;
+    default:
+      runtime_error ("Unknown statement.\n");
+  }
+}
+

A node N_procedure_call is created by the parser when it has found a user defined procedure call. The procedure has been defined in the prior statement. Suppose the parser reads the following example code.

+
dp drawline (angle, distance) {
+  tr angle
+  fd distance
+}
+drawline (90, 100)
+drawline (90, 100)
+drawline (90, 100)
+drawline (90, 100)
+

This example draws a square.

+

When The parser reads the lines from one to four, it creates nodes like this:

+
+
Nodes of drawline
+
+

Runtime routine just stores the procedure to the symbol table with its name and node.

+
+
Symbol table
+
+

When the parser reads the fifth line in the example, it creates nodes like this:

+
+
Nodes of procedure call
+
+

When the runtime routine meets N_procedure_call node, it behaves like this:

+
    +
  1. Searches the symbol table for the procedure with the name.
  2. +
  3. Gets pointers to the node to parameters and the node to the body.
  4. +
  5. Creates a temporary stack. Makes a tuple of each parameter name and argument value. Pushes the tuples into the stack, and (NULL, number of parameters) finally. If no error occurs, copies them from the temporary stack to the parameter stack.
  6. +
  7. Increases prc_level by one. Sets ret_level to the same value as proc_level. proc_level is zero when runtime routine runs on the main routine. If it goes into a procedure, proc_level increases by one. Therefore, proc_level is the depth of the procedure call. ret_level is the level to return. If it is the same as proc_level, runtime routine executes commands in order of the commands in the procedure. If it is smaller than proc_level, runtime routine doesn’t execute commands until it becomes the same level as proc_level. ret_level is used to return the procedure.
  8. +
  9. Executes the node of the body of the procedure.
  10. +
  11. Decreases proc_level by one. Sets ret_level to the same value as proc_level. Calls stack_return.
  12. +
+

When the runtime routine meets N_RT node, it decreases ret_level by one so that the following commands in the procedure are ignored by the runtime routine.

+

Runtime entry and error functions

+

A function run is the entry of the runtime routine. A function runtime_error reports an error occurred during the runtime routine runs. (Errors which occur during the parsing are called syntax error and reported by yyerror.) After runtime_error reports an error, it stops the command execution and goes back to run to exit.

+

Setjmp and longjmp functions are used. They are declared in <setjmp.h>. setjmp (buf) saves state information in buf and returns zero. longjmp(buf, 1) restores the state information from buf and returns 1 (the second argument). Because the information is the status at the time setjmp is called, so longjmp resumes the execution at the next of setjmp function call. In the following program, longjmp resumes at the assignment to the variable i. When setjmp is called, 0 is assigned to i and execute(node_top) is called. On the other hand, when longjmp is called, 1 is assigned to i and execute(node_top) is not called..

+

g_slist_free_full frees all the allocated memories.

+
static jmp_buf buf;
+
+void
+run (void) {
+  int i;
+
+  if (! init_cairo()) {
+    g_print ("Cairo not initialized.\n");
+    return;
+  }
+  init_table();
+  init_stack();
+  ret_level = proc_level = 1;
+  i = setjmp (buf);
+  if (i == 0)
+    execute(node_top);
+  /* else ... get here by calling longjmp */
+  destroy_cairo ();
+  g_slist_free_full (g_steal_pointer (&list), g_free);
+}
+
+/* format supports only %s, %f and %d */
+static void
+runtime_error (char *format, ...) {
+  va_list args;
+  char *f;
+  char b[3];
+  char *s;
+  double v;
+  int i;
+
+  va_start (args, format);
+  for (f = format; *f; f++) {
+    if (*f != '%') {
+      b[0] = *f;
+      b[1] = '\0';
+      g_print ("%s", b);
+      continue;
+    }
+    switch (*++f) {
+      case 's':
+        s = va_arg(args, char *);
+        g_print ("%s", s);
+        break;
+      case 'f':
+        v = va_arg(args, double);
+        g_print("%f", v);
+        break;
+      case 'd':
+        i = va_arg(args, int);
+        g_print("%d", i);
+        break;
+      default:
+        b[0] = '%';
+        b[1] = *f;
+        b[2] = '\0';
+        g_print ("%s", b);
+        break;
+    }
+  }
+  va_end (args);
+
+  longjmp (buf, 1);
+}
+

A function runtime_error has a variable-length argument list.

+
void runtime_error (char *format, ...)
+

This is implemented with <stdarg.h> header file. The va_list type variable args will refer to each argument in turn. A function va_start initializes args. A function va_arg returns an argument and moves the reference of args to the next. A function va_end cleans up everything necessary at the end.

+

The function runtime_error has a similar format of printf standard function. But its format has only %s, %f and %d.

+

The functions declared in <setjmp.h> and <stdarg.h> are explained in the very famous book “The C programming language” written by Brian Kernighan and Dennis Ritchie. I referred to the book to write the program above.

+

The program turtle is unsophisticated and unpolished. If you want to make your own language, you need to know more and more. I don’t know any good textbook about compilers and interpreters. If you know a good book, please let me know.

+

However, the following information is very useful (but old).

+ +

Lately, lots of source codes are in the internet. Maybe reading source codes are the most useful for programmers.

+

Up: index.html, Prev: Section 24, Next: Section 26

+ + diff --git a/docs/sec26.html b/docs/sec26.html new file mode 100644 index 0000000..8c96c21 --- /dev/null +++ b/docs/sec26.html @@ -0,0 +1,481 @@ + + + + + + + Gtk4 tutorial for beginners + + + +

Up: index.html, Prev: Section 25, Next: Section 27

+

GtkListView

+

Gtk4 has added new list objects GtkListView, GtkGridView and GtkColumnView. The new feature is described in Gtk API Reference, List Widget Overview.

+

Gtk4 has other means to implement lists. They are GtkListBox and GtkTreeView which are took over from Gtk3. There’s an article in Gtk Development blog about list widgets by Matthias Clasen. He described why GtkListView are developed to replace GtkListBox and GtkTreeView.

+

I want to explain GtkListView and its related objects in this tutorial.

+

Outline

+

A list is a sequential data structure. For example, an ordered string sequence “one”, “two”, “three”, “four” is a list. Each element of the list is called item. A list is like an array, but in many cases it is implemented with pointers which point to the next item of the list. And it has a start point. So, each item can be referred by the index of the item (first item, second item, …, nth item, …). There are two cases. One is the index starts from one (one-based) and the other is it starts from zero (zero-based).

+

Gio provides GListModel interface. It is a zero-based list of the same type of GObject objects, or objects that implement the same interface. An object implements GListModel is usually not a widget. So, the list is not displayed on the screen directly. There’s another object GtkListView which is a widget to display the list. The items in the list need to be connected to the items in GtkListView. GtkListItemFactory object maps items in the list to GListView.

+
+
List
+
+

The instruction to build the whole list related objects is:

+
    +
  1. Implement the list object which implements GListModel.
  2. +
  3. Build widgets and put GtkListView as a child of GtkScrolledWindow.
  4. +
  5. Set GtkListItemFactory.
  6. +
+

GListModel

+

If you want to make a list of strings with GListModel, for example, “one”, “two”, “three”, “four”, note that strings can’t be items of the list. Because GListModel is a list of GObject objects and strings aren’t GObject objects. So, you need a wrapper which is a GObject and contains a string. GtkStringObject is the wrapper object and GStringList, implements GListModel, is a list of GtkStringObject.

+
char *array[] = {"one", "two", "three", "four", NULL};
+GtkStringList *stringlist = gtk_string_list_new ((const char * const *) array);
+

The function gtk_string_list_new creates GtkStringList object. Its items are GtkStringObject objects which contain the strings “one”, “two”, “three” and “four”. There are functions to add items to the list or remove items from the list.

+ +

See Gtk4 API Reference, GtkStringList for further information.

+

I’ll explain the other list objects later.

+

GtkSelectionModel

+

GtkSelectionModel is an interface to support for selections. Thanks to this model, user can select items by clicking on them. It is implemented by GtkMultiSelection, GtkNoSelection and GtkSingleSelection objects. These three objects are usually enough to build an application. They are created with GListModel. You can also create them alone and add GListModel later.

+ +

GtkListView

+

GtkListView is a widget to show GListModel items. GtkListItem is used by GtkListView to represent items of a list model. But, GtkListItem itself is not a widget, so a user needs to set a widget, for example GtkLabel, as a child of GtkListItem to display an item of the list model. “item” property of GtkListItem points an object that belongs to the list model.

+
+
GtkListItem
+
+

In case the number of items is very big, for example more than a thousand, GtkListItem is recycled and connected to another item which is newly displayed. This recycle makes the number of GtkListItem objects fairly small, less than 200. This is very effective to restrain the growth of memory consumption so that GListModel can contain lots of items, for example, more than a million items.

+

GtkListItemFactory

+

GtkListItemFactory creates or recycles GtkListItem and connects it with an item of the list model. There are two child objects of this factory, GtkSignalListItemFactory and GtkBuilderListItemFactory.

+

GtkSignalListItemFactory

+

GtkSignalListItemFactory provides signals for users to configure a GtkListItem object. There are four signals.

+
    +
  1. “setup” is emitted to set up GtkListItem object. A user sets its child widget in the handler. For example, creates a GtkLabel widget and sets the child property of GtkListItem to it. This setting is kept even the GtkListItem instance is recycled (to bind to another item of GListModel).
  2. +
  3. “bind” is emitted to bind an item in the list model to the widget. For example, a user gets the item from “item” property of the GtkListItem instance. Then gets the string of the item and sets the label property of the GtkLabel instance with the string. This signal is emitted when the GtkListItem is newly created, recycled or some changes has happened to the item of the list.
  4. +
  5. “unbind” is emitted to unbind an item. A user undoes everything done in step 2 in the signal handler. If some object are created in step 2, they must be destroyed.
  6. +
  7. “teardown” is emitted to undo everything done in step 1. So, the widget created in step 1 must be destroyed. After this signal, the list item will be destroyed.
  8. +
+

The following program list1.c shows the list of strings “one”, “two”, “three” and “four”. GtkNoSelection is used, so user can’t select any item.

+
#include <gtk/gtk.h>
+
+static void
+setup_cb (GtkListItemFactory *factory, GtkListItem *listitem, gpointer user_data) {
+  GtkWidget *lb = gtk_label_new (NULL);
+  gtk_list_item_set_child (listitem, lb);
+}
+
+static void
+bind_cb (GtkSignalListItemFactory *self, GtkListItem *listitem, gpointer user_data) {
+  GtkWidget *lb = gtk_list_item_get_child (listitem);
+  GtkStringObject *strobj = gtk_list_item_get_item (listitem);
+  const char *text = gtk_string_object_get_string (strobj);
+
+  gtk_label_set_text (GTK_LABEL (lb), text);
+}
+
+static void
+unbind_cb (GtkSignalListItemFactory *self, GtkListItem *listitem, gpointer user_data) {
+  /* There's nothing to do here. */
+  /* If you does something like setting a signal in bind_cb, */
+  /* then disconnecting the signal is necessary in unbind_cb. */
+}
+
+static void
+teardown_cb (GtkListItemFactory *factory, GtkListItem *listitem, gpointer user_data) {
+  gtk_list_item_set_child (listitem, NULL);
+/*  When the child of listitem is set to NULL, the reference to GtkLabel will be released and lb will be destroyed. */
+/* Therefore, g_object_unref () for the GtkLabel object doesn't need in the user code. */
+}
+
+/* ----- activate, open, startup handlers ----- */
+static void
+app_activate (GApplication *application) {
+  GtkApplication *app = GTK_APPLICATION (application);
+  GtkWidget *win = gtk_application_window_new (app);
+  gtk_window_set_default_size (GTK_WINDOW (win), 600, 400);
+  GtkWidget *scr = gtk_scrolled_window_new ();
+  gtk_window_set_child (GTK_WINDOW (win), scr);
+
+  char *array[] = {
+    "one", "two", "three", "four", NULL
+  };
+  GtkStringList *sl = gtk_string_list_new ((const char * const *) array);
+  GtkNoSelection *ns = gtk_no_selection_new (G_LIST_MODEL (sl));
+
+  GtkListItemFactory *factory = gtk_signal_list_item_factory_new ();
+  g_signal_connect (factory, "setup", G_CALLBACK (setup_cb), NULL);
+  g_signal_connect (factory, "bind", G_CALLBACK (bind_cb), NULL);
+  g_signal_connect (factory, "unbind", G_CALLBACK (unbind_cb), NULL);
+  g_signal_connect (factory, "teardown", G_CALLBACK (teardown_cb), NULL);
+
+  GtkWidget *lv = gtk_list_view_new (GTK_SELECTION_MODEL (ns), factory);
+  gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (scr), lv);
+  gtk_widget_show (win);
+}
+
+static void
+app_startup (GApplication *application) {
+}
+
+/* ----- main ----- */
+#define APPLICATION_ID "com.github.ToshioCP.list1"
+
+int
+main (int argc, char **argv) {
+  GtkApplication *app;
+  int stat;
+
+  app = gtk_application_new (APPLICATION_ID, G_APPLICATION_FLAGS_NONE);
+
+  g_signal_connect (app, "startup", G_CALLBACK (app_startup), NULL);
+  g_signal_connect (app, "activate", G_CALLBACK (app_activate), NULL);
+
+  stat =g_application_run (G_APPLICATION (app), argc, argv);
+  g_object_unref (app);
+  return stat;
+}
+

The file list1.c is located under the directory src/misc. Make a shell script below and save it to your bin directory. (If you’ve installed Gtk4 from the source to $HOME/local, then your bin directory is $Home/local/bin. Otherwise, $Home/bin is your private bin directory.)

+
gcc `pkg-config --cflags gtk4` $1.c `pkg-config --libs gtk4`
+

Change the current directory to the directory includes list1.c and type as follows.

+
$ chmod 755 $HOME/local/bin/comp # or chmod 755 $Home/bin/comp
+$ comp list1
+$ ./a.out
+

Then, list1.c has been compiled and executed.

+
+
list1
+
+

I think the program is not so difficult. If you feel some difficulty, read this section again, especially GtkSignalListItemFactory subsubsection.

+

GtkBuilderListItemFactory

+

GtkBuilderListItemFactory is another GtkListItemFactory. Its behavior is defined with ui file.

+
<interface>
+  <template class="GtkListItem">
+    <property name="child">
+      <object class="GtkLabel">
+        <binding name="label">
+          <lookup name="string" type="GtkStringObject">
+            <lookup name="item">GtkListItem</lookup>
+          </lookup>
+        </binding>
+      </object>
+    </property>
+  </template>
+</interface>
+

Template tag is used to define GtkListItem. And its child property is GtkLabel object. The factory sees this template and creates GtkLabel and sets the child property of GtkListItem. This is the same as what setup handler of GtkSignalListItemFactory did.

+

Then, bind the label property of GtkLabel to string property of GtkStringObject. The string object is referred to by item property of GtkListItem. So, the lookup tag is like this:

+
string <- GtkStringObject <- item <- GtkListItem
+

The last lookup tag has a content GtkListItem. Usually, C type like GtkListItem doesn’t appear in the content of tags. This is a special case. There is an explanation about it in the GTK Development Blog by Matthias Clasen.

+
+

Remember that the classname (GtkListItem) in a ui template is used as the “this” pointer referring to the object that is being instantiated.

+
+

Therefore, GtkListItem instance is used as the this object of the lookup tag when it is evaluated. this object will be explained in section 28.

+

The C source code is as follows. Its name is list2.c and located under src/misc directory.

+
#include <gtk/gtk.h>
+
+/* ----- activate, open, startup handlers ----- */
+static void
+app_activate (GApplication *application) {
+  GtkApplication *app = GTK_APPLICATION (application);
+  GtkWidget *win = gtk_application_window_new (app);
+  gtk_window_set_default_size (GTK_WINDOW (win), 600, 400);
+  GtkWidget *scr = gtk_scrolled_window_new ();
+  gtk_window_set_child (GTK_WINDOW (win), scr);
+
+  char *array[] = {
+    "one", "two", "three", "four", NULL
+  };
+  GtkStringList *sl = gtk_string_list_new ((const char * const *) array);
+  GtkSingleSelection *ss = gtk_single_selection_new (G_LIST_MODEL (sl));
+
+  const char *ui_string =
+"<interface>"
+  "<template class=\"GtkListItem\">"
+    "<property name=\"child\">"
+      "<object class=\"GtkLabel\">"
+        "<binding name=\"label\">"
+          "<lookup name=\"string\" type=\"GtkStringObject\">"
+            "<lookup name=\"item\">GtkListItem</lookup>"
+          "</lookup>"
+        "</binding>"
+      "</object>"
+    "</property>"
+  "</template>"
+"</interface>"
+;
+  GBytes *gbytes = g_bytes_new_static (ui_string, strlen (ui_string));
+  GtkListItemFactory *factory = gtk_builder_list_item_factory_new_from_bytes (NULL, gbytes);
+
+  GtkWidget *lv = gtk_list_view_new (GTK_SELECTION_MODEL (ss), factory);
+  gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (scr), lv);
+  gtk_widget_show (win);
+}
+
+static void
+app_startup (GApplication *application) {
+}
+
+/* ----- main ----- */
+#define APPLICATION_ID "com.github.ToshioCP.list2"
+
+int
+main (int argc, char **argv) {
+  GtkApplication *app;
+  int stat;
+
+  app = gtk_application_new (APPLICATION_ID, G_APPLICATION_FLAGS_NONE);
+
+  g_signal_connect (app, "startup", G_CALLBACK (app_startup), NULL);
+  g_signal_connect (app, "activate", G_CALLBACK (app_activate), NULL);
+
+  stat =g_application_run (G_APPLICATION (app), argc, argv);
+  g_object_unref (app);
+  return stat;
+}
+

No signal handler is needed for GtkBulderListItemFactory. GtkSingleSelection is used, so user can select one item at a time.

+

Because this is a small program, the ui data is given as a string.

+

GtkDirectoryList

+

GtkDirectoryList is a list model containing GFileInfo objects which are information of files under a certain directory. It uses g_file_enumerate_children_async() to get the GFileInfo objects. The list model is created by gtk_directory_list_new function.

+
GtkDirectoryList *gtk_directory_list_new (const char *attributes, GFile *file);
+

attributes is a comma separated list of file attributes. File attributes are key-value pairs. A key consists of a namespace and a name. For example, “standard::name” key is the name of a file. “standard” means general file information. “name” means filename. The following table shows some example.

+ ++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
keymeaning
standard::typefile type. for example, regular file, directory, symbolic link, etc.
standard::namefilename
standard::sizefile size in bytes
access::can-readread privilege if the user is able to read the file
time::modifiedthe time the file was last modified in seconds since the UNIX epoch
+

The current directory is “.”. The following program makes GtkDirectoryList dl and its contents are GFileInfo objects under the current directory.

+
GFile *file = g_file_new_for_path (".");
+GtkDirectoryList *dl = gtk_directory_list_new ("standard::name", file);
+g_object_unref (file);
+

It is not so difficult to make file listing program by changing list2.c in the previous subsection. One problem is that GInfoFile doesn’t have properties. Lookup tag look for a property, so it is useless for looking for a filename from a GFileInfo object. Instead, closure tag is appropriate in this case. Closure tag specifies a function and the type of the return value of the function.

+
char *
+get_file_name (GtkListItem *item, GFileInfo *info) {
+  if (! G_IS_FILE_INFO (info))
+    return NULL;
+  else
+    return g_strdup (g_file_info_get_name (info));
+}
+
+... ...
+... ...
+
+"<interface>"
+  "<template class=\"GtkListItem\">"
+    "<property name=\"child\">"
+      "<object class=\"GtkLabel\">"
+        "<binding name=\"label\">"
+          "<closure type=\"gchararray\" function=\"get_file_name\">"
+            "<lookup name=\"item\">GtkListItem</lookup>"
+          "</closure>"
+        "</binding>"
+      "</object>"
+    "</property>"
+  "</template>"
+"</interface>"
+ +

The whole program (list3.c) is as follows. The program is located in src/misc directory.

+
#include <gtk/gtk.h>
+
+char *
+get_file_name (GtkListItem *item, GFileInfo *info) {
+  if (! G_IS_FILE_INFO (info))
+    return NULL;
+  else
+    return g_strdup (g_file_info_get_name (info));
+}
+
+/* ----- activate, open, startup handlers ----- */
+static void
+app_activate (GApplication *application) {
+  GtkApplication *app = GTK_APPLICATION (application);
+  GtkWidget *win = gtk_application_window_new (app);
+  gtk_window_set_default_size (GTK_WINDOW (win), 600, 400);
+  GtkWidget *scr = gtk_scrolled_window_new ();
+  gtk_window_set_child (GTK_WINDOW (win), scr);
+
+  GFile *file = g_file_new_for_path (".");
+  GtkDirectoryList *dl = gtk_directory_list_new ("standard::name", file);
+  g_object_unref (file);
+  GtkNoSelection *ns = gtk_no_selection_new (G_LIST_MODEL (dl));
+
+  const char *ui_string =
+"<interface>"
+  "<template class=\"GtkListItem\">"
+    "<property name=\"child\">"
+      "<object class=\"GtkLabel\">"
+        "<binding name=\"label\">"
+          "<closure type=\"gchararray\" function=\"get_file_name\">"
+            "<lookup name=\"item\">GtkListItem</lookup>"
+          "</closure>"
+        "</binding>"
+      "</object>"
+    "</property>"
+  "</template>"
+"</interface>"
+;
+  GBytes *gbytes = g_bytes_new_static (ui_string, strlen (ui_string));
+  GtkListItemFactory *factory = gtk_builder_list_item_factory_new_from_bytes (NULL, gbytes);
+
+  GtkWidget *lv = gtk_list_view_new (GTK_SELECTION_MODEL (ns), factory);
+  gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (scr), lv);
+  gtk_widget_show (win);
+}
+
+static void
+app_startup (GApplication *application) {
+}
+
+/* ----- main ----- */
+#define APPLICATION_ID "com.github.ToshioCP.list3"
+
+int
+main (int argc, char **argv) {
+  GtkApplication *app;
+  int stat;
+
+  app = gtk_application_new (APPLICATION_ID, G_APPLICATION_FLAGS_NONE);
+
+  g_signal_connect (app, "startup", G_CALLBACK (app_startup), NULL);
+  g_signal_connect (app, "activate", G_CALLBACK (app_activate), NULL);
+
+  stat =g_application_run (G_APPLICATION (app), argc, argv);
+  g_object_unref (app);
+  return stat;
+}
+

The ui data (xml data above) is used to build the GListItem template at runtime. GtkBuilder refers to the symbol table to find the function get_file_name.

+

Generally, a symbol table is used by a linker to link objects to an executable file. It includes function names and their location. A linker usually doesn’t put a symbol table into the created executable file. But if --export-dynamic option is given, the linker adds the symbol table to the executable file.

+

To accomplish it, an option -Wl,--export-dynamic is given to the C compiler.

+ +

Compile and execute it.

+
$ gcc -Wl,--export-dynamic `pkg-config --cflags gtk4` list3.c `pkg-config --libs gtk4`
+

You can also make a shell script to compile list3.c

+
gcc -Wl,--export-dynamic `pkg-config --cflags gtk4` $1.c `pkg-config --libs gtk4`
+

Save this one liner to a file comp. Then, copy it to $HOME/bin and give it executable permission.

+
$ cp comp $HOME/bin/comp
+$ chmod +x $HOME/bin/comp
+

You can compile list3.c and execute it, like this:

+
$ comp list3
+$ ./a.out
+
+
screenshot list3
+
+

Up: index.html, Prev: Section 25, Next: Section 27

+ + diff --git a/docs/sec27.html b/docs/sec27.html new file mode 100644 index 0000000..45f6ba4 --- /dev/null +++ b/docs/sec27.html @@ -0,0 +1,491 @@ + + + + + + + Gtk4 tutorial for beginners + + + +

Up: index.html, Prev: Section 26, Next: Section 28

+

GtkGridView and activate signal

+

GtkGridView is similar to GtkListView. It displays a GListModel as a grid, which is like a square tessellation.

+
+
Grid
+
+

This is often seen when you use a file browser like nautilus.

+

In this section, let’s make a very simple file browser list4. It just shows the files in the current directory. And a user can choose list or grid by clicking on buttons in the tool bar. Each item in the list or grid has an icon and a filename. In addition, list4 provides the way to open the tfe text editor to show a text file. A user can do that by double clicking on an item or pressing enter key when an item is selected.

+

GtkDirectoryList

+

GtkDirectoryList implements GListModel and it contains information of files in a certain directory. The items of the list are GFileInfo objects.

+

In the list4 source files, GtkDirectoryList is described in a ui file and built by GtkBuilder. The GtkDirectoryList instance is assigned to the “model” property of a GtkSingleSelection instance. And the GtkSingleSelection instance is assigned to the “model” property of a GListView or GGridView instance.

+
GtkListView (model property) => GtkSingleSelection (model property) => GtkDirectoryList
+GtkGridView (model property) => GtkSingleSelection (model property) => GtkDirectoryList
+
+
DirectoryList
+
+

The following is the part of the ui file list4.ui. It defines GtkListView, GtkSingleSelection and GtkDirectoryList. It also defines GtkGridView and GtkSingleSelection.

+
<object class="GtkListView" id="list">
+  <property name="model">
+    <object class="GtkSingleSelection" id="singleselection">
+      <property name="model">
+        <object class="GtkDirectoryList" id="directorylist">
+          <property name="attributes">standard::name,standard::icon,standard::content-type</property>
+        </object>
+      </property>
+    </object>
+  </property>
+</object>
+<object class="GtkGridView" id="grid">
+  <property name="model">singleselection</property>
+</object>
+

GtkDirectoryList has an “attributes” property. It is attributes of GFileInfo such as “standard::name”, “standard::icon” and “standard::content-type”.

+ +

GtkGridView has the same structure as GtkListView. But it is enough to specify its model property to singleselection which is the identification of the GtkSingleSelection. Therefore the description for GtkGridView is very short.

+

Ui file of the window

+

Look at the screenshot of list4 at the top of this section. The widgets are built with the following ui file.

+
<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <object class="GtkApplicationWindow" id="win">
+    <property name="title">file list</property>
+    <property name="default-width">600</property>
+    <property name="default-height">400</property>
+    <child>
+      <object class="GtkBox" id="boxv">
+        <property name="orientation">GTK_ORIENTATION_VERTICAL</property>
+        <child>
+          <object class="GtkBox" id="boxh">
+            <property name="orientation">GTK_ORIENTATION_HORIZONTAL</property>
+            <child>
+              <object class="GtkLabel" id="dmy1">
+                <property name="hexpand">TRUE</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkButton" id="btnlist">
+                <property name="name">btnlist</property>
+                <property name="action-name">win.view</property>
+                <property name="action-target">&apos;list&apos;</property>
+                <child>
+                  <object class="GtkImage">
+                    <property name="resource">/com/github/ToshioCP/list4/list.png</property>
+                  </object>
+                </child>
+              </object>
+            </child>
+            <child>
+              <object class="GtkButton" id="btngrid">
+                <property name="name">btngrid</property>
+                <property name="action-name">win.view</property>
+                <property name="action-target">&apos;grid&apos;</property>
+                <child>
+                  <object class="GtkImage">
+                    <property name="resource">/com/github/ToshioCP/list4/grid.png</property>
+                  </object>
+                </child>
+              </object>
+            </child>
+            <child>
+              <object class="GtkLabel" id="dmy2">
+                <property name="width-chars">10</property>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="GtkScrolledWindow" id="scr">
+            <property name="hexpand">TRUE</property>
+            <property name="vexpand">TRUE</property>
+          </object>
+        </child>
+      </object>
+    </child>
+  </object>
+  <object class="GtkListView" id="list">
+    <property name="model">
+      <object class="GtkSingleSelection" id="singleselection">
+        <property name="model">
+          <object class="GtkDirectoryList" id="directorylist">
+            <property name="attributes">standard::name,standard::icon,standard::content-type</property>
+          </object>
+        </property>
+      </object>
+    </property>
+  </object>
+  <object class="GtkGridView" id="grid">
+    <property name="model">singleselection</property>
+  </object>
+</interface>
+

The file consists of two parts. The first part begins at the third line and ends at the 57th line. This part is the widgets from the top level window to the scrolled window. It also includes two buttons. The second part begins at the 58th line and ends at the 71st line. This is the part of GtkListView and GtkGridView. They are described in the previous section.

+ +

The action view is created, connected to the “activate” signal handler and inserted to the window (action map) as follows.

+
  act_view = g_simple_action_new_stateful ("view", g_variant_type_new("s"), g_variant_new_string ("list"));
+  g_signal_connect (act_view, "activate", G_CALLBACK (view_activated), scr); /* scr is the GtkScrolledWindow object */
+  g_action_map_add_action (G_ACTION_MAP (win), G_ACTION (act_view));
+

The signal handler view_activated will be explained later.

+

Factories

+

Each view (GtkListView and GtkGridView) has its own factory because its items have different structure of widgets. The factories are GtkBuilderListItemFactory objects. Their ui files are as follows.

+

factory_list.ui

+
<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="GtkListItem">
+    <property name="child">
+      <object class="GtkBox">
+        <property name="orientation">GTK_ORIENTATION_HORIZONTAL</property>
+        <property name="spacing">20</property>
+        <child>
+          <object class="GtkImage">
+            <binding name="gicon">
+              <closure type="GIcon" function="get_icon">
+                <lookup name="item">GtkListItem</lookup>
+              </closure>
+            </binding>
+          </object>
+        </child>
+        <child>
+          <object class="GtkLabel">
+            <property name="hexpand">TRUE</property>
+            <property name="xalign">0</property>
+            <binding name="label">
+              <closure type="gchararray" function="get_file_name">
+                <lookup name="item">GtkListItem</lookup>
+              </closure>
+            </binding>
+          </object>
+        </child>
+      </object>
+    </property>
+  </template>
+</interface>
+

factory_grid.ui

+
<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="GtkListItem">
+    <property name="child">
+      <object class="GtkBox">
+        <property name="orientation">GTK_ORIENTATION_VERTICAL</property>
+        <property name="spacing">20</property>
+        <child>
+          <object class="GtkImage">
+            <property name="icon-size">GTK_ICON_SIZE_LARGE</property>
+            <binding name="gicon">
+              <closure type="GIcon" function="get_icon">
+                <lookup name="item">GtkListItem</lookup>
+              </closure>
+            </binding>
+          </object>
+        </child>
+        <child>
+          <object class="GtkLabel">
+            <property name="hexpand">TRUE</property>
+            <property name="xalign">0.5</property>
+            <binding name="label">
+              <closure type="gchararray" function="get_file_name">
+                <lookup name="item">GtkListItem</lookup>
+              </closure>
+            </binding>
+          </object>
+        </child>
+      </object>
+    </property>
+  </template>
+</interface>
+

The two files above are almost same. The difference is:

+ +
$ cd list4; diff factory_list.ui factory_grid.ui
+6c6
+<         <property name="orientation">GTK_ORIENTATION_HORIZONTAL</property>
+---
+>         <property name="orientation">GTK_ORIENTATION_VERTICAL</property>
+9a10
+>             <property name="icon-size">GTK_ICON_SIZE_LARGE</property>
+20c21
+<             <property name="xalign">0</property>
+---
+>             <property name="xalign">0.5</property>
+

Each view item has two properties, “gicon” property of GtkImage and “label” property of GtkLabel. Because GFileInfo doesn’t have properties correspond to icon or filename, the factory uses closure tag to bind “gicon” and “label” properties to GFileInfo information. A function get_icon gets GIcon the GFileInfo object has. And a function get_file_name gets a filename the GFileInfo object has.

+
GIcon *
+get_icon (GtkListItem *item, GFileInfo *info) {
+  GIcon *icon;
+
+  if (! G_IS_FILE_INFO (info))
+    return NULL;
+  else {
+    icon = g_file_info_get_icon (info);
+    g_object_ref (icon);
+    return icon;
+  }
+}
+
+char *
+get_file_name (GtkListItem *item, GFileInfo *info) {
+  if (! G_IS_FILE_INFO (info))
+    return NULL;
+  else
+    return g_strdup (g_file_info_get_name (info));
+}
+

One important thing is view items own the instance or string. It is achieved by g_object_ref to increase the reference count by one, or strdup to create a copy of the string. The object or string will be automatically freed in unbinding process when the view item is recycled.

+

An activate signal handler of the action

+

An activate signal handler view_activate switches the view. It does two things.

+ +
static void
+view_activated(GSimpleAction *action, GVariant *parameter, gpointer user_data) {
+  GtkScrolledWindow *scr = GTK_SCROLLED_WINDOW (user_data);
+  const char *view = g_variant_get_string (parameter, NULL);
+  const char *other;
+  char *css;
+
+  if (strcmp (view, "list") == 0) {
+    other = "grid";
+    gtk_scrolled_window_set_child (scr, list);
+  }else {
+    other = "list";
+    gtk_scrolled_window_set_child (scr, grid);
+  }
+  css = g_strdup_printf ("button#btn%s {background: silver;} button#btn%s {background: white;}", view, other);
+  gtk_css_provider_load_from_data (provider, css, -1);
+  g_free (css);
+  g_action_change_state (G_ACTION (action), parameter);
+}
+

The second parameter of this handler is the target of the clicked button. Its type is GVariant.

+ +

The third parameter user_data points GtkScrolledWindow, which is set in the g_signal_connect function.

+ +

Activate signal of GtkListView and GtkGridView

+

Views (GtkListView and GtkGridView) have an “activate” signal. It is emitted when an item in the view is double clicked or the enter key is pressed. You can do anything you like by connecting the “activate” signal to the handler.

+

The example list4 launches tfe text file editor if the item of the list is a text file.

+
static void
+list_activate (GtkListView *list, int position, gpointer user_data) {
+  GFileInfo *info = G_FILE_INFO (g_list_model_get_item (G_LIST_MODEL (gtk_list_view_get_model (list)), position));
+  launch_tfe_with_file (info);
+}
+
+static void
+grid_activate (GtkGridView *grid, int position, gpointer user_data) {
+  GFileInfo *info = G_FILE_INFO (g_list_model_get_item (G_LIST_MODEL (gtk_grid_view_get_model (grid)), position));
+  launch_tfe_with_file (info);
+}
+
+... ...
+... ...
+
+  g_signal_connect (GTK_LIST_VIEW (list), "activate", G_CALLBACK (list_activate), NULL);
+  g_signal_connect (GTK_GRID_VIEW (grid), "activate", G_CALLBACK (grid_activate), NULL);
+

The second parameter of the handlers is the position of the item (GFileInfo) of the GListModel. So you can get the item with g_list_model_get_item function.

+

Content type and launching an application

+

The function launch_tfe_with_file gets a file from the GFileInfo instance. If the file is a text file, it launches tfe with the file.

+

GFileInfo has information about file type. The file type is like “text/plain”, “text/x-csrc” and so on. It is called content type. Content type can be got with g_file_info_get_content_type function.

+
static void
+launch_tfe_with_file (GFileInfo *info) {
+  GError *err = NULL;
+  GFile *file;
+  GList *files = NULL;
+  const char *content_type;
+  const char *text_type = "text/";
+  GAppInfo *appinfo;
+  int i;
+
+  if (! info)
+    return;
+  content_type = g_file_info_get_content_type (info);
+g_print ("%s\n", content_type);  /* This line can be commented out if unnecessary */
+  if (! content_type)
+    return;
+  for (i=0;i<5;++i) {
+    if (content_type[i] != text_type[i])
+      return;
+  }
+  appinfo = g_app_info_create_from_commandline ("tfe", "tfe", G_APP_INFO_CREATE_NONE, &err);
+  if (err) {
+    g_printerr ("%s\n", err->message);
+    g_error_free (err);
+    return;
+  }
+  err = NULL;
+  file = g_file_new_for_path (g_file_info_get_name (info));
+  files = g_list_append (files, file);
+  if (! (g_app_info_launch (appinfo, files, NULL, &err))) {
+    g_printerr ("%s\n", err->message);
+    g_error_free (err);
+  }
+  g_list_free_full (files, g_object_unref);
+  g_object_unref (appinfo);
+}
+ +

If your distribution supports Gtk4, using g_app_info_launch_default_for_uri is convenient. The function automatically determines the default application from the file and launches it. For example, if the file is text, then it launches gedit with the file. Such functionality comes from desktop.

+

Compilation and execution

+

The source files are located in src/list4 directory. To compile and execute list4, type as follows.

+
$ cd list4 # or cd src/list4. It depends your current directory.
+$ meson _build
+$ ninja -C _build
+$ _build/list4
+

Then a file list appears as a list style. Click on a button on the tool bar so that you can change the style to grid or back to list. Double click “list4.c” item, then tfe text editor runs with the argument “list4.c”. The following is the screenshot.

+
+
Screenshot
+
+

“gbytes” property of GtkBuilderListItemFactory

+

GtkBuilderListItemFactory has “gbytes” property. The property contains a byte sequence of ui data. If you use this property, you can put the contents of factory_list.ui and factory_grid.uiinto list4.ui. The following shows a part of the new ui file (list5.ui).

+
  <object class="GtkListView" id="list">
+    <property name="model">
+      <object class="GtkSingleSelection" id="singleselection">
+        <property name="model">
+          <object class="GtkDirectoryList" id="directorylist">
+            <property name="attributes">standard::name,standard::icon,standard::content-type</property>
+          </object>
+        </property>
+      </object>
+    </property>
+    <property name="factory">
+      <object class="GtkBuilderListItemFactory">
+        <property name="bytes"><![CDATA[
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="GtkListItem">
+    <property name="child">
+      <object class="GtkBox">
+        <property name="orientation">GTK_ORIENTATION_HORIZONTAL</property>
+        <property name="spacing">20</property>
+        <child>
+          <object class="GtkImage">
+            <binding name="gicon">
+              <closure type="GIcon" function="get_icon">
+                <lookup name="item">GtkListItem</lookup>
+              </closure>
+            </binding>
+          </object>
+        </child>
+        <child>
+          <object class="GtkLabel">
+            <property name="hexpand">TRUE</property>
+            <property name="xalign">0</property>
+            <binding name="label">
+              <closure type="gchararray" function="get_file_name">
+                <lookup name="item">GtkListItem</lookup>
+              </closure>
+            </binding>
+          </object>
+        </child>
+      </object>
+    </property>
+  </template>
+</interface>
+        ]]></property>
+      </object>
+    </property>
+  </object>
+

CDATA section begins with “<[CDATA[" and ends with "]]>”. The contents of CDATA section is recognized as a string. Any character, even if it is a key syntax marker such as ‘<’ or ‘>’, is recognized literally. Therefore, the text between “<[CDATA[" and "]]>” is inserted to “bytes” property as it is.

+

This method decreases the number of ui files. But, the new ui file is a bit complicated especially for the beginners. If you feel some difficulty, it is better for you to separate the ui file.

+

A directory src/list5 includes the ui file above.

+

Up: index.html, Prev: Section 26, Next: Section 28

+ + diff --git a/docs/sec28.html b/docs/sec28.html new file mode 100644 index 0000000..be7ae1d --- /dev/null +++ b/docs/sec28.html @@ -0,0 +1,517 @@ + + + + + + + Gtk4 tutorial for beginners + + + +

Up: index.html, Prev: Section 27, Next: Section 29

+

GtkExpression

+

GtkExpression is a fundamental type. It is not a descendant of GObject. GtkExpression provides a way to describe references to values. GtkExpression needs to be evaluated to obtain a value.

+

It is similar to arithmetic calculation.

+
1 + 2 = 3
+

1+2 is an expression. It shows the way how to calculate. 3 is the value comes from the expression. Evaluation is to calculate the expression and get the value.

+

GtkExpression is a way to get a value. Evaluation is like a calculation. A value is got by evaluating the expression.

+

First, I want to show you the C file of the example for GtkExpression. Its name is exp.c and located under src/expression directory. You don’t need to understand the details now, just look at it. It will be explained in the next subsection.

+
#include <gtk/gtk.h>
+
+GtkWidget *win1;
+int width, height;
+GtkExpressionWatch *watch_width;
+GtkExpressionWatch *watch_height;
+
+/* Notify is called when "default-width" or "default-height" property is changed. */
+static void
+notify (gpointer user_data) {
+  GValue value = G_VALUE_INIT;
+  char *title;
+
+  if (watch_width && gtk_expression_watch_evaluate (watch_width, &value))
+    width = g_value_get_int (&value);
+  g_value_unset (&value);
+  if (watch_height && gtk_expression_watch_evaluate (watch_height, &value))
+    height = g_value_get_int (&value);
+  g_value_unset (&value);
+  title = g_strdup_printf ("%d x %d", width, height);
+  gtk_window_set_title (GTK_WINDOW (win1), title);
+  g_free (title);
+}
+
+/* This function is used by closure tag in exp.ui. */
+char *
+set_title (GtkWidget *win, int width, int height) {
+  return g_strdup_printf ("%d x %d", width, height);
+}
+
+/* ----- activate, open, startup handlers ----- */
+static void
+app_activate (GApplication *application) {
+  GtkApplication *app = GTK_APPLICATION (application);
+  GtkWidget *box;
+  GtkWidget *label1, *label2, *label3;
+  GtkWidget *entry;
+  GtkEntryBuffer *buffer;
+  GtkBuilder *build;
+  GtkExpression *expression, *expression1, *expression2;
+  GValue value = G_VALUE_INIT;
+  char *s;
+
+  /* Creates GtkApplicationWindow instance. */
+  /* The codes below are complecated. It does the same as "win1 = gtk_application_window_new (app);". */
+  /* The codes are written just to show how to use GtkExpression. */
+  expression = gtk_cclosure_expression_new (GTK_TYPE_APPLICATION_WINDOW, NULL, 0, NULL,
+                 G_CALLBACK (gtk_application_window_new), NULL, NULL);
+  if (gtk_expression_evaluate (expression, app, &value)) {
+    win1 = GTK_WIDGET (g_value_get_object (&value)); /* GtkApplicationWindow */
+    g_object_ref (win1);
+    g_print ("Got GtkApplicationWindow instance.\n");
+  }else
+    g_print ("The cclosure expression wasn't evaluated correctly.\n");
+  gtk_expression_unref (expression);
+  g_value_unset (&value);    /* At the same time, the reference count of win1 is decreased by one. */
+
+  /* Builds a window with components */
+  box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 10);
+  label1 = gtk_label_new (NULL);
+  label2 = gtk_label_new (NULL);
+  label3 = gtk_label_new (NULL);
+  buffer = gtk_entry_buffer_new (NULL, 0);
+  entry = gtk_entry_new_with_buffer (buffer);
+  gtk_box_append (GTK_BOX (box), label1);
+  gtk_box_append (GTK_BOX (box), label2);
+  gtk_box_append (GTK_BOX (box), label3);
+  gtk_box_append (GTK_BOX (box), entry);
+  gtk_window_set_child (GTK_WINDOW (win1), box);
+
+  /* Constant expression */
+  expression = gtk_constant_expression_new (G_TYPE_INT,100);
+  if (gtk_expression_evaluate (expression, NULL, &value)) {
+    s = g_strdup_printf ("%d", g_value_get_int (&value));
+    gtk_label_set_text (GTK_LABEL (label1), s);
+    g_free (s);
+  } else
+    g_print ("The constant expression wasn't evaluated correctly.\n");
+  gtk_expression_unref (expression);
+  g_value_unset (&value);
+
+  /* Property expression and binding*/
+  expression1 = gtk_property_expression_new (GTK_TYPE_ENTRY, NULL, "buffer");
+  expression2 = gtk_property_expression_new (GTK_TYPE_ENTRY_BUFFER, expression1, "text");
+  gtk_expression_bind (expression2, label2, "label", entry);
+
+  /* Constant expression instead of "this" instance */
+  expression1 = gtk_constant_expression_new (GTK_TYPE_APPLICATION, app);
+  expression2 = gtk_property_expression_new (GTK_TYPE_APPLICATION, expression1, "application-id");
+  if (gtk_expression_evaluate (expression2, NULL, &value))
+    gtk_label_set_text (GTK_LABEL (label3), g_value_get_string (&value));
+  else
+    g_print ("The property expression wasn't evaluated correctly.\n");
+  gtk_expression_unref (expression1); /* expression 2 is also freed. */
+  g_value_unset (&value);
+
+  width = 800;
+  height = 600;
+  gtk_window_set_default_size (GTK_WINDOW (win1), width, height);
+  notify(NULL);
+
+  /* GtkExpressionWatch */
+  expression1 = gtk_property_expression_new (GTK_TYPE_WINDOW, NULL, "default-width");
+  watch_width = gtk_expression_watch (expression1, win1, notify, NULL, NULL);
+  expression2 = gtk_property_expression_new (GTK_TYPE_WINDOW, NULL, "default-height");
+  watch_height = gtk_expression_watch (expression2, win1, notify, NULL, NULL);
+
+  gtk_widget_show (win1);
+
+  /* Builds a window with exp.ui resource */
+  build = gtk_builder_new_from_resource ("/com/github/ToshioCP/exp/exp.ui");
+  GtkWidget *win2 = GTK_WIDGET (gtk_builder_get_object (build, "win2"));
+  gtk_window_set_application (GTK_WINDOW (win2), app);
+  g_object_unref (build);
+
+  gtk_widget_show (win2);
+}
+
+static void
+app_startup (GApplication *application) {
+}
+
+#define APPLICATION_ID "com.github.ToshioCP.exp"
+
+int
+main (int argc, char **argv) {
+  GtkApplication *app;
+  int stat;
+
+  app = gtk_application_new (APPLICATION_ID, G_APPLICATION_FLAGS_NONE);
+
+  g_signal_connect (app, "startup", G_CALLBACK (app_startup), NULL);
+  g_signal_connect (app, "activate", G_CALLBACK (app_activate), NULL);
+/*  g_signal_connect (app, "open", G_CALLBACK (app_open), NULL);*/
+
+  stat =g_application_run (G_APPLICATION (app), argc, argv);
+  g_object_unref (app);
+  return stat;
+}
+

exp.c consists of five functions.

+ +

The function app_activate is an actual main body in exp.c.

+

Constant expression

+

Constant expression provides constant value or instance when it is evaluated.

+ +
  expression = gtk_constant_expression_new (G_TYPE_INT,100);
+  if (gtk_expression_evaluate (expression, NULL, &value)) {
+    s = g_strdup_printf ("%d", g_value_get_int (&value));
+    gtk_label_set_text (GTK_LABEL (label1), s);
+    g_free (s);
+  } else
+    g_print ("The constant expression wasn't evaluated correctly.\n");
+  gtk_expression_unref (expression);
+  g_value_unset (&value);
+ +

Constant expression is usually used to give a constant value or instance to another expression.

+

Property expression

+

Property expression looks up a property in a GObject object. For example, a property expression that refers “label” property in GtkLabel object is created like this.

+
expression = gtk_property_expression_new (GTK_TYPE_LABEL, another_expression, "label");
+

another_expression is expected to give a GtkLabel instance when it is evaluated. For example,

+
label = gtk_label_new ("Hello");
+another_expression = gtk_constant_expression_new (GTK_TYPE_LABEL, label);
+expression = gtk_property_expression_new (GTK_TYPE_LABEL, another_expression, "label");
+

If expression is evaluated, the second parameter another_expression is evaluated in advance. The value of another_expression is label (GtkLabel instance). Then, expression looks up “label” property of label and the evaluation result is “Hello”.

+

In the example above, the second argument of gtk_property_expression_new is another expression. But the second argument can be NULL. If it is NULL, this instance is used instead. this is given by gtk_expression_evaluate function at the evaluation.

+

Now look at exp.c. The lines from 83 to 85 is extracted here.

+
  expression1 = gtk_property_expression_new (GTK_TYPE_ENTRY, NULL, "buffer");
+  expression2 = gtk_property_expression_new (GTK_TYPE_ENTRY_BUFFER, expression1, "text");
+  gtk_expression_bind (expression2, label2, "label", entry);
+ +
  GtkExpressionWatch *watch;
+  watch = gtk_expression_bind (expression2, label2, "label", entry);
+ +

Closure expression

+

Closure expression calls closure when it is evaluated. A closure is a generic representation of a callback (a pointer to a function). For information about closure, see GObject API Reference, The GObject messaging system. A closure expression is created with gtk_cclosure_expression_new function.

+
GtkExpression *
+gtk_cclosure_expression_new (GType value_type,
+                             GClosureMarshal marshal,
+                             guint n_params,
+                             GtkExpression **params,
+                             GCallback callback_func,
+                             gpointer user_data,
+                             GClosureNotify user_destroy);
+ +

The following is extracted from exp.c. It is from line 47 to line 56.

+
expression = gtk_cclosure_expression_new (GTK_TYPE_APPLICATION_WINDOW, NULL, 0, NULL,
+               G_CALLBACK (gtk_application_window_new), NULL, NULL);
+if (gtk_expression_evaluate (expression, app, &value)) {
+  win1 = GTK_WIDGET (g_value_get_object (&value)); /* GtkApplicationWindow */
+  g_object_ref (win1);
+  g_print ("Got GtkApplicationWindow object.\n");
+}else
+  g_print ("The cclosure expression wasn't evaluated correctly.\n");
+gtk_expression_unref (expression);
+g_value_unset (&value);    /* At the same time, the reference count of win1 is decreased by one. */
+

The callback function is gtk_application_window_new. This function has one parameter which is an instance of GtkApplication. And it returns newly created GtkApplicationWindow instance. So, the first argument is GTK_TYPE_APPLICATION_WINDOW which is the type of the return value. The second argument is NULL so general marshaller g_cclosure_marshal_generic () will be used. I think assigning NULL works in most cases when you program in C language.

+

The arguments given to the call back function are this object and parameters which are the fourth argument of gtk_cclosure_expression_new. So, the number of arguments is n_params + 1. Because gtk_application_window_new has one parameter, so n_params is zero and **params is NULL. No user data is necessary, so user_data and user_destroy are NULL.

+

gtk_expression_evaluate evaluates the expression. this instance will be the first argument for gtk_application_window_new, so it is app.

+

If the evaluation succeeds, the GValue value holds a newly created GtkApplicationWindow instance. It is assigned to win1. The GValue will be unset when it is no longer used. And when it is unset, the GtkApplicationWindow instance will be released and its reference count will be decreased by one. It is necessary to increase the reference count by one in advance to keep the instance. gtk_expression_unref frees expression and value is unset.

+

As a result, we got a GtkApplicationWindow instance win1. We can do the same by:

+
win1 = gtk_application_window_new (app);
+

The example is more complicated and not practical than this one line code. The aim of the example is just to show how closure expression works.

+

Closure expression is flexible than other type of expression because you can specify your own callback function.

+

GtkExpressionWatch

+

GtkExpressionWatch watches an expression and if the value of the expression changes it calls its notify handler.

+

The example uses GtkExpressionWatch in the line 103 to 106.

+
expression1 = gtk_property_expression_new (GTK_TYPE_WINDOW, NULL, "default-width");
+watch_width = gtk_expression_watch (expression1, win1, notify, NULL, NULL);
+expression2 = gtk_property_expression_new (GTK_TYPE_WINDOW, NULL, "default-height");
+watch_height = gtk_expression_watch (expression2, win1, notify, NULL, NULL);
+

The expressions above refer to “default-width” and “default-height” properties of GtkWindow. The variable watch_width watches expression1. The second argument win1 is this instance for expression1. So, watch_width watches the value of “default-width” property of win1. If the value changes, it calls notify handler. The fourth and fifth arguments are NULL because no user data is necessary.

+

The variable watch_height connects notify handler to expression2. So, notiry is also called when “default-height” changes.

+

The handler norify is as follows.

+
static void
+notify (gpointer user_data) {
+  GValue value = G_VALUE_INIT;
+  char *title;
+
+  if (watch_width && gtk_expression_watch_evaluate (watch_width, &value))
+    width = g_value_get_int (&value);
+  g_value_unset (&value);
+  if (watch_height && gtk_expression_watch_evaluate (watch_height, &value))
+    height = g_value_get_int (&value);
+  g_value_unset (&value);
+  title = g_strdup_printf ("%d x %d", width, height);
+  gtk_window_set_title (GTK_WINDOW (win1), title);
+  g_free (title);
+}
+ +

The title of the window reflects the size of the window.

+

exp.ui

+

exp.c builds a GtkWindow instance win2 with exp.ui. The ui file exp.ui includes tags to create GtkExpressions. The tags are:

+ +

The window win2 behaves like win1. Because similar expressions are built with the ui file.

+
<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <object class="GtkWindow" id="win2">
+    <binding name="title">
+      <closure type="gchararray" function="set_title">
+        <lookup name="default-width" type="GtkWindow"></lookup>
+        <lookup name="default-height" type="GtkWindow"></lookup>
+      </closure>
+    </binding>
+    <property name="default-width">600</property>
+    <property name="default-height">400</property>
+    <child>
+      <object class="GtkBox">
+        <property name="orientation">GTK_ORIENTATION_VERTICAL</property>
+        <child>
+          <object class="GtkLabel">
+            <binding name="label">
+              <constant type="gint">100</constant>
+            </binding>
+          </object>
+        </child>
+        <child>
+          <object class="GtkLabel">
+            <binding name="label">
+              <lookup name="text">
+                <lookup name="buffer">
+                  entry
+                </lookup>
+              </lookup>
+            </binding>
+          </object>
+        </child>
+        <child>
+          <object class="GtkLabel">
+            <binding name="label">
+              <lookup name="application-id">
+                <lookup name="application">win2</lookup>
+              </lookup>
+            </binding>
+          </object>
+        </child>
+        <child>
+          <object class="GtkEntry" id="entry">
+            <property name="buffer">
+              <object class="GtkEntryBuffer"></object>
+            </property>
+          </object>
+        </child>
+      </object>
+    </child>
+  </object>
+</interface>
+

Constant tag

+

A constant tag corresponds to a constant expression.

+ +

These binding and constant tag works. But they are not good. A property tag is more straightforward.

+
<object class="GtkLabel">
+  <property name="label">100</property>
+</object>
+

This example just shows the way how to use constant tag. Constant tag is mainly used to give a constant argument to a closure.

+

Lookup tag

+

A lookup tag corresponds to a property expression. Line 23 to 31 is copied here.

+
          <object class="GtkLabel">
+            <binding name="label">
+              <lookup name="text">
+                <lookup name="buffer">
+                  entry
+                </lookup>
+              </lookup>
+            </binding>
+          </object>
+ +

As a result, the label of the GtkLabel instance are bound to the text in the field of GtkEntry. If a user input a text in the field in the GtkEntry, GtkLabel displays the same text.

+

Another lookup tag is in the lines from 34 to 40.

+
          <object class="GtkLabel">
+            <binding name="label">
+              <lookup name="application-id">
+                <lookup name="application">win2</lookup>
+              </lookup>
+            </binding>
+          </object>
+ +

As a result, the “label” property in the GtkLabel instance is bound to the “application-id” property. The nested tag makes a chain like:

+
"label" <= "application-id" <= "application" <= `win2`
+

By the way, the application of win2 is set after the objects in ui file are built. Look at exp.c. gtk_window_set_application is called after gtk_build_new_from_resource.

+
build = gtk_builder_new_from_resource ("/com/github/ToshioCP/exp/exp.ui");
+GtkWidget *win2 = GTK_WIDGET (gtk_builder_get_object (build, "win2"));
+gtk_window_set_application (GTK_WINDOW (win2), app);
+

Therefore, before the call for gtk_window_set_application, the “application” property of win2 is not set. So, the evaluation of <lookup name="application">win2</lookup> fails. And the evaluation of <lookup name="application-id"> also fails. A function gtk_expression_bind (), which corresponds to binding tag, doesn’t update the target property if the expression fails. So, the “label” property isn’t updated at the first evaluation.

+

Note that an evaluation can fail. The care is especially necessary when you write a callback for a closure tag which has contents of expressions like lookup tags. The expressions are given to the callback as an argument. If an expression fails the argument will be NULL. You need to check if the argument exactly points the instance that is expected by the callback.

+

Closure tag

+

The lines from 3 to 9 include a closure tag.

+
  <object class="GtkWindow" id="win2">
+    <binding name="title">
+      <closure type="gchararray" function="set_title">
+        <lookup name="default-width" type="GtkWindow"></lookup>
+        <lookup name="default-height" type="GtkWindow"></lookup>
+      </closure>
+    </binding>
+ +

set_title function in exp.c is as follows.

+
char *
+set_title (GtkWidget *win, int width, int height) {
+  return g_strdup_printf ("%d x %d", width, height);
+}
+

It just creates a string, for example, “800 x 600”, and returns it.

+

You’ve probably been noticed that ui file is easier and clearer than the corresponding C program. One of the most useful case of GtkExpression is building GtkListItem instance with GtkBuilderListItemFatory. Such case has already been described in the prior two sections.

+

It will be used in the next section to build GtkListItem in GtkColumnView, which is the most useful view object for GListModel.

+

Compilation and execution

+

All the sources are in src/expression directory. Change your current directory to the directory and run meson and ninja. Then, execute the application.

+
$ meson _build
+$ ninja -C _build
+$ build/exp
+

Then, two windows appear.

+
+
Expression
+
+

If you put some text in the field of the entry, then the same text appears in the second GtkLabel. Because the “label” property of the second GtkLabel instance is bound to the text in the GtkEntryBuffer.

+

If you resize the window, then the size appears in the title bar because the “title” property is bound to “default-width” and “default-height” properties.

+

Up: index.html, Prev: Section 27, Next: Section 29

+ + diff --git a/docs/sec29.html b/docs/sec29.html new file mode 100644 index 0000000..183f053 --- /dev/null +++ b/docs/sec29.html @@ -0,0 +1,491 @@ + + + + + + + Gtk4 tutorial for beginners + + + +

Up: index.html, Prev: Section 28

+

GtkColumnView

+

GtkColumnView

+

GtkColumnView is like GtkListView, but it has multiple columns. Each column is GtkColumnViewColumn.

+
+
Column View
+
+ +

The following diagram shows the image how it works.

+
+
ColumnView
+
+

The example in this section is a window that displays information of files in a current directory. The information is the name, size and last modified datetime of files. So, there are three columns.

+

In addition, the example uses GtkSortListModel and GtkSorter to sort the information.

+

column.ui

+

Ui file specifies whole widgets and their structure.

+
<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <object class="GtkApplicationWindow" id="win">
+    <property name="title">file list</property>
+    <property name="default-width">800</property>
+    <property name="default-height">600</property>
+    <child>
+      <object class="GtkScrolledWindow" id="scr">
+        <property name="hexpand">TRUE</property>
+        <property name="vexpand">TRUE</property>
+        <child>
+          <object class="GtkColumnView" id="columnview">
+            <property name="model">
+              <object class="GtkSingleSelection" id="singleselection">
+                <property name="model">
+                  <object class="GtkSortListModel" id="sortlist">
+                    <property name="model">
+                      <object class="GtkDirectoryList" id="directorylist">
+                        <property name="attributes">standard::name,standard::icon,standard::size,time::modified</property>
+                      </object>
+                    </property>
+                    <binding name="sorter">
+                      <lookup name="sorter">columnview</lookup>
+                    </binding>
+                  </object>
+                </property>
+              </object>
+            </property>
+            <child>
+              <object class="GtkColumnViewColumn" id="column1">
+                <property name="title">Name</property>
+                <property name="expand">TRUE</property>
+                <property name="factory">
+                  <object class="GtkBuilderListItemFactory">
+                    <property name="bytes"><![CDATA[
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="GtkListItem">
+    <property name="child">
+      <object class="GtkBox">
+        <property name="orientation">GTK_ORIENTATION_HORIZONTAL</property>
+        <property name="spacing">20</property>
+        <child>
+          <object class="GtkImage">
+            <binding name="gicon">
+              <closure type="GIcon" function="get_icon_factory">
+                <lookup name="item">GtkListItem</lookup>
+              </closure>
+            </binding>
+          </object>
+        </child>
+        <child>
+          <object class="GtkLabel">
+            <property name="hexpand">TRUE</property>
+            <property name="xalign">0</property>
+            <binding name="label">
+              <closure type="gchararray" function="get_file_name_factory">
+                <lookup name="item">GtkListItem</lookup>
+              </closure>
+            </binding>
+          </object>
+        </child>
+      </object>
+    </property>
+  </template>
+</interface>
+                    ]]></property>
+                  </object>
+                </property>
+                <property name="sorter">
+                  <object class="GtkStringSorter" id="sorter_name">
+                    <property name="expression">
+                      <closure type="gchararray" function="get_file_name">
+                      </closure>
+                    </property>
+                  </object>
+                </property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkColumnViewColumn" id="column2">
+                <property name="title">Size</property>
+                <property name="factory">
+                  <object class="GtkBuilderListItemFactory">
+                    <property name="bytes"><![CDATA[
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="GtkListItem">
+    <property name="child">
+      <object class="GtkLabel">
+        <property name="hexpand">TRUE</property>
+        <property name="xalign">0</property>
+        <binding name="label">
+          <closure type="gchararray" function="get_file_size_factory">
+            <lookup name="item">GtkListItem</lookup>
+          </closure>
+        </binding>
+      </object>
+    </property>
+  </template>
+</interface>
+                    ]]></property>
+                  </object>
+                </property>
+                <property name="sorter">
+                  <object class="GtkNumericSorter" id="sorter_size">
+                    <property name="expression">
+                      <closure type="gint64" function="get_file_size">
+                      </closure>
+                    </property>
+                    <property name="sort-order">GTK_SORT_ASCENDING</property>
+                  </object>
+                </property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkColumnViewColumn" id="column3">
+                <property name="title">Date modified</property>
+                <property name="factory">
+                  <object class="GtkBuilderListItemFactory">
+                    <property name="bytes"><![CDATA[
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="GtkListItem">
+    <property name="child">
+      <object class="GtkLabel">
+        <property name="hexpand">TRUE</property>
+        <property name="xalign">0</property>
+        <binding name="label">
+          <closure type="gchararray" function="get_file_time_modified_factory">
+            <lookup name="item">GtkListItem</lookup>
+          </closure>
+        </binding>
+      </object>
+    </property>
+  </template>
+</interface>
+                    ]]></property>
+                  </object>
+                </property>
+                <property name="sorter">
+                  <object class="GtkNumericSorter" id="sorter_datetime_modified">
+                    <property name="expression">
+                      <closure type="gint64" function="get_file_unixtime_modified">
+                      </closure>
+                    </property>
+                    <property name="sort-order">GTK_SORT_ASCENDING</property>
+                  </object>
+                </property>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </object>
+</interface>
+ +

GtkSortListModel and GtkSorter

+

GtkSortListModel is a list model that sorts its elements according to a GtkSorter. It has “sorter” property that is set with GtkSorter. The property is bound to “sorter” property of GtkColumnView in line 22 to 24.

+
<object class="GtkSortListModel" id="sortlist">
+... ... ...
+  <binding name="sorter">
+    <lookup name="sorter">columnview</lookup>
+  </binding>
+

Therefore, columnview determines the way how to sort the list model. The “sorter” property of GtkColumnView is read-only property and it is a special sorter. It reflects the user’s sorting choice. If a user clicks the header of a column, then the sorter (“sorter” property) of the column is referenced by “sorter” property of the GtkColumnView. If the user clicks the header of another column, then the “sorter” property of the GtkColumnView refers to the newly clicked column’s “sorter” property.

+

The binding above makes a indirect connection between the “sorter” property of GtkSortListModel and the “sorter” property of each column.

+

GtkSorter has several child objects.

+ +

The example uses GtkStringSorter and GtkNumericSorter.

+

GtkStringSorter uses GtkExpression to get the strings from the objects. The GtkExpression is stored in the “expression” property of GtkStringSorter. For example, in the ui file above, the GtkExpression is in the line 71 to 76.

+
<object class="GtkStringSorter" id="sorter_name">
+  <property name="expression">
+    <closure type="gchararray" function="get_file_name">
+    </closure>
+  </property>
+</object>
+

The GtkExpression calls get_file_name function when it is evaluated.

+
char *
+get_file_name (GFileInfo *info) {
+  g_return_val_if_fail (G_IS_FILE_INFO (info), NULL);
+
+  return g_strdup(g_file_info_get_name (info));
+}
+

The function is given the item (GFileInfo) of the GtkSortListModel as an argument (this object). The function retrieves a filename from info. The string is owned by info so it is necessary to duplicate it. And it returns the copied string. The string will be owned by the expression.

+

GtkNumericSorter compares numbers. It is used in the line 106 to 112 and line 142 to 148. The lines from 106 to 112 is:

+
<object class="GtkNumericSorter" id="sorter_size">
+  <property name="expression">
+    <closure type="gint64" function="get_file_size">
+    </closure>
+  </property>
+  <property name="sort-order">GTK_SORT_ASCENDING</property>
+</object>
+

The closure tag specifies a callback function get_file_size.

+
goffset
+get_file_size (GFileInfo *info) {
+  g_return_val_if_fail (G_IS_FILE_INFO (info), -1);
+
+  return g_file_info_get_size (info);
+}
+

It just returns the size of info. The type of the size is goffset. The type goffset is the same as gint64.

+

The lines from 142 to 148 is:

+
<object class="GtkNumericSorter" id="sorter_datetime_modified">
+  <property name="expression">
+    <closure type="gint64" function="get_file_unixtime_modified">
+    </closure>
+  </property>
+  <property name="sort-order">GTK_SORT_ASCENDING</property>
+</object>
+

The closure tag specifies a callback function get_file_unixtime_modified.

+
gint64
+get_file_unixtime_modified (GFileInfo *info) {
+  g_return_val_if_fail (G_IS_FILE_INFO (info), -1);
+
+  GDateTime *dt;
+
+  dt = g_file_info_get_modification_date_time (info);
+  return g_date_time_to_unix (dt);
+}
+

It gets the modification date and time (GDateTime type) of info. Then it gets a unix time from dt. Unix time, sometimes called unix epoch, is the number of seconds that have elapsed since 00:00:00 UTC on 1 January 1970. It returns the unix time (gint64 type).

+

column.c

+

column.c is as follows.

+
#include <gtk/gtk.h>
+
+/* functions (closures) for GtkBuilderListItemFactory */
+GIcon *
+get_icon_factory (GtkListItem *item, GFileInfo *info) {
+  GIcon *icon;
+  if (! G_IS_FILE_INFO (info))
+    return NULL;
+  else {
+    icon = g_file_info_get_icon (info);
+    g_object_ref (icon);
+    return icon;
+  }
+}
+
+char *
+get_file_name_factory (GtkListItem *item, GFileInfo *info) {
+  if (! G_IS_FILE_INFO (info))
+    return NULL;
+  else
+    return g_strdup (g_file_info_get_name (info));
+}
+
+char *
+get_file_size_factory (GtkListItem *item, GFileInfo *info) {
+  /* goffset is gint64 */
+  goffset size;
+
+  if (! G_IS_FILE_INFO (info))
+    return NULL;
+  else {
+    size = g_file_info_get_size (info);
+    return g_strdup_printf ("%ld", (long int) size);
+  }
+}
+
+char *
+get_file_time_modified_factory (GtkListItem *item, GFileInfo *info) {
+  GDateTime *dt;
+
+  if (! G_IS_FILE_INFO (info))
+    return NULL;
+  else {
+    dt = g_file_info_get_modification_date_time (info);
+    return g_date_time_format (dt, "%F");
+  }
+}
+
+/* Functions (closures) for GtkSorter */
+char *
+get_file_name (GFileInfo *info) {
+  g_return_val_if_fail (G_IS_FILE_INFO (info), NULL);
+
+  return g_strdup(g_file_info_get_name (info));
+}
+
+goffset
+get_file_size (GFileInfo *info) {
+  g_return_val_if_fail (G_IS_FILE_INFO (info), -1);
+
+  return g_file_info_get_size (info);
+}
+
+gint64
+get_file_unixtime_modified (GFileInfo *info) {
+  g_return_val_if_fail (G_IS_FILE_INFO (info), -1);
+
+  GDateTime *dt;
+
+  dt = g_file_info_get_modification_date_time (info);
+  return g_date_time_to_unix (dt);
+}
+
+/* ----- activate, open, startup handlers ----- */
+static void
+app_activate (GApplication *application) {
+  GtkApplication *app = GTK_APPLICATION (application);
+  GFile *file;
+
+  GtkBuilder *build = gtk_builder_new_from_resource ("/com/github/ToshioCP/column/column.ui");
+  GtkWidget *win = GTK_WIDGET (gtk_builder_get_object (build, "win"));
+  GtkDirectoryList *directorylist = GTK_DIRECTORY_LIST (gtk_builder_get_object (build, "directorylist"));
+  g_object_unref (build);
+
+  gtk_window_set_application (GTK_WINDOW (win), app);
+
+  file = g_file_new_for_path (".");
+  gtk_directory_list_set_file (directorylist, file);
+  g_object_unref (file);
+
+  gtk_widget_show (win);
+}
+
+static void
+app_startup (GApplication *application) {
+}
+
+#define APPLICATION_ID "com.github.ToshioCP.columnview"
+
+int
+main (int argc, char **argv) {
+  GtkApplication *app;
+  int stat;
+
+  app = gtk_application_new (APPLICATION_ID, G_APPLICATION_FLAGS_NONE);
+
+  g_signal_connect (app, "startup", G_CALLBACK (app_startup), NULL);
+  g_signal_connect (app, "activate", G_CALLBACK (app_activate), NULL);
+/*  g_signal_connect (app, "open", G_CALLBACK (app_open), NULL);*/
+
+  stat =g_application_run (G_APPLICATION (app), argc, argv);
+  g_object_unref (app);
+  return stat;
+}
+ +

exp.c is simple and short thanks to exp.ui.

+

Compilation and execution.

+

All the source files are in src/column directory. Change your current directory to the directory and type the following.

+
$ meson _build
+$ ninja -C _build
+$ _build/column
+

Then, a window appears.

+
+
Column View
+
+

If you click the header of a column, then the whole lists are sorted by the column. If you click the header of another column, then the whole lists are sorted by the newly selected column.

+

GtkColumnView is very useful and it can manage very big GListModel. It is possible to use it for file list, application list, database frontend and so on.

+

Up: index.html, Prev: Section 28

+ + diff --git a/docs/sec3.html b/docs/sec3.html new file mode 100644 index 0000000..19ec48f --- /dev/null +++ b/docs/sec3.html @@ -0,0 +1,258 @@ + + + + + + + Gtk4 tutorial for beginners + + + +

Up: index.html, Prev: Section 2, Next: Section 4

+

GtkApplication and GtkApplicationWindow

+

GtkApplication

+

GtkApplication and g_application_run

+

Usually people write programming code to make an application. What are applications? Applications are software that runs using libraries, which includes the OS, frameworks and so on. In Gtk4 programming, the GtkApplication is a program (or executable) that runs using Gtk libraries.

+

The basic way to write a GtkApplication is as follows.

+ +

That’s all. Very simple. The following is the C code representing the scenario above.

+
#include <gtk/gtk.h>
+
+int
+main (int argc, char **argv) {
+  GtkApplication *app;
+  int stat;
+
+  app = gtk_application_new ("com.github.ToshioCP.pr1", G_APPLICATION_FLAGS_NONE);
+  stat =g_application_run (G_APPLICATION (app), argc, argv);
+  g_object_unref (app);
+  return stat;
+}
+

The first line says that this program includes the header files of the Gtk libraries. The function main above is a startup function in C language. The variable app is defined as a pointer to a GtkApplication instance. The function gtk_application_new creates a GtkApplication instance and returns a pointer to the instance. The GtkApplication instance is a C structure data in which the information about the application is stored. The meaning of the arguments will be explained later. The function g_application_run runs an application that the instance defined. (We often say that the function runs app. Actually, app is not an application but a pointer to the instance of the application. However, it is simple and short, and probably no confusion occurs.)

+

To compile this, the following command needs to be run. The string pr1.c is the filename of the C source code above.

+
$ gcc `pkg-config --cflags gtk4` pr1.c `pkg-config --libs gtk4`
+

The C compiler gcc generates an executable file, a.out. Let’s run it.

+
$ ./a.out
+
+(a.out:13533): GLib-GIO-WARNING **: 15:30:17.449: Your application does not implement
+g_application_activate() and has no handlers connected to the "activate" signal.
+It should do one of these.
+$
+

Oh, it just produces an error message. This error message means that the GtkApplication object ran, without a doubt. Now, let’s think about what this message means.

+

signal

+

The message tells us that:

+
    +
  1. The application GtkApplication doesn’t implement g_application_activate(),
  2. +
  3. It has no handlers connected to the “activate” signal, and
  4. +
  5. You will need to solve at least one of these.
  6. +
+

These two causes of the error are related to signals. So, I will explain that to you first.

+

A signal is emitted when something happens. For example, a window is created, a window is destroyed and so on. The signal “activate” is emitted when the application is activated, or started. If the signal is connected to a function, which is called a signal handler or simply handler, then the function is invoked when the signal emits.

+

The flow is like this:

+
    +
  1. Something happens.
  2. +
  3. If it’s related to a certain signal, then the signal is emitted.
  4. +
  5. If the signal as been connected to a handler, then the handler is invoked.
  6. +
+

Signals are defined in objects. For example, the “activate” signal belongs to the GApplication object, which is a parent object of GtkApplication object.

+

The GApplication object is a child object of the GObject object. GObject is the top object in the hierarchy of all the objects.

+
GObject -- GApplication -- GtkApplication
+<---parent                      --->child
+

A child object inherits signals, functions, properties and so on from its parent object. So, GtkApplication also has the “activate” signal.

+

Now we can solve the problem in pr1.c. We need to connect the “activate” signal to a handler. We use a function g_signal_connect which connects a signal to a handler.

+
#include <gtk/gtk.h>
+
+static void
+app_activate (GApplication *app, gpointer *user_data) {
+  g_print ("GtkApplication is activated.\n");
+}
+
+int
+main (int argc, char **argv) {
+  GtkApplication *app;
+  int stat;
+
+  app = gtk_application_new ("com.github.ToshioCP.pr2", G_APPLICATION_FLAGS_NONE);
+  g_signal_connect (app, "activate", G_CALLBACK (app_activate), NULL);
+  stat =g_application_run (G_APPLICATION (app), argc, argv);
+  g_object_unref (app);
+  return stat;
+}
+

First, we define the handler app_activate which simply displays a message. In the function main, we add g_signal_connect before g_application_run. The function g_signal_connect has four arguments.

+
    +
  1. An instance to which the signal belongs.
  2. +
  3. The name of the signal.
  4. +
  5. A handler function (also called callback), which needs to be casted by G_CALLBACK.
  6. +
  7. Data to pass to the handler. If no data is necessary, NULL should be given.
  8. +
+

You can find the description of each signal in the API reference manual. For example, “activate” signal is in GApplication section in GIO API Reference. The handler function is described in it.

+

In addition, g_signal_connect is described in GObject API Reference. API reference manual is very important. You should see and understand it to write Gtk applications. They are located in ‘GTK Documentation’.

+

Let’s compile the source file above (pr2.c) and run it.

+
$ gcc `pkg-config --cflags gtk4` pr2.c `pkg-config --libs gtk4`
+$ ./a.out
+GtkApplication is activated.
+$
+

OK, well done. However, you may have noticed that it’s painful to type such a long line to compile. It is a good idea to use shell script to solve this problem. Make a text file which contains the following line.

+
gcc `pkg-config --cflags gtk4` $1.c `pkg-config --libs gtk4`
+

Then, save it under the directory $HOME/bin, which is usually /home/(username)/bin. (If your user name is James, then the directory is /home/james/bin). And turn on the execute bit of the file. If the filename is comp, do like this:

+
$ chmod 755 $HOME/bin/comp
+$ ls -log $HOME/bin
+    ...  ...  ...
+-rwxr-xr-x 1   62 May 23 08:21 comp
+    ...  ...  ...
+

If this is the first time that you make a $HOME/bin directory and save a file in it, then you need to logout and login again.

+
$ comp pr2
+$ ./a.out
+GtkApplication is activated.
+$
+

GtkWindow and GtkApplicationWindow

+

GtkWindow

+

A message “GtkApplication is activated.” was printed out in the previous subsection. It was good in terms of a test of GtkApplication. However, it is insufficient because Gtk is a framework for graphical user interface (GUI). Now we go ahead with adding a window into this program. What we need to do is:

+
    +
  1. Create a GtkWindow.
  2. +
  3. Connect it to GtkApplication.
  4. +
  5. Show the window.
  6. +
+

Now rewrite the function app_activate.

+

Create a GtkWindow

+
static void
+app_activate (GApplication *app, gpointer user_data) {
+  GtkWidget *win;
+
+  win = gtk_window_new ();
+  gtk_window_set_application (GTK_WINDOW (win), GTK_APPLICATION (app));
+  gtk_widget_show (win);
+}
+

Widget is an abstract concept that includes all the GUI interfaces such as windows, dialogs, buttons, multi-line text, containers and so on. And GtkWidget is a base object from which all the GUI objects derive.

+
parent <-----> child
+GtkWidget -- GtkWindow
+

GtkWindow includes GtkWidget at the top of its object.

+
+
GtkWindow and GtkWidget
+
+

The function gtk_window_new is defined as follows.

+
GtkWidget *
+gtk_window_new (void);
+

By this definition, it returns a pointer to GtkWidget, not GtkWindow. It actually creates a new GtkWindow instance (not GtkWidget) but returns a pointer to GtkWidget. However,the pointer points the GtkWidget and at the same time it also points GtkWindow that contains GtkWidget in it.

+

If you want to use win as a pointer to the GtkWindow, you need to cast it.

+
(GtkWindow *) win
+

Or you can use GTK_WINDOW macro that performs a similar function.

+
GTK_WINDOW (win)
+

This is a recommended way.

+

Connect it to GtkApplication.

+

The function gtk_window_set_application is used to connect GtkWindow to GtkApplication.

+
gtk_window_set_application (GTK_WINDOW (win), GTK_APPLICATION (app));
+

You need to cast win to GtkWindow and app to GtkApplication. GTK_WINDOW and GTK_APPLICATION macro is appropriate for that.

+

GtkApplication continues to run until the related window is destroyed. If you didn’t connect GtkWindow and GtkApplication, GtkApplication destroys itself immediately. Because no window is connected to GtkApplication, GtkApplication doesn’t need to wait anything. As it destroys itself, the GtkWindow is also destroyed.

+

Show the window.

+

The function gtk_widget_show is used to show the window.

+

Gtk4 changes the default widget visibility to on, so every widget doesn’t need this function to show itself. But, there’s an exception. Top window (this term will be explained later) isn’t visible when it is created. So you need to use the function above to show the window.

+

Save the program as pr3.c and compile and run it.

+
$ comp pr3
+$ ./a.out
+

A small window appears.

+
+
Screenshot of the window
+
+

Click on the close button then the window disappears and the program finishes.

+

GtkApplicationWindow

+

GtkApplicationWindow is a child object of GtkWindow. It has some extra functionality for better integration with GtkApplication. It is recommended to use it instead of GtkWindow when you use GtkApplication.

+

Now rewrite the program and use GtkAppliction Window.

+
static void
+app_activate (GApplication *app, gpointer user_data) {
+  GtkWidget *win;
+
+  win = gtk_application_window_new (GTK_APPLICATION (app));
+  gtk_window_set_title (GTK_WINDOW (win), "pr4");
+  gtk_window_set_default_size (GTK_WINDOW (win), 400, 300);
+  gtk_widget_show (win);
+}
+

When you create GtkApplicationWindow, you need to give GtkApplication instance as an argument. Then it automatically connect these two instances. So you don’t need to call gtk_window_set_application any more.

+

The program sets the title and the default size of the window. Compile it and run a.out, then you will see a bigger window with its title “pr4”.

+
+
Screenshot of the window
+
+

Up: index.html, Prev: Section 2, Next: Section 4

+ + diff --git a/docs/sec4.html b/docs/sec4.html new file mode 100644 index 0000000..bb70575 --- /dev/null +++ b/docs/sec4.html @@ -0,0 +1,332 @@ + + + + + + + Gtk4 tutorial for beginners + + + +

Up: index.html, Prev: Section 3, Next: Section 5

+

Widgets (1)

+

GtkLabel, GtkButton and Gtkbox

+

GtkLabel

+

In the previous section we made a window and displayed it on the screen. Now we go on to the next topic, where we add widgets to this window. The simplest widget is GtkLabel. It is a widget with text in it.

+
#include <gtk/gtk.h>
+
+static void
+app_activate (GApplication *app, gpointer user_data) {
+  GtkWidget *win;
+  GtkWidget *lab;
+
+  win = gtk_application_window_new (GTK_APPLICATION (app));
+  gtk_window_set_title (GTK_WINDOW (win), "lb1");
+  gtk_window_set_default_size (GTK_WINDOW (win), 400, 300);
+
+  lab = gtk_label_new ("Hello.");
+  gtk_window_set_child (GTK_WINDOW (win), lab);
+
+  gtk_widget_show (win);
+}
+
+int
+main (int argc, char **argv) {
+  GtkApplication *app;
+  int stat;
+
+  app = gtk_application_new ("com.github.ToshioCP.lb1", G_APPLICATION_FLAGS_NONE);
+  g_signal_connect (app, "activate", G_CALLBACK (app_activate), NULL);
+  stat =g_application_run (G_APPLICATION (app), argc, argv);
+  g_object_unref (app);
+  return stat;
+}
+

Save this program to a file lb1.c. Then compile and run it.

+
$ comp lb1
+$ ./a.out
+

A window with a message “Hello.” appears.

+
+
Screenshot of the label
+
+

There’s only a little change between pr4.c and lb1.c. A program diff is good to know the difference between two files.

+
$ cd misc; diff pr4.c lb1.c
+5a6
+>   GtkWidget *lab;
+8c9
+<   gtk_window_set_title (GTK_WINDOW (win), "pr4");
+---
+>   gtk_window_set_title (GTK_WINDOW (win), "lb1");
+9a11,14
+> 
+>   lab = gtk_label_new ("Hello.");
+>   gtk_window_set_child (GTK_WINDOW (win), lab);
+> 
+18c23
+<   app = gtk_application_new ("com.github.ToshioCP.pr4", G_APPLICATION_FLAGS_NONE);
+---
+>   app = gtk_application_new ("com.github.ToshioCP.lb1", G_APPLICATION_FLAGS_NONE);
+

This tells us:

+ +

The function gtk_window_set_child (GTK_WINDOW (win), lab) makes the label lab a child widget of the window win. Be careful. A child widget is different from a child object. Objects have parent-child relationships and widgets also have parent-child relationships. But these two relationships are totally different. Don’t be confused. In the program lb1.c, lab is a child widget of win. Child widgets are always located in their parent widget on the screen. See how the window has appeared on the screen. The application window includes the label.

+

The window win doesn’t have any parents. We call such a window top-level window. An application can have more than one top-level window.

+

GtkButton

+

The next widget to introduce is GtkButton. It displays a button on the screen with a label or icon on it. In this subsection, we will make a button with a label. When the button is clicked, it emits a “clicked” signal. The following program shows how to catch the signal to then do something.

+
#include <gtk/gtk.h>
+
+static void
+click_cb (GtkButton *btn, gpointer user_data) {
+  g_print ("Clicked.\n");
+}
+
+static void
+app_activate (GApplication *app, gpointer user_data) {
+  GtkWidget *win;
+  GtkWidget *btn;
+
+  win = gtk_application_window_new (GTK_APPLICATION (app));
+  gtk_window_set_title (GTK_WINDOW (win), "lb2");
+  gtk_window_set_default_size (GTK_WINDOW (win), 400, 300);
+
+  btn = gtk_button_new_with_label ("Click me");
+  gtk_window_set_child (GTK_WINDOW (win), btn);
+  g_signal_connect (btn, "clicked", G_CALLBACK (click_cb), NULL);
+
+  gtk_widget_show (win);
+}
+
+int
+main (int argc, char **argv) {
+  GtkApplication *app;
+  int stat;
+
+  app = gtk_application_new ("com.github.ToshioCP.lb2", G_APPLICATION_FLAGS_NONE);
+  g_signal_connect (app, "activate", G_CALLBACK (app_activate), NULL);
+  stat =g_application_run (G_APPLICATION (app), argc, argv);
+  g_object_unref (app);
+  return stat;
+}
+

Look at the line 17 to 19. First, it creates a GtkButton instance btn with a label “Click me”. Then, adds the button to the window win as a child. Finally, connects a “clicked” signal of the button to a handler (function) click_cb. So, if btn is clicked, the function click_cb is invoked. The suffix “cb” means “call back”.

+

Name the program lb2.c and save it. Now compile and run it.

+
+
Screenshot of the label
+
+

A window with the button appears. Click the button (it is a large button, you can click everywhere in the window), then a string “Clicked.” appears on the terminal. It shows the handler was invoked by clicking the button.

+

It’s good that we make sure that the clicked signal was caught and the handler was invoked by using g_print. However, using g_print is out of harmony with Gtk which is a GUI library. So, we will change the handler. The following code is lb3.c.

+
static void
+click_cb (GtkButton *btn, gpointer user_data) {
+  GtkWindow *win = GTK_WINDOW (user_data);
+  gtk_window_destroy (win);
+}
+
+static void
+app_activate (GApplication *app, gpointer user_data) {
+  GtkWidget *win;
+  GtkWidget *btn;
+
+  win = gtk_application_window_new (GTK_APPLICATION (app));
+  gtk_window_set_title (GTK_WINDOW (win), "lb3");
+  gtk_window_set_default_size (GTK_WINDOW (win), 400, 300);
+
+  btn = gtk_button_new_with_label ("Quit");
+  gtk_window_set_child (GTK_WINDOW (win), btn);
+  g_signal_connect (btn, "clicked", G_CALLBACK (click_cb), win);
+
+  gtk_widget_show (win);
+}
+

And the difference between lb2.c and lb3.c is as follows.

+
$ cd misc; diff lb2.c lb3.c
+5c5,6
+<   g_print ("Clicked.\n");
+---
+>   GtkWindow *win = GTK_WINDOW (user_data);
+>   gtk_window_destroy (win);
+14c15
+<   gtk_window_set_title (GTK_WINDOW (win), "lb2");
+---
+>   gtk_window_set_title (GTK_WINDOW (win), "lb3");
+17c18
+<   btn = gtk_button_new_with_label ("Click me");
+---
+>   btn = gtk_button_new_with_label ("Quit");
+19c20
+<   g_signal_connect (btn, "clicked", G_CALLBACK (click_cb), NULL);
+---
+>   g_signal_connect (btn, "clicked", G_CALLBACK (click_cb), win);
+29c30
+<   app = gtk_application_new ("com.github.ToshioCP.lb2", G_APPLICATION_FLAGS_NONE);
+---
+>   app = gtk_application_new ("com.github.ToshioCP.lb3", G_APPLICATION_FLAGS_NONE);
+35d35
+< 
+

The changes are:

+ +

The most important change is the fourth argument of g_signal_connect. This argument is described as “data to pass to handler” in the definition of g_signal_connect in GObject API Reference. Therefore, win which is a pointer to GtkApplicationWindow is passed to the handler as a second parameter user_data. The handler then casts it to a pointer to GtkWindow and calls gtk_window_destroy to destroy the top-level window. The application then quits.

+

GtkBox

+

GtkWindow and GtkApplicationWindow can have only one child. If you want to add two or more widgets in a window, you need a container widget. GtkBox is one of the containers. It arranges two or more child widgets into a single row or column. The following procedure shows the way to add two buttons in a window.

+ +

After this, the Widgets are connected as the following diagram.

+
+
Parent-child relationship
+
+

The program lb4.c includes these widgets. It is as follows.

+
#include <gtk/gtk.h>
+
+static void
+click1_cb (GtkButton *btn, gpointer user_data) {
+  const gchar *s;
+
+  s = gtk_button_get_label (btn);
+  if (g_strcmp0 (s, "Hello.") == 0)
+    gtk_button_set_label (btn, "Good-bye.");
+  else
+    gtk_button_set_label (btn, "Hello.");
+}
+
+static void
+click2_cb (GtkButton *btn, gpointer user_data) {
+  GtkWindow *win = GTK_WINDOW (user_data);
+  gtk_window_destroy (win);
+}
+
+static void
+app_activate (GApplication *app, gpointer user_data) {
+  GtkWidget *win;
+  GtkWidget *box;
+  GtkWidget *btn1;
+  GtkWidget *btn2;
+
+  win = gtk_application_window_new (GTK_APPLICATION (app));
+  gtk_window_set_title (GTK_WINDOW (win), "lb4");
+  gtk_window_set_default_size (GTK_WINDOW (win), 400, 300);
+
+  box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 5);
+  gtk_box_set_homogeneous (GTK_BOX (box), TRUE);
+  gtk_window_set_child (GTK_WINDOW (win), box);
+
+  btn1 = gtk_button_new_with_label ("Hello.");
+  g_signal_connect (btn1, "clicked", G_CALLBACK (click1_cb), NULL);
+
+  btn2 = gtk_button_new_with_label ("Quit");
+  g_signal_connect (btn2, "clicked", G_CALLBACK (click2_cb), win);
+
+  gtk_box_append (GTK_BOX (box), btn1);
+  gtk_box_append (GTK_BOX (box), btn2);
+
+  gtk_widget_show (win);
+}
+
+int
+main (int argc, char **argv) {
+  GtkApplication *app;
+  int stat;
+
+  app = gtk_application_new ("com.github.ToshioCP.lb4", G_APPLICATION_FLAGS_NONE);
+  g_signal_connect (app, "activate", G_CALLBACK (app_activate), NULL);
+  stat =g_application_run (G_APPLICATION (app), argc, argv);
+  g_object_unref (app);
+  return stat;
+}
+

Look at the function app_activate.

+

After the creation of a GtkApplicationWindow instance, a GtkBox instance is created.

+
box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 5);
+gtk_box_set_homogeneous (GTK_BOX (box), TRUE);
+

The first argument arranges the children of the box vertically. The second argument is the size between the children. The next function fills the box with the children, giving them the same space.

+

After that, two buttons btn1 and btn2 are created and the signal handlers are set. Then, these two buttons are appended to the box.

+
+
Screenshot of the box
+
+

The handler corresponds to btn1 toggles its label. The handler corresponds to btn2 destroys the top-level window and the application quits.

+

Up: index.html, Prev: Section 3, Next: Section 5

+ + diff --git a/docs/sec5.html b/docs/sec5.html new file mode 100644 index 0000000..13b8304 --- /dev/null +++ b/docs/sec5.html @@ -0,0 +1,222 @@ + + + + + + + Gtk4 tutorial for beginners + + + +

Up: index.html, Prev: Section 4, Next: Section 6

+

Widgets (2)

+

GtkTextView, GtkTextbuffer and GtkScrolledWindow

+

GtkTextView and GtkTextBuffer

+

GtkTextview is a widget for multi-line text editing. GtkTextBuffer is a text buffer which is connected to GtkTextView. See the sample program tfv1.c below.

+
#include <gtk/gtk.h>
+
+static void
+app_activate (GApplication *app, gpointer user_data) {
+  GtkWidget *win;
+  GtkWidget *tv;
+  GtkTextBuffer *tb;
+  gchar *text;
+
+  text =
+      "Once upon a time, there was an old man who was called Taketori-no-Okina. "
+      "It is a japanese word that means a man whose work is making bamboo baskets.\n"
+      "One day, he went into a mountain and found a shining bamboo. "
+      "\"What a mysterious bamboo it is!,\" he said. "
+      "He cut it, then there was a small cute baby girl in it. "
+      "The girl was shining faintly. "
+      "He thought this baby girl is a gift from Heaven and took her home.\n"
+      "His wife was surprized at his tale. "
+      "They were very happy because they had no children. "
+      ;
+  win = gtk_application_window_new (GTK_APPLICATION (app));
+  gtk_window_set_title (GTK_WINDOW (win), "Taketori");
+  gtk_window_set_default_size (GTK_WINDOW (win), 400, 300);
+
+  tv = gtk_text_view_new ();
+  tb = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv));
+  gtk_text_buffer_set_text (tb, text, -1);
+  gtk_text_view_set_wrap_mode (GTK_TEXT_VIEW (tv), GTK_WRAP_WORD_CHAR);
+
+  gtk_window_set_child (GTK_WINDOW (win), tv);
+
+  gtk_widget_show (win);
+}
+
+int
+main (int argc, char **argv) {
+  GtkApplication *app;
+  int stat;
+
+  app = gtk_application_new ("com.github.ToshioCP.tfv1", G_APPLICATION_FLAGS_NONE);
+  g_signal_connect (app, "activate", G_CALLBACK (app_activate), NULL);
+  stat = g_application_run (G_APPLICATION (app), argc, argv);
+  g_object_unref (app);
+  return stat;
+}
+

Look at line 25. A GtkTextView instance is created and its pointer is assigned to tv. When the GtkTextView instance is created, a GtkTextBuffer instance is also created and connected to the GtkTextView automatically. “GtkTextBuffer instance” will be referred to simply as “GtkTextBuffer” or “buffer”. In the next line, the pointer to the buffer is got and assigned to tb. Then, the text from line 10 to 20 is assigned to the buffer.

+

GtkTextView has a wrap mode. When it is set to GTK_WRAP_WORD_CHAR, text wraps in between words, or if that is not enough, also between graphemes.

+

In line 30, tv is added to win as a child.

+

Now compile and run it.

+
+
GtkTextView
+
+

There’s an I-beam pointer in the window. You can add or delete any characters on the GtkTextview, and your changes are kept in the GtkTextBuffer. If you add more characters beyond the limit of the window, the height increases and the window extends. If the height gets bigger than the height of the display screen, you won’t be able to control the size of the window, and change it back to the original size. This is a problem and shows that there is a bug in our program. This can solve it by adding a GtkScrolledWindow between the GtkApplicationWindow and GtkTextView.

+

GtkScrolledWindow

+

What we need to do is:

+ +

Modify tfv1.c and save it as tfv2.c. The difference between these two files is small.

+
$ cd tfv; diff tfv1.c tfv2.c
+5a6
+>   GtkWidget *scr;
+24a26,28
+>   scr = gtk_scrolled_window_new ();
+>   gtk_window_set_child (GTK_WINDOW (win), scr);
+> 
+30c34
+<   gtk_window_set_child (GTK_WINDOW (win), tv);
+---
+>   gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (scr), tv);
+40c44
+<   app = gtk_application_new ("com.github.ToshioCP.tfv1", G_APPLICATION_FLAGS_NONE);
+---
+>   app = gtk_application_new ("com.github.ToshioCP.tfv2", G_APPLICATION_FLAGS_NONE);
+

Here is the complete code of tfv2.c.

+
#include <gtk/gtk.h>
+
+static void
+app_activate (GApplication *app, gpointer user_data) {
+  GtkWidget *win;
+  GtkWidget *scr;
+  GtkWidget *tv;
+  GtkTextBuffer *tb;
+  gchar *text;
+
+  text =
+      "Once upon a time, there was an old man who was called Taketori-no-Okina. "
+      "It is a japanese word that means a man whose work is making bamboo baskets.\n"
+      "One day, he went into a mountain and found a shining bamboo. "
+      "\"What a mysterious bamboo it is!,\" he said. "
+      "He cut it, then there was a small cute baby girl in it. "
+      "The girl was shining faintly. "
+      "He thought this baby girl is a gift from Heaven and took her home.\n"
+      "His wife was surprized at his tale. "
+      "They were very happy because they had no children. "
+      ;
+  win = gtk_application_window_new (GTK_APPLICATION (app));
+  gtk_window_set_title (GTK_WINDOW (win), "Taketori");
+  gtk_window_set_default_size (GTK_WINDOW (win), 400, 300);
+
+  scr = gtk_scrolled_window_new ();
+  gtk_window_set_child (GTK_WINDOW (win), scr);
+
+  tv = gtk_text_view_new ();
+  tb = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv));
+  gtk_text_buffer_set_text (tb, text, -1);
+  gtk_text_view_set_wrap_mode (GTK_TEXT_VIEW (tv), GTK_WRAP_WORD_CHAR);
+
+  gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (scr), tv);
+
+  gtk_widget_show (win);
+}
+
+int
+main (int argc, char **argv) {
+  GtkApplication *app;
+  int stat;
+
+  app = gtk_application_new ("com.github.ToshioCP.tfv2", G_APPLICATION_FLAGS_NONE);
+  g_signal_connect (app, "activate", G_CALLBACK (app_activate), NULL);
+  stat = g_application_run (G_APPLICATION (app), argc, argv);
+  g_object_unref (app);
+  return stat;
+}
+

Compile and run it. Notice how this time the window doesn’t extend when you type a lot of characters, it just scrolls and displays a slider.

+

Up: index.html, Prev: Section 4, Next: Section 6

+ + diff --git a/docs/sec6.html b/docs/sec6.html new file mode 100644 index 0000000..f8df88c --- /dev/null +++ b/docs/sec6.html @@ -0,0 +1,194 @@ + + + + + + + Gtk4 tutorial for beginners + + + +

Up: index.html, Prev: Section 5, Next: Section 7

+

String and memory management

+

GtkTextView and GtkTextBuffer have functions that use string parameters or return a string. The knowledge of strings and memory management is useful to understand how to use these functions.

+

String and memory

+

A String is an array of characters that is terminated with ‘\0’. Strings are not a C type such as char, int, float or double, but exist as a pointer to a character array. They behaves like a string type which you may be familiar from other languages. So, this pointer is often called ‘a string’.

+

In the following, a and b defined as character arrays, and are strings.

+
char a[10], *b;
+
+a[0] = 'H';
+a[1] = 'e';
+a[2] = 'l';
+a[3] = 'l';
+a[4] = 'o';
+a[5] = '\0';
+
+b = a;
+/* *b is 'H' */
+/* *(++b) is 'e' */
+

The array a has char elements and the size of ten. The first six elements are ‘H’, ‘e’, ‘l’, ‘l’, ‘o’ and ‘\0’. This array represents the string “Hello”. The first five elements are character codes that correspond to the characters. The sixth element is ‘\0’, which is the same as zero, and indicates that the string ends there. The size of the array is 10, so 4 bytes aren’t used, but that’s OK, they are just ignored.

+

The variable ‘b’ is a pointer to a character. Because b is assigned to be a, a and b point the same character (‘H’). The variable a is defined as an array and it can’t be changed. It always point the top address of the array. On the other hand, ‘b’ is a pointer, which is mutable, so b can be change. It is then possible to write statements like ++b, which means take the value in b (n address), increase it by one, and store that back in b.

+

If a pointer is NULL, it points to nothing. So, the pointer is not a string. A NULL string on the other hand will be a pointer which points to a location that contains \0, which is a string of length 0 (or ""). Programs that use strings will include bugs if you aren’t careful when using NULL pointers.

+

Another annoying problem is the memory that a string is allocated. There are four cases:

+ +

Read only string

+

A string literal in a C program is surrounded by double quotes and written as the following:

+
char *s;
+s = "Hello"
+

“Hello” is a string literal, and is stored in program memory. A string literal is read only. In the program above, s points the string literal.

+

So, the following program is illegal.

+
*(s+1) = 'a';
+

The result is undefined. Probably a bad thing will happen, for example, a segmentation fault.

+

NOTE: The memory of the literal string is allocated when the program is compiled. It is possible to view all the literal strings defined in your program by using the string command.

+

Strings defined as arrays

+

If a string is defined as an array, it’s in either stored in the static memory area or stack. This depends on the class of the array. If the array’s class is static, then it’s placed in static memory area. This allocation and memory address is fixed for the life of the program. This area can be changed and is writable.

+

If the array’s class is auto, then it’s placed in stack. If the array is defined inside a function, its default class is auto. The stack area will disappear when the function exits and returns to the caller. Arrays defined on the stack are writable.

+

+static char a[] = {'H', 'e', 'l', 'l', 'o', '\0'};
+
+void
+print_strings (void) {
+  char b[] = "Hello";
+
+  a[1] = 'a'; /* Because the array is static, it's writable. */
+  b[1] = 'a'; /* Because the array is auto, it's writable. */
+
+  printf ("%s\n", a); /* Hallo */
+  printf ("%s\n", b); /* Hallo */
+}
+

The array a is defined externally to a function and is global in its scope. Such variables are placed in static memory area even if the static class is left out. The compiler calculates the number of the elements in the right hand side (six), and then creates code that allocates six bytes in the static memory area and copies the data to this memory.

+

The array b is defined inside the function so its class is auto. The compiler calculates the number of the elements in the string literal. It has six elements as the zero termination character is also included. The compiler creates code which allocates six bytes memory in the stack and copies the data to the memory.

+

Both a and b are writable.

+

The memory is managed by the executable program. You don’t need your program to allocate or free the memory for a and b. The array a is created then the program is first run and remains for the life of the program. The array b is created on the stack then the function is called, disappears when the function returns.

+

Strings in the heap area

+

You can also get, use and release memory from the heap area. The standard C library provides malloc to get memory and free to put back memory. GLib provides the functions g_new and g_free to do the same thing, with support for some additional Glib functionality.

+
g_new (struct_type, n_struct)
+

g_new is a macro to allocate memory for an array.

+ +

For example,

+
char *s;
+s = g_new (char, 10);
+/* s points an array of char. The size of the array is 10. */
+
+struct tuple {int x, y;} *t;
+t = g_new (struct tuple, 5);
+/* t points an array of struct tuple. */
+/* The size of the array is 5. */
+

g_free frees memory.

+
void
+g_free (gpointer mem);
+

If mem is NULL, g_free does nothing. gpointer is a type of general pointer. It is the same as void *. This pointer can be casted to any pointer type. Conversely, any pointer type can be casted to gpointer.

+
g_free (s);
+/* Frees the memory allocated to s. */
+
+g_free (t);
+/* Frees the memory allocated to t. */
+

If the argument doesn’t point allocated memory it will cause an error, specifically, a segmentation fault.

+

Some Glib functions allocate memory. For example, g_strdup allocates memory and copies a string given as an argument.

+
char *s;
+s = g_strdup ("Hello");
+g_free (s);
+

The string literal “Hello” has 6 bytes because the string has ‘\0’ at the end it. g_strdup gets 6 bytes from the heap area and copies the string to the memory. s is assigned the top address of the memory. g_free returns the memory to the heap area.

+

g_strdup is described in GLib API Reference. The following is extracted from the reference.

+
+

The returned string should be freed with g_free() when no longer needed.

+
+

The function reference will describe if the returned value needs to be freed. If you forget to free the allocated memory it will remain allocated. Repeated use will cause more memory to be allocated to the program, which will grow over time. This is called a memory leak, and the only way to address this bug is to close the program (and restart it), which will automatically release all of the programs memory back to the system.

+

Some GLib functions return a string which mustn’t be freed by the caller.

+
const char *
+g_quark_to_string (GQuark quark);
+

This function returns const char* type. The qualifier const means that the returned value is immutable. The characters pointed by the returned value aren’t be allowed to be changed or freed.

+

If a variable is qualified with const, the variable can’t be assigned except during initialization.

+
const int x = 10; /* initialization is OK. */
+
+x = 20; /* This is illegal because x is qualified with const */
+

Up: index.html, Prev: Section 5, Next: Section 7

+ + diff --git a/docs/sec7.html b/docs/sec7.html new file mode 100644 index 0000000..97e9cb4 --- /dev/null +++ b/docs/sec7.html @@ -0,0 +1,341 @@ + + + + + + + Gtk4 tutorial for beginners + + + +

Up: index.html, Prev: Section 6, Next: Section 8

+

Widgets (3)

+

Open signal

+

G_APPLICATION_HANDLES_OPEN flag

+

The GtkTextView, GtkTextBuffer and GtkScrolledWindow widgets have given us a minimum editor in the previous section. We will now add a function to read a file and rework the program into a file viewer. There are many ways to implement the function and because this is a tutorial for beginners, we’ll take the easiest one.

+

When the program starts, we will give the filename to open as an argument.

+
$ ./a.out filename
+

It will open the file and insert its contents into the GtkTextBuffer.

+

To do this, we need to know how GtkApplication (or GApplication) recognizes arguments. This is described in the GIO API Reference, Application.

+

When GtkApplication is created, a flag (with the type GApplicationFlags) is provided as an argument.

+
GtkApplication *
+gtk_application_new (const gchar *application_id, GApplicationFlags flags);
+

This tutorial explains only two flags, G_APPLICATION_FLAGS_NONE and G_APPLICATION_HANDLES_OPEN. If you want to handle command line arguments, the G_APPLICATION_HANDLES_COMMAND_LINE flag is what you need. How to use the new application method is described in GIO API Reference, g_application_run, and the flag is described in the GIO API Reference, ApplicationFlags.

+
GApplicationFlags' Members
+
+G_APPLICATION_FLAGS_NONE  Default. (No argument allowed)
+  ... ... ...
+G_APPLICATION_HANDLES_OPEN  This application handles opening files (in the primary instance).
+  ... ... ...
+

There are ten flags in total, but we only need two of them so far. We’ve already used G_APPLICATION_FLAGS_NONE, as it is the simplest option, and no arguments are allowed. If you provide arguments when running the application, an error will occur.

+

The flag G_APPLICATION_HANDLES_OPEN is the second simplest option. It allows arguments but only files. The application assumes all the arguments are filenames and we will use this flag when creating our GtkApplication.

+
app = gtk_application_new ("com.github.ToshioCP.tfv3", G_APPLICATION_HANDLES_OPEN);
+

open signal

+

Now, when the application starts, two signals can be emitted.

+ +

The handler of the “open” signal is defined as follows.

+
void user_function (GApplication *application,
+                   gpointer      files,
+                   gint          n_files,
+                   gchar        *hint,
+                   gpointer      user_data)
+

The parameters are:

+ +

How to read a specified file (GFile) will be described next.

+

Making a file viewer

+

What is a file viewer?

+

A file viewer is a program that displays the text file that is given as an argument. Our file viewer will work as follows.

+ +

The program which does this is shown below.

+
#include <gtk/gtk.h>
+
+static void
+app_activate (GApplication *app, gpointer user_data) {
+  g_print ("You need a filename argument.\n");
+}
+
+static void
+app_open (GApplication *app, GFile ** files, gint n_files, gchar *hint, gpointer user_data) {
+  GtkWidget *win;
+  GtkWidget *scr;
+  GtkWidget *tv;
+  GtkTextBuffer *tb;
+  char *contents;
+  gsize length;
+  char *filename;
+
+  win = gtk_application_window_new (GTK_APPLICATION (app));
+  gtk_window_set_default_size (GTK_WINDOW (win), 400, 300);
+
+  scr = gtk_scrolled_window_new ();
+  gtk_window_set_child (GTK_WINDOW (win), scr);
+
+  tv = gtk_text_view_new ();
+  tb = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv));
+  gtk_text_view_set_wrap_mode (GTK_TEXT_VIEW (tv), GTK_WRAP_WORD_CHAR);
+  gtk_text_view_set_editable (GTK_TEXT_VIEW (tv), FALSE);
+  gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (scr), tv);
+
+  if (g_file_load_contents (files[0], NULL, &contents, &length, NULL, NULL)) {
+    gtk_text_buffer_set_text (tb, contents, length);
+    g_free (contents);
+    if ((filename = g_file_get_basename (files[0])) != NULL) {
+      gtk_window_set_title (GTK_WINDOW (win), filename);
+      g_free (filename);
+    }
+    gtk_widget_show (win);
+  } else {
+    if ((filename = g_file_get_path (files[0])) != NULL) {
+      g_print ("No such file: %s.\n", filename);
+      g_free (filename);
+    }
+    gtk_window_destroy (GTK_WINDOW (win));
+  }
+}
+
+int
+main (int argc, char **argv) {
+  GtkApplication *app;
+  int stat;
+
+  app = gtk_application_new ("com.github.ToshioCP.tfv3", G_APPLICATION_HANDLES_OPEN);
+  g_signal_connect (app, "activate", G_CALLBACK (app_activate), NULL);
+  g_signal_connect (app, "open", G_CALLBACK (app_open), NULL);
+  stat = g_application_run (G_APPLICATION (app), argc, argv);
+  g_object_unref (app);
+  return stat;
+}
+

Save it as tfv3.c. Then compile and run it.

+
$ comp tfv3
+$ ./a.out tfv3.c
+
+
File viewer
+
+

Let’s explain how the program tfv3.c works. First, the function main has only two changes from the previous version.

+ +

Next, the handler app_activate is added and is very simple. It just outputs the error message and the application quits immediately because no window is created.

+

The main functionality is the in the handler app_open. It

+ +

The following is the important file reading part of the program and is shown again below.

+
if (g_file_load_contents (files[0], NULL, &contents, &length, NULL, NULL)) {
+  gtk_text_buffer_set_text (tb, contents, length);
+  g_free (contents);
+  if ((filename = g_file_get_basename (files[0])) != NULL) {
+    gtk_window_set_title (GTK_WINDOW (win), filename);
+    g_free (filename);
+  }
+  gtk_widget_show (win);
+} else {
+  if ((filename = g_file_get_path (files[0])) != NULL) {
+    g_print ("No such file: %s.\n", filename);
+    g_free (filename);
+  }
+  gtk_window_destroy (GTK_WINDOW (win));
+}
+

The function g_file_load_contents loads the file contents into a buffer, which is automatically allocated and sets contents to point that buffer. The length of the buffer is set to length. It returns TRUE if the file’s contents are successfully loaded and FALSE if an error occurs.

+

If this function succeeds, it inserts the contents into GtkTextBuffer, frees the buffer pointed by contents, sets the title of the window, frees the memories pointed by filename and then shows the window. If it fails, it outputs an error message and destroys the window, causing the program to quit.

+

GtkNotebook

+

GtkNotebook is a container widget that uses tabs and contains multiple children. The child that is displayed depends on which tab has been selected.

+
+
GtkNotebook
+
+

Looking at the screenshots above, the left one is the window at the startup. It shows the file pr1.c and the filename is in the left tab. After clicking on the right tab, the contents of the file tfv1.c are shown instead. This is shown in the right screenshot.

+

The GtkNotebook widget is inserted as a child of GtkApplicationWindow and contains a GtkScrolledWindow for each file that is being displayed. The code to do this is given in tfv4.c and is:

+
#include <gtk/gtk.h>
+
+static void
+app_activate (GApplication *app, gpointer user_data) {
+  g_print ("You need a filename argument.\n");
+}
+
+static void
+app_open (GApplication *app, GFile ** files, gint n_files, gchar *hint, gpointer user_data) {
+  GtkWidget *win;
+  GtkWidget *nb;
+  GtkWidget *lab;
+  GtkNotebookPage *nbp;
+  GtkWidget *scr;
+  GtkWidget *tv;
+  GtkTextBuffer *tb;
+  char *contents;
+  gsize length;
+  char *filename;
+  int i;
+
+  win = gtk_application_window_new (GTK_APPLICATION (app));
+  gtk_window_set_title (GTK_WINDOW (win), "file viewer");
+  gtk_window_set_default_size (GTK_WINDOW (win), 400, 300);
+  gtk_window_maximize (GTK_WINDOW (win));
+
+  nb = gtk_notebook_new ();
+  gtk_window_set_child (GTK_WINDOW (win), nb);
+
+  for (i = 0; i < n_files; i++) {
+    if (g_file_load_contents (files[i], NULL, &contents, &length, NULL, NULL)) {
+      scr = gtk_scrolled_window_new ();
+      tv = gtk_text_view_new ();
+      tb = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv));
+      gtk_text_view_set_wrap_mode (GTK_TEXT_VIEW (tv), GTK_WRAP_WORD_CHAR);
+      gtk_text_view_set_editable (GTK_TEXT_VIEW (tv), FALSE);
+      gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (scr), tv);
+
+      gtk_text_buffer_set_text (tb, contents, length);
+      g_free (contents);
+      if ((filename = g_file_get_basename (files[i])) != NULL) {
+        lab = gtk_label_new (filename);
+        g_free (filename);
+      } else
+        lab = gtk_label_new ("");
+      gtk_notebook_append_page (GTK_NOTEBOOK (nb), scr, lab);
+      nbp = gtk_notebook_get_page (GTK_NOTEBOOK (nb), scr);
+      g_object_set (nbp, "tab-expand", TRUE, NULL);
+    } else if ((filename = g_file_get_path (files[i])) != NULL) {
+        g_print ("No such file: %s.\n", filename);
+        g_free (filename);
+    } else
+        g_print ("No valid file is given\n");
+  }
+  if (gtk_notebook_get_n_pages (GTK_NOTEBOOK (nb)) > 0)
+    gtk_widget_show (win);
+  else
+    gtk_window_destroy (GTK_WINDOW (win));
+}
+
+int
+main (int argc, char **argv) {
+  GtkApplication *app;
+  int stat;
+
+  app = gtk_application_new ("com.github.ToshioCP.tfv4", G_APPLICATION_HANDLES_OPEN);
+  g_signal_connect (app, "activate", G_CALLBACK (app_activate), NULL);
+  g_signal_connect (app, "open", G_CALLBACK (app_open), NULL);
+  stat = g_application_run (G_APPLICATION (app), argc, argv);
+  g_object_unref (app);
+  return stat;
+}
+

Most of the changes are in the function app_open. The numbers at the left of the following items are line numbers in the source code.

+ +
    GtkNotebook -- GtkNotebookPage -- GtkScrolledWindow
+ +

Up: index.html, Prev: Section 6, Next: Section 8

+ + diff --git a/docs/sec8.html b/docs/sec8.html new file mode 100644 index 0000000..0768e25 --- /dev/null +++ b/docs/sec8.html @@ -0,0 +1,356 @@ + + + + + + + Gtk4 tutorial for beginners + + + +

Up: index.html, Prev: Section 7, Next: Section 9

+

Defining a Child object

+

A Very Simple Editor

+

In the previous section we made a very simple file viewer. Now we go on to rewrite it and turn it into very simple editor. Its source file is in tfe1.c (text file editor 1).

+

GtkTextView has a feature for editing multiple lines. Therefore, we don’t need to write the program from scratch, we just add two things to the file viewer:

+ +

There are a couple of ways to store the details of GFile.

+ +

Using global variables is easy to implement. Define a sufficient size array of pointers to GFile. For example,

+
GFile *f[20];
+

The variable f[i] corresponds to the file associated to the i-th GtkNotebookPage. There are however two problems with this. The first concerns the size of the array. If a user gives too many arguments (more than 20 in the example above), it is impossible to store the additional pointers to the GFile instances. The second is the increasing difficulty for maintenance of the program. We have a small program so far, but however, if you continue developing it, the size of the program will grow. Generally speaking, the bigger the program size, the more difficult it is to keep track of and maintain global variables. Global variables can be used and changed anywhere throughout the entire program.

+

Making a child object is a good idea in terms of maintenance. One thing you need to be careful of is the difference between “child object” and “child widget”. Here we are describing a “child object”. A child object includes, and expands on its parent object, as a child object derives everything from the parent object.

+
+
Child object of GtkTextView
+
+

We will define TfeTextView as a child object of GtkTextView. It has everything that GtkTextView has. Specifically, TfeTextView has a GtkTextbuffer which corresponds to the GtkTextView inside TfeTextView. The additional important thing is that TfeTextView can also keep an additional pointer to GFile.

+

In general, this is how GObjects work. Understanding the general theory about Gobject’s is difficult, particularly for beginners. So, I will just show you the way how to write the code and avoid the theoretical side. If you want to know about GObject system, refer to the separate tutorial](https://github.com/ToshioCP/Gobject-tutorial).

+

How to Define a Child Object of GtkTextView

+

Let’s define the TfeTextView object, which is a child object of GtkTextView. First, look at the program below.

+
#define TFE_TYPE_TEXT_VIEW tfe_text_view_get_type ()
+G_DECLARE_FINAL_TYPE (TfeTextView, tfe_text_view, TFE, TEXT_VIEW, GtkTextView)
+
+struct _TfeTextView
+{
+  GtkTextView parent;
+  GFile *file;
+};
+
+G_DEFINE_TYPE (TfeTextView, tfe_text_view, GTK_TYPE_TEXT_VIEW);
+
+static void
+tfe_text_view_init (TfeTextView *tv) {
+}
+
+static void
+tfe_text_view_class_init (TfeTextViewClass *class) {
+}
+
+void
+tfe_text_view_set_file (TfeTextView *tv, GFile *f) {
+  tv -> file = f;
+}
+
+GFile *
+tfe_text_view_get_file (TfeTextView *tv) {
+  return tv -> file;
+}
+
+GtkWidget *
+tfe_text_view_new (void) {
+  return GTK_WIDGET (g_object_new (TFE_TYPE_TEXT_VIEW, NULL));
+}
+

If you are curious about the background theory of this program, that’s good, because knowing the theory is very important if you want to program GTK applications. Look at GObject API Reference. All you need is described there, or refer to GObject tutorial. It’s a tough journey especially for beginners so for now, you don’t need to know about this difficult theory. It is enough to just remember the instructions below.

+ +

This program is not perfect. It has some problems. It will be modified later.

+

Close-request signal

+

Imagine that you are using this editor. First, you run the editor with arguments. The arguments are filenames. The editor reads the files and shows the window with the text of files in it. Then you edit the text. After you finish editing, you exit the editor. The editor updates files just before the window closes.

+

GtkWindow emits the “close-request” signal before it closes. We connect the signal and the handler before_close. A handler is a C function. When a function is connected to a certain signal, we call it a handler. The function before_close is invoked when the signal “close-request” is emitted.

+
g_signal_connect (win, "close-request", G_CALLBACK (before_close), NULL);
+

The argument win is a GtkApplicationWindow, in which the signal “close-request” is defined, and before_close is the handler. G_CALLBACK cast is necessary for the handler. The program of before_close is as follows.

+
static gboolean
+before_close (GtkWindow *win, gpointer user_data) {
+  GtkWidget *nb = GTK_WIDGET (user_data);
+  GtkWidget *scr;
+  GtkWidget *tv;
+  GFile *file;
+  char *pathname;
+  GtkTextBuffer *tb;
+  GtkTextIter start_iter;
+  GtkTextIter end_iter;
+  char *contents;
+  unsigned int n;
+  unsigned int i;
+
+  n = gtk_notebook_get_n_pages (GTK_NOTEBOOK (nb));
+  for (i = 0; i < n; ++i) {
+    scr = gtk_notebook_get_nth_page (GTK_NOTEBOOK (nb), i);
+    tv = gtk_scrolled_window_get_child (GTK_SCROLLED_WINDOW (scr));
+    file = tfe_text_view_get_file (TFE_TEXT_VIEW (tv));
+    tb = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv));
+    gtk_text_buffer_get_bounds (tb, &start_iter, &end_iter);
+    contents = gtk_text_buffer_get_text (tb, &start_iter, &end_iter, FALSE);
+    if (! g_file_replace_contents (file, contents, strlen (contents), NULL, TRUE, G_FILE_CREATE_NONE, NULL, NULL, NULL)) {
+      pathname = g_file_get_path (file);
+      g_print ("ERROR : Can't save %s.", pathname);
+      g_free (pathname);
+    }
+    g_free (contents);
+  }
+  return FALSE;
+}
+

The numbers on the left of items are line numbers in the source code.

+ +

Source code of tfe1.c

+

The following is the complete source code of tfe1.c.

+
#include <gtk/gtk.h>
+
+/* Define TfeTextView Widget which is the child object of GtkTextView */
+
+#define TFE_TYPE_TEXT_VIEW tfe_text_view_get_type ()
+G_DECLARE_FINAL_TYPE (TfeTextView, tfe_text_view, TFE, TEXT_VIEW, GtkTextView)
+
+struct _TfeTextView
+{
+  GtkTextView parent;
+  GFile *file;
+};
+
+G_DEFINE_TYPE (TfeTextView, tfe_text_view, GTK_TYPE_TEXT_VIEW);
+
+static void
+tfe_text_view_init (TfeTextView *tv) {
+}
+
+static void
+tfe_text_view_class_init (TfeTextViewClass *class) {
+}
+
+void
+tfe_text_view_set_file (TfeTextView *tv, GFile *f) {
+  tv -> file = f;
+}
+
+GFile *
+tfe_text_view_get_file (TfeTextView *tv) {
+  return tv -> file;
+}
+
+GtkWidget *
+tfe_text_view_new (void) {
+  return GTK_WIDGET (g_object_new (TFE_TYPE_TEXT_VIEW, NULL));
+}
+
+/* ---------- end of the definition of TfeTextView ---------- */
+
+static gboolean
+before_close (GtkWindow *win, gpointer user_data) {
+  GtkWidget *nb = GTK_WIDGET (user_data);
+  GtkWidget *scr;
+  GtkWidget *tv;
+  GFile *file;
+  char *pathname;
+  GtkTextBuffer *tb;
+  GtkTextIter start_iter;
+  GtkTextIter end_iter;
+  char *contents;
+  unsigned int n;
+  unsigned int i;
+
+  n = gtk_notebook_get_n_pages (GTK_NOTEBOOK (nb));
+  for (i = 0; i < n; ++i) {
+    scr = gtk_notebook_get_nth_page (GTK_NOTEBOOK (nb), i);
+    tv = gtk_scrolled_window_get_child (GTK_SCROLLED_WINDOW (scr));
+    file = tfe_text_view_get_file (TFE_TEXT_VIEW (tv));
+    tb = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv));
+    gtk_text_buffer_get_bounds (tb, &start_iter, &end_iter);
+    contents = gtk_text_buffer_get_text (tb, &start_iter, &end_iter, FALSE);
+    if (! g_file_replace_contents (file, contents, strlen (contents), NULL, TRUE, G_FILE_CREATE_NONE, NULL, NULL, NULL)) {
+      pathname = g_file_get_path (file);
+      g_print ("ERROR : Can't save %s.", pathname);
+      g_free (pathname);
+    }
+    g_free (contents);
+  }
+  return FALSE;
+}
+
+static void
+app_activate (GApplication *app, gpointer user_data) {
+  g_print ("You need to give filenames as arguments.\n");
+}
+
+static void
+app_open (GApplication *app, GFile ** files, gint n_files, gchar *hint, gpointer user_data) {
+  GtkWidget *win;
+  GtkWidget *nb;
+  GtkWidget *lab;
+  GtkNotebookPage *nbp;
+  GtkWidget *scr;
+  GtkWidget *tv;
+  GtkTextBuffer *tb;
+  char *contents;
+  gsize length;
+  char *filename;
+  int i;
+
+  win = gtk_application_window_new (GTK_APPLICATION (app));
+  gtk_window_set_title (GTK_WINDOW (win), "file editor");
+  gtk_window_maximize (GTK_WINDOW (win));
+
+  nb = gtk_notebook_new ();
+  gtk_window_set_child (GTK_WINDOW (win), nb);
+
+  for (i = 0; i < n_files; i++) {
+    if (g_file_load_contents (files[i], NULL, &contents, &length, NULL, NULL)) {
+      scr = gtk_scrolled_window_new ();
+      tv = tfe_text_view_new ();
+      tb = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv));
+      gtk_text_view_set_wrap_mode (GTK_TEXT_VIEW (tv), GTK_WRAP_WORD_CHAR);
+      gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (scr), tv);
+
+      tfe_text_view_set_file (TFE_TEXT_VIEW (tv),  g_file_dup (files[i]));
+      gtk_text_buffer_set_text (tb, contents, length);
+      g_free (contents);
+      filename = g_file_get_basename (files[i]);
+      lab = gtk_label_new (filename);
+      gtk_notebook_append_page (GTK_NOTEBOOK (nb), scr, lab);
+      nbp = gtk_notebook_get_page (GTK_NOTEBOOK (nb), scr);
+      g_object_set (nbp, "tab-expand", TRUE, NULL);
+      g_free (filename);
+    } else if ((filename = g_file_get_path (files[i])) != NULL) {
+        g_print ("No such file: %s.\n", filename);
+        g_free (filename);
+    } else
+        g_print ("No valid file is given\n");
+  }
+  if (gtk_notebook_get_n_pages (GTK_NOTEBOOK (nb)) > 0) {
+    g_signal_connect (win, "close-request", G_CALLBACK (before_close), nb);
+    gtk_widget_show (win);
+  } else
+    gtk_window_destroy (GTK_WINDOW (win));
+}
+
+int
+main (int argc, char **argv) {
+  GtkApplication *app;
+  int stat;
+
+  app = gtk_application_new ("com.github.ToshioCP.tfe1", G_APPLICATION_HANDLES_OPEN);
+  g_signal_connect (app, "activate", G_CALLBACK (app_activate), NULL);
+  g_signal_connect (app, "open", G_CALLBACK (app_open), NULL);
+  stat =g_application_run (G_APPLICATION (app), argc, argv);
+  g_object_unref (app);
+  return stat;
+}
+ +

Now compile and run it. There’s a sample file in the directory tfe. Type ./a.out taketori.txt. Modify the contents and close the window. Make sure that the file is modified.

+

Now we got a very simple editor. It’s not smart. We need more features like open, save, saveas, change font and so on. We will add them in the next section and after.

+

Up: index.html, Prev: Section 7, Next: Section 9

+ + diff --git a/docs/sec9.html b/docs/sec9.html new file mode 100644 index 0000000..8bf0840 --- /dev/null +++ b/docs/sec9.html @@ -0,0 +1,463 @@ + + + + + + + Gtk4 tutorial for beginners + + + +

Up: index.html, Prev: Section 8, Next: Section 10

+

The User Interface (UI) file and GtkBuilder

+

New, Open and Save button

+

In the last section we made the almost simplest editor possible. It reads files in the app_open function at start-up and writes them out when closing the window. It works but is not very good. It would be better if we had “New”, “Open”, “Save” and “Close” buttons. This section describes how to put those buttons into the window. Signals and handlers will be explained later.

+
+
Screenshot of the file editor
+
+

The screenshot above shows the layout. The function app_open in the source code tfe2.c is as follows.

+
static void
+app_open (GApplication *app, GFile ** files, gint n_files, gchar *hint, gpointer user_data) {
+  GtkWidget *win;
+  GtkWidget *nb;
+  GtkWidget *lab;
+  GtkNotebookPage *nbp;
+  GtkWidget *scr;
+  GtkWidget *tv;
+  GtkTextBuffer *tb;
+  char *contents;
+  gsize length;
+  char *filename;
+  int i;
+
+  GtkWidget *boxv;
+  GtkWidget *boxh;
+  GtkWidget *dmy1;
+  GtkWidget *dmy2;
+  GtkWidget *dmy3;
+  GtkWidget *btnn; /* button for new */
+  GtkWidget *btno; /* button for open */
+  GtkWidget *btns; /* button for save */
+  GtkWidget *btnc; /* button for close */
+
+  win = gtk_application_window_new (GTK_APPLICATION (app));
+  gtk_window_set_title (GTK_WINDOW (win), "file editor");
+  gtk_window_set_default_size (GTK_WINDOW (win), 600, 400);
+
+  boxv = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
+  gtk_window_set_child (GTK_WINDOW (win), boxv);
+
+  boxh = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
+  gtk_box_append (GTK_BOX (boxv), boxh);
+
+  dmy1 = gtk_label_new(NULL); /* dummy label for left space */
+  gtk_label_set_width_chars (GTK_LABEL (dmy1), 10);
+  dmy2 = gtk_label_new(NULL); /* dummy label for center space */
+  gtk_widget_set_hexpand (dmy2, TRUE);
+  dmy3 = gtk_label_new(NULL); /* dummy label for right space */
+  gtk_label_set_width_chars (GTK_LABEL (dmy3), 10);
+  btnn = gtk_button_new_with_label ("New");
+  btno = gtk_button_new_with_label ("Open");
+  btns = gtk_button_new_with_label ("Save");
+  btnc = gtk_button_new_with_label ("Close");
+
+  gtk_box_append (GTK_BOX (boxh), dmy1);
+  gtk_box_append (GTK_BOX (boxh), btnn);
+  gtk_box_append (GTK_BOX (boxh), btno);
+  gtk_box_append (GTK_BOX (boxh), dmy2);
+  gtk_box_append (GTK_BOX (boxh), btns);
+  gtk_box_append (GTK_BOX (boxh), btnc);
+  gtk_box_append (GTK_BOX (boxh), dmy3);
+
+  nb = gtk_notebook_new ();
+  gtk_widget_set_hexpand (nb, TRUE);
+  gtk_widget_set_vexpand (nb, TRUE);
+  gtk_box_append (GTK_BOX (boxv), nb);
+
+  for (i = 0; i < n_files; i++) {
+    if (g_file_load_contents (files[i], NULL, &contents, &length, NULL, NULL)) {
+      scr = gtk_scrolled_window_new ();
+      tv = tfe_text_view_new ();
+      tb = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv));
+      gtk_text_view_set_wrap_mode (GTK_TEXT_VIEW (tv), GTK_WRAP_WORD_CHAR);
+      gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (scr), tv);
+
+      tfe_text_view_set_file (TFE_TEXT_VIEW (tv),  g_file_dup (files[i]));
+      gtk_text_buffer_set_text (tb, contents, length);
+      g_free (contents);
+      filename = g_file_get_basename (files[i]);
+      lab = gtk_label_new (filename);
+      gtk_notebook_append_page (GTK_NOTEBOOK (nb), scr, lab);
+      nbp = gtk_notebook_get_page (GTK_NOTEBOOK (nb), scr);
+      g_object_set (nbp, "tab-expand", TRUE, NULL);
+      g_free (filename);
+    } else if ((filename = g_file_get_path (files[i])) != NULL) {
+        g_print ("No such file: %s.\n", filename);
+        g_free (filename);
+    } else
+        g_print ("No valid file is given\n");
+  }
+  if (gtk_notebook_get_n_pages (GTK_NOTEBOOK (nb)) > 0) {
+    gtk_widget_show (win);
+  } else
+    gtk_window_destroy (GTK_WINDOW (win));
+}
+

The aim is to build the widgets of the main application window.

+ +

The number of lines to build the widgets is 33(=57-25+1). We also needed many additional variables (boxv, boxh, dmy1, …), most of which weren’t necessary, except for building the widgets. Are there any good solution to reduce these work?

+

Gtk provides GtkBuilder. It reads user interface (UI) data and builds a window. It reduces this cumbersome work.

+

The UI File

+

First, let’s look at the UI file tfe3.ui that is used to define the widget structure.

+
<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <object class="GtkApplicationWindow" id="win">
+    <property name="title">file editor</property>
+    <property name="default-width">600</property>
+    <property name="default-height">400</property>
+    <child>
+      <object class="GtkBox" id="boxv">
+        <property name="orientation">GTK_ORIENTATION_VERTICAL</property>
+        <child>
+          <object class="GtkBox" id="boxh">
+            <property name="orientation">GTK_ORIENTATION_HORIZONTAL</property>
+            <child>
+              <object class="GtkLabel" id="dmy1">
+                <property name="width-chars">10</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkButton" id="btnn">
+                <property name="label">New</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkButton" id="btno">
+                <property name="label">Open</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkLabel" id="dmy2">
+                <property name="hexpand">TRUE</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkButton" id="btns">
+                <property name="label">Save</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkButton" id="btnc">
+                <property name="label">Close</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkLabel" id="dmy3">
+                <property name="width-chars">10</property>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="GtkNotebook" id="nb">
+            <property name="hexpand">TRUE</property>
+            <property name="vexpand">TRUE</property>
+          </object>
+        </child>
+      </object>
+    </child>
+  </object>
+</interface>
+

The structure of this file is XML. Constructs that begin with < and end with > are called tags. There are two types of tags, the start tag and the end tag. For example, <interface> is a start tag and </interface> is an end tag. The UI file begins and ends with interface tags. Some tags, for example object tags, can have a class and id attributes in their start tag.

+ +

Compare this ui file and the lines 25-57 in the source code of app_open function. Those two describe the same structure of widgets.

+

You can check the ui file with gtk4-builder-tool.

+ +

It is a good idea to check your ui file before compiling.

+

GtkBuilder

+

GtkBuilder builds widgets based on the ui file.

+
GtkBuilder *build;
+
+build = gtk_builder_new_from_file ("tfe3.ui");
+win = GTK_WIDGET (gtk_builder_get_object (build, "win"));
+gtk_window_set_application (GTK_WINDOW (win), GTK_APPLICATION (app));
+nb = GTK_WIDGET (gtk_builder_get_object (build, "nb"));
+

The function gtk_builder_new_from_file reads the file given as an argument. Then, it builds the widgets and creates GtkBuilder object. The function gtk_builder_get_object (build, "win") returns the pointer to the widget win, which is the id in the ui file. All the widgets are connected based on the parent-children relationship described in the ui file. We only need win and nb for the program after this, so we don’t need to take out any other widgets. This reduces lines in the C source file.

+
$ cd tfe; diff tfe2.c tfe3.c
+58a59
+>   GtkBuilder *build;
+60,103c61,65
+<   GtkWidget *boxv;
+<   GtkWidget *boxh;
+<   GtkWidget *dmy1;
+<   GtkWidget *dmy2;
+<   GtkWidget *dmy3;
+<   GtkWidget *btnn; /* button for new */
+<   GtkWidget *btno; /* button for open */
+<   GtkWidget *btns; /* button for save */
+<   GtkWidget *btnc; /* button for close */
+< 
+<   win = gtk_application_window_new (GTK_APPLICATION (app));
+<   gtk_window_set_title (GTK_WINDOW (win), "file editor");
+<   gtk_window_set_default_size (GTK_WINDOW (win), 600, 400);
+< 
+<   boxv = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
+<   gtk_window_set_child (GTK_WINDOW (win), boxv);
+< 
+<   boxh = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
+<   gtk_box_append (GTK_BOX (boxv), boxh);
+< 
+<   dmy1 = gtk_label_new(NULL); /* dummy label for left space */
+<   gtk_label_set_width_chars (GTK_LABEL (dmy1), 10);
+<   dmy2 = gtk_label_new(NULL); /* dummy label for center space */
+<   gtk_widget_set_hexpand (dmy2, TRUE);
+<   dmy3 = gtk_label_new(NULL); /* dummy label for right space */
+<   gtk_label_set_width_chars (GTK_LABEL (dmy3), 10);
+<   btnn = gtk_button_new_with_label ("New");
+<   btno = gtk_button_new_with_label ("Open");
+<   btns = gtk_button_new_with_label ("Save");
+<   btnc = gtk_button_new_with_label ("Close");
+< 
+<   gtk_box_append (GTK_BOX (boxh), dmy1);
+<   gtk_box_append (GTK_BOX (boxh), btnn);
+<   gtk_box_append (GTK_BOX (boxh), btno);
+<   gtk_box_append (GTK_BOX (boxh), dmy2);
+<   gtk_box_append (GTK_BOX (boxh), btns);
+<   gtk_box_append (GTK_BOX (boxh), btnc);
+<   gtk_box_append (GTK_BOX (boxh), dmy3);
+< 
+<   nb = gtk_notebook_new ();
+<   gtk_widget_set_hexpand (nb, TRUE);
+<   gtk_widget_set_vexpand (nb, TRUE);
+<   gtk_box_append (GTK_BOX (boxv), nb);
+< 
+---
+>   build = gtk_builder_new_from_file ("tfe3.ui");
+>   win = GTK_WIDGET (gtk_builder_get_object (build, "win"));
+>   gtk_window_set_application (GTK_WINDOW (win), GTK_APPLICATION (app));
+>   nb = GTK_WIDGET (gtk_builder_get_object (build, "nb"));
+>   g_object_unref(build);
+138c100
+<   app = gtk_application_new ("com.github.ToshioCP.tfe2", G_APPLICATION_HANDLES_OPEN);
+---
+>   app = gtk_application_new ("com.github.ToshioCP.tfe3", G_APPLICATION_HANDLES_OPEN);
+

60,103c61,65 means 44 (=103-60+1) lines are changed to 5 (=65-61+1) lines. Therefore, 39 lines are reduced. Using ui file not only shortens C source files, but also makes the widgets’ structure clear.

+

Now I’ll show you app_open function in the C file tfe3.c.

+
static void
+app_open (GApplication *app, GFile ** files, gint n_files, gchar *hint, gpointer user_data) {
+  GtkWidget *win;
+  GtkWidget *nb;
+  GtkWidget *lab;
+  GtkNotebookPage *nbp;
+  GtkWidget *scr;
+  GtkWidget *tv;
+  GtkTextBuffer *tb;
+  char *contents;
+  gsize length;
+  char *filename;
+  int i;
+  GtkBuilder *build;
+
+  build = gtk_builder_new_from_file ("tfe3.ui");
+  win = GTK_WIDGET (gtk_builder_get_object (build, "win"));
+  gtk_window_set_application (GTK_WINDOW (win), GTK_APPLICATION (app));
+  nb = GTK_WIDGET (gtk_builder_get_object (build, "nb"));
+  g_object_unref(build);
+  for (i = 0; i < n_files; i++) {
+    if (g_file_load_contents (files[i], NULL, &contents, &length, NULL, NULL)) {
+      scr = gtk_scrolled_window_new ();
+      tv = tfe_text_view_new ();
+      tb = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv));
+      gtk_text_view_set_wrap_mode (GTK_TEXT_VIEW (tv), GTK_WRAP_WORD_CHAR);
+      gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (scr), tv);
+
+      tfe_text_view_set_file (TFE_TEXT_VIEW (tv),  g_file_dup (files[i]));
+      gtk_text_buffer_set_text (tb, contents, length);
+      g_free (contents);
+      filename = g_file_get_basename (files[i]);
+      lab = gtk_label_new (filename);
+      gtk_notebook_append_page (GTK_NOTEBOOK (nb), scr, lab);
+      nbp = gtk_notebook_get_page (GTK_NOTEBOOK (nb), scr);
+      g_object_set (nbp, "tab-expand", TRUE, NULL);
+      g_free (filename);
+    } else if ((filename = g_file_get_path (files[i])) != NULL) {
+        g_print ("No such file: %s.\n", filename);
+        g_free (filename);
+    } else
+        g_print ("No valid file is given\n");
+  }
+  if (gtk_notebook_get_n_pages (GTK_NOTEBOOK (nb)) > 0) {
+    gtk_widget_show (win);
+  } else
+    gtk_window_destroy (GTK_WINDOW (win));
+}
+

The whole source code of tfe3.c is stored in src/tfe directory. If you want to see it, click the link above.

+

Using ui string

+

GtkBuilder can build widgets using string. Use the function gtk_builder_new_from_string instead of gtk_builder_new_from_file.

+
char *uistring;
+
+uistring =
+"<interface>"
+  "<object class="GtkApplicationWindow" id="win">"
+    "<property name=\"title\">file editor</property>"
+    "<property name=\"default-width\">600</property>"
+    "<property name=\"default-height\">400</property>"
+    "<child>"
+      "<object class=\"GtkBox\" id=\"boxv\">"
+        "<property name="orientation">GTK_ORIENTATION_VERTICAL</property>"
+... ... ...
+... ... ...
+"</interface>";
+
+build = gtk_builder_new_from_stringfile (uistring);
+

This method has an advantage and disadvantage. The advantage is that the ui string is written in the source code. So ui file is not necessary on runtime. The disadvantage is that writing C string is a bit bothersome because of the double quotes. If you want to use this method, you should write a script that transforms ui file into C-string.

+ +

Using Gresource

+

Using Gresource is similar to using string. But Gresource is compressed binary data, not text data. And there’s a compiler that compiles ui file into Gresource. It can compile not only text files but also binary files such as images, sounds and so on. And after compilation, it bundles them up into one Gresource object.

+

An xml file is necessary for the resource compiler glib-compile-resources. It describes resource files.

+
<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/com/github/ToshioCP/tfe3">
+    <file>tfe3.ui</file>
+  </gresource>
+</gresources>
+ +

Save this xml text to tfe3.gresource.xml. The gresource compiler glib-compile-resources shows its usage with the argument --help.

+
$ LANG=C glib-compile-resources --help
+Usage:
+  glib-compile-resources [OPTION?] FILE
+
+Compile a resource specification into a resource file.
+Resource specification files have the extension .gresource.xml,
+and the resource file have the extension called .gresource.
+
+Help Options:
+  -h, --help                   Show help options
+
+Application Options:
+  --version                    Show program version and exit
+  --target=FILE                Name of the output file
+  --sourcedir=DIRECTORY        The directories to load files referenced in FILE from (default: current directory)
+  --generate                   Generate output in the format selected for by the target filename extension
+  --generate-header            Generate source header
+  --generate-source            Generate source code used to link in the resource file into your code
+  --generate-dependencies      Generate dependency list
+  --dependency-file=FILE       Name of the dependency file to generate
+  --generate-phony-targets     Include phony targets in the generated dependency file
+  --manual-register            Don?t automatically create and register resource
+  --internal                   Don?t export functions; declare them G_GNUC_INTERNAL
+  --external-data              Don?t embed resource data in the C file; assume it's linked externally instead
+  --c-name                     C identifier name used for the generated source code
+
+

Now run the compiler.

+
$ glib-compile-resources tfe3.gresource.xml --target=resources.c --generate-source
+

Then a C source file resources.c is generated. Modify tfe3.c and save it as tfe3_r.c.

+
#include "resources.c"
+... ... ...
+... ... ...
+build = gtk_builder_new_from_resource ("/com/github/ToshioCP/tfe3/tfe3.ui");
+... ... ...
+... ... ...
+

Then, compile and run it. The window appears and it is the same as the screenshot at the beginning of this page.

+

Up: index.html, Prev: Section 8, Next: Section 10

+ + diff --git a/docs/tfetextview_doc.html b/docs/tfetextview_doc.html new file mode 100644 index 0000000..f4c7caf --- /dev/null +++ b/docs/tfetextview_doc.html @@ -0,0 +1,215 @@ + + + + + + + Gtk4 tutorial for beginners + + + +

TfeTextView API reference

+

TfeTextView – Child object of GtkTextView. It holds GFile which the contents of GtkTextBuffer correponds to.

+

Functions

+ +

Signals

+ +

Types and Values

+ +

Object Hierarchy

+
GObject
++--GInitiallyUnowned
+   +--GtkWidget
+      +--GtkTextView
+         +--TfeTextView
+

Includes

+
#include <gtk/gtk.h>
+

Description

+

TfeTextView holds GFile which the contents of GtkTextBuffer corresponds to. File manipulation functions are added to this object.

+

Functions

+

tfe_text_view_get_file()

+
GFile *
+tfe_text_view_get_file (TfeTextView *tv);
+

Returns the copy of the GFile in the TfeTextView.

+

Parameters

+ +

tfe_text_view_open()

+
void
+tfe_text_view_open (TfeTextView *tv, GtkWidget *win);
+

Just shows a GtkFileChooserDialog so that a user can choose a file to read. This function doesn’t do any I/O operations. They are done by the signal handler connected to the response signal emitted by GtkFileChooserDialog. Therefore the caller can’t know the I/O status directly from the function. Instead, the status is informed by open-response signal. The caller needs to set a handler to this signal in advance.

+

parameters

+ +

tfe_text_view_save()

+
void
+tfe_text_view_save (TfeTextView *tv);
+

Saves the contents of a TfeTextView to a file. If tv holds a GFile, it is used. Otherwise, this function shows GtkFileChosserDialog so that a user can choose a file to save.

+

Parameters

+ +

tfe_text_view_saveas()

+
void
+tfe_text_view_saveas (TfeTextView *tv);
+

Saves the content of a TfeTextView to a file. This function shows GtkFileChosserDialog so that a user can choose a file to save.

+

Parameters

+ +

tfe_text_view_new_with_file()

+
GtkWidget *
+tfe_text_view_new_with_file (GFile *file);
+

Creates a new TfeTextView and reads the contents of the file and set it to the GtkTextBuffer corresponds to the newly created TfeTextView. Then returns the TfeTextView as GtkWidget. If an error happens, it returns NULL.

+

Parameters

+ +

Returns

+ +

tfe_text_view_new()

+
GtkWidget *
+tfe_text_view_new (void);
+

Creates a new TfeTextView and returns the TfeTextView as GtkWidget. If an error happens, it returns NULL.

+

Returns

+ +

Types and Values

+

TfeTextView

+
typedef struct _TfeTextView TfeTextView
+struct _TfeTextView
+{
+  GtkTextView parent;
+  GFile *file;
+};
+

The members of this structure are not allowed to be accessed by any outer objects. If you want to obtain a copy of the GFile, use tfe_text_view_get_file.

+

TfeTextViewClass

+
typedef struct {
+  GtkTextViewClass parent_class;
+} TfeTextViewClass;
+

No member is added because TfeTextView is a final type object.

+

enum TfeTextViewOpenResponseType

+

Predefined values for the response id given by open-response signal.

+

Members:

+ +

Signals

+

change-file

+
void
+user_function (TfeTextView *tv,
+               gpointer user_data)
+

Emitted when the GFile in the TfeTextView object is changed. The signal is emitted when:

+ +

open-response

+
void
+user_function (TfeTextView *tv,
+               TfeTextViewOpenResponseType response-id,
+               gpointer user_data)
+

Emitted after the user calls tfe_text_view_open. This signal informs the status of file opening and reading.

+ + diff --git a/docs/turtle_doc.html b/docs/turtle_doc.html new file mode 100644 index 0000000..2072d18 --- /dev/null +++ b/docs/turtle_doc.html @@ -0,0 +1,372 @@ + + + + + + + Gtk4 tutorial for beginners + + + +

Turtle’s manual

+

Turtle is a simple interpreter for turtle graphics.

+

Prerequisite and compiling

+

Turtle is written in C language. You need:

+ +

It is easy to compile the source file of turtle. If you have installed gtk4 with an option --prefix=$HOME/local, put the same option to meson so that you can install turtle under the directory $HOME/local/bin. The instruction is:

+
$ meson --prefix=$HOME/local _build
+$ ninja -C _build
+$ ninja -C _build install
+

Type the following command then turtle shows the following window.

+
$ turtle
+
+
Screenshot just after it’s executed
+
+

The left half is a text editor and the right half is a surface. Surface is like a canvas to draw shapes.

+

Write turtle language in the text editor and click on run button, then the program will be executed and it draws shapes on the surface.

+
+
Tree
+
+

If you add the following line in turtle.h, then codes to inform the status will also be compiled. However, the speed will be quite slow because of the output messages.

+
# define debug 1
+

Example

+

Imagine a turtle. The turtle has a pen and initially he is at the center of the screen, facing to the north (to the north means up on the screen). You can let the turtle down the pen or up the pen. You can order the turtle to move forward.

+
pd
+fd 100
+ +

If you click on run button, then a line segment appears on the screen. One of the endpoints of the line segment is at the center of the surface and the other is at 100 pixels up from the center. The point at the center is the start point of the turtle and the other endpoint is the end point of the movement.

+

If the turtle picks the pen up, then no line segment appears.

+
pu
+fd 100
+

The command pu means “Pen Up”.

+

The turtle can change the direction.

+
pd
+fd 100
+tr 90
+fd 100
+

The command tr is “Turn Right”. The argument is angle with degrees. Therefore, tr 90 means “Turn right by 90 degrees”. If you click on the runbutton, then two line segments appears. One is vertical and the other is horizontal.

+
+
Two line segments on the surface
+
+

Background and foreground color

+

Colors are specified with RGB. A vector (r, g, b) denotes RGB color. Each of the elements is a real number between 0 and 1.

+ +

You can express a variety of colors by changing each element.

+

There are two commands to change colors.

+ +
+
Change the foreground color
+
+

Other simple commands

+ +

An order such as fd 100, pd and so on is a statement. Statements are executed in the order from the top to the end

+

Comment and spaces

+

Characters between # (hash mark) and \n (new line) inclusive are comment. If the comment is at the end of the file, the trailing new line can be left out. Comments are ignored.

+
# draw a triangle
+fd 100 # forward 100 pixels<NEW LINE>
+tr 120 # turn right by 90 degrees<NEW LINE>
+fd 100<NEW LINE>
+tr 120<NEW LINE>
+fd 100 # Now a triangle appears.<EOF>
+

<NEW LINE> and <EOF> indicate newline code and end of file respectively. The comments in the line 1, 2, 3 and 6 are correct syntactically.

+

Spaces (white space, tab and new line) are ignored. They are used only as delimiters. Tabs are recognized as eight spaces to calculate the column number.

+

Variables and expressions

+

Variable begins alphabet followed by alphabet or digit. Key words like fd, tr can’t be variables. Distance and angle5 are variables, but 1step isn’t a variable because the first character isn’t alphabet. Variable names are case sensitive. Variables keep real numbers. Their type is the same as double in C language. Integers are casted to real numbers automatically. So 1 and 1.0 are the same value. Numbers begin digits, not signs (+ or -).

+ +
distance = 100
+fd distance
+

A value 100 is assigned to the variable distance in the first line. Assignment is a statement. Most of statements begin with commands like fd. Assignment is the only exception.

+

The example above draws a line segment of 100 pixels long.

+

You can use variables in expressions. There are 8 kinds of calculations available.

+ +

The last three symbols are mainly used in the condition of if statement.

+

Variables are registered to a symbol table when it is assigned a value for the first time. Evaluating a variable before the registration isn’t allowed and occurs an error.

+

If statement

+

Turtle language has very simple if statement.

+
if (x > 50) {
+  fd x
+}
+

There is no else part.

+

Procedures

+

Procedures are similar to functions in C language. The difference is that procedures don’t have return values.

+
dp triangle (side) {
+  fd side
+  tr 120
+  fd side
+  tr 120
+  fd side
+}
+
+triangle (100)
+

dp (Define Procedure) is a key word followed by procedure name, parameters, and body. Procedure names start alphabet followed by alphabet or digit. Parameters are a list of variables. For example

+
dp abc (a) { ... ... }
+dp abc (a, b) { ... ... }
+dp abc (a, b, c) { ... ... }
+

Body is a sequence of statements. The statements aren’t executed when the procedure is defined. They will be executed when the procedure is called later.

+

Procedures are called by the name followed by arguments.

+
dp proc (a, b, c) { ... ... }
+
+proc (100, 0, -20*3)
+

The number of parameters and arguments must be the same. Arguments can be any expressions. When you call a procedure, brackets following the procedure name must exist even if the procedure has no argument.

+

Procedure names and variable names don’t conflict.

+
dp a () {fd a}
+a=100
+a ()
+

This is a correct program.

+ +

However, using the same name to a procedure and variable makes confusing. You should avoid that.

+

Recursive call

+

Procedures can be called recursively.

+
dp repeat (n) {
+  n = n - 1
+  if (n < 0) {
+    rt
+  }
+  fd 100
+  tr 90
+  repeat (n)
+}
+
+repeat (4)
+

Repeat is called in the body of repeat. The call to itself is a recursive call. Parameters are created and set each time the procedure is called. So, parameter n is 4 at the first call but it is 3 at the second call. Each time the procedure is called, the parameter n decreases by one. Finally, it becomes less than zero, then the procedures return.

+

The program above draws a square.

+

Turtle doesn’t have any primary loop statements. It should probably be added to the future version. However, the program above shows that we can program loop with a recursive call.

+

Fractal curves

+

Recursive call can be applied to draw fractal curves. Fractal curves appear when a procedure is applied to it repeatedly. The procedure replaces a part of the curve with the contracted curve.

+
+
Tree
+
+

This shape is called tree. The basic pattern of this shape is a line segment. It is the first stage. The second stage adds two shorter line segments at the endpoint of the original segment. The new segment has 70 percent length to the original segment and the orientation is +30 or -30 degrees different. The third stage adds two shorter line segments to the second stage line segments. And repeats it several times.

+

This repeating is programmed by recursive call. Two more examples are shown here. They are Koch curve and Square Koch curve.

+
+
Koch curve
+
+
+
Square Koch curve
+
+

Tokens and punctuations

+

The following is the list of tokens.

+

Keywords:

+ +

identifiers and numbers:

+ +

Symbols for expression

+ +

Delimiters

+ +

Comments and spaces:

+ +

These characters are used to separate tokens explicitly. They doesn’t have any syntactic meaning and are ignored by the parser.

+

Grammar

+
program:
+  statement
+| program statement
+;
+
+statement:
+  primary_procedure
+| procedure_definition
+;
+
+primary_procedure:
+  PU
+| PD
+| PW expression
+| FD expression
+| TR expression
+| BC '(' expression ',' expression ',' expression ')'
+| FC '(' expression ',' expression ',' expression ')'
+| ID '=' expression
+| IF '(' expression ')' '{' primary_procedure_list '}'
+| RT
+| RS
+| ID '(' ')'
+| ID '(' argument_list ')'
+;
+
+procedure_definition:
+  DP ID '('  ')' '{' primary_procedure_list '}'
+| DP ID '(' parameter_list ')' '{' primary_procedure_list '}'
+;
+
+parameter_list:
+  ID
+| parameter_list ',' ID
+;
+
+argument_list:
+  expression
+| argument_list ',' expression
+;
+
+primary_procedure_list:
+  primary_procedure
+| primary_procedure_list primary_procedure
+;
+
+expression:
+  expression '=' expression
+| expression '>' expression
+| expression '<' expression
+| expression '+' expression
+| expression '-' expression
+| expression '*' expression
+| expression '/' expression
+| '-' expression %prec UMINUS
+| '(' expression ')'
+| ID
+| NUM
+;
+ +