1/* Text View/Hypertext
2 * #Keywords: GtkTextView, GtkTextBuffer
3 *
4 * Usually, tags modify the appearance of text in the view, e.g. making it
5 * bold or colored or underlined. But tags are not restricted to appearance.
6 * They can also affect the behavior of mouse and key presses, as this demo
7 * shows.
8 *
9 * We also demonstrate adding other things to a text view, such as
10 * clickable icons and widgets which can also replace a character
11 * (try copying the ghost text).
12 */
13
14#include <gtk/gtk.h>
15#include <gdk/gdkkeysyms.h>
16
17/* Inserts a piece of text into the buffer, giving it the usual
18 * appearance of a hyperlink in a web browser: blue and underlined.
19 * Additionally, attaches some data on the tag, to make it recognizable
20 * as a link.
21 */
22static void
23insert_link (GtkTextBuffer *buffer,
24 GtkTextIter *iter,
25 const char *text,
26 int page)
27{
28 GtkTextTag *tag;
29
30 tag = gtk_text_buffer_create_tag (buffer, NULL,
31 first_property_name: "foreground", "blue",
32 "underline", PANGO_UNDERLINE_SINGLE,
33 NULL);
34 g_object_set_data (G_OBJECT (tag), key: "page", GINT_TO_POINTER (page));
35 gtk_text_buffer_insert_with_tags (buffer, iter, text, len: -1, first_tag: tag, NULL);
36}
37
38/* Quick-and-dirty text-to-speech for a single word. If you don't hear
39 * anything, you are missing espeak-ng on your system.
40 */
41static void
42say_word (GtkGestureClick *gesture,
43 guint n_press,
44 double x,
45 double y,
46 const char *word)
47{
48 const char *argv[3];
49
50 argv[0] = "espeak-ng";
51 argv[1] = word;
52 argv[2] = NULL;
53
54 g_spawn_async (NULL, argv: (char **)argv, NULL, flags: G_SPAWN_SEARCH_PATH, NULL, NULL, NULL, NULL);
55}
56
57/* Fills the buffer with text and interspersed links. In any real
58 * hypertext app, this method would parse a file to identify the links.
59 */
60static void
61show_page (GtkTextView *text_view,
62 int page)
63{
64 GtkTextBuffer *buffer;
65 GtkTextIter iter, start;
66 GtkTextMark *mark;
67 GtkWidget *child;
68 GtkTextChildAnchor *anchor;
69 GtkEventController *controller;
70 GtkTextTag *bold, *mono, *nobreaks;
71
72 buffer = gtk_text_view_get_buffer (text_view);
73
74 bold = gtk_text_buffer_create_tag (buffer, NULL,
75 first_property_name: "weight", PANGO_WEIGHT_BOLD,
76 "scale", PANGO_SCALE_X_LARGE,
77 NULL);
78 mono = gtk_text_buffer_create_tag (buffer, NULL,
79 first_property_name: "family", "monospace",
80 NULL);
81 nobreaks = gtk_text_buffer_create_tag (buffer, NULL,
82 first_property_name: "allow-breaks", FALSE,
83 NULL);
84
85 gtk_text_buffer_set_text (buffer, text: "", len: 0);
86 gtk_text_buffer_get_iter_at_offset (buffer, iter: &iter, char_offset: 0);
87 gtk_text_buffer_begin_irreversible_action (buffer);
88 if (page == 1)
89 {
90 GtkIconPaintable *icon;
91 GtkIconTheme *theme;
92
93 gtk_text_buffer_insert (buffer, iter: &iter, text: "Some text to show that simple ", len: -1);
94 insert_link (buffer, iter: &iter, text: "hypertext", page: 3);
95 gtk_text_buffer_insert (buffer, iter: &iter, text: " can easily be realized with ", len: -1);
96 insert_link (buffer, iter: &iter, text: "tags", page: 2);
97 gtk_text_buffer_insert (buffer, iter: &iter, text: ".\n", len: -1);
98 gtk_text_buffer_insert (buffer, iter: &iter, text: "Of course you can also embed Emoji 😋, ", len: -1);
99 gtk_text_buffer_insert (buffer, iter: &iter, text: "icons ", len: -1);
100
101 theme = gtk_icon_theme_get_for_display (display: gtk_widget_get_display (GTK_WIDGET (text_view)));
102 icon = gtk_icon_theme_lookup_icon (self: theme,
103 icon_name: "eye-not-looking-symbolic",
104 NULL,
105 size: 16,
106 scale: 1,
107 direction: GTK_TEXT_DIR_LTR,
108 flags: 0);
109 gtk_text_buffer_insert_paintable (buffer, iter: &iter, paintable: GDK_PAINTABLE (ptr: icon));
110 g_object_unref (object: icon);
111 gtk_text_buffer_insert (buffer, iter: &iter, text: ", or even widgets ", len: -1);
112 anchor = gtk_text_buffer_create_child_anchor (buffer, iter: &iter);
113 child = gtk_level_bar_new_for_interval (min_value: 0, max_value: 100);
114 gtk_level_bar_set_value (GTK_LEVEL_BAR (child), value: 50);
115 gtk_widget_set_size_request (widget: child, width: 100, height: -1);
116 gtk_text_view_add_child_at_anchor (text_view, child, anchor);
117 gtk_text_buffer_insert (buffer, iter: &iter, text: " and labels with ", len: -1);
118 anchor = gtk_text_child_anchor_new_with_replacement (character: "👻");
119 gtk_text_buffer_insert_child_anchor (buffer, iter: &iter, anchor);
120 child = gtk_label_new (str: "ghost");
121 gtk_text_view_add_child_at_anchor (text_view, child, anchor);
122 gtk_text_buffer_insert (buffer, iter: &iter, text: " text.", len: -1);
123 }
124 else if (page == 2)
125 {
126 mark = gtk_text_buffer_create_mark (buffer, mark_name: "mark", where: &iter, TRUE);
127
128 gtk_text_buffer_insert_with_tags (buffer, iter: &iter, text: "tag", len: -1, first_tag: bold, NULL);
129 gtk_text_buffer_insert (buffer, iter: &iter, text: " /", len: -1);
130
131 gtk_text_buffer_get_iter_at_mark (buffer, iter: &start, mark);
132 gtk_text_buffer_apply_tag (buffer, tag: nobreaks, start: &start, end: &iter);
133 gtk_text_buffer_insert (buffer, iter: &iter, text: " ", len: -1);
134
135 gtk_text_buffer_move_mark (buffer, mark, where: &iter);
136 gtk_text_buffer_insert_with_tags (buffer, iter: &iter, text: "tag", len: -1, first_tag: mono, NULL);
137 gtk_text_buffer_insert (buffer, iter: &iter, text: " /", len: -1);
138
139 gtk_text_buffer_get_iter_at_mark (buffer, iter: &start, mark);
140 gtk_text_buffer_apply_tag (buffer, tag: nobreaks, start: &start, end: &iter);
141 gtk_text_buffer_insert (buffer, iter: &iter, text: " ", len: -1);
142
143 anchor = gtk_text_buffer_create_child_anchor (buffer, iter: &iter);
144 child = gtk_image_new_from_icon_name (icon_name: "audio-volume-high-symbolic");
145 gtk_widget_set_cursor_from_name (widget: child, name: "pointer");
146 controller = GTK_EVENT_CONTROLLER (gtk_gesture_click_new ());
147 g_signal_connect (controller, "pressed", G_CALLBACK (say_word), (gpointer)"tag");
148 gtk_widget_add_controller (widget: child, controller);
149 gtk_text_view_add_child_at_anchor (text_view, child, anchor);
150
151 gtk_text_buffer_insert (buffer, iter: &iter, text: "\n"
152 "An attribute that can be applied to some range of text. For example, "
153 "a tag might be called “bold” and make the text inside the tag bold.\n"
154 "However, the tag concept is more general than that; "
155 "tags don't have to affect appearance. They can instead affect the "
156 "behavior of mouse and key presses, “lock” a range of text so the "
157 "user can't edit it, or countless other things.\n", len: -1);
158 insert_link (buffer, iter: &iter, text: "Go back", page: 1);
159
160 gtk_text_buffer_delete_mark (buffer, mark);
161 }
162 else if (page == 3)
163 {
164 mark = gtk_text_buffer_create_mark (buffer, mark_name: "mark", where: &iter, TRUE);
165
166 gtk_text_buffer_insert_with_tags (buffer, iter: &iter, text: "hypertext", len: -1, first_tag: bold, NULL);
167 gtk_text_buffer_insert (buffer, iter: &iter, text: " /", len: -1);
168
169 gtk_text_buffer_get_iter_at_mark (buffer, iter: &start, mark);
170 gtk_text_buffer_apply_tag (buffer, tag: nobreaks, start: &start, end: &iter);
171 gtk_text_buffer_insert (buffer, iter: &iter, text: " ", len: -1);
172
173 gtk_text_buffer_move_mark (buffer, mark, where: &iter);
174 gtk_text_buffer_insert_with_tags (buffer, iter: &iter, text: "ˈhaɪ pərˌtɛkst", len: -1, first_tag: mono, NULL);
175 gtk_text_buffer_insert (buffer, iter: &iter, text: " /", len: -1);
176 gtk_text_buffer_get_iter_at_mark (buffer, iter: &start, mark);
177 gtk_text_buffer_apply_tag (buffer, tag: nobreaks, start: &start, end: &iter);
178 gtk_text_buffer_insert (buffer, iter: &iter, text: " ", len: -1);
179
180 anchor = gtk_text_buffer_create_child_anchor (buffer, iter: &iter);
181 child = gtk_image_new_from_icon_name (icon_name: "audio-volume-high-symbolic");
182 gtk_widget_set_cursor_from_name (widget: child, name: "pointer");
183 controller = GTK_EVENT_CONTROLLER (gtk_gesture_click_new ());
184 g_signal_connect (controller, "pressed", G_CALLBACK (say_word), (gpointer)"hypertext");
185 gtk_widget_add_controller (widget: child, controller);
186 gtk_text_view_add_child_at_anchor (text_view, child, anchor);
187
188 gtk_text_buffer_insert (buffer, iter: &iter, text: "\n"
189 "Machine-readable text that is not sequential but is organized "
190 "so that related items of information are connected.\n", len: -1);
191 insert_link (buffer, iter: &iter, text: "Go back", page: 1);
192
193 gtk_text_buffer_delete_mark (buffer, mark);
194 }
195 gtk_text_buffer_end_irreversible_action (buffer);
196}
197
198/* Looks at all tags covering the position of iter in the text view,
199 * and if one of them is a link, follow it by showing the page identified
200 * by the data attached to it.
201 */
202static void
203follow_if_link (GtkWidget *text_view,
204 GtkTextIter *iter)
205{
206 GSList *tags = NULL, *tagp = NULL;
207
208 tags = gtk_text_iter_get_tags (iter);
209 for (tagp = tags; tagp != NULL; tagp = tagp->next)
210 {
211 GtkTextTag *tag = tagp->data;
212 int page = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (tag), "page"));
213
214 if (page != 0)
215 {
216 show_page (GTK_TEXT_VIEW (text_view), page);
217 break;
218 }
219 }
220
221 if (tags)
222 g_slist_free (list: tags);
223}
224
225/* Links can be activated by pressing Enter.
226 */
227static gboolean
228key_pressed (GtkEventController *controller,
229 guint keyval,
230 guint keycode,
231 GdkModifierType modifiers,
232 GtkWidget *text_view)
233{
234 GtkTextIter iter;
235 GtkTextBuffer *buffer;
236
237 switch (keyval)
238 {
239 case GDK_KEY_Return:
240 case GDK_KEY_KP_Enter:
241 buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (text_view));
242 gtk_text_buffer_get_iter_at_mark (buffer, iter: &iter,
243 mark: gtk_text_buffer_get_insert (buffer));
244 follow_if_link (text_view, iter: &iter);
245 break;
246
247 default:
248 break;
249 }
250
251 return GDK_EVENT_PROPAGATE;
252}
253
254static void set_cursor_if_appropriate (GtkTextView *text_view,
255 int x,
256 int y);
257
258static void
259released_cb (GtkGestureClick *gesture,
260 guint n_press,
261 double x,
262 double y,
263 GtkWidget *text_view)
264{
265 GtkTextIter start, end, iter;
266 GtkTextBuffer *buffer;
267 int tx, ty;
268
269 if (gtk_gesture_single_get_button (GTK_GESTURE_SINGLE (gesture)) > 1)
270 return;
271
272 gtk_text_view_window_to_buffer_coords (GTK_TEXT_VIEW (text_view),
273 win: GTK_TEXT_WINDOW_WIDGET,
274 window_x: x, window_y: y, buffer_x: &tx, buffer_y: &ty);
275
276 buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (text_view));
277
278 /* we shouldn't follow a link if the user has selected something */
279 gtk_text_buffer_get_selection_bounds (buffer, start: &start, end: &end);
280 if (gtk_text_iter_get_offset (iter: &start) != gtk_text_iter_get_offset (iter: &end))
281 return;
282
283 if (gtk_text_view_get_iter_at_location (GTK_TEXT_VIEW (text_view), iter: &iter, x: tx, y: ty))
284 follow_if_link (text_view, iter: &iter);
285}
286
287static void
288motion_cb (GtkEventControllerMotion *controller,
289 double x,
290 double y,
291 GtkTextView *text_view)
292{
293 int tx, ty;
294
295 gtk_text_view_window_to_buffer_coords (text_view,
296 win: GTK_TEXT_WINDOW_WIDGET,
297 window_x: x, window_y: y, buffer_x: &tx, buffer_y: &ty);
298
299 set_cursor_if_appropriate (text_view, x: tx, y: ty);
300}
301
302static gboolean hovering_over_link = FALSE;
303
304/* Looks at all tags covering the position (x, y) in the text view,
305 * and if one of them is a link, change the cursor to the "hands" cursor
306 * typically used by web browsers.
307 */
308static void
309set_cursor_if_appropriate (GtkTextView *text_view,
310 int x,
311 int y)
312{
313 GSList *tags = NULL, *tagp = NULL;
314 GtkTextIter iter;
315 gboolean hovering = FALSE;
316
317 if (gtk_text_view_get_iter_at_location (text_view, iter: &iter, x, y))
318 {
319 tags = gtk_text_iter_get_tags (iter: &iter);
320 for (tagp = tags; tagp != NULL; tagp = tagp->next)
321 {
322 GtkTextTag *tag = tagp->data;
323 int page = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (tag), "page"));
324
325 if (page != 0)
326 {
327 hovering = TRUE;
328 break;
329 }
330 }
331 }
332
333 if (hovering != hovering_over_link)
334 {
335 hovering_over_link = hovering;
336
337 if (hovering_over_link)
338 gtk_widget_set_cursor_from_name (GTK_WIDGET (text_view), name: "pointer");
339 else
340 gtk_widget_set_cursor_from_name (GTK_WIDGET (text_view), name: "text");
341 }
342
343 if (tags)
344 g_slist_free (list: tags);
345}
346
347GtkWidget *
348do_hypertext (GtkWidget *do_widget)
349{
350 static GtkWidget *window = NULL;
351
352 if (!window)
353 {
354 GtkWidget *view;
355 GtkWidget *sw;
356 GtkTextBuffer *buffer;
357 GtkEventController *controller;
358
359 window = gtk_window_new ();
360 gtk_window_set_title (GTK_WINDOW (window), title: "Hypertext");
361 gtk_window_set_display (GTK_WINDOW (window),
362 display: gtk_widget_get_display (widget: do_widget));
363 gtk_window_set_default_size (GTK_WINDOW (window), width: 330, height: 330);
364 gtk_window_set_resizable (GTK_WINDOW (window), FALSE);
365 g_object_add_weak_pointer (G_OBJECT (window), weak_pointer_location: (gpointer *)&window);
366
367 view = gtk_text_view_new ();
368 gtk_text_view_set_wrap_mode (GTK_TEXT_VIEW (view), wrap_mode: GTK_WRAP_WORD);
369 gtk_text_view_set_top_margin (GTK_TEXT_VIEW (view), top_margin: 20);
370 gtk_text_view_set_bottom_margin (GTK_TEXT_VIEW (view), bottom_margin: 20);
371 gtk_text_view_set_left_margin (GTK_TEXT_VIEW (view), left_margin: 20);
372 gtk_text_view_set_right_margin (GTK_TEXT_VIEW (view), right_margin: 20);
373 gtk_text_view_set_pixels_below_lines (GTK_TEXT_VIEW (view), pixels_below_lines: 10);
374 controller = gtk_event_controller_key_new ();
375 g_signal_connect (controller, "key-pressed", G_CALLBACK (key_pressed), view);
376 gtk_widget_add_controller (widget: view, controller);
377
378 controller = GTK_EVENT_CONTROLLER (gtk_gesture_click_new ());
379 g_signal_connect (controller, "released",
380 G_CALLBACK (released_cb), view);
381 gtk_widget_add_controller (widget: view, controller);
382
383 controller = gtk_event_controller_motion_new ();
384 g_signal_connect (controller, "motion",
385 G_CALLBACK (motion_cb), view);
386 gtk_widget_add_controller (widget: view, controller);
387
388 buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (view));
389 gtk_text_buffer_set_enable_undo (buffer, TRUE);
390
391 sw = gtk_scrolled_window_new ();
392 gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (sw),
393 hscrollbar_policy: GTK_POLICY_NEVER,
394 vscrollbar_policy: GTK_POLICY_AUTOMATIC);
395 gtk_window_set_child (GTK_WINDOW (window), child: sw);
396 gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (sw), child: view);
397
398 show_page (GTK_TEXT_VIEW (view), page: 1);
399 }
400
401 if (!gtk_widget_get_visible (widget: window))
402 gtk_widget_show (widget: window);
403 else
404 gtk_window_destroy (GTK_WINDOW (window));
405
406 return window;
407}
408

source code of gtk/demos/gtk-demo/hypertext.c