Upgrade tfe. It's in src/tfe7. Insert sec20.

This commit is contained in:
Toshio Sekiya 2021-02-19 22:20:23 +09:00
parent 2b2c26a1ca
commit bb632f024d
28 changed files with 2851 additions and 751 deletions

1
.gitignore vendored
View file

@ -16,6 +16,7 @@ src/tfe4/_build
src/tfe5/_build
src/tfe5/hello.txt
src/tfe6/_build
src/tfe7/_build
src/menu/a.out
src/color/_build
src/turtle/_build

View file

@ -32,6 +32,7 @@ You can read it without download.
1. [Menu and action](gfm/sec16.md)
1. [Stateful action](gfm/sec17.md)
1. [Ui file for menu and action entries](gfm/sec18.md)
1. [Upgrade text file editor](gfm/sec19.md)
1. [GtkDrawingArea and Cairo](gfm/sec20.md)
1. [Combine GtkDrawingArea and TfeTextView](gfm/sec21.md)
1. [GtkMenuButton, accelerators, font, pango and gsettings](gfm/sec19.md)
1. [Template XML](gfm/sec20.md)
1. [GtkDrawingArea and Cairo](gfm/sec21.md)
1. [Combine GtkDrawingArea and TfeTextView](gfm/sec22.md)

View file

@ -1,6 +1,6 @@
Up: [Readme.md](../Readme.md), Prev: [Section 18](sec18.md), Next: [Section 20](sec20.md)
# Upgrade text file editor
# GtkMenuButton, accelerators, font, pango and gsettings
Traditional menu structure is fine.
However, Buttons or menu items we often use are not so many.

File diff suppressed because it is too large Load diff

View file

@ -1,373 +1,201 @@
Up: [Readme.md](../Readme.md), Prev: [Section 20](sec20.md)
Up: [Readme.md](../Readme.md), Prev: [Section 20](sec20.md), Next: [Section 22](sec22.md)
# Combine GtkDrawingArea and TfeTextView
# GtkDrawingArea and Cairo
Now, we will make a new application which has GtkDrawingArea and TfeTextView in it.
Its name is "color".
If you write a color in TfeTextView and click on the `run` button, then the color of GtkDrawingArea changes to the color given by you.
If you want to draw dynamically, like an image window of gimp graphics editor, GtkDrawingArea widget is the most suitable widget.
You can draw or redraw an image in this widget freely.
It is called custom drawing.
![color](../image/color.png)
GtkDrawingArea provides a cairo context so users can draw images by cairo functions.
In this section, I will explain:
The following colors are available.
1. Cairo, but briefly.
2. GtkDrawingArea with very simple example.
- white
- black
- red
- green
- blue
## Cairo
In addition the following two options are also available.
Cairo is a two dimensional graphics library.
First, you need to know surface, source, mask, destination, cairo context and transformation.
- light: Make the color of the drawing area lighter.
- dark: Make the color of the drawing area darker.
- Surface represents an image.
It is like a canvas.
We can draw shapes and images with different colors on surfaces.
- Source pattern, or simply source, is a kind of paint, which will be transferred to destination surface by cairo functions.
- Mask is image mask used in the transference.
- Destination is a target surface.
- Cairo context manages the transference from source to destination through mask with its functions.
For example, `cairo_stroke` is a function to draw a path to the destination by the transference.
- Transformation is applied before the transfer completes.
The transformation is called affine, which is a mathematics terminology, and represented by matrix multiplication and vector addition.
Scaling, rotation, reflection, shearing and translation are examples of affine transformation.
In this section, we don't use it.
That means we only use identity transformation.
Therefore, the coordinate in source and mask is the same as the coordinate in destination.
This application can only do very simple things.
However, it tells us that if we add powerful parser to it, we will be able to make it more efficient.
I want to show it to you in the later section by making a turtle graphics language like Logo program language.
![Stroke a rectangle](../image/cairo.png)
In this section, we focus on how to bind the two objects.
The instruction is as follows:
## Color.ui and color.gresource.xml
1. Create a surface.
This will be a destination.
2. Create a cairo context with the surface and the surface will be the destination of the context.
3. Create a source pattern within the context.
4. Create paths, which are lines, rectangles, arcs, texts or more complicated shapes, to generate a mask.
5. Use drawing operator such as `cairo_stroke` to transfer the paint in the source to the destination.
6. Save the destination surface to a file if necessary.
First, We need to make the ui file of the widgets.
The image in the previous subsection gives us the structure of the widgets.
Title bar, four buttons in the tool bar and two widgets textview and drawing area.
The ui file is as follows.
~~~xml
1 <?xml version="1.0" encoding="UTF-8"?>
2 <interface>
3 <object class="GtkApplicationWindow" id="win">
4 <property name="title">color changer</property>
5 <property name="default-width">600</property>
6 <property name="default-height">400</property>
7 <child>
8 <object class="GtkBox" id="boxv">
9 <property name="orientation">GTK_ORIENTATION_VERTICAL</property>
10 <child>
11 <object class="GtkBox" id="boxh1">
12 <property name="orientation">GTK_ORIENTATION_HORIZONTAL</property>
13 <child>
14 <object class="GtkLabel" id="dmy1">
15 <property name="width-chars">10</property>
16 </object>
17 </child>
18 <child>
19 <object class="GtkButton" id="btnr">
20 <property name="label">Run</property>
21 <signal name="clicked" handler="run_cb"></signal>
22 </object>
23 </child>
24 <child>
25 <object class="GtkButton" id="btno">
26 <property name="label">Open</property>
27 <signal name="clicked" handler="open_cb"></signal>
28 </object>
29 </child>
30 <child>
31 <object class="GtkLabel" id="dmy2">
32 <property name="hexpand">TRUE</property>
33 </object>
34 </child>
35 <child>
36 <object class="GtkButton" id="btns">
37 <property name="label">Save</property>
38 <signal name="clicked" handler="save_cb"></signal>
39 </object>
40 </child>
41 <child>
42 <object class="GtkButton" id="btnc">
43 <property name="label">Close</property>
44 <signal name="clicked" handler="close_cb"></signal>
45 </object>
46 </child>
47 <child>
48 <object class="GtkLabel" id="dmy3">
49 <property name="width-chars">10</property>
50 </object>
51 </child>
52 </object>
53 </child>
54 <child>
55 <object class="GtkBox" id="boxh2">
56 <property name="orientation">GTK_ORIENTATION_HORIZONTAL</property>
57 <property name="homogeneous">TRUE</property>
58 <child>
59 <object class="GtkScrolledWindow" id="scr">
60 <property name="hexpand">TRUE</property>
61 <property name="vexpand">TRUE</property>
62 <child>
63 <object class="TfeTextView" id="tv">
64 <property name="wrap-mode">GTK_WRAP_WORD_CHAR</property>
65 </object>
66 </child>
67 </object>
68 </child>
69 <child>
70 <object class="GtkDrawingArea" id="da">
71 <property name="hexpand">TRUE</property>
72 <property name="vexpand">TRUE</property>
73 </object>
74 </child>
75 </object>
76 </child>
77 </object>
78 </child>
79 </object>
80 </interface>
~~~
- 10-53: This part describes the tool bar which has four buttons, `Run`, `Open`, `Save` and `Close`.
This is similar to the toolbar of tfe text editor in [Section 8](sec8.md).
There are two differences.
`Run` button replaces `New` button.
A signal element is added to each button object.
It has "name" attribute which is a signal name and "handler" attribute which is the name of its signal handler function.
Options "-WI, --export-dynamic" CFLAG is necessary when you compile the application.
You can achieve this by adding "export_dynamic: true" argument to executable function in `meson.build`.
And be careful that the handler must be defined without 'static' class.
- 54-76: Puts GtkScrolledWindow and GtkDrawingArea into GtkBox.
GtkBox has "homogeneous property" with TRUE value, so the two children have the same width in the box.
TfeTextView is a child of GtkScrolledWindow.
The xml file for the resource compiler is almost same as before.
Just substitute "color" for "tfe".
~~~xml
1 <?xml version="1.0" encoding="UTF-8"?>
2 <gresources>
3 <gresource prefix="/com/github/ToshioCP/color">
4 <file>color.ui</file>
5 </gresource>
6 </gresources>
~~~
## Tfetextview.h, tfetextview.c and color.h
First two files are the same as before.
Color.h just includes tfetextview.h.
Here's a simple example code that draws a small square and save it as a png file.
~~~C
1 #include <gtk/gtk.h>
2
3 #include "../tfetextview/tfetextview.h"
~~~
## Colorapplication.c
This is the main file.
It deals with:
- Building widgets by GtkBuilder.
- Seting a drawing function of GtkDrawingArea.
And connecting a handler to "resize" signal on GtkDrawingArea.
- Implementing each call back functions.
Particularly, `Run` signal handler is the point in this program.
The following is `colorapplication.c`.
~~~C
1 #include "color.h"
2
3 static GtkWidget *win;
4 static GtkWidget *tv;
5 static GtkWidget *da;
6
7 static cairo_surface_t *surface = NULL;
8
9 static void
10 run (void) {
11 GtkTextBuffer *tb = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv));
12 GtkTextIter start_iter;
13 GtkTextIter end_iter;
14 char *contents;
15 cairo_t *cr;
16
17 gtk_text_buffer_get_bounds (tb, &start_iter, &end_iter);
18 contents = gtk_text_buffer_get_text (tb, &start_iter, &end_iter, FALSE);
19 if (surface) {
20 cr = cairo_create (surface);
21 if (g_strcmp0 ("red", contents) == 0)
22 cairo_set_source_rgb (cr, 1, 0, 0);
23 else if (g_strcmp0 ("green", contents) == 0)
24 cairo_set_source_rgb (cr, 0, 1, 0);
25 else if (g_strcmp0 ("blue", contents) == 0)
26 cairo_set_source_rgb (cr, 0, 0, 1);
27 else if (g_strcmp0 ("white", contents) == 0)
28 cairo_set_source_rgb (cr, 1, 1, 1);
29 else if (g_strcmp0 ("black", contents) == 0)
30 cairo_set_source_rgb (cr, 0, 0, 0);
31 else if (g_strcmp0 ("light", contents) == 0)
32 cairo_set_source_rgba (cr, 1, 1, 1, 0.5);
33 else if (g_strcmp0 ("dark", contents) == 0)
34 cairo_set_source_rgba (cr, 0, 0, 0, 0.5);
35 else
36 cairo_set_source_surface (cr, surface, 0, 0);
37 cairo_paint (cr);
38 cairo_destroy (cr);
39 }
40 }
41
42 void
43 run_cb (GtkWidget *btnr) {
44 run ();
45 gtk_widget_queue_draw (GTK_WIDGET (da));
46 }
47
48 void
49 open_cb (GtkWidget *btno) {
50 tfe_text_view_open (TFE_TEXT_VIEW (tv), win);
51 }
52
53 void
54 save_cb (GtkWidget *btns) {
55 tfe_text_view_save (TFE_TEXT_VIEW (tv));
56 }
57
58 void
59 close_cb (GtkWidget *btnc) {
60 if (surface)
61 cairo_surface_destroy (surface);
62 gtk_window_destroy (GTK_WINDOW (win));
63 }
64
65 static void
66 resize_cb (GtkDrawingArea *drawing_area, int width, int height, gpointer user_data) {
67 if (surface)
68 cairo_surface_destroy (surface);
69 surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, width, height);
70 run ();
71 }
72
73 static void
74 draw_func (GtkDrawingArea *drawing_area, cairo_t *cr, int width, int height, gpointer user_data) {
75 if (surface) {
76 cairo_set_source_surface (cr, surface, 0, 0);
77 cairo_paint (cr);
78 }
79 }
80
81 static void
82 activate (GApplication *application) {
83 gtk_widget_show (win);
84 }
85
86 static void
87 startup (GApplication *application) {
88 GtkApplication *app = GTK_APPLICATION (application);
89 GtkBuilder *build;
90
91 build = gtk_builder_new_from_resource ("/com/github/ToshioCP/color/color.ui");
92 win = GTK_WIDGET (gtk_builder_get_object (build, "win"));
93 gtk_window_set_application (GTK_WINDOW (win), app);
94 tv = GTK_WIDGET (gtk_builder_get_object (build, "tv"));
95 da = GTK_WIDGET (gtk_builder_get_object (build, "da"));
96 g_object_unref(build);
97 g_signal_connect (GTK_DRAWING_AREA (da), "resize", G_CALLBACK (resize_cb), NULL);
98 gtk_drawing_area_set_draw_func (GTK_DRAWING_AREA (da), draw_func, NULL, NULL);
99
100 GdkDisplay *display;
101
102 display = gtk_widget_get_display (GTK_WIDGET (win));
103 GtkCssProvider *provider = gtk_css_provider_new ();
104 gtk_css_provider_load_from_data (provider, "textview {padding: 10px; font-family: monospace; font-size: 12pt;}", -1);
105 gtk_style_context_add_provider_for_display (display, GTK_STYLE_PROVIDER (provider), GTK_STYLE_PROVIDER_PRIORITY_USER);
106 }
107
108 int
109 main (int argc, char **argv) {
110 GtkApplication *app;
111 int stat;
112
113 app = gtk_application_new ("com.github.ToshioCP.color", G_APPLICATION_FLAGS_NONE);
114
115 g_signal_connect (app, "startup", G_CALLBACK (startup), NULL);
116 g_signal_connect (app, "activate", G_CALLBACK (activate), NULL);
117
118 stat =g_application_run (G_APPLICATION (app), argc, argv);
119 g_object_unref (app);
120 return stat;
121 }
122
~~~
- 108-121: The function `main` is almost same as before but there are some differences.
The application ID is "com.github.ToshioCP.color".
`G_APPLICATION_FLAGS_NONE` is specified so no open signal handler is necessary.
- 86-106: Startup handler.
- 91-96: Builds widgets.
The pointers of the top window, TfeTextView and GtkDrawingArea objects are stored to static variables `win`, `tv` and `da` respectively.
This is because these objects are often used in handlers.
They never be rewritten so they're thread safe.
- 97: connects "resize" signal and the handler.
- 98: sets the drawing function.
- 81-84: Activates handler, which just shows the widgets.
- 73-79: The drawing function.
It just copies `surface` to destination.
- 65-71: Resize handler.
Re-creates the surface to fit the width and height of the drawing area and paints by calling the function `run`.
- 58-63: Closes the handler.
It destroys `surface` if it exists.
Then it destroys the top window and quits the application.
- 48-56: Open and save handler.
They just call the corresponding functions of TfeTextView.
- 42-46: Run handler.
It calls run function to paint the surface.
After that `gtk_widget_queue_draw` is called.
This fhunction adds the widget (GtkDrawingArea) to the queue to be redrawn.
It is important to know that the drawing function is called when it is necessary.
For example, when another window is moved and uncovers part of the widget, or when the window containing it is resized.
But repaint of `surface` is not automatically notified to gtk.
Therefore, you need to call `gtk_widget_queue_draw` to redraw the widget.
- 9-40: Run function paints the surface.
First, it gets the contents of GtkTextBuffer.
Then it compares it to "red", "green" and so on.
If it matches the color, then the surface is painted the color.
If it matches "light" or "dark", then the color of the surface is lightened or darkened respectively.
Alpha channel is used.
## Meson.build
This file is almost same as before.
An argument "export_dynamic: true" is added to executable function.
~~~meson
1 project('color', 'c')
1 #include <cairo.h>
2
3 gtkdep = dependency('gtk4')
4
5 gnome=import('gnome')
6 resources = gnome.compile_resources('resources','color.gresource.xml')
7
8 sourcefiles=files('colorapplication.c', '../tfetextview/tfetextview.c')
9
10 executable('color', sourcefiles, resources, dependencies: gtkdep, export_dynamic: true)
3 int
4 main (int argc, char **argv)
5 {
6 cairo_surface_t *surface;
7 cairo_t *cr;
8 int width = 100;
9 int height = 100;
10
11 /* Generate surface and cairo */
12 surface = cairo_image_surface_create (CAIRO_FORMAT_RGB24, width, height);
13 cr = cairo_create (surface);
14
15 /* Drawing starts here. */
16 /* Paint the background white */
17 cairo_set_source_rgb (cr, 1.0, 1.0, 1.0);
18 cairo_paint (cr);
19 /* Draw a black rectangle */
20 cairo_set_source_rgb (cr, 0.0, 0.0, 0.0);
21 cairo_set_line_width (cr, 2.0);
22 cairo_rectangle (cr, width/2.0 - 20.0, height/2.0 - 20.0, 40.0, 40.0);
23 cairo_stroke (cr);
24
25 /* Write the surface to a png file and clean up cairo and surface. */
26 cairo_surface_write_to_png (surface, "rectangle.png");
27 cairo_destroy (cr);
28 cairo_surface_destroy (surface);
29
30 return 0;
31 }
~~~
## Compile and execute it
- 1: Includes the header file of cairo.
- 12: `cairo_image_surface_create` creates an image surface.
`CAIRO_FORMAT_RGB24` is a constant which means that each pixel has red, green and blue data.
Each data has 8 bit quantity.
Modern displays have this type of color depth.
Width and height are pixels and given as integers.
- 13: Creates cairo context.
The surface given as an argument will be the destination of the context.
- 17: `cairo_set_source_rgb` creates a source pattern, which is a solid white paint.
The second to fourth argument is red, green and blue color depth respectively.
Their type is float and the values are between zero and one.
(0,0,0) is black and (1,1,1) is white.
- 18: `cairo_paint` copies everywhere in the source to destination.
The destination is filled with white pixels by this command.
- 20: Sets the source color to black.
- 21: `cairo_set_line_width` set the width of lines.
In this case, the line width is set to two pixels.
(It is because the transformation is identity.
If the transformation isn't identity, for example scaling with the factor three, the actual width in destination will be six (2x3=6) pixels.)
- 22: Draws a rectangle (square).
The top-left coordinate is (width/2.0-20.0, height/2.0-20.0) and the width and height have the same length 40.0.
- 23: `cairo_stroke` transfer the source to destination through the rectangle in mask.
- 26: Outputs the image to a png file `rectangle.png`.
- 27: Destroys the context. At the same time the source is destroyed.
- 28: Destroys the destination surface.
First you need to export some variables (refer to [Section 2](sec2.md)).
To compile this, type the following.
$ . env.sh
$ gcc `pkg-config --cflags cairo` cairo.c `pkg-config --libs cairo`
Then type the following to compile it.
![rectangle.png](../src/misc/rectangle.png)
$ meson _build
$ ninja -C _build
There are lots of documentations in [Cairo's website](https://www.cairographics.org/).
If you aren't familiar with cairo, it is strongly recommended to read the [tutorial](https://www.cairographics.org/tutorial/) in the website.
The application is made in `_build` directory.
Type the following to execute it.
## GtkDrawingArea
$ _build/color
The following is a very simple example.
Type "red", "green", "blue", "white", black", "light" or "dark" in the TfeTextView.
Then, click on `Run` button.
Make sure the color of GtkDrawingArea changes.
~~~C
1 #include <gtk/gtk.h>
2
3 static void
4 draw_function (GtkDrawingArea *area, cairo_t *cr, int width, int height, gpointer user_data) {
5 cairo_set_source_rgb (cr, 1.0, 1.0, 1.0); /* whilte */
6 cairo_paint (cr);
7 cairo_set_line_width (cr, 2.0);
8 cairo_set_source_rgb (cr, 0.0, 0.0, 0.0); /* black */
9 cairo_rectangle (cr, width/2.0 - 20.0, height/2.0 - 20.0, 40.0, 40.0);
10 cairo_stroke (cr);
11 }
12
13 static void
14 on_activate (GApplication *app, gpointer user_data) {
15 GtkWidget *win = gtk_application_window_new (GTK_APPLICATION (app));
16 GtkWidget *area = gtk_drawing_area_new ();
17
18 gtk_window_set_title (GTK_WINDOW (win), "da1");
19 /* Set initial size of width and height */
20 gtk_drawing_area_set_content_width (GTK_DRAWING_AREA (area), 100);
21 gtk_drawing_area_set_content_height (GTK_DRAWING_AREA (area), 100);
22 gtk_drawing_area_set_draw_func (GTK_DRAWING_AREA (area), draw_function, NULL, NULL);
23 gtk_window_set_child (GTK_WINDOW (win), area);
24
25 gtk_widget_show (win);
26 }
27
28 int
29 main (int argc, char **argv) {
30 GtkApplication *app;
31 int stat;
32
33 app = gtk_application_new ("com.github.ToshioCP.da1", G_APPLICATION_FLAGS_NONE);
34 g_signal_connect (app, "activate", G_CALLBACK (on_activate), NULL);
35 stat =g_application_run (G_APPLICATION (app), argc, argv);
36 g_object_unref (app);
37 return stat;
38 }
39
~~~
In this program TfeTextView is used to change the color.
You can use buttons or menus instead of textview.
Probably it is more appropriate.
Using textview is unnatural.
It is a good practice to make such application by yourself.
The function `main` is almost same as before.
The two functions `on_activate` and `draw_function` is important in this example.
Up: [Readme.md](../Readme.md), Prev: [Section 20](sec20.md)
- 16: Generates a GtkDrawingArea object.
- 20,21: Sets the width and height of the contents of the GtkDrawingArea widget.
These width and height is the size of the destination surface of the cairo context provided by the widget.
- 22: Sets a drawing function of the widget.
GtkDrawingArea widget uses the function to draw the contents of itself whenever its necessary.
For example, when a user drag a mouse pointer and resize a top level window, GtkDrawingArea also changes the size.
Then, the whole window needs to be redrawn.
The drawing function has five parameters.
void drawing_function (GtkDrawingArea *drawing_area, cairo_t *cr, int width, int height,
gpointer user_data);
The first parameter is the GtkDrawingArea widget which calls the drawing function.
However, you can't change any properties, for example `content-width` or `content-height`, in this function.
The second parameter is a cairo context given by the widget.
The destination surface of the context is connected to the contents of the widget.
What you draw to this surface will appear in the widget on the screen.
The third and fourth parameters are the size of the destination surface.
- 3-11: The drawing function.
- 4-5: Sets the source to be white and paint the destination white.
- 7: Sets the line width to be 2.
- 8: Sets the source to be black.
- 9: Adds a rectangle to the mask.
- 10: Draws the rectangle with black color to the destination.
Compile and run it, then a window with a black rectangle (square) appears.
Try resizing the window.
The square always appears at the center of the window because the drawing function is invoked every moment the window is resized.
![Square in the window](../image/da1.png)
Up: [Readme.md](../Readme.md), Prev: [Section 20](sec20.md), Next: [Section 22](sec22.md)

373
gfm/sec22.md Normal file
View file

@ -0,0 +1,373 @@
Up: [Readme.md](../Readme.md), Prev: [Section 21](sec21.md)
# Combine GtkDrawingArea and TfeTextView
Now, we will make a new application which has GtkDrawingArea and TfeTextView in it.
Its name is "color".
If you write a color in TfeTextView and click on the `run` button, then the color of GtkDrawingArea changes to the color given by you.
![color](../image/color.png)
The following colors are available.
- white
- black
- red
- green
- blue
In addition the following two options are also available.
- light: Make the color of the drawing area lighter.
- dark: Make the color of the drawing area darker.
This application can only do very simple things.
However, it tells us that if we add powerful parser to it, we will be able to make it more efficient.
I want to show it to you in the later section by making a turtle graphics language like Logo program language.
In this section, we focus on how to bind the two objects.
## Color.ui and color.gresource.xml
First, We need to make the ui file of the widgets.
The image in the previous subsection gives us the structure of the widgets.
Title bar, four buttons in the tool bar and two widgets textview and drawing area.
The ui file is as follows.
~~~xml
1 <?xml version="1.0" encoding="UTF-8"?>
2 <interface>
3 <object class="GtkApplicationWindow" id="win">
4 <property name="title">color changer</property>
5 <property name="default-width">600</property>
6 <property name="default-height">400</property>
7 <child>
8 <object class="GtkBox" id="boxv">
9 <property name="orientation">GTK_ORIENTATION_VERTICAL</property>
10 <child>
11 <object class="GtkBox" id="boxh1">
12 <property name="orientation">GTK_ORIENTATION_HORIZONTAL</property>
13 <child>
14 <object class="GtkLabel" id="dmy1">
15 <property name="width-chars">10</property>
16 </object>
17 </child>
18 <child>
19 <object class="GtkButton" id="btnr">
20 <property name="label">Run</property>
21 <signal name="clicked" handler="run_cb"></signal>
22 </object>
23 </child>
24 <child>
25 <object class="GtkButton" id="btno">
26 <property name="label">Open</property>
27 <signal name="clicked" handler="open_cb"></signal>
28 </object>
29 </child>
30 <child>
31 <object class="GtkLabel" id="dmy2">
32 <property name="hexpand">TRUE</property>
33 </object>
34 </child>
35 <child>
36 <object class="GtkButton" id="btns">
37 <property name="label">Save</property>
38 <signal name="clicked" handler="save_cb"></signal>
39 </object>
40 </child>
41 <child>
42 <object class="GtkButton" id="btnc">
43 <property name="label">Close</property>
44 <signal name="clicked" handler="close_cb"></signal>
45 </object>
46 </child>
47 <child>
48 <object class="GtkLabel" id="dmy3">
49 <property name="width-chars">10</property>
50 </object>
51 </child>
52 </object>
53 </child>
54 <child>
55 <object class="GtkBox" id="boxh2">
56 <property name="orientation">GTK_ORIENTATION_HORIZONTAL</property>
57 <property name="homogeneous">TRUE</property>
58 <child>
59 <object class="GtkScrolledWindow" id="scr">
60 <property name="hexpand">TRUE</property>
61 <property name="vexpand">TRUE</property>
62 <child>
63 <object class="TfeTextView" id="tv">
64 <property name="wrap-mode">GTK_WRAP_WORD_CHAR</property>
65 </object>
66 </child>
67 </object>
68 </child>
69 <child>
70 <object class="GtkDrawingArea" id="da">
71 <property name="hexpand">TRUE</property>
72 <property name="vexpand">TRUE</property>
73 </object>
74 </child>
75 </object>
76 </child>
77 </object>
78 </child>
79 </object>
80 </interface>
~~~
- 10-53: This part describes the tool bar which has four buttons, `Run`, `Open`, `Save` and `Close`.
This is similar to the toolbar of tfe text editor in [Section 8](sec8.md).
There are two differences.
`Run` button replaces `New` button.
A signal element is added to each button object.
It has "name" attribute which is a signal name and "handler" attribute which is the name of its signal handler function.
Options "-WI, --export-dynamic" CFLAG is necessary when you compile the application.
You can achieve this by adding "export_dynamic: true" argument to executable function in `meson.build`.
And be careful that the handler must be defined without 'static' class.
- 54-76: Puts GtkScrolledWindow and GtkDrawingArea into GtkBox.
GtkBox has "homogeneous property" with TRUE value, so the two children have the same width in the box.
TfeTextView is a child of GtkScrolledWindow.
The xml file for the resource compiler is almost same as before.
Just substitute "color" for "tfe".
~~~xml
1 <?xml version="1.0" encoding="UTF-8"?>
2 <gresources>
3 <gresource prefix="/com/github/ToshioCP/color">
4 <file>color.ui</file>
5 </gresource>
6 </gresources>
~~~
## Tfetextview.h, tfetextview.c and color.h
First two files are the same as before.
Color.h just includes tfetextview.h.
~~~C
1 #include <gtk/gtk.h>
2
3 #include "../tfetextview/tfetextview.h"
~~~
## Colorapplication.c
This is the main file.
It deals with:
- Building widgets by GtkBuilder.
- Seting a drawing function of GtkDrawingArea.
And connecting a handler to "resize" signal on GtkDrawingArea.
- Implementing each call back functions.
Particularly, `Run` signal handler is the point in this program.
The following is `colorapplication.c`.
~~~C
1 #include "color.h"
2
3 static GtkWidget *win;
4 static GtkWidget *tv;
5 static GtkWidget *da;
6
7 static cairo_surface_t *surface = NULL;
8
9 static void
10 run (void) {
11 GtkTextBuffer *tb = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv));
12 GtkTextIter start_iter;
13 GtkTextIter end_iter;
14 char *contents;
15 cairo_t *cr;
16
17 gtk_text_buffer_get_bounds (tb, &start_iter, &end_iter);
18 contents = gtk_text_buffer_get_text (tb, &start_iter, &end_iter, FALSE);
19 if (surface) {
20 cr = cairo_create (surface);
21 if (g_strcmp0 ("red", contents) == 0)
22 cairo_set_source_rgb (cr, 1, 0, 0);
23 else if (g_strcmp0 ("green", contents) == 0)
24 cairo_set_source_rgb (cr, 0, 1, 0);
25 else if (g_strcmp0 ("blue", contents) == 0)
26 cairo_set_source_rgb (cr, 0, 0, 1);
27 else if (g_strcmp0 ("white", contents) == 0)
28 cairo_set_source_rgb (cr, 1, 1, 1);
29 else if (g_strcmp0 ("black", contents) == 0)
30 cairo_set_source_rgb (cr, 0, 0, 0);
31 else if (g_strcmp0 ("light", contents) == 0)
32 cairo_set_source_rgba (cr, 1, 1, 1, 0.5);
33 else if (g_strcmp0 ("dark", contents) == 0)
34 cairo_set_source_rgba (cr, 0, 0, 0, 0.5);
35 else
36 cairo_set_source_surface (cr, surface, 0, 0);
37 cairo_paint (cr);
38 cairo_destroy (cr);
39 }
40 }
41
42 void
43 run_cb (GtkWidget *btnr) {
44 run ();
45 gtk_widget_queue_draw (GTK_WIDGET (da));
46 }
47
48 void
49 open_cb (GtkWidget *btno) {
50 tfe_text_view_open (TFE_TEXT_VIEW (tv), win);
51 }
52
53 void
54 save_cb (GtkWidget *btns) {
55 tfe_text_view_save (TFE_TEXT_VIEW (tv));
56 }
57
58 void
59 close_cb (GtkWidget *btnc) {
60 if (surface)
61 cairo_surface_destroy (surface);
62 gtk_window_destroy (GTK_WINDOW (win));
63 }
64
65 static void
66 resize_cb (GtkDrawingArea *drawing_area, int width, int height, gpointer user_data) {
67 if (surface)
68 cairo_surface_destroy (surface);
69 surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, width, height);
70 run ();
71 }
72
73 static void
74 draw_func (GtkDrawingArea *drawing_area, cairo_t *cr, int width, int height, gpointer user_data) {
75 if (surface) {
76 cairo_set_source_surface (cr, surface, 0, 0);
77 cairo_paint (cr);
78 }
79 }
80
81 static void
82 activate (GApplication *application) {
83 gtk_widget_show (win);
84 }
85
86 static void
87 startup (GApplication *application) {
88 GtkApplication *app = GTK_APPLICATION (application);
89 GtkBuilder *build;
90
91 build = gtk_builder_new_from_resource ("/com/github/ToshioCP/color/color.ui");
92 win = GTK_WIDGET (gtk_builder_get_object (build, "win"));
93 gtk_window_set_application (GTK_WINDOW (win), app);
94 tv = GTK_WIDGET (gtk_builder_get_object (build, "tv"));
95 da = GTK_WIDGET (gtk_builder_get_object (build, "da"));
96 g_object_unref(build);
97 g_signal_connect (GTK_DRAWING_AREA (da), "resize", G_CALLBACK (resize_cb), NULL);
98 gtk_drawing_area_set_draw_func (GTK_DRAWING_AREA (da), draw_func, NULL, NULL);
99
100 GdkDisplay *display;
101
102 display = gtk_widget_get_display (GTK_WIDGET (win));
103 GtkCssProvider *provider = gtk_css_provider_new ();
104 gtk_css_provider_load_from_data (provider, "textview {padding: 10px; font-family: monospace; font-size: 12pt;}", -1);
105 gtk_style_context_add_provider_for_display (display, GTK_STYLE_PROVIDER (provider), GTK_STYLE_PROVIDER_PRIORITY_USER);
106 }
107
108 int
109 main (int argc, char **argv) {
110 GtkApplication *app;
111 int stat;
112
113 app = gtk_application_new ("com.github.ToshioCP.color", G_APPLICATION_FLAGS_NONE);
114
115 g_signal_connect (app, "startup", G_CALLBACK (startup), NULL);
116 g_signal_connect (app, "activate", G_CALLBACK (activate), NULL);
117
118 stat =g_application_run (G_APPLICATION (app), argc, argv);
119 g_object_unref (app);
120 return stat;
121 }
122
~~~
- 108-121: The function `main` is almost same as before but there are some differences.
The application ID is "com.github.ToshioCP.color".
`G_APPLICATION_FLAGS_NONE` is specified so no open signal handler is necessary.
- 86-106: Startup handler.
- 91-96: Builds widgets.
The pointers of the top window, TfeTextView and GtkDrawingArea objects are stored to static variables `win`, `tv` and `da` respectively.
This is because these objects are often used in handlers.
They never be rewritten so they're thread safe.
- 97: connects "resize" signal and the handler.
- 98: sets the drawing function.
- 81-84: Activates handler, which just shows the widgets.
- 73-79: The drawing function.
It just copies `surface` to destination.
- 65-71: Resize handler.
Re-creates the surface to fit the width and height of the drawing area and paints by calling the function `run`.
- 58-63: Closes the handler.
It destroys `surface` if it exists.
Then it destroys the top window and quits the application.
- 48-56: Open and save handler.
They just call the corresponding functions of TfeTextView.
- 42-46: Run handler.
It calls run function to paint the surface.
After that `gtk_widget_queue_draw` is called.
This fhunction adds the widget (GtkDrawingArea) to the queue to be redrawn.
It is important to know that the drawing function is called when it is necessary.
For example, when another window is moved and uncovers part of the widget, or when the window containing it is resized.
But repaint of `surface` is not automatically notified to gtk.
Therefore, you need to call `gtk_widget_queue_draw` to redraw the widget.
- 9-40: Run function paints the surface.
First, it gets the contents of GtkTextBuffer.
Then it compares it to "red", "green" and so on.
If it matches the color, then the surface is painted the color.
If it matches "light" or "dark", then the color of the surface is lightened or darkened respectively.
Alpha channel is used.
## Meson.build
This file is almost same as before.
An argument "export_dynamic: true" is added to executable function.
~~~meson
1 project('color', 'c')
2
3 gtkdep = dependency('gtk4')
4
5 gnome=import('gnome')
6 resources = gnome.compile_resources('resources','color.gresource.xml')
7
8 sourcefiles=files('colorapplication.c', '../tfetextview/tfetextview.c')
9
10 executable('color', sourcefiles, resources, dependencies: gtkdep, export_dynamic: true)
~~~
## Compile and execute it
First you need to export some variables (refer to [Section 2](sec2.md)).
$ . env.sh
Then type the following to compile it.
$ meson _build
$ ninja -C _build
The application is made in `_build` directory.
Type the following to execute it.
$ _build/color
Type "red", "green", "blue", "white", black", "light" or "dark" in the TfeTextView.
Then, click on `Run` button.
Make sure the color of GtkDrawingArea changes.
In this program TfeTextView is used to change the color.
You can use buttons or menus instead of textview.
Probably it is more appropriate.
Using textview is unnatural.
It is a good practice to make such application by yourself.
Up: [Readme.md](../Readme.md), Prev: [Section 21](sec21.md)

View file

@ -1,4 +1,4 @@
# Upgrade text file editor
# GtkMenuButton, accelerators, font, pango and gsettings
Traditional menu structure is fine.
However, Buttons or menu items we often use are not so many.

View file

@ -1,129 +1,280 @@
# GtkDrawingArea and Cairo
# Template XML
If you want to draw dynamically, like an image window of gimp graphics editor, GtkDrawingArea widget is the most suitable widget.
You can draw or redraw an image in this widget freely.
It is called custom drawing.
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.
GtkDrawingArea provides a cairo context so users can draw images by cairo functions.
In this section, I will explain:
~~~C
static GtkDialog *pref;
static GtkFontButton *fontbtn;
static GSettings *settings;
static GtkDialog *alert;
static GtkLabel *lb_alert;
static GtkButton *btn_accept;
1. Cairo, but briefly.
2. GtkDrawingArea with very simple example.
static gulong pref_close_request_handler_id = 0;
static gulong alert_close_request_handler_id = 0;
static gboolean is_quit;
~~~
## Cairo
Generally, if there are many global or static variables in the program, it is not a good program.
Such programs are difficult to maintain.
Cairo is a two dimensional graphics library.
First, you need to know surface, source, mask, destination, cairo context and transformation.
The file `tfeapplication.c` should be divided into several files.
- Surface represents an image.
It is like a canvas.
We can draw shapes and images with different colors on surfaces.
- Source pattern, or simply source, is a kind of paint, which will be transferred to destination surface by cairo functions.
- Mask is image mask used in the transference.
- Destination is a target surface.
- Cairo context manages the transference from source to destination through mask with its functions.
For example, `cairo_stroke` is a function to draw a path to the destination by the transference.
- Transformation is applied before the transfer completes.
The transformation is called affine, which is a mathematics terminology, and represented by matrix multiplication and vector addition.
Scaling, rotation, reflection, shearing and translation are examples of affine transformation.
In this section, we don't use it.
That means we only use identity transformation.
Therefore, the coordinate in source and mask is the same as the coordinate in destination.
- `tfeapplication.c` only has codes related to GtkApplication.
- A file about GtkApplicationWindow
- A file about a preference dialog
- A file about an alert dialog
![Stroke a rectangle](../image/cairo.png){width=9.0cm height=6.0cm}
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 of the parent 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.
The instruction is as follows:
## Preference dialog
1. Create a surface.
This will be a destination.
2. Create a cairo context with the surface and the surface will be the destination of the context.
3. Create a source pattern within the context.
4. Create paths, which are lines, rectangles, arcs, texts or more complicated shapes, to generate a mask.
5. Use drawing operator such as `cairo_stroke` to transfer the paint in the source to the destination.
6. Save the destination surface to a file if necessary.
Here's a simple example code that draws a small square and save it as a png file.
First, write a template XML file.
@@@include
misc/cairo.c
tfe7/tfepref.ui
@@@
- 1: Includes the header file of cairo.
- 12: `cairo_image_surface_create` creates an image surface.
`CAIRO_FORMAT_RGB24` is a constant which means that each pixel has red, green and blue data.
Each data has 8 bit quantity.
Modern displays have this type of color depth.
Width and height are pixels and given as integers.
- 13: Creates cairo context.
The surface given as an argument will be the destination of the context.
- 17: `cairo_set_source_rgb` creates a source pattern, which is a solid white paint.
The second to fourth argument is red, green and blue color depth respectively.
Their type is float and the values are between zero and one.
(0,0,0) is black and (1,1,1) is white.
- 18: `cairo_paint` copies everywhere in the source to destination.
The destination is filled with white pixels by this command.
- 20: Sets the source color to black.
- 21: `cairo_set_line_width` set the width of lines.
In this case, the line width is set to two pixels.
(It is because the transformation is identity.
If the transformation isn't identity, for example scaling with the factor three, the actual width in destination will be six (2x3=6) pixels.)
- 22: Draws a rectangle (square).
The top-left coordinate is (width/2.0-20.0, height/2.0-20.0) and the width and height have the same length 40.0.
- 23: `cairo_stroke` transfer the source to destination through the rectangle in mask.
- 26: Outputs the image to a png file `rectangle.png`.
- 27: Destroys the context. At the same time the source is destroyed.
- 28: Destroys the destination surface.
- 3: Template tag specifies a composite widget.
The value of a class attribute is the object name of the composite widget.
This XML file names the object "TfePref".
It is defined in a C source file and it will be shown later.
A parent attribute specifies the direct parent object of the composite widget.
`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.
To compile this, type the following.
$ gcc `pkg-config --cflags cairo` cairo.c `pkg-config --libs cairo`
![rectangle.png](misc/rectangle.png)
There are lots of documentations in [Cairo's website](https://www.cairographics.org/).
If you aren't familiar with cairo, it is strongly recommended to read the [tutorial](https://www.cairographics.org/tutorial/) in the website.
## GtkDrawingArea
The following is a very simple example.
Other lines are the same as before.
The object `TfePref` is defined in `tfepref.h` and `tfepref.c`.
@@@include
misc/da1.c
tfe7/tfepref.h
@@@
The function `main` is almost same as before.
The two functions `on_activate` and `draw_function` is important in this example.
- 6-7: When you define a new object, you need to write these two lines.
Refer to [Section 7](sec7.src.md).
- 9-10: `tfe_pref_new` generates a new TfePref object.
It has a parameter which the object use as a transient parent to show the dialog.
- 16: Generates a GtkDrawingArea object.
- 20,21: Sets the width and height of the contents of the GtkDrawingArea widget.
These width and height is the size of the destination surface of the cairo context provided by the widget.
- 22: Sets a drawing function of the widget.
GtkDrawingArea widget uses the function to draw the contents of itself whenever its necessary.
For example, when a user drag a mouse pointer and resize a top level window, GtkDrawingArea also changes the size.
Then, the whole window needs to be redrawn.
@@@include
tfe7/tfepref.c
@@@
The drawing function has five parameters.
- 3-8: The structure of an instance of this object.
It has two variables, settings and fontbtn.
- 10: G\_DEFINE\_TYPE macro generates lines to register the type.
- 12-18: dispose handler.
This handler is called when this object is finalizing.
The process has two stages, disposing and finalizing.
When disposing, the object releases all the objects it has had.
TfePref object holds a GSetting object.
It is released in line 16.
After that parents dispose handler is called in line 17.
Refer to [Section 10](sec10.src.md).
- 27-34: Class initialization function.
This is called in the class generation process.
- 31: Set the dispose handler.
- 32: `gtk_widget_class_set_template_from_resource` function associates the description in the XML file with the widget.
At this moment no object is generated.
It just make the class to know the structure of the object.
That's why the top level tag is not an object but template in the XML file.
- 33: `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 (in the line 7) and the id (in the line 24) in the XML file must be the same.
In the program above, the name is `fontbtn`.
The pointer to the object will be assigned to the variable when an instance is generated.
- 20-25: Instance initialization function.
- 22: Initializes the template of this object.
The template has been made during the class initialization process.
Now it is implemented to the instance.
- 23: Create GSettings object with the id `com.github.ToshioCP.tfe`.
- 24: Bind the font key in the GSettings object and the font property in the GtkFontButton.
void drawing_function (GtkDrawingArea *drawing_area, cairo_t *cr, int width, int height,
gpointer user_data);
- 36-39: The function `tfe_pref_new` creates an instance of TfePref.
The parameter `win` is a transient parent.
The first parameter is the GtkDrawingArea widget which calls the drawing function.
However, you can't change any properties, for example `content-width` or `content-height`, in this function.
The second parameter is a cairo context given by the widget.
The destination surface of the context is connected to the contents of the widget.
What you draw to this surface will appear in the widget on the screen.
The third and fourth parameters are the size of the destination surface.
Now, It is very simple to use this dialog.
A caller just creates this object and shows it.
- 3-11: The drawing function.
- 4-5: Sets the source to be white and paint the destination white.
- 7: Sets the line width to be 2.
- 8: Sets the source to be black.
- 9: Adds a rectangle to the mask.
- 10: Draws the rectangle with black color to the destination.
~~~C
TfePref *pref;
pref = tfe_pref_new (win) /* win is the top level window */
gtk_widget_show (GTK_WINDOW (win));
~~~
Compile and run it, then a window with a black rectangle (square) appears.
Try resizing the window.
The square always appears at the center of the window because the drawing function is invoked every moment the window is resized.
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.
![Square in the window](../image/da1.png)
## Alert dialog
It is almost same as preference dialog.
Its XML file is:
@@@include
tfe7/tfealert.ui
@@@
The header file is:
@@@include
tfe7/tfealert.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:
~~~C
tfe_alert_set_message (alert, "Are you really close without saving?"); /* alert points to a TfeAlert object */
tfe_alert_set_button_label (alert, "Close");
~~~
The function `tfe_alert_new` creates a TfeAlert dialog.
The C source file is:
@@@include
tfe7/tfealert.c
@@@
The program is almost same as `tfepref.c`.
The instruction how to use this object is as follows.
1. Write a "response" signal handler.
2. Create a TfeAlert object.
3. Connect "response" signal to a handler
4. Show the dialog
5. In the signal handler do something along the response-id.
Then destroy the dialog.
## Top level window
In the same way, create a child object of GtkApplicationWindow.
The object name is "TfeWindow".
@@@include
tfe7/tfewindow.ui
@@@
This XML file is the same as before except template tag.
@@@include
tfe7/tfewindow.h
@@@
There are three public functions.
The function `tfe_window_notebook_page_new` creates a new notebook page.
This is a wrapper function of `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
tfe7/tfewindow.c
@@@
- 20-32: `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`.
- 34-60: Handlers of Button clicked signal.
- 62-123: Handlers of action activated signal.
The `user_data` is a pointer to TfeWindow instance.
- 125-135: A handler of "changed::font" signal of GSettings object.
- 132: Gets the font from GSettings data.
- 133: Gets a PangoFontDescription from the font.
In the previous version, the program gets the font description from the GtkFontButton.
The button data and GSettings data are the same.
Therefore, the data got here is the same as the data in the GtkFontButton.
In addition, we don't need to worry about the preference dialog is alive or not thanks to the GSettings.
- 134: Sets CSS on the display with the font description.
- 137-152: Public functions.
- 155-161: Dispose handler.
The GSettings object needs to be released.
- 163-191: Object initialize function.
- 168: Generates a composite widget with the template.
- 170-173: Insert menu to the menu button.
- 175-176: Creates a GSettings object with the id.
Connects "changed::font" signal to the handler `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 object 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".
- "changed::a" is emitted when the value of "a" is changed. It isn't emitted when the value of "b" or "c" is changed.
- "changed::b" is emitted when the value of "b" is changed. It isn't emitted when the value of "a" or "c" is changed.
- "changed::c" is emitted when the value of "c" is changed. It isn't emitted when the value of "a" or "b" is changed.
In this version of tfe, there is only one key ("font").
So, even if the signal doesn't have a detail, the result is the same.
But in the future version, it will probably need details.
- 178-188: Creates actions.
- 190: Sets CSS font.
- 193-207: Class initialization function.
- 197: Sets the dispose handler.
- 198: Sets the composite widget template
- 199-203: Binds private variable with child objects in the template.
- 204-206: Binds signal handlers with signal tags in the template.
- 209-212: `tfe_window_new`.
This function creates TfeWindow instance.
## GtkApplication
The file `tfeapplication.c` is now very simple.
@@@include
tfe7/tfeapplication.c
@@@
- 3-11: Activate signal handler.
It uses `tfe_window_notebook_page_new` instead of `notebook_page_new`.
- 13-20: Open signal handler.
Thanks to `tfe_window_notebook_page_new_with_files`, this handler becomes very simple.
- 22-46: Startup signal handler.
Most of the task is moved to TfeWindow, the remaining task is creating a window and setting accelerations.
- 49-63: A function main.
## Other files
Resource XML file.
@@@include
tfe7/tfe.gresource.xml
@@@
GSchema XML file
@@@include
tfe7/com.github.ToshioCP.tfe.gschema.xml
@@@
Meson.build
@@@include
tfe7/meson.build
@@@
## Compiling and installation.
~~~
$ meson --prefix=$HOME/local _build
$ ninja -C _build
$ ninja -C _build install
===
Source files are in [src/tfe7](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.

View file

@ -1,153 +1,129 @@
# Combine GtkDrawingArea and TfeTextView
# GtkDrawingArea and Cairo
Now, we will make a new application which has GtkDrawingArea and TfeTextView in it.
Its name is "color".
If you write a color in TfeTextView and click on the `run` button, then the color of GtkDrawingArea changes to the color given by you.
If you want to draw dynamically, like an image window of gimp graphics editor, GtkDrawingArea widget is the most suitable widget.
You can draw or redraw an image in this widget freely.
It is called custom drawing.
![color](../image/color.png){width=7.0cm height=5.13cm}
GtkDrawingArea provides a cairo context so users can draw images by cairo functions.
In this section, I will explain:
The following colors are available.
1. Cairo, but briefly.
2. GtkDrawingArea with very simple example.
- white
- black
- red
- green
- blue
## Cairo
In addition the following two options are also available.
Cairo is a two dimensional graphics library.
First, you need to know surface, source, mask, destination, cairo context and transformation.
- light: Make the color of the drawing area lighter.
- dark: Make the color of the drawing area darker.
- Surface represents an image.
It is like a canvas.
We can draw shapes and images with different colors on surfaces.
- Source pattern, or simply source, is a kind of paint, which will be transferred to destination surface by cairo functions.
- Mask is image mask used in the transference.
- Destination is a target surface.
- Cairo context manages the transference from source to destination through mask with its functions.
For example, `cairo_stroke` is a function to draw a path to the destination by the transference.
- Transformation is applied before the transfer completes.
The transformation is called affine, which is a mathematics terminology, and represented by matrix multiplication and vector addition.
Scaling, rotation, reflection, shearing and translation are examples of affine transformation.
In this section, we don't use it.
That means we only use identity transformation.
Therefore, the coordinate in source and mask is the same as the coordinate in destination.
This application can only do very simple things.
However, it tells us that if we add powerful parser to it, we will be able to make it more efficient.
I want to show it to you in the later section by making a turtle graphics language like Logo program language.
![Stroke a rectangle](../image/cairo.png){width=9.0cm height=6.0cm}
In this section, we focus on how to bind the two objects.
The instruction is as follows:
## Color.ui and color.gresource.xml
1. Create a surface.
This will be a destination.
2. Create a cairo context with the surface and the surface will be the destination of the context.
3. Create a source pattern within the context.
4. Create paths, which are lines, rectangles, arcs, texts or more complicated shapes, to generate a mask.
5. Use drawing operator such as `cairo_stroke` to transfer the paint in the source to the destination.
6. Save the destination surface to a file if necessary.
First, We need to make the ui file of the widgets.
The image in the previous subsection gives us the structure of the widgets.
Title bar, four buttons in the tool bar and two widgets textview and drawing area.
The ui file is as follows.
Here's a simple example code that draws a small square and save it as a png file.
@@@include
color/color.ui
misc/cairo.c
@@@
- 10-53: This part describes the tool bar which has four buttons, `Run`, `Open`, `Save` and `Close`.
This is similar to the toolbar of tfe text editor in [Section 8](sec8.src.md).
There are two differences.
`Run` button replaces `New` button.
A signal element is added to each button object.
It has "name" attribute which is a signal name and "handler" attribute which is the name of its signal handler function.
Options "-WI, --export-dynamic" CFLAG is necessary when you compile the application.
You can achieve this by adding "export_dynamic: true" argument to executable function in `meson.build`.
And be careful that the handler must be defined without 'static' class.
- 54-76: Puts GtkScrolledWindow and GtkDrawingArea into GtkBox.
GtkBox has "homogeneous property" with TRUE value, so the two children have the same width in the box.
TfeTextView is a child of GtkScrolledWindow.
- 1: Includes the header file of cairo.
- 12: `cairo_image_surface_create` creates an image surface.
`CAIRO_FORMAT_RGB24` is a constant which means that each pixel has red, green and blue data.
Each data has 8 bit quantity.
Modern displays have this type of color depth.
Width and height are pixels and given as integers.
- 13: Creates cairo context.
The surface given as an argument will be the destination of the context.
- 17: `cairo_set_source_rgb` creates a source pattern, which is a solid white paint.
The second to fourth argument is red, green and blue color depth respectively.
Their type is float and the values are between zero and one.
(0,0,0) is black and (1,1,1) is white.
- 18: `cairo_paint` copies everywhere in the source to destination.
The destination is filled with white pixels by this command.
- 20: Sets the source color to black.
- 21: `cairo_set_line_width` set the width of lines.
In this case, the line width is set to two pixels.
(It is because the transformation is identity.
If the transformation isn't identity, for example scaling with the factor three, the actual width in destination will be six (2x3=6) pixels.)
- 22: Draws a rectangle (square).
The top-left coordinate is (width/2.0-20.0, height/2.0-20.0) and the width and height have the same length 40.0.
- 23: `cairo_stroke` transfer the source to destination through the rectangle in mask.
- 26: Outputs the image to a png file `rectangle.png`.
- 27: Destroys the context. At the same time the source is destroyed.
- 28: Destroys the destination surface.
The xml file for the resource compiler is almost same as before.
Just substitute "color" for "tfe".
To compile this, type the following.
$ gcc `pkg-config --cflags cairo` cairo.c `pkg-config --libs cairo`
![rectangle.png](misc/rectangle.png)
There are lots of documentations in [Cairo's website](https://www.cairographics.org/).
If you aren't familiar with cairo, it is strongly recommended to read the [tutorial](https://www.cairographics.org/tutorial/) in the website.
## GtkDrawingArea
The following is a very simple example.
@@@include
color/color.gresource.xml
misc/da1.c
@@@
## Tfetextview.h, tfetextview.c and color.h
The function `main` is almost same as before.
The two functions `on_activate` and `draw_function` is important in this example.
First two files are the same as before.
Color.h just includes tfetextview.h.
- 16: Generates a GtkDrawingArea object.
- 20,21: Sets the width and height of the contents of the GtkDrawingArea widget.
These width and height is the size of the destination surface of the cairo context provided by the widget.
- 22: Sets a drawing function of the widget.
GtkDrawingArea widget uses the function to draw the contents of itself whenever its necessary.
For example, when a user drag a mouse pointer and resize a top level window, GtkDrawingArea also changes the size.
Then, the whole window needs to be redrawn.
@@@include
color/color.h
@@@
The drawing function has five parameters.
## Colorapplication.c
void drawing_function (GtkDrawingArea *drawing_area, cairo_t *cr, int width, int height,
gpointer user_data);
This is the main file.
It deals with:
The first parameter is the GtkDrawingArea widget which calls the drawing function.
However, you can't change any properties, for example `content-width` or `content-height`, in this function.
The second parameter is a cairo context given by the widget.
The destination surface of the context is connected to the contents of the widget.
What you draw to this surface will appear in the widget on the screen.
The third and fourth parameters are the size of the destination surface.
- Building widgets by GtkBuilder.
- Seting a drawing function of GtkDrawingArea.
And connecting a handler to "resize" signal on GtkDrawingArea.
- Implementing each call back functions.
Particularly, `Run` signal handler is the point in this program.
- 3-11: The drawing function.
- 4-5: Sets the source to be white and paint the destination white.
- 7: Sets the line width to be 2.
- 8: Sets the source to be black.
- 9: Adds a rectangle to the mask.
- 10: Draws the rectangle with black color to the destination.
The following is `colorapplication.c`.
Compile and run it, then a window with a black rectangle (square) appears.
Try resizing the window.
The square always appears at the center of the window because the drawing function is invoked every moment the window is resized.
@@@include
color/colorapplication.c
@@@
![Square in the window](../image/da1.png)
- 108-121: The function `main` is almost same as before but there are some differences.
The application ID is "com.github.ToshioCP.color".
`G_APPLICATION_FLAGS_NONE` is specified so no open signal handler is necessary.
- 86-106: Startup handler.
- 91-96: Builds widgets.
The pointers of the top window, TfeTextView and GtkDrawingArea objects are stored to static variables `win`, `tv` and `da` respectively.
This is because these objects are often used in handlers.
They never be rewritten so they're thread safe.
- 97: connects "resize" signal and the handler.
- 98: sets the drawing function.
- 81-84: Activates handler, which just shows the widgets.
- 73-79: The drawing function.
It just copies `surface` to destination.
- 65-71: Resize handler.
Re-creates the surface to fit the width and height of the drawing area and paints by calling the function `run`.
- 58-63: Closes the handler.
It destroys `surface` if it exists.
Then it destroys the top window and quits the application.
- 48-56: Open and save handler.
They just call the corresponding functions of TfeTextView.
- 42-46: Run handler.
It calls run function to paint the surface.
After that `gtk_widget_queue_draw` is called.
This fhunction adds the widget (GtkDrawingArea) to the queue to be redrawn.
It is important to know that the drawing function is called when it is necessary.
For example, when another window is moved and uncovers part of the widget, or when the window containing it is resized.
But repaint of `surface` is not automatically notified to gtk.
Therefore, you need to call `gtk_widget_queue_draw` to redraw the widget.
- 9-40: Run function paints the surface.
First, it gets the contents of GtkTextBuffer.
Then it compares it to "red", "green" and so on.
If it matches the color, then the surface is painted the color.
If it matches "light" or "dark", then the color of the surface is lightened or darkened respectively.
Alpha channel is used.
## Meson.build
This file is almost same as before.
An argument "export_dynamic: true" is added to executable function.
@@@include
color/meson.build
@@@
## Compile and execute it
First you need to export some variables (refer to [Section 2](sec2.src.md)).
$ . env.sh
Then type the following to compile it.
$ meson _build
$ ninja -C _build
The application is made in `_build` directory.
Type the following to execute it.
$ _build/color
Type "red", "green", "blue", "white", black", "light" or "dark" in the TfeTextView.
Then, click on `Run` button.
Make sure the color of GtkDrawingArea changes.
In this program TfeTextView is used to change the color.
You can use buttons or menus instead of textview.
Probably it is more appropriate.
Using textview is unnatural.
It is a good practice to make such application by yourself.

153
src/sec22.src.md Normal file
View file

@ -0,0 +1,153 @@
# Combine GtkDrawingArea and TfeTextView
Now, we will make a new application which has GtkDrawingArea and TfeTextView in it.
Its name is "color".
If you write a color in TfeTextView and click on the `run` button, then the color of GtkDrawingArea changes to the color given by you.
![color](../image/color.png){width=7.0cm height=5.13cm}
The following colors are available.
- white
- black
- red
- green
- blue
In addition the following two options are also available.
- light: Make the color of the drawing area lighter.
- dark: Make the color of the drawing area darker.
This application can only do very simple things.
However, it tells us that if we add powerful parser to it, we will be able to make it more efficient.
I want to show it to you in the later section by making a turtle graphics language like Logo program language.
In this section, we focus on how to bind the two objects.
## Color.ui and color.gresource.xml
First, We need to make the ui file of the widgets.
The image in the previous subsection gives us the structure of the widgets.
Title bar, four buttons in the tool bar and two widgets textview and drawing area.
The ui file is as follows.
@@@include
color/color.ui
@@@
- 10-53: This part describes the tool bar which has four buttons, `Run`, `Open`, `Save` and `Close`.
This is similar to the toolbar of tfe text editor in [Section 8](sec8.src.md).
There are two differences.
`Run` button replaces `New` button.
A signal element is added to each button object.
It has "name" attribute which is a signal name and "handler" attribute which is the name of its signal handler function.
Options "-WI, --export-dynamic" CFLAG is necessary when you compile the application.
You can achieve this by adding "export_dynamic: true" argument to executable function in `meson.build`.
And be careful that the handler must be defined without 'static' class.
- 54-76: Puts GtkScrolledWindow and GtkDrawingArea into GtkBox.
GtkBox has "homogeneous property" with TRUE value, so the two children have the same width in the box.
TfeTextView is a child of GtkScrolledWindow.
The xml file for the resource compiler is almost same as before.
Just substitute "color" for "tfe".
@@@include
color/color.gresource.xml
@@@
## Tfetextview.h, tfetextview.c and color.h
First two files are the same as before.
Color.h just includes tfetextview.h.
@@@include
color/color.h
@@@
## Colorapplication.c
This is the main file.
It deals with:
- Building widgets by GtkBuilder.
- Seting a drawing function of GtkDrawingArea.
And connecting a handler to "resize" signal on GtkDrawingArea.
- Implementing each call back functions.
Particularly, `Run` signal handler is the point in this program.
The following is `colorapplication.c`.
@@@include
color/colorapplication.c
@@@
- 108-121: The function `main` is almost same as before but there are some differences.
The application ID is "com.github.ToshioCP.color".
`G_APPLICATION_FLAGS_NONE` is specified so no open signal handler is necessary.
- 86-106: Startup handler.
- 91-96: Builds widgets.
The pointers of the top window, TfeTextView and GtkDrawingArea objects are stored to static variables `win`, `tv` and `da` respectively.
This is because these objects are often used in handlers.
They never be rewritten so they're thread safe.
- 97: connects "resize" signal and the handler.
- 98: sets the drawing function.
- 81-84: Activates handler, which just shows the widgets.
- 73-79: The drawing function.
It just copies `surface` to destination.
- 65-71: Resize handler.
Re-creates the surface to fit the width and height of the drawing area and paints by calling the function `run`.
- 58-63: Closes the handler.
It destroys `surface` if it exists.
Then it destroys the top window and quits the application.
- 48-56: Open and save handler.
They just call the corresponding functions of TfeTextView.
- 42-46: Run handler.
It calls run function to paint the surface.
After that `gtk_widget_queue_draw` is called.
This fhunction adds the widget (GtkDrawingArea) to the queue to be redrawn.
It is important to know that the drawing function is called when it is necessary.
For example, when another window is moved and uncovers part of the widget, or when the window containing it is resized.
But repaint of `surface` is not automatically notified to gtk.
Therefore, you need to call `gtk_widget_queue_draw` to redraw the widget.
- 9-40: Run function paints the surface.
First, it gets the contents of GtkTextBuffer.
Then it compares it to "red", "green" and so on.
If it matches the color, then the surface is painted the color.
If it matches "light" or "dark", then the color of the surface is lightened or darkened respectively.
Alpha channel is used.
## Meson.build
This file is almost same as before.
An argument "export_dynamic: true" is added to executable function.
@@@include
color/meson.build
@@@
## Compile and execute it
First you need to export some variables (refer to [Section 2](sec2.src.md)).
$ . env.sh
Then type the following to compile it.
$ meson _build
$ ninja -C _build
The application is made in `_build` directory.
Type the following to execute it.
$ _build/color
Type "red", "green", "blue", "white", black", "light" or "dark" in the TfeTextView.
Then, click on `Run` button.
Make sure the color of GtkDrawingArea changes.
In this program TfeTextView is used to change the color.
You can use buttons or menus instead of textview.
Probably it is more appropriate.
Using textview is unnatural.
It is a good practice to make such application by yourself.

View file

@ -0,0 +1,10 @@
<?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>

92
src/tfe7/css.c Normal file
View file

@ -0,0 +1,92 @@
/* css.h */
#include <gtk/gtk.h>
void
set_css_for_display (GtkWindow *win, char *css) {
GdkDisplay *display;
display = gtk_widget_get_display (GTK_WIDGET (win));
GtkCssProvider *provider = gtk_css_provider_new ();
gtk_css_provider_load_from_data (provider, css, -1);
gtk_style_context_add_provider_for_display (display, GTK_STYLE_PROVIDER (provider), GTK_STYLE_PROVIDER_PRIORITY_USER);
}
void
set_font_for_display (GtkWindow *win, const char *fontfamily, const char *fontstyle, const char *fontweight, int fontsize) {
char *textview_css;
textview_css = g_strdup_printf ("textview {padding: 10px; font-family: \"%s\"; font-style: %s; font-weight: %s; font-size: %dpt;}",
fontfamily, fontstyle, fontweight, fontsize);
set_css_for_display (win, textview_css);
}
void
set_font_for_display_with_pango_font_desc (GtkWindow *win, PangoFontDescription *pango_font_desc) {
int pango_style;
int pango_weight;
const char *family;
const char *style;
const char *weight;
int fontsize;
family = pango_font_description_get_family (pango_font_desc);
pango_style = pango_font_description_get_style (pango_font_desc);
switch (pango_style) {
case PANGO_STYLE_NORMAL:
style = "normal";
break;
case PANGO_STYLE_ITALIC:
style = "italic";
break;
case PANGO_STYLE_OBLIQUE:
style = "oblique";
break;
default:
style = "normal";
break;
}
pango_weight = pango_font_description_get_weight (pango_font_desc);
switch (pango_weight) {
case PANGO_WEIGHT_THIN:
weight = "100";
break;
case PANGO_WEIGHT_ULTRALIGHT:
weight = "200";
break;
case PANGO_WEIGHT_LIGHT:
weight = "300";
break;
case PANGO_WEIGHT_SEMILIGHT:
weight = "350";
break;
case PANGO_WEIGHT_BOOK:
weight = "380";
break;
case PANGO_WEIGHT_NORMAL:
weight = "400"; /* or "normal" */
break;
case PANGO_WEIGHT_MEDIUM:
weight = "500";
break;
case PANGO_WEIGHT_SEMIBOLD:
weight = "600";
break;
case PANGO_WEIGHT_BOLD:
weight = "700"; /* or "bold" */
break;
case PANGO_WEIGHT_ULTRABOLD:
weight = "800";
break;
case PANGO_WEIGHT_HEAVY:
weight = "900";
break;
case PANGO_WEIGHT_ULTRAHEAVY:
weight = "900"; /* In PangoWeight definition, the weight is 1000. But CSS allows the weight below 900. */
break;
default:
weight = "normal";
break;
}
fontsize = pango_font_description_get_size (pango_font_desc) / PANGO_SCALE;
set_font_for_display (win, family, style, weight, fontsize);
}

14
src/tfe7/css.h Normal file
View file

@ -0,0 +1,14 @@
#ifndef __TFE_CSS_H__
#define __TFE_CSS_H__
void
set_css (GtkWindow *win, char *css);
void
set_font_for_display (GtkWindow *win, const char *fontfamily, const char *fontstyle, const char *fontweight, int size);
void
set_font_for_display_with_pango_font_desc (GtkWindow *win, PangoFontDescription *pango_font_desc);
#endif /* __TFE_CSS_H__ */

27
src/tfe7/menu.ui Executable file
View file

@ -0,0 +1,27 @@
<?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>

16
src/tfe7/meson.build Normal file
View file

@ -0,0 +1,16 @@
project('tfe', 'c')
gtkdep = dependency('gtk4')
gnome=import('gnome')
resources = gnome.compile_resources('resources','tfe.gresource.xml')
gnome.compile_schemas(build_by_default: true, depend_files: 'com.github.ToshioCP.tfe.gschema.xml')
sourcefiles=files('tfeapplication.c', '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)

View file

@ -0,0 +1,9 @@
<?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>

38
src/tfe7/tfealert.c Normal file
View file

@ -0,0 +1,38 @@
#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));
}

19
src/tfe7/tfealert.h Normal file
View file

@ -0,0 +1,19 @@
#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__ */

47
src/tfe7/tfealert.ui Normal file
View file

@ -0,0 +1,47 @@
<?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>

64
src/tfe7/tfeapplication.c Normal file
View file

@ -0,0 +1,64 @@
#include "tfewindow.h"
/* ----- activate, open, startup handlers ----- */
static void
tfe_activate (GApplication *application) {
GtkApplication *app = GTK_APPLICATION (application);
GtkWidget *win = GTK_WIDGET (gtk_application_get_active_window (app));
tfe_window_notebook_page_new (win);
gtk_widget_show (GTK_WIDGET (win));
}
static void
tfe_open (GApplication *application, GFile ** files, gint n_files, const gchar *hint) {
GtkApplication *app = GTK_APPLICATION (application);
GtkWidget *win = GTK_WIDGET (gtk_application_get_active_window (app));
tfe_window_notebook_page_new_with_files (win, files, n_files);
gtk_widget_show (win);
}
static void
tfe_startup (GApplication *application) {
GtkApplication *app = GTK_APPLICATION (application);
GtkBuilder *build;
TfeWindow *win;
int i;
win = 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 ----- */
int
main (int argc, char **argv) {
GtkApplication *app;
int stat;
app = gtk_application_new ("com.github.ToshioCP.tfe", G_APPLICATION_HANDLES_OPEN);
g_signal_connect (app, "startup", G_CALLBACK (tfe_startup), NULL);
g_signal_connect (app, "activate", G_CALLBACK (tfe_activate), NULL);
g_signal_connect (app, "open", G_CALLBACK (tfe_open), NULL);
stat =g_application_run (G_APPLICATION (app), argc, argv);
g_object_unref (app);
return stat;
}

211
src/tfe7/tfenotebook.c Normal file
View file

@ -0,0 +1,211 @@
#include "tfenotebook.h"
#include "../tfetextview/tfetextview.h"
/* The returned string should be freed with g_free() when no longer needed. */
static gchar*
get_untitled () {
static int c = -1;
if (++c == 0)
return g_strdup_printf("Untitled");
else
return g_strdup_printf ("Untitled%u", c);
}
static TfeTextView *
get_current_textview (GtkNotebook *nb) {
int i;
GtkWidget *scr;
GtkWidget *tv;
i = gtk_notebook_get_current_page (nb);
scr = gtk_notebook_get_nth_page (nb, i);
tv = gtk_scrolled_window_get_child (GTK_SCROLLED_WINDOW (scr));
return TFE_TEXT_VIEW (tv);
}
static void
file_changed_cb (TfeTextView *tv) {
GtkWidget *nb = gtk_widget_get_ancestor (GTK_WIDGET (tv), GTK_TYPE_NOTEBOOK);
GtkWidget *scr;
GtkWidget *label;
GFile *file;
char *filename;
if (! GTK_IS_NOTEBOOK (nb)) /* tv not connected to nb yet */
return;
file = tfe_text_view_get_file (tv);
scr = gtk_widget_get_parent (GTK_WIDGET (tv));
if (G_IS_FILE (file)) {
filename = g_file_get_basename (file);
g_object_unref (file);
} else
filename = get_untitled ();
label = gtk_label_new (filename);
gtk_notebook_set_tab_label (GTK_NOTEBOOK (nb), scr, label);
}
static void
modified_changed_cb (GtkTextBuffer *tb, gpointer user_data) {
TfeTextView *tv = TFE_TEXT_VIEW (user_data);
GtkWidget *scr = gtk_widget_get_parent (GTK_WIDGET (tv));
GtkWidget *nb = gtk_widget_get_ancestor (GTK_WIDGET (tv), GTK_TYPE_NOTEBOOK);
GtkWidget *label;
const char *filename;
char *text;
if (! GTK_IS_NOTEBOOK (nb)) /* tv not connected to nb yet */
return;
else if (gtk_text_buffer_get_modified (tb)) {
filename = gtk_notebook_get_tab_label_text (GTK_NOTEBOOK (nb), scr);
text = g_strdup_printf ("*%s", filename);
label = gtk_label_new (text);
gtk_notebook_set_tab_label (GTK_NOTEBOOK (nb), scr, label);
} else
file_changed_cb (tv);
}
gboolean
has_saved (GtkNotebook *nb) {
g_return_val_if_fail (GTK_IS_NOTEBOOK (nb), false);
TfeTextView *tv;
GtkTextBuffer *tb;
tv = get_current_textview (nb);
tb = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv));
if (gtk_text_buffer_get_modified (tb))
return false;
else
return true;
}
gboolean
has_saved_all (GtkNotebook *nb) {
g_return_val_if_fail (GTK_IS_NOTEBOOK (nb), false);
int i, n;
GtkWidget *scr;
GtkWidget *tv;
GtkTextBuffer *tb;
n = gtk_notebook_get_n_pages (nb);
for (i = 0; i < n; ++i) {
scr = gtk_notebook_get_nth_page (nb, i);
tv = gtk_scrolled_window_get_child (GTK_SCROLLED_WINDOW (scr));
tb = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv));
if (gtk_text_buffer_get_modified (tb))
return false;
}
return true;
}
void
notebook_page_save (GtkNotebook *nb) {
g_return_if_fail(GTK_IS_NOTEBOOK (nb));
TfeTextView *tv;
tv = get_current_textview (nb);
tfe_text_view_save (TFE_TEXT_VIEW (tv));
}
void
notebook_page_saveas (GtkNotebook *nb) {
g_return_if_fail(GTK_IS_NOTEBOOK (nb));
TfeTextView *tv;
tv = get_current_textview (nb);
tfe_text_view_saveas (TFE_TEXT_VIEW (tv));
}
void
notebook_page_close (GtkNotebook *nb) {
g_return_if_fail(GTK_IS_NOTEBOOK (nb));
GtkWidget *win;
int i;
if (gtk_notebook_get_n_pages (nb) == 1) {
win = gtk_widget_get_ancestor (GTK_WIDGET (nb), GTK_TYPE_WINDOW);
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 ();
GtkTextBuffer *tb = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv));
GtkNotebookPage *nbp;
GtkWidget *lab;
int i;
gtk_text_view_set_wrap_mode (GTK_TEXT_VIEW (tv), GTK_WRAP_WORD_CHAR);
gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (scr), tv);
lab = gtk_label_new (filename);
i = gtk_notebook_append_page (nb, scr, lab);
nbp = gtk_notebook_get_page (nb, scr);
g_object_set (nbp, "tab-expand", TRUE, NULL);
gtk_notebook_set_current_page (nb, i);
g_signal_connect (GTK_TEXT_VIEW (tv), "change-file", G_CALLBACK (file_changed_cb), NULL);
g_signal_connect (tb, "modified-changed", G_CALLBACK (modified_changed_cb), tv);
}
static void
open_response (TfeTextView *tv, int response, GtkNotebook *nb) {
GFile *file;
char *filename;
if (response != TFE_OPEN_RESPONSE_SUCCESS) {
g_object_ref_sink (tv);
g_object_unref (tv);
}else if (! G_IS_FILE (file = tfe_text_view_get_file (tv))) {
g_object_ref_sink (tv);
g_object_unref (tv);
}else {
filename = g_file_get_basename (file);
g_object_unref (file);
notebook_page_build (nb, GTK_WIDGET (tv), filename);
}
}
void
notebook_page_open (GtkNotebook *nb) {
g_return_if_fail(GTK_IS_NOTEBOOK (nb));
GtkWidget *tv;
tv = tfe_text_view_new ();
g_signal_connect (TFE_TEXT_VIEW (tv), "open-response", G_CALLBACK (open_response), nb);
tfe_text_view_open (TFE_TEXT_VIEW (tv), gtk_widget_get_ancestor (GTK_WIDGET (nb), GTK_TYPE_WINDOW));
}
void
notebook_page_new_with_file (GtkNotebook *nb, GFile *file) {
g_return_if_fail(GTK_IS_NOTEBOOK (nb));
g_return_if_fail(G_IS_FILE (file));
GtkWidget *tv;
char *filename;
if ((tv = tfe_text_view_new_with_file (file)) == NULL)
return; /* read error */
filename = g_file_get_basename (file);
notebook_page_build (nb, tv, filename);
}
void
notebook_page_new (GtkNotebook *nb) {
g_return_if_fail(GTK_IS_NOTEBOOK (nb));
GtkWidget *tv;
char *filename;
tv = tfe_text_view_new ();
filename = get_untitled ();
notebook_page_build (nb, tv, filename);
}

31
src/tfe7/tfenotebook.h Normal file
View file

@ -0,0 +1,31 @@
#ifndef __TFE_NOTEBOOK_H__
#define __TFE_NOTEBOOK_H__
#include <gtk/gtk.h>
gboolean
has_saved (GtkNotebook *nb);
gboolean
has_saved_all (GtkNotebook *nb);
void
notebook_page_save(GtkNotebook *nb);
void
notebook_page_saveas(GtkNotebook *nb);
void
notebook_page_close (GtkNotebook *nb);
void
notebook_page_open (GtkNotebook *nb);
void
notebook_page_new_with_file (GtkNotebook *nb, GFile *file);
void
notebook_page_new (GtkNotebook *nb);
#endif /* __TFE_NOTEBOOK_H__ */

40
src/tfe7/tfepref.c Normal file
View file

@ -0,0 +1,40 @@
#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));
}

13
src/tfe7/tfepref.h Normal file
View file

@ -0,0 +1,13 @@
#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__ */

33
src/tfe7/tfepref.ui Normal file
View file

@ -0,0 +1,33 @@
<?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>

213
src/tfe7/tfewindow.c Normal file
View file

@ -0,0 +1,213 @@
#include "tfewindow.h"
#include "tfenotebook.h"
#include "tfepref.h"
#include "tfealert.h"
#include "css.h"
struct _TfeWindow {
GtkApplicationWindow parent;
GtkButton *btno;
GtkButton *btns;
GtkButton *btnc;
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));
}
/* ----- button handlers ----- */
void
open_cb (GtkNotebook *nb) {
notebook_page_open (nb);
}
void
save_cb (GtkNotebook *nb) {
notebook_page_save (nb);
}
void
close_cb (GtkNotebook *nb) {
TfeAlert *alert;
TfeWindow *win = TFE_WINDOW (gtk_widget_get_ancestor (GTK_WIDGET (nb), TFE_TYPE_WINDOW));
if (has_saved (nb))
notebook_page_close (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));
}
}
/* ----- action activated handlers ----- */
static void
open_activated (GSimpleAction *action, GVariant *parameter, gpointer user_data) {
TfeWindow *win = TFE_WINDOW (user_data);
open_cb (GTK_NOTEBOOK (win->nb));
}
static void
save_activated (GSimpleAction *action, GVariant *parameter, gpointer user_data) {
TfeWindow *win = TFE_WINDOW (user_data);
save_cb (GTK_NOTEBOOK (win->nb));
}
static void
close_activated (GSimpleAction *action, GVariant *parameter, gpointer user_data) {
TfeWindow *win = TFE_WINDOW (user_data);
close_cb (GTK_NOTEBOOK (win->nb));
}
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);
const char *font;
PangoFontDescription *pango_font_desc;
font = g_settings_get_string (settings, "font");
pango_font_desc = pango_font_description_from_string (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, btno);
gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (class), TfeWindow, btns);
gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (class), TfeWindow, btnc);
gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (class), TfeWindow, btnm);
gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (class), TfeWindow, nb);
gtk_widget_class_bind_template_callback (GTK_WIDGET_CLASS (class), open_cb);
gtk_widget_class_bind_template_callback (GTK_WIDGET_CLASS (class), save_cb);
gtk_widget_class_bind_template_callback (GTK_WIDGET_CLASS (class), close_cb);
}
GtkWidget *
tfe_window_new (GtkApplication *app) {
return GTK_WIDGET (g_object_new (TFE_TYPE_WINDOW, "application", app, NULL));
}

19
src/tfe7/tfewindow.h Normal file
View file

@ -0,0 +1,19 @@
#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__ */

65
src/tfe7/tfewindow.ui Normal file
View file

@ -0,0 +1,65 @@
<?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>
<signal name="clicked" handler="open_cb" swapped="TRUE" object="nb"></signal>
</object>
</child>
<child>
<object class="GtkButton" id="btns">
<property name="label">Save</property>
<signal name="clicked" handler="save_cb" swapped="TRUE" object="nb"></signal>
</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>
<signal name="clicked" handler="close_cb" swapped="TRUE" object="nb"></signal>
</object>
</child>
<child>
<object class="GtkMenuButton" id="btnm">
<property name="direction">down</property>
<property name="halign">start</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>