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 @@ + + +
+ + + +rake html
to create html files. The files are created under html
directory.rake pdf
to create pdf file. The file is created under latex
directory.git clone https://github.com/ToshioCP/Gtk4-tutorial.git
on the command-line.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.
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 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.
+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.
+.c
=> C.rb
=> ruby.xml
=> xmlThe 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:
+-n
: Inserts a line number at the top of each line (default).-N
: No line number is inserted.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.
+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
+~~~
+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.
+ +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.
+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:
+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
.
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.
rake
converts src.md files to GFM files and store them in this directory.rake html
will convert src.md files to html files and store them in this directory.rake latex
will convert src.md files to latex files and store them in this directory. rake pdf
creates pdf file in latex
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
.
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.
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.
+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 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.
+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.
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.
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 @@ + + + + + + +The github page of this tutorial is also available. Click here.
+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.
+tfe
(Text File Editor).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.
+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.
+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.
+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.
Up: index.html, Next: Section 2
+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:
+This repository includes Ruby programs. They are used to make Markdown files, HTML files, Latex files and a PDF file.
+You need:
+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 @@ + + + + + + +Up: index.html, Prev: Section 9, Next: Section 11
+We’ve compiled a small editor so far. But Some bad signs are beginning to appear.
+gcc
and glib-compile-resources
. We should control them by one building tool.These ideas are useful to manage big source files.
+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
.
tfetextview.c
includes the definition and functions of TfeTextView.tfe.c
includes functions like main
, app_activate
, app_open
and so on, which relate to GtkApplication and GtkApplicationWindowNow 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>
Dividing a file makes it easy to maintain source files. But now we are faced with a new problem. The building step increases.
+tfe.ui
into resources.c
.tfe.c
into tfe.o
(object file).tfetextview.c
into tfetextview.o
.resources.c
into resources.o
.tfe
.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
.
sample.o
is called target.sample.c
is prerequisite.gcc -o sample.o sample.c
is recipe. Recipes follow tab characters, not spaces. (It is very important. Use tab not space, or make won’t work as you expected).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 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
.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.
rake clean
is typed on the command line.targetfile
. The task default
is the final goal of tasks.targetfile
depends on objfiles
. The variable t
is a task object.
+t.name
is a target namet.prerequisites
is an array of prerequisites.t.source
is the first element of prerequisites.sh
is a method to give the following string to shell as an argument and executes the shell.objfiles
. Each object depends on corresponding source file.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 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)
project
defines things about the project. The first parameter is the name of the project and the second is the programming language.dependency
function defines a dependency that is taken by pkg-config
. We put gtk4
as an argument.import
function imports a module. In line 5, the gnome module is imported and assigned to the variable gnome
. The gnome module provides helper tools to build GTK programs..compile_resources
is a method of the gnome module and compiles files to resources under the instruction of xml file. In line 6, the resource filename is resources
, which means resources.c
and resources.h
, and xml file is tfe.gresource.xml
. This method generates C source file by default.dependencies
. gtkdep
is used in the compilation.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 @@ + + + + + + +Up: index.html, Prev: Section 10, Next: Section 12
+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.
We’ve divided C source file into two parts. But it is not enough in terms of encapsulation.
+tfe.c
includes everything other than TfeTextView. It should be divided at least into two parts, tfeapplication.c
and tfenotebook.c
.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 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:
+parent
is GtkTextView which is C structure. It is declared in gtktextview.h
. GtkTextView is the parent of TfeTextView.file
is a pointer to GFile. It can be NULL if no file corresponds to the TfeTextView object.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 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.
+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
.
This function just initializes tv->file
to be NULL
.
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.
constructor
is called when the instance is generated. It completes the initialization of the instance.dispose
is called when the instance destructs itself. Destruction process is divided into two phases. The first one is called disposing. In this phase, the instance releases all the references to other instances. The second phase is finalizing.finalize
finishes the destruction process.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 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;
G_DECLARE_FINAL_TYPE
. So, they are not written in either tfe_text_view.h
or tfe_text_view.c
.tfe_text_view_class_init
function. For example, the dispose
pointer in GObjectClass will be overridden later in tfe_text_view_class_init
. (Override is an object oriented programming terminology. Override is rewriting ancestors’ class methods in the descendant class.)set_property
, get_property
, dispose
, finalize
and constructed
are such methods.TfeTextViewClass includes its ancestors’ class in it. It is illustrated in the following diagram.
+ +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.
+ +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);
+}
tv->file
points a GFile, decrease its reference count. g_clear_object
decreases the reference count and assigns NULL to tv->file
. In dispose handlers, we usually use g_clear_object
rather than g_object_unref
.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
.
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 @@ + + + + + + +Up: index.html, Prev: Section 11, Next: Section 13
+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.
+tb = gtk_text_view_get_buffer (tv)
. The caller requests tv
to give tb
, which is a GtkTextBuffer instance connected to tv
to the caller.activate
signal on GApplication object. When the application is activated, the signal is emitted. Then the handler, which has been connected to the signal, is invoked.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.
+g_connect_signal
or its family functions. The connection is usually done out of the object.In TfeTextView, two signals are registered.
+tv->file
is changed.tfe_text_view_open
function is not able to return the status because it uses GtkFileChooserDialog. This signal is emitted instead of the return value of the function.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
+ );
+}
g_signal_new
function is used. The signal “change-file” has no default handler (object method handler). You usually don’t need to set a default handler. If you need it, use g_signal_new_class_handler
function. See GObject API Reference, g_signal_new_class_handler for further information.g_signal_new
is the signal id. The type of signal id is guint, which is the same as unsigned int. It is used in the function g_signal_emit
.G_TYPE_INT
is a type of integer. Such fundamental types are described in GObject reference manual.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)
tv
is the object instance on which the signal is emitted.user_data
comes from the fourth argument of g_signal_connect
.parameter
comes from the fourth argument of g_signal_emit
.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 };
TFE_OPEN_RESPONSE_SUCCESS
when tfe_text_view_open
has successfully opened a file and read it.TFE_OPEN_RESPONSE_CANCEL
when the user has canceled.TFE_OPEN_RESPONSE_ERROR
when an error has occurred.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.
"change-file", G_CALLBACK (file_changed), nb);
+ g_signal_connect (GTK_TEXT_VIEW (tv),
+"open-response", G_CALLBACK (open_response), nb); g_signal_connect (TFE_TEXT_VIEW (tv),
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.
0);
+ g_signal_emit (tv, tfe_text_view_signals[CHANGE_FILE], 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); g_signal_emit (tv, tfe_text_view_signals[OPEN_RESPONSE],
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 @@ + + + + + + +Up: index.html, Prev: Section 12, Next: Section 14
+In this section I will explain functions in TfeTextView object.
+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.
../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__ */
gtk4
also has the same mechanism to avoid including it multiple times.A TfeTextView instance is created with tfe_text_view_new
or tfe_text_view_new_with_file
.
void); GtkWidget *tfe_text_view_new (
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));
+}
tfe_text_view_new
function. Just returns the value from the function g_object_new
but casts it to the pointer to GtkWidget. Initialization is done in tfe_text_view_init
which is called in the process of g_object_new
function.tfe_text_view_new_with_file
function.g_return_val_if_fail
is described in GLib API Reference, g_return_val_if_fail. And also GLib API Reference, Message Logging. It tests whether the argument file
is a pointer to GFile. If it’s true, then the program goes on to the next line. If it’s false, then it returns NULL (the second argument) immediately. And at the same time it logs out the error message (usually the log is outputted to stderr or stdout). This function is used to check the programmer’s error. If an error occurs, the solution is usually to change the (caller) program and fix the bug. You need to distinguish programmer’s errors and runtime errors. You shouldn’t use this function to find runtime errors.tfe_text_view_new
. The function creates TfeTextView instance and returns the pointer to the instance. If an error happens in tfe_text_view_new
, it returns NULL.tv
. The pointer is assigned to tb
tb
.file
and sets tv->file
to point it.gtk_text_buffer_set_modified (tb, FALSE)
sets the modification flag of tb
to FALSE. The modification flag indicates that the contents of the buffer is modified. It is used when the contents are saved. If the modification flag is FALSE, it doesn’t need to save the contents.contents
.tv
, which is a pointer to the newly created TfeTextView instance. If an error happens, NULL is returned.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);
+}
save_file
function. This function is called from saveas_dialog_response
and tfe_text_view_save
. This function saves the contents of the buffer to the file given as an argument. If error happens, it displays an error message. The class of this function is static
. Therefore, only functions in this file (tfeTetview.c
) call this function. Such static functions usally don’t have g_return_val_if_fail
function.stat
to be TRUE.gtk_window_destroy
, so that the dialog disappears when a user clicked on the button.err
and set stat
to be FLASE.contents
.saveas_dialog_response
function. This is a signal handler for the “response” signal on GtkFileChooserDialog instance created by tfe_text_view_saveas
function. This handler analyzes the response and determines whether to save the contents.GTK_RESPONSE_ACCEPT
, the user has clicked on the Save
button. So, it tries to save.file
from GtkFileChooserDialog.save_file
to save the contents to the file.save_file
has successfully saved the contents, tv->file
is updated. If the old GFile pointed by tv->file
exists, it is freed in advance. Emits “change-file” signal.file
.tfe_text_view_save
function.tfe_text_view_save
is public, i.e. it is open to the other files. So, it doesn’t have static
class. Public functions should check the parameter type with g_return_if_fail
function. If tv
is not a pointer to a TfeTextView instance, then it logs an error message and immediately returns. This function is similar to g_return_val_if_fail
, but no value is returned because tfe_text_view_save
doesn’t return a value.tb
andwin
respectively.tv->file
is NULL, no file has given yet. It calls tfe_text_view_saveas
which prompts a user to select a file or specify a new file to save.tv->file
doesn’t point GFile, somethig bad has happened. Logs an error message.save_file
to save the contents to the file.tfe_text_view_saveas
function. It shows GtkFileChooserDialog and prompts the user to choose a file.win
, which is the top-level window. The action is save mode. The buttons are Cancel and Save.saveas_dialog_response
handler.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 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);
+}
tfe_text_view_open
function.open_dialog_response
signal handler.open_dialog_response
signal handler.GTK_RESPONSE_ACCEPT
, the user has clicked on the “Cancel” button or close button on the header bar. Then, “open-response” signal is emitted. The parameter of the signal is TFE_OPEN_RESPONSE_CANCEL
.gtk_file_chooser_get_file
. If it doesn’t point GFile, maybe an error has occurred. Then it emits “open-response” signal with the parameter TFE_OPEN_RESPONSE_ERROR
.TFE_OPEN_RESPONSE_ERROR
.contents
and sets tv->file
to point the file (no duplication is not necessary). Then, it emits “open-response” signal with the parameter TFE_OPEN_RESPONSE_SUCCESS
and emits “change-file” signal.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.
tv
to a TfeTextView instance by calling tfe_text_view_new
.tfe_text_view_open
to prompt the user to select a file from GtkFileChooserDialog.open_dialog_response
.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.
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 @@ + + + + + + +Up: index.html, Prev: Section 13, Next: Section 15
+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
.
notebook_page_save
saves the current page to the file of which the name specified in the tab. If the name is untitled
or untitled
followed by digits, FileChooserDialog appears and a user can choose or specify a filename.notebook_page_close
closes the current page.notebook_page_open
shows a file chooser dialog and a user can choose a file. The file is inserted to a new page.notebook_page_new_with_file
creates a new page and the file given as an argument is read and inserted into the page.notebook_page_new
creates a new empty page.You probably find that the functions except notebook_page_close
are higher level functions of
tfe_text_view_save
tef_text_view_open
tfe_text_view_new_with_file
tfe_text_view_new
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.
+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
function.g_return_if_fail
is used to check the argument.get_untitled
function.c
is initialized at the first call of this function. After that c
keeps its value unless it is changed explicitly.c
by one and if it is zero then it returns “Untitled”. If it is a positive integer then it returns “Untitled<the integer>”, for example, “Untitled1”, “Untitled2”, and so on. The function g_strdup_printf
creates a string and it should be freed by g_free
when it becomes useless. The caller of get_untitled
is in charge of freeing the string.notebook_page_build
to build the contents of the page.notebook_page_build
function.tv
to GTK_WRAP_WORD_CHAR so that lines are broken between words or graphemes.tv
to GtkscrolledWindow as a child.scr
and lab
to the GtkNotebook instance nb
.g_object_set
sets properties on an object. The object is any object derived from GObject. In many cases, an object has its own function to set its properties, but sometimes not. In that case, use g_object_set
to set the property.nb
to the newly created page.file_changed_cb
handler.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);
+}
tfe_text_view_new_with_file
. If the function returns NULL, an error has happend. Then, it does nothing and returns.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)));
+}
notebook_page_open
function.open_response
.tfe_text_view_open
. The “open-response” signal will be emitted later to inform the result of opening and reading a file.open_response
handler.TFE_OPEN_RESPONSE_SUCCESS
or tfe_text_view_get_file
doesn’t return the pointer to a GFile, it has failed to open and read a new file. Then, what notebook_page_open
did in advance need to be canceled. The instance tv
hasn’t been a child widget of GtkScrolledWindow yet. Such instance has floating reference. Floating reference will be explained later in this subsection. You need to call g_object_ref_sink
first. Then the floating reference is converted into an ordinary reference. Now you call g_object_unref
to decrease the reference count by one.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.
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.
+gtk_window_destroy
to destroys the top-level window.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));
+}
notebook_page_save
.tfe_text_view_save
.get_current_textview
. This function gets the TfeTextView object belongs to the current page.scr
, which is a GtkScrolledWindow instance, of the current page.scr
, which is a TfeTextView instance, and returns it.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);
+}
tv
.tv
.file
points GFile, then assigns the filename of the GFile into filename
. Then, unref the GFile object file
.filename
.label
with the filename and set the label of the GtkNotebookPage with 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 @@ + + + + + + +Up: index.html, Prev: Section 14, Next: Section 16
+tfeapplication.c
includes all the code other than tfetxtview.c
and tfenotebook.c
. It does:
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;
+}
gtk_application_new
.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);
+}
gtk_window_set_application
.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.
+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 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.
gtk_widget_get_display
.gtk_style_context_add_provider_for_display
is the priority of the style provider. GTK_STYLE_PROVIDER_PRIORITY_APPLICATION
is a priority for application-specific style information. GTK_STYLE_PROVIDER_PRIORITY_USER
is also often used and it is the highest priority. So, GTK_STYLE_PROVIDER_PRIORITY_USER
is often used to a specific widget.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 *void) {
+ tfe_text_view_new (
+ 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 ();"textview {padding: 10px; font-family: monospace; font-size: 12pt;}", -1);
+ gtk_css_provider_load_from_data (provider,
+ 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.
+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);
+}
app_activate
.app_open
.These codes have become really simple thanks to tfenotebook.c and tfetextview.c.
+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.
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.
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.
+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 @@ + + + + + + +Up: index.html, Prev: Section 15, Next: Section 17
+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.
+--prefix $HOME/local
(see Section 2), type . env.sh
to set the environment variables.$ . env.sh
+src/tfe5
directory.meson _build
for configuration.ninja -C _build
for compilation. Then the application tfe
is built under the _build
directory._build/tfe
to execute it.Then the window appears. There are four buttons, New
, Open
, Save
and Close
.
Open
button, then a FileChooserDialog appears. Choose a file in the list and click on Open
button. Then the file is read and a new Notebook Page appears.Save
button, then the text is saved to the original file.Close
, then the Notebook Page disappears.Close
again, then the Untitled
Notebook Page disappears and at the same time the application quits.This is a very simple editor. It is a good practice for you to add more features.
+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)
<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+ <gresource prefix="/com/github/ToshioCP/tfe">
+ <file>tfe.ui</file>
+ </gresource>
+</gresources>
<?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>
#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;
+}
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);
#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);
+}
#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__ */
#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));
+}
$ 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 @@ + + + + + + +Up: index.html, Prev: Section 16, Next: Section 18
+Users often use menus to tell a command to the computer. It is like this:
+ +Now let’s analyze the menu above. There are two types of object.
+Menus can build a complicated structure thanks to the links of menu items.
+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.
+The following code is an example.
+static void
+
+ quit_activated(GSimpleAction *action, GVariant *parameter, gpointer app) { ... ... ...}
+"quit", NULL);
+ GSimpleAction *act_quit = g_simple_action_new (
+ g_action_map_add_action (G_ACTION_MAP (app), G_ACTION (act_quit));"activate", G_CALLBACK (quit_activated), app);
+ g_signal_connect (act_quit, "Quit", "app.quit"); GMenuItem *menu_item_quit = g_menu_item_new (
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.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.act_quit
is added to the GtkApplication instance with g_action_map_add_action
. When act_quit
is activated, it will emit “activate” signal.quit_activated
. So, if the action is activated, the handler will be invoked.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;
+}
quit_activated
is a handler of the “activate” signal on the action act_quit
. Handlers of the “activate” signal have three parameters.
+NULL
because the second argument of g_simple_action_new
(line 15) is NULL
. You don’ t need to care about it.g_signal_connect
(line 18) that connects the action and the handler.g_application_quit
immediately quits the application.app_activate
is a handler of “activate” signal on the GtkApplication instance.win
. And sets the title and the default size.act_quit
. It is stateless. The first argument of g_simple_action_new
is a name of the action and the second argument is a parameter. If you don’t need the parameter, pass NULL
. Therefore, act_quit
has a name “quit” and no parameter.app
. GtkApplication implements an interface GActionMap and GActionGroup. GtkApplication (GActionMap) can have a group of actions and the actions are added with the function g_action_map_add_action
. This function is described in Gio API Reference, g_action_map_add_action.quit_activated
.menubar
and menu
are GMenu. menu_item_menu
and menu_item_quit
are GMenuItem. menu_item_menu
has a label “Menu” and no action. menu_item_quit
has a label “Quit” and an action “app.quit”. The action “app.quit” is a combination of “app” and “quit”. “app” is a prefix and it means that the action belongs to GtkApplication. “quit” is the name of the action. Therefore, “app.quit” points the action which belongs to the GtkApplication instance and is named “quit”.menu_item_quit
to menu
. As I mentioned before, all the attributes and links are copied and used to form a new item in menu
. Therefore after the appending, menu_item_quit
is no longer needed. It is freed by g_object_unref
.menu_item_menu
to point menu
.menu_item_menu
to menubar
. Then frees menu_item_menu
. GMenu and GMenuItem are connected and finally a menu is made up. The structure of the menu is shown in the diagram below.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 @@ + + + + + + +Up: index.html, Prev: Section 17, Next: Section 19
+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.
+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) {
+ ... ... ..."fullscreen",
+ GSimpleAction *act_fullscreen = g_simple_action_new_stateful (
+ NULL, g_variant_new_boolean (FALSE));"Full Screen", "win.fullscreen");
+ GMenuItem *menu_item_fullscreen = g_menu_item_new ("change-state", G_CALLBACK (fullscreen_changed), win);
+ g_signal_connect (act_fullscreen,
+ ... ... ... }
act_fullscreen
is a GSimpleAction instance. It is created with g_simple_action_new_stateful
. The function has three arguments. The first argument “fullscreen” is the name of the action. The second argument is a parameter type. NULL
means the action doesn’t have a parameter. The third argument is the initial state of the action. It is a GVariant value. GVariant will be explained in the next subsection. The function g_variant_new_boolean (FALSE)
returns a GVariant value which is the boolean value FALSE
.menu_item_fullscreen
is a GMenuItem instance. There are two arguments. The first argument “Full Screen” is a label of menu_item_fullscreen
. The second argument is an action. The action “win.fullscreen” has a prefix “win” and an action name “fullscreen”. The prefix says that the action belongs to the window.act_fullscreen
and the “change-state” signal handler fullscreen_changed
. If the fullscreen menu is clicked, then the corresponding action act_fullscreen
is activated. But no handler is connected to the “activate” signal. Then, the default behavior for boolean-stated actions with a NULL parameter type like act_fullscreen
is to toggle them via the “change-state” signal.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); }
g_signal_connect
.TRUE
, then it maximizes the window. Otherwise unmaximizes.value
. Note: the second argument was the toggled state value, but at this stage the state of the action has the original value. So, you need to set the state with the new value by g_simple_action_set_state
.You can use “activate” signal instead of “change-state” signal, or both signals. But the way above is the simplest and the best.
+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:
+"Hello"); GVariant *value2 = g_variant_new_string (
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.
+bool = g_variant_get_boolean (value); gboolean
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.
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) {
+ ... ... ..."color",
+ GSimpleAction *act_color = g_simple_action_new_stateful ("s"), g_variant_new_string ("red"));
+ g_variant_type_new("Red", "win.color::red");
+ GMenuItem *menu_item_red = g_menu_item_new ("Green", "win.color::green");
+ GMenuItem *menu_item_green = g_menu_item_new ("Blue", "win.color::blue");
+ GMenuItem *menu_item_blue = g_menu_item_new ("activate", G_CALLBACK (color_activated), win);
+ g_signal_connect (act_color,
+ ... ... ... }
act_color
is a GSimpleAction instance. It is created with g_simple_action_new_stateful
. The function has three arguments. The first argument “color” is the name of the action. The second argument is a parameter type which is GVariantType. g_variant_type_new("s")
creates GVariantType which is a string type (G_VARIANT_TYPE_STRING
). The third argument is the initial state of the action. It is a GVariant. GVariantType will be explained in the next subsection. The function g_variant_new_string ("red")
returns a GVariant value which has the string value “red”.menu_item_red
is a GMenuItem instance. There are two arguments. The first argument “Red” is the label of menu_item_red
. The second argument is a detailed action. Its prefix is “win”, action name is “color” and target is “red”. Target is sent to the action as a parameter. The same goes for menu_item_green
and menu_item_blue
.act_color
and the “activate” signal handler color_activated
. If one of the three menus is clicked, then the action act_color
is activated with the target (parameter) which is given by the menu. No handler is connected to “change-state” signal. Then the default behavior is to call g_simple_action_set_state()
to set the state to the requested value.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));1);
+ gtk_css_provider_load_from_data (provider, color, -
+ g_free (color);
+ g_action_change_state (G_ACTION (action), parameter); }
g_signal_connect
.color
is a CSS string created by g_strdup_printf
. The parameter of g_strdup_printf
is the same as printf C standard function. g_variant_get_string
gets the string contained in parameter
. You mustn’t change or free the string.color
.g_action_change_state
. The function just sets the state of the action to the parameter by g_simple_action_set_state
. Therefore, you can use g_simple_action_set_state
instead of g_action_change_state
.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 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);
+}
g_variant_type_new
creates GVariantType. It uses a type string “s” which means string.g_variant_type_peek_string
takes a peek at vtype
. It is the string “s” given to vtype
when it was created.The following code includes stateful actions above. This program has menus like this:
+ +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;
+}
win
and lb
are GtkApplicationWindow and GtkLabel respectively. win
has a title “menu2” and its default size is 400x300. lb
is named as “lb”. The name is used in CSS. lb
is set to win
as a child.act_fullscreen
and act_color
have “win” prefix and belong to GtkApplicationWindow, they are added to win
. GtkApplicationWindow implements GActionModel interface like GtkApplication. act_quit
has “app” prefix and belongs to GtkApplication. It is added to app
.menubar
is inserted to app
. Sets show menubar property of win
to TRUE
. Note: gtk_application_window_set_show_menubar
creates GtkPopoverMenubar from GMenuModel. This is a different point between Gtk3 and Gtk4. And you can use GtkPopoverMenubar directly and set it as a descendant widget of the window. You may use GtkBox as a child widget of the window and insert GtkPopoverMenubar as the first child of the box.provider
is GtkCssProvider which is defined in line three as a static variable. Its CSS data is: label#lb {background-color: red;}
. “label#lb” is called selector. “label” is the node of GtkLabel. “#” precedes an ID which is an identifiable name of the widget. “lb” is the name of GtkLabel lb
. (See line 35). The style is surrounded by open and close braces. The style is applied to GtkLabel which has a name “lb”. Other GtkLabel have no effect from this. The provider is added to GdkDisplay.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 @@ + + + + + + +Up: index.html, Prev: Section 18, Next: Section 20
+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
.
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.
+"/com/github/ToshioCP/menu3/menu3.ui");
+ GtkBuilder *builder = gtk_builder_new_from_resource ("menubar"));
+ GMenuModel *menubar = G_MENU_MODEL (gtk_builder_get_object (builder,
+
+ 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.
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 >*/
+ 3];
+ gsize padding[ };
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:
+quit_activated
app
.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:
+fullscreen_changed
color_activated
win
.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 @@ + + + + + + +Up: index.html, Prev: Section 1, Next: Section 3
+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.
+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.
+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.
+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.
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).
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.
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
+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"
+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.
+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.
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.
+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.
+$ sudo apt-get install gnome-boxes
++
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.Fedora 34 Workstation
on the upper left of the window. Click on the button then Fedora will be executed.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
+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.
Code
.Download ZIP
and download the codes from the repository.src/tfe7
.$ 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/'
+$ 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 @@ + + + + + + +Up: index.html, Prev: Section 19, Next: Section 21
+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:
+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.
"clicked", G_CALLBACK (open_cb), nb); g_signal_connect_swapped (btno,
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”.
+tfe
has only font preference.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.
"/com/github/ToshioCP/tfe/menu.ui");
+ build = gtk_builder_new_from_resource ("menu"));
+ menu = G_MENU_MODEL (gtk_builder_get_object (build, gtk_menu_button_set_menu_model (btnm, menu);
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
.
action
is “win.open”. This specifies the action “open” belongs to the window object.accels
is an array of two strings, “<Control>o” and NULL. The first string specifies a key combination. Control key and ‘o’. If you keep pressing the control key and push ‘o’ key, then it activates the action win.open
. The second string NULL (or zero) means the end of the list (array). You can define more than one accelerator keys and the list must ends with NULL (zero). If you want to do so, the array length needs to be three or more. The parser recognizes “<control>o”, “<Shift><Alt>F2”, “<Ctrl>minus” and so on. If you want to use symbol key like “<Ctrl>-”, use “<Ctrl>minus” instead. Such relation between lower case and symbol (its character code) is specified in gdkkeysyms.h
in the Gtk4 source code.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 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>
win
. Therefore, There’s no child tag that surrounds the dialog object.gtkdialog.ui
, which is the ui file of GtkDialog. (It is in the Gtk4 source files.) This box is provided for users to add content widgets in it. The tag <child internal-child="content_area">
is put at the top of the contents. Then you need to specify an object tag and define its class as GtkBox and its id as content_area. This object is defined in gtkdialog.ui
but you need to define it again in the child tag.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;
+
+ }
+"close-request", G_CALLBACK (pref_close_cb), NULL); g_signal_connect (GTK_DIALOG (pref),
Generally, signal emission consists of five stages.
+G_SIGNAL_RUN_FIRST
. Default handler is set when a signal is registered. It is different from user signal handler, simply called signal handler, connected by g_signal_connect
series function. Default handler can be invoked in either stage 1, 3 or 5. Most of the default handlers are G_SIGNAL_RUN_FIRST
or G_SIGNAL_RUN_LAST
.g_signal_connect_after
.G_SIGNAL_RUN_LAST
.g_signal_connect_after
.G_SIGNAL_RUN_CLEANUP
.In the case of “close-request” signal, the default handler’s flag is G_SIGNAL_RUN_LAST
. The handler pref_close_cb
is not connected by g_signal_connect_after
. So the number of stages are two.
pref_close_cb
is invoked.And If the user signal handler returns TRUE, then other handlers will be stopped being invoked. Therefore, the program above prevents the invocation of the default handler and stop the closing process of the dialog.
+The following codes are extracted from tfeapplication.c
.
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"));
+ pref = GTK_DIALOG (gtk_builder_get_object (build, "close-request", G_CALLBACK (dialog_close_cb), NULL);
+ pref_close_request_handler_id = g_signal_connect (GTK_DIALOG (pref),
+
+ ... ... }
The function tfe_application_quit
destroys top-level windows and quits the application. It first disconnects the handlers from the signal “close-request”.
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.
+ +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.
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 {
+ "Contents aren't saved yet.\nAre you sure to close?");
+ gtk_label_set_text (lb_alert, "Close");
+ gtk_button_set_label (close_btn_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
+int response_id, gpointer user_data) {
+ alert_response_cb (GtkDialog *alert,
+ 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 {
+ "Contents aren't saved yet.\nAre you sure to quit?");
+ gtk_label_set_text (lb_alert, "Quit");
+ gtk_button_set_label (btn_accept,
+ gtk_widget_show (GTK_WIDGET (alert));
+ }
+ }
+static void
+
+ tfe_startup (GApplication *application) {
+
+ ... ...
+"alert"));
+ alert = GTK_DIALOG (gtk_builder_get_object (build, "close-request", G_CALLBACK (dialog_close_cb), NULL);
+ alert_close_request_handler_id = g_signal_connect (GTK_DIALOG (alert), "lb_alert"));
+ lb_alert = GTK_LABEL (gtk_builder_get_object (build, "btn_accept"));
+ btn_accept = GTK_BUTTON (gtk_builder_get_object (build,
+
+ ... ...
+ }
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;
+}
has_saved
function.gtk_text_buffer_get_modified
returns true if the content of the buffer has been modified since the modified flag had set false. The flag is set to false when:
+has_saved_all
function. This function is similar to has_saved
function. It returns true if all the pages have been saved. It returns false if at least one page has been modified since it last had been saved.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
+char *filename) {
+ notebook_page_build (GtkNotebook *nb, GtkWidget *tv,
+ ... ..."change-file", G_CALLBACK (file_changed_cb), NULL);
+ g_signal_connect (GTK_TEXT_VIEW (tv), "modified-changed", G_CALLBACK (modified_changed_cb), tv);
+ g_signal_connect (tb, }
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);
+}
file_changed_cb
handler.tv
isn’t a descendant of nb
. That is, there’s no page corresponds to tv
. Then, it isn’t necessary to change the name of the tab because no tab exists.file
is GFile, then it gets the filename and unrefs file
.file
is probably NULL and it assigns “Untitled” related name to filename
filename
and sets the tab of the page with the GtkLabel.modified_changed_cb
handler.tv
isn’t a descendant of nb
, then nothing needs to be done.file_changed_cb
and resets the filename, that means it leaves out the asterisk.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"));
+ fontbtn = GTK_FONT_BUTTON (gtk_builder_get_object (build, "font-set", G_CALLBACK (font_set_cb), win);
+ g_signal_connect (fontbtn,
+
+ ... ...
+ }
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
.
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);
+}
set_css_for_display
. This function sets CSS for GdkDisplay. The content of the function is the same as the part of startup handler in the previous version of tfeapplication.c
.set_font_for_display
. This function sets CSS with font-family, font-style, font-weight and font-size.
+g_strdup_printf
creates a new string with printf-like formatting.set_font_for_display_with_pango_font_desc
. This function takes out font-family, font-style, font-weight and font-size from the PangoFontDescription object and calls set_font
for_display`.pango_font_desc
.pango_font_desc
. The functions pango_font_description_get_style
returns an enumerated value.pango_font_desc
. The function pango_font_description_get_weight
returns an enumerated value. They corresponds to the numbers from 100 to 900.pango_font_desc
. The function pango_font_description_get_size
returns the size of a font. The unit of this size is (1/PANGO_SCALE)pt. If the font size is 10pt, the function returns 10PANGO_SCALE. PANGO_SCALE is defined as 1024. Therefore, 10PANGO_SCALE is 10240.set_font_for_display
to set CSS for the GdkDisplay.For further information, see Pango API Reference.
+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 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.
+font
is defined with a path /com/github/ToshioCP/tfe/
, the key’s location in the database is /com/github/ToshioCP/tfe/font
. Path is a string begins with and ends with a slash (/
). And it is delimited by slashes.-
) and ends with lower case or digit. No consecutive dashes are allowed. Values can be any type. GSettings stores values as GVariant type, which may contain, for example, integer, double, boolean, string or complex types like an array. The type of values needs to be defined in the schema.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.
+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
+
+Then, change the mode to advanced and quit.
+ +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.
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.
+<schemalist>
.path
and id
attributes. A path determines where the settings are stored in the conceptual global tree of settings. An id identifies the schema.font
is Monospace 12
.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.
+glib-2.0/schemas
subdirectories of all the directories specified in the environment variable XDG_DATA_DIRS
. Most common directory is /usr/share/glib-2.0/schemas
.GSETTINGS_SCHEMA_DIR
environment variable is defined, it searches all the directories specified in the variable. GSETTINGS_SCHEMA_DIR
can specify multiple directories delimited by colon (:).In the directories above, all the .gschema.xml
files are stored. Therefore, when you install your application, follow the instruction below to install your schemas.
.gschema.xml
file./usr/local/share/glib-2.0/schemas
.glib-compile-schemas
on the directory above.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')
+build_by_default
: If it is true, the target will be build by default.depend_files
: XML files to be compiled.In the example above, this method runs glib-compile-schemas
to generate gschemas.compiled
from the XML file com.github.ToshioCP.tfe.gschema.xml
. The file gschemas.compiled
is located under the build directory. If you run meson as meson _build
and ninja as ninja -C _build
, then it is under _build
directory.
After compilation, you can test your application like this:
+$ GSETTINGS_SCHEMA_DIR=_build:$GSETTINGS_SCHEMA_DIR _build/tfe
+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) {
+ ... ..."com.github.ToshioCP.tfe");
+ settings = g_settings_new ("font", fontbtn, "font", G_SETTINGS_BIND_DEFAULT);
+ g_settings_bind (settings,
+ ... ... }
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.
+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)
+$HOME/local/share/glib-2.0/schemas
is assigned to the variable 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.
+ +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 @@ + + + + + + +Up: index.html, Prev: Section 20, Next: Section 22
+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.
tfeapplication.c
only has codes related to GtkApplication.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.
+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>
TfePref
is a child object of GtkDialog
. Therefore the value of the attribute is “GtkDialog”. A parent attribute is optional but it is recommended to specify.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__ */
tfe_pref_new
creates a new TfePref object. It has a parameter win
which is used as a transient parent window to show the dialog.#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));
+}
G_DEFINE_TYPE
macro. This macro registers the TfePref type.gtk_widget_class_set_template_from_resource
function associates the description in the XML file (tfepref.ui
) with the widget. At this moment no instance is created. It just make the class to know the structure of the object. That’s why the top level tag is not <object>
but <template>
in the XML file.gtk_widget_class_bind_template_child
function binds a private variable of the object with a child object in the template. This function is a macro. The name of the private variable (fontbtn
in line 7) and the id fontbtn
in the XML file (line 24) must be the same. The pointer to the instance will be assigned to the variable fontbtn
when the instance is created.tfe_pref_new
creates an instance of TfePref. The parameter win
is a transient parent.Now, It is very simple to use this dialog. A caller just creates this object and shows it.
+
+ TfePref *pref;/* win is the top-level window */
+ pref = tfe_pref_new (win) 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.
+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:
"Are you really close without saving?"); /* alert points to a TfeAlert instance */
+ tfe_alert_set_message (alert, "Close"); tfe_alert_set_button_label (alert,
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.
+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));
+}
alert_response_cb
is a call back function of the “response” signal of TfeAlert dialog. This is the same as before except gtk_window_destroy(GTK_WINDOW (win))
is used instead of tfe_application_quit
.user_data
is a pointer to TfeWindow instance.menu
to the menu button.changed_font_cb
. This signal emits when the GSettings data is changed. The second part “font” of the signal name “changed::font” is called details. Signals can have details. If a GSettings instance has more than one key, “changed” signal emits only if the key, which has the same name as the detail, changes its value. For example, Suppose a GSettings object has three keys “a”, “b” and “c”.
+tfe_window_new
. This function creates TfeWindow instance.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;
+}
tfe_window_notebook_page_new
instead of notebook_page_new
.tfe_window_notebook_page_new_with_files
, this handler becomes very simple.main
.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)
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 @@ + + + + + + +Up: index.html, Prev: Section 21, Next: Section 23
+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:
+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.
+cairo_stroke
is a function to draw a path to the destination by the transfer.The instruction is as follows:
+cairo_stroke
to transfer the paint in the source to the destination.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;
+}
cairo_surface_t
is the type of a surface.cairo_t
is the type of a cairo context.width
and height
are the size of surface
. square_size
is the size of a square to be drawn on the surface.cairo_image_surface_create
creates an image surface. CAIRO_FORMAT_RGB24
is a constant which means that each pixel has red, green and blue data, and each data point is an 8 bits number (for 24 bits in total). Modern displays have this type of color depth. Width and height are in pixels and given as integers.cairo_set_source_rgb
creates a source pattern, which in this case is a solid white paint. The second to fourth argument are red, green and blue color values respectively, and they are of type float. The values are between zero (0.0) and one (1.0), with black being given by (0.0,0.0,0.0) and white by (1.0,1.0,1.0).cairo_paint
copies everywhere in the source to destination. The destination is filled with white pixels with this command.cairo_set_line_width
set the width of lines. In this case, the line width is set to be two pixels and will end up that same size. (It is because the transformation is identity. If the transformation isn’t identity, for example scaling with the factor three, the actual width in destination will be six (2x3=6) pixels.)cairo_stroke
transfer the source to destination through the rectangle in the mask.rectangle.png
.To compile this, type the following.
+$ gcc `pkg-config --cflags cairo` cairo.c `pkg-config --libs cairo`
+
+See the Cairo’s website for more details.
+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.
gtk_drawing_area_set_draw_func
, see Gtk API Reference, gtk_drawing_area_set_draw_func.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.
+ +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 @@ + + + + + + +Up: index.html, Prev: Section 22, Next: Section 24
+This chapter was written by Paul Schulz paul@mawsonlakes.org.
+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.
+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.
.. 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.
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 @@ + + + + + + +Up: index.html, Prev: Section 23, Next: Section 25
+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.
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.
+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>
Run
, Open
, Save
and Close
. This is similar to the toolbar of tfe text editor in Section 9. There are two differences. Run
button replaces New
button. A signal element is added to each button object. It has “name” attribute which is a signal name and “handler” attribute which is the name of its signal handler function. Options “-WI, –export-dynamic” CFLAG is necessary when you compile the application. You can achieve this by adding “export_dynamic: true” argument to executable function in meson.build
. And be careful that the handler must be defined without ‘static’ class.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>
First two files are the same as before. Color.h just includes tfetextview.h.
+ +This is the main file. It deals with:
+Run
signal handler is the point in this program.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;
+}
main
is almost same as before but there are some differences. The application ID is “com.github.ToshioCP.color”. G_APPLICATION_FLAGS_NONE
is specified so no open signal handler is necessary.win
, tv
and da
respectively. This is because these objects are often used in handlers. They never be rewritten so they’re thread safe.surface
to destination.run
.surface
if it exists. Then it destroys the top-level window and quits the application.gtk_widget_queue_draw
is called. This function adds the widget (GtkDrawingArea) to the queue to be redrawn. It is important to know that the window is redrawn whenever it is necessary. For example, when another window is moved and uncovers part of the widget, or when the window containing it is resized. But repainting surface
is not automatically notified to gtk. Therefore, you need to call gtk_widget_queue_draw
to redraw the widget.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)
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 @@ + + + + + + +Up: index.html, Prev: Section 24, Next: Section 26
+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.
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.
+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
+turtle
(See the documentation above). Then, run turtle
.Run
button, then a red square appears on the right part of the window. The side of the square is 100 pixels long.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.
Turtle uses TfeTextView and GtkDrawingArea. It is similar to color
program in the previous section.
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);
+}
busy
holds a status of the interpreter. If it is TRUE
, the interpreter is running and it is not possible to call the interpreter again because it’s not a re-entrant program. If it is FALSE
, it is safe to call the interpreter.busy
to TRUE.tb
.surface
is a static variable. It points to a cairo_surface_t
instance. It is created when the GtkDrawingArea instance is realized and whenever it is resized. Therefore, surface
isn’t NULL usually. But if it is NULL, the interpreter won’t be called.run
(runtime routine).contents
.busy
is now changed to FALSE.surface
isn’t NULL, it destroys the old surface. Then it creates a new surface. Its size is the same as the surface of the GtkDrawingArea instance.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.
Suppose that the turtle runs with the following program.
+distance = 100
+fd distance*2
+The turtle recognizes the program above and works as follows.
+yylex
to read a token in the source file. yylex
returns a code which is called “token kind” and sets a global variable yylval
with a value, which is called a semantic value. The type of yylval
is union and yylval.ID
is string and yylval.NUM
is double. There are seven tokens in the program so yylex
is called seven times.+ | token kind | +yylval.ID | +yylval.NUM | +
---|---|---|---|
1 | +ID | +distance | ++ |
2 | += | ++ | + |
3 | +NUM | ++ | 100 | +
4 | +FD | ++ | + |
5 | +ID | +distance | ++ |
6 | +* | ++ | + |
7 | +NUM | ++ | 2 | +
yylex
returns a token kind every time, but it doesn’t set yylval.ID
or yylval.NUM
every time. It is because keywords (FD
) and symbols (=
and *
) don’t have any semantic values. The function yylex
is called lexical analyzer or scanner.turtle
makes a tree structured data. This part of turtle
is called parser.turtle
analyzes the tree and executes it. This part of turtle
is called runtime routine or interpreter. The tree consists of rectangles and line segments between the rectangles. The rectangles are called nodes. For example, N_PROGRAM, N_ASSIGN, N_FD and N_MUL are nodes.
+turtle
checks if the first child is ID. If it’s ID, then turtle
looks for the variable in the variable table. If it doesn’t exist, it registers the ID (distance
) to the table. Then go back to the N_ASSIGN node.turtle
calculates the second child. In this case its a number 100. Saves 100 to the variable table at the distance
record.turtle
goes back to N_PROGRAM then go to the next node N_FD. It has only one child. Goes down to the child N_MUL.distance
and gets the value 100. The second child is a number 2. Multiplies 100 by 2 and gets 200. Then turtle
goes back to N_FD.turtle
knows the distance is 200. It moves the cursor forward by 200 pixels. The segment is drawn on the surface (surface
).run_cb
.run_cb
calls gtk_widget_queue_draw
and put the GtkDrawingArea widget to the queue.draw_func
is called. The function copies the surface (surface
) to the surface in the GtkDrawingArea.Actual turtle program is more complicated than the example above. However, what turtle does is basically the same. Interpretation consists of three parts.
+The source files are:
+turtle.lex
turtle.y
turtle.h
, turtle_lex.h
turtleapplication.c
turtle.ui
, turtle.gresources.xml
and meson.build
The compilation process is a bit complicated.
+turtle.ui
to resources.c
according to turtle.gresource.xml
. It also generates resources.h
.turtle.y
to turtle_parser.c
and generates turtle_parser.h
turtle.lex
to turtle_lex.c
.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
.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)
gcc
in linux.#include <math.h>
and also link the library with the linker.turtle.gresource.xml
.turtle.y
to turtle_parser.c
and turtle_parser.h
by bison. The function custom_target
creates a custom top level target. See Meson build system website, custom target for further information.turtle.lex
to turtle_lex.c
by flex.turtleparser[1]
refers to tirtle_parser.h
which is the second output in the line 13.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.
+strlen
, in line 62, is defined in string.h
The function atof
, in line 37, is defined in stdlib.h
.nline
and ncolumn
. The function get_location
(line 58-63) sets yylloc
to point the start and end point of yytext
in the buffer. This function is declared here so that it can be called before the function is defined.%option noyywrap
) must be specified when you have only single source file to the scanner. Refer to “9 The Generated Scanner” in the flex documentation in your distribution for further information. (The documentation is not on the internet.)REAL_NUMBER
and IDENTIFIER
are names. A name begins with a letter or an underscore followed by zero or more letters, digits, underscores (_
) or dashes (-
). They are followed by regular expressions which are their definition. They will be used in rules section and will expand to the definition. You can leave out such definitions here and use regular expressions in rules section directly.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 a patternget_location (yytext); yylval.NUM = atof (yytext); return NUM;
is an action.{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.
{REAL_NUMBER}
.ncolumn
and yylloc
with get_location
.atof
converts the string “123.4” to double type number 123.4.yylval.NUM
.yylex
returns NUM
to the caller.Then the caller knows the input is NUM
(number), and its value is 123.4.
.
(dot) matches any character except newline. Therefore, a comment begins #
followed by any characters except newline. No action happens.ncolumn
by one.nline
by one and resets ncolumn
.ncolumn
and yylloc
, and return the codes of the keywords.IDENTIFIER
is defined in line 17. The location variables are updated and the name of the identifier is assigned to yylval.ID
. The memory of the name is allocated by the function g_strdup
. The memory is registered to the list (GSlist type list). The memory will be freed after the runtime routine finishes. Returns ID
.YYUNDEF
.This section is just copied to C source file.
+get_location
. The location of the input is recorded to nline
and ncolumn
. A variable yylloc
is referred by the parser. It is a C structure and has four members, first_line
, first_column
, last_line
and last_column
. They point the start and end of the current input text.YY_BUFFER_STATE
is a pointer points the input buffer.init_flex
is called by run_cb
signal handler, which is called when Run
button is clicked on. run_cb
calls init_flex
with one argument which is the copy of the content of GtkTextBuffer. yy_scan_string
sets the input buffer to read from the text.finalize_flex
is called after runtime routine finishes. It deletes the input buffer.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.
+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 kind | +yylval.ID | +yylval.NUM | +
---|---|---|---|
1 | +FC | ++ | + |
2 | +( | ++ | + |
3 | +NUM | ++ | 1.0 | +
4 | +, | ++ | + |
5 | +NUM | ++ | 0.0 | +
6 | +, | ++ | + |
7 | +NUM | ++ | 0.0 | +
8 | +) | ++ | + |
9 | +PD | ++ | + |
10 | +ID | +distance | ++ |
11 | += | ++ | + |
12 | +NUM | ++ | 100.0 | +
13 | +ID | +angle | ++ |
14 | += | ++ | + |
15 | +NUM | ++ | 90.0 | +
16 | +FD | ++ | + |
17 | +ID | +distance | ++ |
18 | +TR | ++ | + |
19 | +ID | +angle | ++ |
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.
+statement
is program.statement statement
is program statemet
. Therefore, it is program.statement statement statement
is program statemet
. Therefore, it is program.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 kind | +yylval.ID | +yylval.NUM | +parse | +S/R | +
---|---|---|---|---|---|
1 | +FC | ++ | + | FC | +S | +
2 | +( | ++ | + | FC( | +S | +
3 | +NUM | ++ | 1.0 | +FC(NUM | +S | +
+ | + | + | + | FC(expression | +R | +
4 | +, | ++ | + | FC(expression, | +S | +
5 | +NUM | ++ | 0.0 | +FC(expression,NUM | +S | +
+ | + | + | + | FC(expression,expression | +R | +
6 | +, | ++ | + | FC(expression,expression, | +S | +
7 | +NUM | ++ | 0.0 | +FC(expression,expression,NUM | +S | +
+ | + | + | + | FC(expression,expression,expression | +R | +
8 | +) | ++ | + | FC(expression,expression,expression) | +S | +
+ | + | + | + | primary_procedure | +R | +
+ | + | + | + | statement | +R | +
+ | + | + | + | program | +R | +
9 | +PD | ++ | + | program PD | +S | +
+ | + | + | + | program primary_procedure | +R | +
+ | + | + | + | program statement | +R | +
+ | + | + | + | program | +R | +
10 | +ID | +distance | ++ | program ID | +S | +
11 | += | ++ | + | program ID= | +S | +
12 | +NUM | ++ | 100.0 | +program ID=NUM | +S | +
+ | + | + | + | program ID=expression | +R | +
+ | + | + | + | program primary_procedure | +R | +
+ | + | + | + | program statement | +R | +
+ | + | + | + | program | +R | +
13 | +ID | +angle | ++ | program ID | +S | +
14 | += | ++ | + | program ID= | +S | +
15 | +NUM | ++ | 90.0 | +program ID=NUM | +S | +
+ | + | + | + | program ID=expression | +R | +
+ | + | + | + | program primary_procedure | +R | +
+ | + | + | + | program statement | +R | +
+ | + | + | + | program | +R | +
16 | +FD | ++ | + | program FD | +S | +
17 | +ID | +distance | ++ | program FD ID | +S | +
+ | + | + | + | program FD expression | +R | +
+ | + | + | + | program primary_procedure | +R | +
+ | + | + | + | program statement | +R | +
+ | + | + | + | program | +R | +
18 | +TR | ++ | + | program TR | +S | +
19 | +ID | +angle | ++ | program TR ID | +S | +
+ | + | + | + | program TR expression | +R | +
+ | + | + | + | program primary_procedure | +R | +
+ | + | + | + | program statement | +R | +
+ | + | + | + | program | +R | +
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 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;
+ };
+}
+yylex
is shared by parser implementation file and scanner file.yyparse
and run
is called by run_cb
in turtleapplication.c
.node_t
is the type of the semantic value of nterms. The header file defines YYSTYPE
, which is the semantic value type, with all the token and nterm value types. The following is extracted from the header file./* 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 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 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); }
+;
+program
is statement
.statement
is reduced to program
, an action node_top=$$=$1;
is executed.node_top
is a static variable. It points the top node of the tree.$$
is a semantic value of the result, which is program
in the second line of the example above. The semantic value of program
is a pointer to node_t
type structure. It was defined in the declaration section.$1
is a semantic value of the first component, which is statement
. The semantic value of statement
is also a pointer to node_t
.statement
is primary_procedure
. There’s no action specified. Then, the default action is executed. It is $$ = $1
.primary_procedure
is FD
followed by expression. The action calls tree1
and assigns its return value to $$
. tree1
makes a tree node. The tree node has type and union of three pointers to children nodes, string or double.node --+-- type
+ +-- union contents
+ +---struct {node_t *child1, *child2, *child3;};
+ +---char *name
+ +---double value
+tree1
assigns the four arguments to type, child1, child2 and child3 members.expression
is NUM
.tree2
makes a tree node. The paremeters of tree2
are a type and a semantic value.Suppose the parser reads the following program.
+fd 100
+What does the parser do?
+FD
. Maybe it is the start of primary_procedure
, but parser needs to read the next token.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.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
.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
.statement
to program
. The semantic value of statement
is assigned to the one of program
and the static variable node_top
.node_top
points the node N_FD
and the node N_FD
points the node N_NUM
.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); }
+;
+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.
+There are three functions, tree1
, tree2
and tree3
.
tree1
creates a node and sets the node type and pointers to its three children (NULL is possible).tree2
creates a node and sets the node type and a value (double).tree3
creates a node and sets the node type and a pointer to a string.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 *int type, node_t *child1, node_t *child2, node_t *child3) {
+ tree1 (
+ node_t *new_node;
+sizeof (node_t)));
+ list = g_slist_prepend (list, g_malloc (
+ 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 *int type, double value) {
+ tree2 (
+ node_t *new_node;
+sizeof (node_t)));
+ list = g_slist_prepend (list, g_malloc (
+ new_node = (node_t *) list->data;
+ new_node->type = type;
+ value(new_node) = value;return new_node;
+
+ }
+
+ node_t *int type, char *name) {
+ tree3 (
+ node_t *new_node;
+sizeof (node_t)));
+ list = g_slist_prepend (list, g_malloc (
+ new_node = (node_t *) list->data;
+ new_node->type = type;
+ name(new_node) = name;return new_node;
+ }
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
+void) {
+ init_table (0;
+ tp = }
init_table
initializes the table. This must be called before any registrations.
There are five functions to access the table,
+proc_install
installs a procedure.var_install
installs a variable.proc_lookup
looks up a procedure. If the procedure is found, it returns a pointer to the node. Otherwise it returns NULL.var_lookup
looks up a variable. If the variable is found, it returns TRUE and sets the pointer (argument) to point the value. Otherwise it returns FALSE.var_replace
replaces the value of a variable. If the variable hasn’t registered yet, it installs the variable.int
+int type, char *name) {
+ tbl_lookup (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
+int type, char *name, object_t object) {
+ tbl_install (if (tp >= MAX_TABLE_SIZE)
+ "Symbol table overflow.\n");
+ runtime_error (else if (tbl_lookup (type, name) >= 0)
+ "Name %s is already registered.\n", name);
+ runtime_error (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
+char *name, node_t *node) {
+ proc_install (
+ object_t object;
+ object.node = node;
+ tbl_install (PROC, name, object);
+ }
+void
+char *name, double value) {
+ var_install (
+ object_t object;
+ object.value = value;
+ tbl_install (VAR, name, object);
+ }
+void
+char *name, double value) {
+ var_replace (int i;
+ if ((i = tbl_lookup (VAR, name)) >= 0)
+
+ table[i].object.value = value;else
+
+ var_install (name, value);
+ }
+
+ node_t *char *name) {
+ proc_lookup (int i;
+ if ((i = tbl_lookup (PROC, name)) < 0)
+ return NULL;
+ else
+ return table[i].object.node;
+
+ }
+
+ gbooleanchar *name, double *value) {
+ var_lookup (int i;
+ if ((i = tbl_lookup (VAR, name)) < 0)
+ return FALSE;
+ else {
+
+ *value = table[i].object.value;return TRUE;
+
+ } }
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
+void) {
+ init_stack (0;
+ sp = sp_biggest = }
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)
+drawline
. The runtime routine stores the name drawline
and the node of the procedure to the symbol table.angle
and distance
.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.
+ +There are four functions to access the stack.
+stack_push
pushes data to the stack.stack_lookup
searches the stack for the variable given its name as an argument. It searches only the parameters of the latest procedure. It returns TRUE and sets the argument value
to point the value, if the variable has been found. Otherwise it returns FALSE.stack_replace
replaces the value of the variable in the stack. If it succeeds, it returns TRUE. Otherwise returns FALSE.stack_return
throws away the latest parameters. The stack pointer goes back to the point before the latest procedure call so that it points to parameters of the previous called procedure.void
+char *name, double value) {
+ stack_push (if (sp >= MAX_STACK_SIZE)
+ "Stack overflow.\n");
+ runtime_error (else {
+
+ stack[sp].name = name;
+ stack[sp++].value = value;
+ sp_biggest = sp > sp_biggest ? sp : sp_biggest;
+ }
+ }
+int
+char *name) {
+ stack_search (int depth, i;
+
+if (sp == 0)
+ return -1;
+ int) stack[sp-1].value;
+ depth = (if (depth + 1 > sp) /* something strange */
+ "Stack error.\n");
+ runtime_error (for (i=0; i<depth; ++i)
+ if (strcmp(name, stack[sp-(i+2)].name) == 0) {
+ return sp-(i+2);
+
+ }return -1;
+
+ }
+
+ gbooleanchar *name, double *value) {
+ stack_lookup (int i;
+
+if ((i = stack_search (name)) < 0)
+ return FALSE;
+ else {
+
+ *value = stack[i].value;return TRUE;
+
+ }
+ }
+
+ gbooleanchar *name, double value) {
+ stack_replace (int i;
+
+if ((i = stack_search (name)) < 0)
+ return FALSE;
+ else {
+
+ stack[i].value = value;return TRUE;
+
+ }
+ }
+void
+void) {
+ stack_return(int depth;
+
+if (sp <= 0)
+ return;
+ int) stack[sp-1].value;
+ depth = (if (depth + 1 > sp) /* something strange */
+ "Stack error.\n");
+ runtime_error (1;
+ sp -= depth + }
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
.
init_cairo
initializes static variables and cairo context. The variables keep pen status (up or down), direction, initial location, line width and color. The size of the surface
changes according to the size of the window. Whenever a user drags and resizes the window, the surface
is also resized. init_cairo
gets the size first and sets the initial location of the turtle (center of the surface) and the transformation matrix.destroy_cairo
just destroys the cairo context.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).
+ +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;
+
+ gbooleanvoid) {
+ init_cairo (int width, height;
+
+ cairo_matrix_t matrix;
+
+ pen = TRUE;90.0;
+ angle = 0.0;
+ cur_x = 0.0;
+ cur_y = 2.0;
+ line_width = 0.95; bc.green = 0.95; bc.blue = 0.95;
+ bc.red = 0.0; fc.green = 0.0; fc.blue = 0.0;
+ fc.red =
+if (surface) {
+
+ width = cairo_image_surface_get_width (surface);
+ height = cairo_image_surface_get_height (surface);1.0; matrix.xy = 0.0; matrix.x0 = (double) width / 2.0;
+ matrix.xx = 0.0; matrix.yy = -1.0; matrix.y0 = (double) height / 2.0;
+ matrix.yx =
+
+ 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); }
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:
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)
+ "No expression to evaluate.\n");
+ runtime_error (#define calc(op) eval (child1(node)) op eval (child2(node))
+switch (node->type) {
+ case N_EQ:
+ double) calc(==);
+ value = (break;
+ case N_GT:
+ double) calc(>);
+ value = (break;
+ case N_LT:
+ double) calc(<);
+ value = (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)
+ "Division by zerp.\n");
+ runtime_error (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) )
+ "Variable %s not defined.\n", name(node));
+ runtime_error (break;
+ case N_NUM:
+
+ value = value(node);break;
+ default:
+ "Illegal expression.\n");
+ runtime_error (
+ }return value;
+ }
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)
+ "Node is NULL.\n");
+ runtime_error (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 */
+ line_width = eval (child1(node)); break;
+ case N_FD:
+ /* distance */
+ d = eval (child1(node)); 180);
+ x = d * cos (angle*M_PI/180);
+ y = d * sin (angle*M_PI//* 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).*/
+ /* If the above fails, tries to replace the value in the table. If the variable isn't in the table, installs it, */
+ var_replace (name, d); break;
+ case N_IF:
+ if (eval (child1(node)))
+
+ execute (child2(node));break;
+ case N_RT:
+
+ ret_level--;break;
+ case N_RS:
+
+ pen = TRUE;90.0;
+ angle = 0.0;
+ cur_x = 0.0;
+ cur_y = 2.0;
+ line_width = 0.0; fc.green = 0.0; fc.blue = 0.0;
+ fc.red = /* To change background color, use bc. */
+ break;
+ case N_procedure_call:
+
+ name = name(child1(node));
+ node_t *proc = proc_lookup (name);if (! proc)
+ "Procedure %s not defined.\n", name);
+ runtime_error (if (strcmp (name, name(child1(proc))) != 0)
+ "Unexpected error. Procedure %s is called, but invoked procedure is %s.\n", name, name(child1(proc)));
+ runtime_error (/* 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) {
+ 0.0); /* number of argument == 0 */
+ stack_push (NULL, else
+ } "Procedure %s has different number of argument and parameter.\n", name);
+ runtime_error (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];
+ 0;
+ n = for (; param_list->type == N_parameter_list; param_list = child1(param_list)) {
+ if (arg_list->type != N_argument_list)
+ "Procedure %s has different number of argument and parameter.\n", name);
+ runtime_error (if (n >= TEMP_STACK_SIZE)
+ "Too many parameters. the number must be %d or less.\n", TEMP_STACK_SIZE);
+ runtime_error (
+ 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)
+ "Too many parameters. the number must be %d or less.\n", TEMP_STACK_SIZE);
+ runtime_error (
+ temp_param[n] = NULL;double) n;
+ temp_arg[n] = (
+ ++n;else
+ } "Unexpected error.\n");
+ runtime_error (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:
+ "Unknown statement.\n");
+ runtime_error (
+ } }
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:
+ +Runtime routine just stores the procedure to the symbol table with its name and node.
+ +When the parser reads the fifth line in the example, it creates nodes like this:
+ +When the runtime routine meets N_procedure_call
node, it behaves like this:
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.proc_level
by one. Sets ret_level
to the same value as proc_level
. Calls stack_return
.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.
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
+void) {
+ run (int i;
+
+if (! init_cairo()) {
+ "Cairo not initialized.\n");
+ g_print (return;
+
+ }
+ init_table();
+ init_stack();1;
+ ret_level = proc_level =
+ 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
+char *format, ...) {
+ runtime_error (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 != '%') {
+ 0] = *f;
+ b[1] = '\0';
+ b["%s", b);
+ g_print (continue;
+
+ }switch (*++f) {
+ case 's':
+ char *);
+ s = va_arg(args, "%s", s);
+ g_print (break;
+ case 'f':
+ double);
+ v = va_arg(args, "%f", v);
+ g_print(break;
+ case 'd':
+ int);
+ i = va_arg(args, "%d", i);
+ g_print(break;
+ default:
+ 0] = '%';
+ b[1] = *f;
+ b[2] = '\0';
+ b["%s", b);
+ g_print (break;
+
+ }
+ }
+ va_end (args);
+1);
+ longjmp (buf, }
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 @@ + + + + + + +Up: index.html, Prev: Section 25, Next: Section 27
+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.
+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.
+ +The instruction to build the whole list related objects is:
+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};
+const char * const *) array); GtkStringList *stringlist = gtk_string_list_new ((
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.
gtk_string_list_append
appends an item to the listgtk_string_list_remove
removes an item from the listgtk_string_list_get_string
gets a string in the listSee Gtk4 API Reference, GtkStringList for further information.
+I’ll explain the other list objects later.
+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 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.
+ +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 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 provides signals for users to configure a GtkListItem object. There are four signals.
+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.
I think the program is not so difficult. If you feel some difficulty, read this section again, especially GtkSignalListItemFactory subsubsection.
+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 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.
const char *attributes, GFile *file); GtkDirectoryList *gtk_directory_list_new (
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.
key | +meaning | +
---|---|
standard::type | +file type. for example, regular file, directory, symbolic link, etc. | +
standard::name | +filename | +
standard::size | +file size in bytes | +
access::can-read | +read privilege if the user is able to read the file | +
time::modified | +the 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 ("standard::name", file);
+ GtkDirectoryList *dl = gtk_directory_list_new ( 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>"
<lookup name="item">GtkListItem</lookup>
gives the value of the item property of the GtkListItem. This will be the second argument of the function. The first parameter is always the GListItem instance.gtk_file_name
function first check the info
parameter. Because it can be NULL when GListItem item
is unbound. If its GFileInfo, then return the filename (copy of the filename).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.
-Wl
is a C compiler option that passes the following option to the linker.--export-dynamic
is a linker option. The following is cited from the linker document. “When creating a dynamically linked executable, add all symbols to the dynamic symbol table. The dynamic symbol table is the set of symbols which are visible from dynamic objects at run time.”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
+
+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 @@ + + + + + + +Up: index.html, Prev: Section 26, Next: Section 28
+GtkGridView is similar to GtkListView. It displays a GListModel as a grid, which is like a square tessellation.
+ +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 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
+
+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.
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">'list'</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">'grid'</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.
+btnlist
and btngrid
. These two buttons work as selection buttons to switch from list to grid and vice versa. These two buttons are connected to a stateful action win.view
. This action is stateful and has a parameter. Such action consists of prefix, action name and parameter. The prefix of the action is win
, which means the action belongs to the top level window. So, a prefix gives the scope of the action. The action name is view
. The parameters are list
or grid
, which show the state of the action. A parameter is also called a target, because it is a target to which the buttons are clicked on to change the action state. We often write the detailed action like “win.view::list” or “win.view::grid”.The action view
is created, connected to the “activate” signal handler and inserted to the window (action map) as follows.
"view", g_variant_type_new("s"), g_variant_new_string ("list"));
+ act_view = g_simple_action_new_stateful ("activate", G_CALLBACK (view_activated), scr); /* scr is the GtkScrolledWindow object */
+ g_signal_connect (act_view, g_action_map_add_action (G_ACTION_MAP (win), G_ACTION (act_view));
The signal handler view_activated
will be explained later.
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 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.
+btnlist
has been clicked, then parameter
is a GVariant of the string “list”.btngrid
has been clicked, then parameter
is a GVariant of the string “grid”.The third parameter user_data
points GtkScrolledWindow, which is set in the g_signal_connect
function.
g_variant_get_string
gets the string from the GVariant variable.scr
. The function gtk_scrolled_window_set_child
decreases the reference count of the old child by one. And it increases the reference count of the new child by one.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
+int position, gpointer user_data) {
+ list_activate (GtkListView *list,
+ 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
+int position, gpointer user_data) {
+ grid_activate (GtkGridView *grid,
+ 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);
+ }
+
+ ... ...
+ ... ...
+"activate", G_CALLBACK (list_activate), NULL);
+ g_signal_connect (GTK_LIST_VIEW (list), "activate", G_CALLBACK (grid_activate), NULL); g_signal_connect (GTK_GRID_VIEW (grid),
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.
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);
+}
tfe
application. GAppInfo is an interface and the variable appinfo
points a GDesktopAppInfo instance. GAppInfo is a collection of information of an application.tfe
) with an argument file
. g_app_info_launch
has four parameters. The first parameter is GAppInfo object. The second parameter is a list of GFile objects. In this function, only one GFile instance is given to tfe
, but you can give more arguments. The third parameter is GAppLaunchContext, but this program gives NULL instead. The last parameter is the pointer to the pointer to a GError.g_list_free_full
frees the memories used by the list and items.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.
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.
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.ui
into 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 @@ + + + + + + +Up: index.html, Prev: Section 27, Next: Section 29
+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.
notify
set_title
app_activate
. This is a handler of “activate” signal on GtkApplication instance.app_startup
. This is a handler of “startup”signal. But nothing is done in this function.main
.The function app_activate
is an actual main body in exp.c
.
Constant expression provides constant value or instance when it is evaluated.
+100);
+ expression = gtk_constant_expression_new (G_TYPE_INT,if (gtk_expression_evaluate (expression, NULL, &value)) {
+ "%d", g_value_get_int (&value));
+ s = g_strdup_printf (
+ gtk_label_set_text (GTK_LABEL (label1), s);
+ g_free (s);else
+ } "The constant expression wasn't evaluated correctly.\n");
+ g_print (
+ gtk_expression_unref (expression); g_value_unset (&value);
gtk_constant_expression_new
function. The parameter of the function is a type (GType) and a value (or instance).gtk_expression_evaluate
evaluates the expression. It has three parameters, the expression to evaluate, this
instance and GValue for being set with the value. this
instance isn’t necessary for constant expressions. Therefore the second argument is NULL. gtk_expression_evaluate
returns TRUE if it successfully evaluates the expression. Otherwise it returns FALSE.value
is set with the value of the expression. The type of the value is int. g_strdup_printf
converts the value to a string s
.label1
is set with s
. The string s
needs to be freed.Constant expression is usually used to give a constant value or instance to another 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.
+"label"); expression = gtk_property_expression_new (GTK_TYPE_LABEL, another_expression,
another_expression
is expected to give a GtkLabel instance when it is evaluated. For example,
"Hello");
+ label = gtk_label_new (
+ another_expression = gtk_constant_expression_new (GTK_TYPE_LABEL, label);"label"); expression = gtk_property_expression_new (GTK_TYPE_LABEL, another_expression,
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.
"buffer");
+ expression1 = gtk_property_expression_new (GTK_TYPE_ENTRY, NULL, "text");
+ expression2 = gtk_property_expression_new (GTK_TYPE_ENTRY_BUFFER, expression1, "label", entry); gtk_expression_bind (expression2, label2,
expression1
looks up “buffer” property of this
object, which is GTK_TYPE_ENTRY
type.expression2
looks up “text” property of GtkEntryBuffer object given by epression1
.gtk_expression_bind
binds a property to a value given by the expression. In this program, it binds a “label” property in label2
to the value evaluated with expresion2
with entry
as this
object. The evaluation process is as follows.
+expression2
is evaluated. But it includes expression1
so expression1
is evaluated in advance.expression1
is NULL, this
object is used. this
is given by gtk_expression_bind
. It is entry
(GtkEntry instance). expression1
looks up “buffer” property in entry
. It is a GtkEntryBuffer instance buffer
. (See line 64 in exp.c
.)expression2
looks up “text” property in buffer
. It is a text held in entry
.label2
.gtk_expression_bind
creates a GtkExpressionWatch. (But it isn’t assigned to a variable in the program above. If you want to keep the GtkExpressionWatch instance, assign it to a variable.)
+ GtkExpressionWatch *watch;"label", entry); watch = gtk_expression_bind (expression2, label2,
expression2
changes, it evaluates expression2
and set “label” property in label2
. So, the change of the text in entry
makes the “label” property reflect it immediately.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);
value_type
is the type of the value when it is evaluated.marshal
is a marshaller. You can assign NULL. If it is NULL, then g_cclosure_marshal_generic ()
is used as a marshaller. It is a generic marshaller function implemented via libffi.n_params
is the number of parameters.params
points expressions for each parameter of the call back function.callback_func
is a callback function.user_data
is user data. You can add it for the closure. It is like user_data
in g_signal_connect
. If it is not necessary, assign NULL.user_destroy
is a destroy notify for user_data
. It is called to destroy user_data
when it is no longer needed. If NULL is assigned to user_data
, assign NULL to user_destroy
, too.The following is extracted from exp.c
. It is from line 47 to line 56.
0, NULL,
+ expression = gtk_cclosure_expression_new (GTK_TYPE_APPLICATION_WINDOW, NULL,
+ G_CALLBACK (gtk_application_window_new), NULL, NULL);if (gtk_expression_evaluate (expression, app, &value)) {
+/* GtkApplicationWindow */
+ win1 = GTK_WIDGET (g_value_get_object (&value));
+ g_object_ref (win1);"Got GtkApplicationWindow object.\n");
+ g_print (else
+ }"The cclosure expression wasn't evaluated correctly.\n");
+ g_print (
+ gtk_expression_unref (expression);/* At the same time, the reference count of win1 is decreased by one. */ g_value_unset (&value);
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 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.
+"default-width");
+ expression1 = gtk_property_expression_new (GTK_TYPE_WINDOW, NULL,
+ watch_width = gtk_expression_watch (expression1, win1, notify, NULL, NULL);"default-height");
+ expression2 = gtk_property_expression_new (GTK_TYPE_WINDOW, NULL, 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);
+}
expression1
and expression2
with expression_watch_evaluate
function.title
. It contains the width and height, for example, “800 x 600”.win1
with the string title
.The title of the window reflects the size of the window.
+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>
A constant tag corresponds to a constant expression.
+G_TYPE_INT
type. Similarly, the types which is registered to the type system has type and name. For example, “gchararray” is a name of G_TYPE_STRING
type. You need to use the name of types for the type
attribute. See GObject tutorial.gtk_expression_bind
function. name
attribute specifies the “label” property of the GtkLabel object just before the binding tag. The expression returns a int type GValue. On the other hand “label” property holds a string type GValue. When a GValue is copied to another GValue, the type is automatically converted if possible. In this case, an int 100
is converted to a string "100"
.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.
+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>
buffer
property of the entry
instance. The entry
instance is defined in the line 43. It is a GtkEntry entry
. A lookup tag takes an instance in some ways to look up for a property.
+this
instance when it is evaluated.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>
win2
instance.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
.
"/com/github/ToshioCP/exp/exp.ui");
+ build = gtk_builder_new_from_resource ("win2"));
+ GtkWidget *win2 = GTK_WIDGET (gtk_builder_get_object (build, 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.
+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>
gtk_expression_bind
function. name
attribute specifies the “title” property of win2
. Binding tag gives win2
as the this
instance to the expressions, which are the contents of the binding tag. So, closure tag and lookup tags use win2
as the this
object when they are evaluated.set_title
and it returns “gchararray” type, which is “an array of characters” i.e. a string. The contents of the closure tag are assigned to parameters of the function. So, set_title
has three parameters, win2
(this
instance), default width and default height.win2
(this
instance).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.
+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.
+ +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 @@ + + + + + + +Up: index.html, Prev: Section 28
+GtkColumnView is like GtkListView, but it has multiple columns. Each column is GtkColumnViewColumn.
+ +The following diagram shows the image how it works.
+ +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.
+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>
factory_list.ui
in the section 27.get_file_name
is used here. The function will be explained later.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
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;
+}
app_activate
is an “activate” handler of GApplication.win
and directorylist
.app
.directorylist
with “.” (current directory).main
function.exp.c
is simple and short thanks to exp.ui
.
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.
+ +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 @@ + + + + + + +Up: index.html, Prev: Section 2, Next: Section 4
+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.
+The message tells us that:
+g_application_activate()
,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:
+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.
G_CALLBACK
.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.
+$
+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:
+Now rewrite the function app_activate
.
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.
+ +The function gtk_window_new
is defined as follows.
+ GtkWidget *void); gtk_window_new (
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.
+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.
+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.
+ +Click on the close button then the window disappears and the program finishes.
+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”.
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 @@ + + + + + + +Up: index.html, Prev: Section 3, Next: Section 5
+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.
+ +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:
+lab
is added.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.
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.
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:
+g_print
in lb2.c
was deleted and the two lines above are inserted instead.btn
is changed from “Click me” to “Quit”.g_signal_connect
is changed from NULL
to win
.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.
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.
+ +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.
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 @@ + + + + + + +Up: index.html, Prev: Section 4, Next: Section 6
+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.
+ +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.
+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 @@ + + + + + + +Up: index.html, Prev: Section 5, Next: Section 7
+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.
+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;
+
+0] = 'H';
+ a[1] = 'e';
+ a[2] = 'l';
+ a[3] = 'l';
+ a[4] = 'o';
+ a[5] = '\0';
+ a[
+
+ 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:
+A string literal in a C program is surrounded by double quotes and written as the following:
+char *s;
+"Hello" s =
“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.
+1) = 'a'; *(s+
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.
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
+void) {
+ print_strings (char b[] = "Hello";
+
+1] = 'a'; /* Because the array is static, it's writable. */
+ a[1] = 'a'; /* Because the array is auto, it's writable. */
+ b[
+"%s\n", a); /* Hallo */
+ printf ("%s\n", b); /* Hallo */
+ printf ( }
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.
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.
struct_type
is the type of the element of the array.n_struct
is the size of the array.struct_type
.For example,
+char *s;
+char, 10);
+ s = g_new (/* s points an array of char. The size of the array is 10. */
+
+struct tuple {int x, y;} *t;
+struct tuple, 5);
+ t = g_new (/* 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;
+"Hello");
+ s = g_strdup ( 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. */
+
+20; /* This is illegal because x is qualified with const */ x =
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 @@ + + + + + + +Up: index.html, Prev: Section 6, Next: Section 8
+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 *const gchar *application_id, GApplicationFlags flags); gtk_application_new (
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.
"com.github.ToshioCP.tfv3", G_APPLICATION_HANDLES_OPEN); app = gtk_application_new (
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:
+application
— the application (usually GtkApplication)files
— an array of GFiles. [array length=n_files] [element-type GFile]n_files
— the number of the elements of files
hint
— a hint provided by the calling instance (usually it can be ignored)user_data
— user data set when the signal handler was connected.How to read a specified file (GFile) will be described next.
+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
+
+Let’s explain how the program tfv3.c
works. First, the function main
has only two changes from the previous version.
G_APPLICATION_FLAGS_NONE
is replaced by G_APPLICATION_HANDLES_OPEN
; andg_signal_connect (app, "open", G_CALLBACK (on_open), NULL)
is added.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
GTK_WRAP_WORD_CHAR
in GtktextView;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) {
+ "No such file: %s.\n", filename);
+ g_print (
+ 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 is a container widget that uses tabs and contains multiple children. The child that is displayed depends on which tab has been selected.
+ +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.
nb
, lab
and nbp
are defined and will point to a new GtkNotebook, GtkLabel and GtkNotebookPage widget respectively.files[i]
is GFile object containing the i-th argument.contents
.filename
.filename
is NULL, creates GtkLabel with the empty string. GtkNotebook -- GtkNotebookPage -- GtkScrolledWindow
+nbp
to point to this GtkNotebookPage.g_object_set
is a general function to set properties in objects. See GObject API Reference, g_object_set.filename
buffer is freed.filename
is NULL, the “No valid file is given” message is outputted.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 @@ + + + + + + +Up: index.html, Prev: Section 7, Next: Section 9
+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,
+20]; GFile *f[
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.
+ +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).
+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 *void) {
+ tfe_text_view_new (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.
+tv
is a pointer to the TfeTextView object instance which is a C-structure declared with the tag _TfeTextView. So, the structure has a member file
as a pointer to a GFile instance. tv->file = f
is an assignment of f
to a member file
of the structure pointed by tv
. This is an example how to use the extended memory in a child widget.This program is not perfect. It has some problems. It will be modified later.
+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.
"close-request", G_CALLBACK (before_close), NULL); g_signal_connect (win,
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.
+nb
has.app_open
handler had run. It will be shown later.start_iter
and end_iter
are iterators of the buffer. I don’t want to explain them now because it would take a lot of time. Just remember these lines for the present.contents
.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;
+}
files[i]
is a pointer to GFile structure. It will be freed by the system. So you need to copy it. g_file_dup
duplicates the given GFile structure.before_close
handler. The fourth argument is called user data and it is given to the signal handler. So, nb
is given to before_close
as the second argument.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 @@ + + + + + + +Up: index.html, Prev: Section 8, Next: Section 10
+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.
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.
+boxv
. It is a vertical box and a child of GtkApplicationWindow. It has two children. The first child is a horizontal box. The second child is a GtkNotebook.boxh
and appends it to boxv
as a first child.dmy1
and dmy3
has a character width of ten. The other label dmy2
has hexpand property which is set to be TRUE. This makes the label expands horizontally as long as possible.boxh
.boxv
as the second child.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.
+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.
GtkApplicationWindow
class and win
id is defined. This is the top level window. And the three properties of the window are defined. title
property is “file editor”, default-width
property is 600 and default-height
property is 400.win
.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
.
gtk4-builder-tool validate <ui file name>
validates the ui file. If the ui file includes some syntactical error, gtk4-builder-tool
prints the error.gtk4-builder-tool simplify <ui file name>
simplifies the ui file and prints the result. If --replace
option is given, it replaces the ui file with the simplified one. If the ui file specifies a value of property but it is default, then it will be removed. And some values are simplified. For example, “TRUE”and “FALSE” becomes “1” and “0” respectively. However, “TRUE” or “FALSE” is better for maintenance.It is a good idea to check your ui file before compiling.
+GtkBuilder builds widgets based on the ui file.
+
+ GtkBuilder *build;
+"tfe3.ui");
+ build = gtk_builder_new_from_file ("win"));
+ win = GTK_WIDGET (gtk_builder_get_object (build,
+ gtk_window_set_application (GTK_WINDOW (win), GTK_APPLICATION (app));"nb")); nb = GTK_WIDGET (gtk_builder_get_object (build,
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.
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 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>
gresources
tag can include multiple gresources (gresource tags). However, this xml has only one gresource./com/github/ToshioCP/tfe3
.tfe3.ui
. And it is pointed by /com/github/ToshioCP/tfe3/tfe3.ui
because it needs prefix. If you want to add more files, then insert them between line 4 and 5.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"
+
+ ... ... ...
+ ... ... ..."/com/github/ToshioCP/tfe3/tfe3.ui");
+ build = gtk_builder_new_from_resource (
+ ... ... ... ... ... ...
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 @@ + + + + + + +TfeTextView – Child object of GtkTextView. It holds GFile which the contents of GtkTextBuffer correponds to.
+GObject
++--GInitiallyUnowned
+ +--GtkWidget
+ +--GtkTextView
+ +--TfeTextView
+#include <gtk/gtk.h>
+TfeTextView holds GFile which the contents of GtkTextBuffer corresponds to. File manipulation functions are added to this object.
+GFile *
+tfe_text_view_get_file (TfeTextView *tv);
+Returns the copy of the GFile in the TfeTextView.
+Parameters
+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
+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
+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
+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
+GtkWidget *
+tfe_text_view_new (void);
+Creates a new TfeTextView and returns the TfeTextView as GtkWidget. If an error happens, it returns NULL
.
Returns
+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
.
typedef struct {
+ GtkTextViewClass parent_class;
+} TfeTextViewClass;
+No member is added because TfeTextView is a final type object.
+Predefined values for the response id given by open-response
signal.
Members:
+void
+user_function (TfeTextView *tv,
+ gpointer user_data)
+Emitted when the GFile in the TfeTextView object is changed. The signal is emitted when:
+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.
Turtle is a simple interpreter for turtle graphics.
+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
+
+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.
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
+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 run
button, then two line segments appears. One is vertical and the other is horizontal.
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.
+bc (1,0,0)
changes the background color to red. This command clear the surface and change the background color. So, the shapes on the surface disappears.fc (0,1,0)
changes the foreground color to green. This command changes the pen color. The prior shapes on the surface aren’t affected. After this command, the turtle draws lines with the new color.pw 5
makes lines thick and pw 1
makes it thin.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
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.
+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.
+=
works as ==
in C language.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.
+Turtle language has very simple if statement.
+if (x > 50) {
+ fd x
+}
+There is no else part.
+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.
+a
. A variable a
is in its body.a
.a
is called.However, using the same name to a procedure and variable makes confusing. You should avoid that.
+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.
+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.
+ +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.
+ + +The following is the list of tokens.
+Keywords:
+identifiers and numbers:
+[a-zA-Z][a-zA-Z0-9]*
in regular expression.(0|[1-9][0-9]*)(\.[0-9]+)?
in regular expression. It doesn’t have +
or -
sign because they bring some syntactic confusion. However negative number such as -10
can be recognized as unary minus and a number.Symbols for expression
+=
>
<
+
-
*
/
(
)
Delimiters
+(
)
{
}
,
Comments and spaces:
+#
and new line inclusive. If a comment is at the end of the file, the trailing new line can be left out.These characters are used to separate tokens explicitly. They doesn’t have any syntactic meaning and are ignored by the parser.
+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
+;
+
+