1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- */
2 /* SPDX-FileCopyrightText: 2001-2003 Mikael Hallendal <micke@imendio.com>
3 * SPDX-FileCopyrightText: 2003 CodeFactory AB
4 * SPDX-FileCopyrightText: 2008 Imendio AB
5 * SPDX-FileCopyrightText: 2010 Lanedo GmbH
6 * SPDX-FileCopyrightText: 2015, 2017, 2018 Sébastien Wilmet <swilmet@gnome.org>
7 * SPDX-License-Identifier: GPL-3.0-or-later
8 */
9
10 #include "config.h"
11 #include "dh-book-tree.h"
12 #include <glib/gi18n-lib.h>
13 #include "dh-book.h"
14 #include "dh-book-list.h"
15 #include "dh-settings.h"
16
17 /**
18 * SECTION:dh-book-tree
19 * @Title: DhBookTree
20 * @Short_description: A #GtkTreeView containing the tree structure of a
21 * #DhBookList
22 *
23 * #DhBookTree is a #GtkTreeView (showing a tree, not a list) containing the
24 * general tree structure of the #DhBook's contained in a #DhBookList (the
25 * #DhBookList part of the provided #DhProfile).
26 *
27 * #DhBookTree calls the dh_book_get_tree() function to get the tree structure
28 * of a #DhBook. As such the tree contains only #DhLink's of type
29 * %DH_LINK_TYPE_BOOK or %DH_LINK_TYPE_PAGE.
30 *
31 * When an element is selected, the #DhBookTree::link-selected signal is
32 * emitted. Only one element can be selected at a time.
33 */
34
35 typedef struct {
36 DhProfile *profile;
37 GtkTreeStore *store;
38 DhLink *selected_link;
39 GtkMenu *context_menu;
40 } DhBookTreePrivate;
41
42 typedef struct {
43 const gchar *uri;
44 GtkTreeIter iter;
45 GtkTreePath *path;
46 guint found : 1;
47 } FindURIData;
48
49 enum {
50 LINK_SELECTED,
51 N_SIGNALS
52 };
53
54 enum {
55 PROP_0,
56 PROP_PROFILE,
57 N_PROPERTIES
58 };
59
60 enum {
61 COL_TITLE,
62 COL_LINK,
63 COL_BOOK,
64 COL_WEIGHT,
65 COL_UNDERLINE,
66 N_COLUMNS
67 };
68
69 G_DEFINE_TYPE_WITH_PRIVATE (DhBookTree, dh_book_tree, GTK_TYPE_TREE_VIEW);
70
71 static guint signals[N_SIGNALS] = { 0 };
72 static GParamSpec *properties[N_PROPERTIES];
73
74 static gboolean
get_group_books_by_language(DhBookTree * tree)75 get_group_books_by_language (DhBookTree *tree)
76 {
77 DhBookTreePrivate *priv = dh_book_tree_get_instance_private (tree);
78 DhSettings *settings;
79
80 settings = dh_profile_get_settings (priv->profile);
81 return dh_settings_get_group_books_by_language (settings);
82 }
83
84 static void
book_tree_selection_changed_cb(GtkTreeSelection * selection,DhBookTree * tree)85 book_tree_selection_changed_cb (GtkTreeSelection *selection,
86 DhBookTree *tree)
87 {
88 DhBookTreePrivate *priv = dh_book_tree_get_instance_private (tree);
89 DhLink *link;
90
91 link = dh_book_tree_get_selected_link (tree);
92
93 if (link != NULL &&
94 link != priv->selected_link) {
95 if (priv->selected_link != NULL)
96 dh_link_unref (priv->selected_link);
97
98 priv->selected_link = dh_link_ref (link);
99 g_signal_emit (tree, signals[LINK_SELECTED], 0, link);
100 }
101
102 if (link != NULL)
103 dh_link_unref (link);
104 }
105
106 static void
book_tree_setup_selection(DhBookTree * tree)107 book_tree_setup_selection (DhBookTree *tree)
108 {
109 GtkTreeSelection *selection;
110
111 selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (tree));
112
113 gtk_tree_selection_set_mode (selection, GTK_SELECTION_BROWSE);
114
115 g_signal_connect_object (selection,
116 "changed",
117 G_CALLBACK (book_tree_selection_changed_cb),
118 tree,
119 0);
120 }
121
122 /* Tries to find:
123 * - An exact match of the language group
124 * - Or the language group which should be just after our given language group.
125 * - Or both.
126 *
127 * FIXME: not great code. Maybe have a DhLanguage object, and add a new column
128 * in the GtkTreeModel storing a DhLanguage object instead of a string.
129 */
130 static void
book_tree_find_language_group(DhBookTree * tree,const gchar * language,GtkTreeIter * exact_iter,gboolean * exact_found,GtkTreeIter * next_iter,gboolean * next_found)131 book_tree_find_language_group (DhBookTree *tree,
132 const gchar *language,
133 GtkTreeIter *exact_iter,
134 gboolean *exact_found,
135 GtkTreeIter *next_iter,
136 gboolean *next_found)
137 {
138 DhBookTreePrivate *priv = dh_book_tree_get_instance_private (tree);
139 GtkTreeIter loop_iter;
140
141 g_assert ((exact_iter != NULL && exact_found != NULL) ||
142 (next_iter != NULL && next_found != NULL));
143
144 /* Reset all flags to not found */
145 if (exact_found != NULL)
146 *exact_found = FALSE;
147 if (next_found != NULL)
148 *next_found = FALSE;
149
150 /* If we're not doing language grouping, return not found */
151 if (!get_group_books_by_language (tree))
152 return;
153
154 if (!gtk_tree_model_get_iter_first (GTK_TREE_MODEL (priv->store),
155 &loop_iter)) {
156 /* Store is empty, not found */
157 return;
158 }
159
160 do {
161 gchar *title = NULL;
162 DhLink *link;
163
164 /* Look for language titles, which are those where there
165 * is no book object associated in the row */
166 gtk_tree_model_get (GTK_TREE_MODEL (priv->store),
167 &loop_iter,
168 COL_TITLE, &title,
169 COL_LINK, &link,
170 -1);
171
172 if (link != NULL) {
173 /* Not a language */
174 g_free (title);
175 dh_link_unref (link);
176 g_return_if_reached ();
177 }
178
179 if (exact_iter != NULL &&
180 g_ascii_strcasecmp (title, language) == 0) {
181 /* Exact match found! */
182 *exact_iter = loop_iter;
183 *exact_found = TRUE;
184 if (next_iter == NULL) {
185 /* If we were not requested to look for the next one, end here */
186 g_free (title);
187 return;
188 }
189 } else if (next_iter != NULL &&
190 g_ascii_strcasecmp (title, language) > 0) {
191 *next_iter = loop_iter;
192 *next_found = TRUE;
193 /* There's no way to have an exact match after the next, so end here */
194 g_free (title);
195 return;
196 }
197
198 g_free (title);
199 } while (gtk_tree_model_iter_next (GTK_TREE_MODEL (priv->store),
200 &loop_iter));
201 }
202
203 /* Tries to find, starting at 'first' (if given), and always in the same
204 * level of the tree:
205 * - An exact match of the book
206 * - Or the book which should be just after our given book
207 * - Or both.
208 */
209 static void
book_tree_find_book(DhBookTree * tree,DhBook * book,const GtkTreeIter * first,GtkTreeIter * exact_iter,gboolean * exact_found,GtkTreeIter * next_iter,gboolean * next_found)210 book_tree_find_book (DhBookTree *tree,
211 DhBook *book,
212 const GtkTreeIter *first,
213 GtkTreeIter *exact_iter,
214 gboolean *exact_found,
215 GtkTreeIter *next_iter,
216 gboolean *next_found)
217 {
218 DhBookTreePrivate *priv = dh_book_tree_get_instance_private (tree);
219 GtkTreeIter loop_iter;
220
221 g_assert ((exact_iter != NULL && exact_found != NULL) ||
222 (next_iter != NULL && next_found != NULL));
223
224 /* Reset all flags to not found */
225 if (exact_found != NULL)
226 *exact_found = FALSE;
227 if (next_found != NULL)
228 *next_found = FALSE;
229
230 /* Setup iteration start */
231 if (first == NULL) {
232 /* If no first given, start iterating from the start of the model */
233 if (!gtk_tree_model_get_iter_first (GTK_TREE_MODEL (priv->store),
234 &loop_iter)) {
235 /* Store is empty, not found */
236 return;
237 }
238 } else {
239 loop_iter = *first;
240 }
241
242 do {
243 DhBook *in_tree_book = NULL;
244
245 gtk_tree_model_get (GTK_TREE_MODEL (priv->store),
246 &loop_iter,
247 COL_BOOK, &in_tree_book,
248 -1);
249
250 g_return_if_fail (DH_IS_BOOK (in_tree_book));
251
252 /* We can compare pointers directly as we're playing with references
253 * of the same object */
254 if (exact_iter != NULL &&
255 in_tree_book == book) {
256 *exact_iter = loop_iter;
257 *exact_found = TRUE;
258 if (next_iter == NULL) {
259 /* If we were not requested to look for the next one, end here */
260 g_object_unref (in_tree_book);
261 return;
262 }
263 } else if (next_iter != NULL &&
264 dh_book_cmp_by_title (in_tree_book, book) > 0) {
265 *next_iter = loop_iter;
266 *next_found = TRUE;
267 g_object_unref (in_tree_book);
268 return;
269 }
270
271 g_object_unref (in_tree_book);
272 } while (gtk_tree_model_iter_next (GTK_TREE_MODEL (priv->store),
273 &loop_iter));
274 }
275
276 static void
book_tree_insert_node(DhBookTree * tree,GNode * node,GtkTreeIter * current_iter,DhBook * book)277 book_tree_insert_node (DhBookTree *tree,
278 GNode *node,
279 GtkTreeIter *current_iter,
280 DhBook *book)
281
282 {
283 DhBookTreePrivate *priv = dh_book_tree_get_instance_private (tree);
284 DhLink *link;
285 PangoWeight weight;
286 GNode *child;
287
288 link = node->data;
289 g_assert (link != NULL);
290
291 if (dh_link_get_link_type (link) == DH_LINK_TYPE_BOOK)
292 weight = PANGO_WEIGHT_BOLD;
293 else
294 weight = PANGO_WEIGHT_NORMAL;
295
296 gtk_tree_store_set (priv->store,
297 current_iter,
298 COL_TITLE, dh_link_get_name (link),
299 COL_LINK, link,
300 COL_BOOK, book,
301 COL_WEIGHT, weight,
302 COL_UNDERLINE, PANGO_UNDERLINE_NONE,
303 -1);
304
305 for (child = g_node_first_child (node);
306 child != NULL;
307 child = g_node_next_sibling (child)) {
308 GtkTreeIter iter;
309
310 /* Append new iter */
311 gtk_tree_store_append (priv->store, &iter, current_iter);
312 book_tree_insert_node (tree, child, &iter, NULL);
313 }
314 }
315
316 static void
book_tree_add_book_to_store(DhBookTree * tree,DhBook * book)317 book_tree_add_book_to_store (DhBookTree *tree,
318 DhBook *book)
319 {
320 DhBookTreePrivate *priv = dh_book_tree_get_instance_private (tree);
321 GtkTreeIter book_iter;
322
323 /* If grouping by language we need to add the language categories */
324 if (get_group_books_by_language (tree)) {
325 GtkTreeIter language_iter;
326 gboolean language_iter_found;
327 GtkTreeIter next_language_iter;
328 gboolean next_language_iter_found;
329 const gchar *language_title;
330 gboolean new_language = FALSE;
331
332 language_title = dh_book_get_language (book);
333
334 /* Look for the proper language group */
335 book_tree_find_language_group (tree,
336 language_title,
337 &language_iter,
338 &language_iter_found,
339 &next_language_iter,
340 &next_language_iter_found);
341 /* New language group needs to be created? */
342 if (!language_iter_found) {
343 if (!next_language_iter_found) {
344 gtk_tree_store_append (priv->store,
345 &language_iter,
346 NULL);
347 } else {
348 gtk_tree_store_insert_before (priv->store,
349 &language_iter,
350 NULL,
351 &next_language_iter);
352 }
353
354 gtk_tree_store_set (priv->store,
355 &language_iter,
356 COL_TITLE, language_title,
357 COL_LINK, NULL,
358 COL_BOOK, NULL,
359 COL_WEIGHT, PANGO_WEIGHT_BOLD,
360 COL_UNDERLINE, PANGO_UNDERLINE_SINGLE,
361 -1);
362
363 new_language = TRUE;
364 }
365
366 /* If we got to add first book in a given language group, just append it. */
367 if (new_language) {
368 GtkTreePath *path;
369
370 gtk_tree_store_append (priv->store,
371 &book_iter,
372 &language_iter);
373
374 /* Make sure we start with the language row expanded */
375 path = gtk_tree_model_get_path (GTK_TREE_MODEL (priv->store),
376 &language_iter);
377 gtk_tree_view_expand_row (GTK_TREE_VIEW (tree),
378 path,
379 FALSE);
380 gtk_tree_path_free (path);
381 } else {
382 GtkTreeIter first_book_iter;
383 GtkTreeIter next_book_iter;
384 gboolean next_book_iter_found;
385
386 /* The language will have at least one book, so we move iter to it */
387 gtk_tree_model_iter_children (GTK_TREE_MODEL (priv->store),
388 &first_book_iter,
389 &language_iter);
390
391 /* Find next possible book in language group */
392 book_tree_find_book (tree,
393 book,
394 &first_book_iter,
395 NULL,
396 NULL,
397 &next_book_iter,
398 &next_book_iter_found);
399
400 if (!next_book_iter_found) {
401 gtk_tree_store_append (priv->store,
402 &book_iter,
403 &language_iter);
404 } else {
405 gtk_tree_store_insert_before (priv->store,
406 &book_iter,
407 &language_iter,
408 &next_book_iter);
409 }
410 }
411 } else {
412 /* No language grouping, just order by book title */
413 GtkTreeIter next_book_iter;
414 gboolean next_book_iter_found;
415
416 book_tree_find_book (tree,
417 book,
418 NULL,
419 NULL,
420 NULL,
421 &next_book_iter,
422 &next_book_iter_found);
423
424 if (!next_book_iter_found) {
425 gtk_tree_store_append (priv->store,
426 &book_iter,
427 NULL);
428 } else {
429 gtk_tree_store_insert_before (priv->store,
430 &book_iter,
431 NULL,
432 &next_book_iter);
433 }
434 }
435
436 /* Now book_iter contains the proper iterator where we'll add the whole
437 * book tree. */
438 book_tree_insert_node (tree,
439 dh_book_get_tree (book),
440 &book_iter,
441 book);
442 }
443
444 static void
add_book_cb(DhBookList * book_list,DhBook * book,DhBookTree * tree)445 add_book_cb (DhBookList *book_list,
446 DhBook *book,
447 DhBookTree *tree)
448 {
449 book_tree_add_book_to_store (tree, book);
450 }
451
452 static void
remove_book_cb(DhBookList * book_list,DhBook * book,DhBookTree * tree)453 remove_book_cb (DhBookList *book_list,
454 DhBook *book,
455 DhBookTree *tree)
456 {
457 DhBookTreePrivate *priv = dh_book_tree_get_instance_private (tree);
458 GtkTreeIter exact_iter;
459 gboolean exact_iter_found = FALSE;
460 GtkTreeIter language_iter;
461 gboolean language_iter_found = FALSE;
462
463 if (get_group_books_by_language (tree)) {
464 GtkTreeIter first_book_iter;
465
466 book_tree_find_language_group (tree,
467 dh_book_get_language (book),
468 &language_iter,
469 &language_iter_found,
470 NULL,
471 NULL);
472
473 if (language_iter_found &&
474 gtk_tree_model_iter_children (GTK_TREE_MODEL (priv->store),
475 &first_book_iter,
476 &language_iter)) {
477 book_tree_find_book (tree,
478 book,
479 &first_book_iter,
480 &exact_iter,
481 &exact_iter_found,
482 NULL,
483 NULL);
484 }
485 } else {
486 book_tree_find_book (tree,
487 book,
488 NULL,
489 &exact_iter,
490 &exact_iter_found,
491 NULL,
492 NULL);
493 }
494
495 if (exact_iter_found) {
496 /* Remove the book from the tree */
497 gtk_tree_store_remove (priv->store, &exact_iter);
498 /* If this book was inside a language group, check if the group
499 * is now empty and so removable */
500 if (language_iter_found) {
501 GtkTreeIter first_book_iter;
502
503 if (!gtk_tree_model_iter_children (GTK_TREE_MODEL (priv->store),
504 &first_book_iter,
505 &language_iter)) {
506 /* Oh, well, no more books in this language... remove! */
507 gtk_tree_store_remove (priv->store, &language_iter);
508 }
509 }
510 }
511 }
512
513 static void
book_tree_init_selection(DhBookTree * tree)514 book_tree_init_selection (DhBookTree *tree)
515 {
516 DhBookTreePrivate *priv;
517 GtkTreeSelection *selection;
518 GtkTreeIter iter;
519 gboolean iter_found = FALSE;
520
521 priv = dh_book_tree_get_instance_private (tree);
522
523 /* Mark the first item as selected, or it would get automatically
524 * selected when the treeview will get focus (a behavior that we want to
525 * avoid); but that's not even enough as a selection ::changed would
526 * still be emitted when there is no change, hence the manual tracking
527 * of selection with priv->selected_link.
528 *
529 * If there is no manual tracking with selected_link, there is this bug:
530 * 1. Open Devhelp.
531 * 2. The first book is initially selected (thanks to this function),
532 * OK.
533 * 3. Click on the arrow of another book to expand it.
534 * --> The other book gets correctly expanded (and not selected), but
535 * the selection ::changed signal is emitted for the *first* book
536 * (even though it was already selected, strange).
537 *
538 * https://bugzilla.gnome.org/show_bug.cgi?id=492206 - GtkTreeView bug
539 * https://bugzilla.gnome.org/show_bug.cgi?id=603040 - Devhelp bug
540 */
541 selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (tree));
542 g_signal_handlers_block_by_func (selection,
543 book_tree_selection_changed_cb,
544 tree);
545
546 /* If grouping by languages, get first book in the first language */
547 if (get_group_books_by_language (tree)) {
548 GtkTreeIter language_iter;
549
550 if (gtk_tree_model_get_iter_first (GTK_TREE_MODEL (priv->store),
551 &language_iter)) {
552 iter_found = gtk_tree_model_iter_children (GTK_TREE_MODEL (priv->store),
553 &iter,
554 &language_iter);
555 }
556 } else {
557 iter_found = gtk_tree_model_get_iter_first (GTK_TREE_MODEL (priv->store),
558 &iter);
559 }
560
561 if (iter_found) {
562 DhLink *link;
563
564 gtk_tree_model_get (GTK_TREE_MODEL (priv->store),
565 &iter,
566 COL_LINK, &link,
567 -1);
568
569 if (link == NULL || dh_link_get_link_type (link) != DH_LINK_TYPE_BOOK)
570 g_warn_if_reached ();
571
572 if (priv->selected_link != NULL)
573 dh_link_unref (priv->selected_link);
574
575 priv->selected_link = link;
576 gtk_tree_selection_select_iter (selection, &iter);
577 }
578
579 g_signal_handlers_unblock_by_func (selection,
580 book_tree_selection_changed_cb,
581 tree);
582 }
583
584 static void
book_tree_populate_tree(DhBookTree * tree)585 book_tree_populate_tree (DhBookTree *tree)
586 {
587 DhBookTreePrivate *priv = dh_book_tree_get_instance_private (tree);
588 GList *books;
589 GList *l;
590
591 gtk_tree_view_set_model (GTK_TREE_VIEW (tree), NULL);
592 gtk_tree_store_clear (priv->store);
593 gtk_tree_view_set_model (GTK_TREE_VIEW (tree),
594 GTK_TREE_MODEL (priv->store));
595
596 books = dh_book_list_get_books (dh_profile_get_book_list (priv->profile));
597
598 for (l = books; l != NULL; l = l->next) {
599 DhBook *book = DH_BOOK (l->data);
600 book_tree_add_book_to_store (tree, book);
601 }
602
603 book_tree_init_selection (tree);
604 }
605
606 static void
group_books_by_language_notify_cb(DhSettings * settings,GParamSpec * pspec,DhBookTree * tree)607 group_books_by_language_notify_cb (DhSettings *settings,
608 GParamSpec *pspec,
609 DhBookTree *tree)
610 {
611 book_tree_populate_tree (tree);
612 }
613
614 static void
set_profile(DhBookTree * tree,DhProfile * profile)615 set_profile (DhBookTree *tree,
616 DhProfile *profile)
617 {
618 DhBookTreePrivate *priv = dh_book_tree_get_instance_private (tree);
619
620 g_return_if_fail (profile == NULL || DH_IS_PROFILE (profile));
621
622 g_assert (priv->profile == NULL);
623 g_set_object (&priv->profile, profile);
624 }
625
626 static void
dh_book_tree_get_property(GObject * object,guint prop_id,GValue * value,GParamSpec * pspec)627 dh_book_tree_get_property (GObject *object,
628 guint prop_id,
629 GValue *value,
630 GParamSpec *pspec)
631 {
632 DhBookTree *tree = DH_BOOK_TREE (object);
633
634 switch (prop_id) {
635 case PROP_PROFILE:
636 g_value_set_object (value, dh_book_tree_get_profile (tree));
637 break;
638
639 default:
640 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
641 break;
642 }
643 }
644
645 static void
dh_book_tree_set_property(GObject * object,guint prop_id,const GValue * value,GParamSpec * pspec)646 dh_book_tree_set_property (GObject *object,
647 guint prop_id,
648 const GValue *value,
649 GParamSpec *pspec)
650 {
651 DhBookTree *tree = DH_BOOK_TREE (object);
652
653 switch (prop_id) {
654 case PROP_PROFILE:
655 set_profile (tree, g_value_get_object (value));
656 break;
657
658 default:
659 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
660 break;
661 }
662 }
663
664 static void
dh_book_tree_constructed(GObject * object)665 dh_book_tree_constructed (GObject *object)
666 {
667 DhBookTree *tree = DH_BOOK_TREE (object);
668 DhBookTreePrivate *priv = dh_book_tree_get_instance_private (tree);
669 DhBookList *book_list;
670 DhSettings *settings;
671
672 if (G_OBJECT_CLASS (dh_book_tree_parent_class)->constructed != NULL)
673 G_OBJECT_CLASS (dh_book_tree_parent_class)->constructed (object);
674
675 if (priv->profile == NULL)
676 priv->profile = g_object_ref (dh_profile_get_default ());
677
678 book_tree_setup_selection (tree);
679
680 book_list = dh_profile_get_book_list (priv->profile);
681
682 g_signal_connect_object (book_list,
683 "add-book",
684 G_CALLBACK (add_book_cb),
685 tree,
686 G_CONNECT_AFTER);
687
688 g_signal_connect_object (book_list,
689 "remove-book",
690 G_CALLBACK (remove_book_cb),
691 tree,
692 G_CONNECT_AFTER);
693
694 settings = dh_profile_get_settings (priv->profile);
695 g_signal_connect_object (settings,
696 "notify::group-books-by-language",
697 G_CALLBACK (group_books_by_language_notify_cb),
698 tree,
699 0);
700
701 book_tree_populate_tree (tree);
702 }
703
704 static void
dh_book_tree_dispose(GObject * object)705 dh_book_tree_dispose (GObject *object)
706 {
707 DhBookTreePrivate *priv = dh_book_tree_get_instance_private (DH_BOOK_TREE (object));
708
709 g_clear_object (&priv->profile);
710 g_clear_object (&priv->store);
711 priv->context_menu = NULL;
712
713 if (priv->selected_link != NULL) {
714 dh_link_unref (priv->selected_link);
715 priv->selected_link = NULL;
716 }
717
718 G_OBJECT_CLASS (dh_book_tree_parent_class)->dispose (object);
719 }
720
721 static void
collapse_all_activate_cb(GtkMenuItem * menu_item,DhBookTree * tree)722 collapse_all_activate_cb (GtkMenuItem *menu_item,
723 DhBookTree *tree)
724 {
725 gtk_tree_view_collapse_all (GTK_TREE_VIEW (tree));
726 }
727
728 static void
do_popup_menu(DhBookTree * tree,GdkEventButton * event)729 do_popup_menu (DhBookTree *tree,
730 GdkEventButton *event)
731 {
732 DhBookTreePrivate *priv = dh_book_tree_get_instance_private (tree);
733
734 if (priv->context_menu == NULL) {
735 GtkWidget *menu_item;
736
737 /* Create the menu only once. At first I wanted to create a new
738 * menu each time this function is called, connect to the
739 * GtkMenuShell::deactivate signal to call gtk_widget_destroy().
740 * But GtkMenuShell::deactivate is emitted before
741 * collapse_all_activate_cb(), so collapse_all_activate_cb() was
742 * never called... It's maybe a GTK bug.
743 */
744 priv->context_menu = GTK_MENU (gtk_menu_new ());
745
746 /* When tree is destroyed, the context menu is destroyed too. */
747 gtk_menu_attach_to_widget (priv->context_menu, GTK_WIDGET (tree), NULL);
748
749 menu_item = gtk_menu_item_new_with_mnemonic (_("_Collapse All"));
750 gtk_menu_shell_append (GTK_MENU_SHELL (priv->context_menu), menu_item);
751 gtk_widget_show (menu_item);
752
753 g_signal_connect_object (menu_item,
754 "activate",
755 G_CALLBACK (collapse_all_activate_cb),
756 tree,
757 0);
758 }
759
760 if (event != NULL) {
761 gtk_menu_popup_at_pointer (priv->context_menu, (GdkEvent *) event);
762 } else {
763 gtk_menu_popup_at_widget (priv->context_menu,
764 GTK_WIDGET (tree),
765 GDK_GRAVITY_NORTH_EAST,
766 GDK_GRAVITY_NORTH_WEST,
767 NULL);
768 }
769 }
770
771 static gboolean
dh_book_tree_button_press_event(GtkWidget * widget,GdkEventButton * event)772 dh_book_tree_button_press_event (GtkWidget *widget,
773 GdkEventButton *event)
774 {
775 DhBookTree *tree = DH_BOOK_TREE (widget);
776
777 if (gdk_event_triggers_context_menu ((GdkEvent *) event) &&
778 event->type == GDK_BUTTON_PRESS) {
779 do_popup_menu (tree, event);
780 return GDK_EVENT_STOP;
781 }
782
783 if (GTK_WIDGET_CLASS (dh_book_tree_parent_class)->button_press_event != NULL)
784 return GTK_WIDGET_CLASS (dh_book_tree_parent_class)->button_press_event (widget, event);
785
786 return GDK_EVENT_PROPAGATE;
787 }
788
789 static gboolean
dh_book_tree_popup_menu(GtkWidget * widget)790 dh_book_tree_popup_menu (GtkWidget *widget)
791 {
792 if (GTK_WIDGET_CLASS (dh_book_tree_parent_class)->popup_menu != NULL)
793 g_warning ("%s(): chain-up?", G_STRFUNC);
794
795 do_popup_menu (DH_BOOK_TREE (widget), NULL);
796 return TRUE;
797 }
798
799 static void
dh_book_tree_class_init(DhBookTreeClass * klass)800 dh_book_tree_class_init (DhBookTreeClass *klass)
801 {
802 GObjectClass *object_class = G_OBJECT_CLASS (klass);
803 GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
804
805 object_class->get_property = dh_book_tree_get_property;
806 object_class->set_property = dh_book_tree_set_property;
807 object_class->constructed = dh_book_tree_constructed;
808 object_class->dispose = dh_book_tree_dispose;
809
810 widget_class->button_press_event = dh_book_tree_button_press_event;
811 widget_class->popup_menu = dh_book_tree_popup_menu;
812
813 /**
814 * DhBookTree::link-selected:
815 * @tree: the #DhBookTree.
816 * @link: the selected #DhLink.
817 */
818 signals[LINK_SELECTED] =
819 g_signal_new ("link-selected",
820 G_TYPE_FROM_CLASS (klass),
821 G_SIGNAL_RUN_LAST,
822 0,
823 NULL, NULL, NULL,
824 G_TYPE_NONE,
825 1, DH_TYPE_LINK);
826
827 /**
828 * DhBookTree:profile:
829 *
830 * The #DhProfile. If set to %NULL, the default profile as returned by
831 * dh_profile_get_default() is used.
832 *
833 * Since: 3.30
834 */
835 properties[PROP_PROFILE] =
836 g_param_spec_object ("profile",
837 "Profile",
838 "",
839 DH_TYPE_PROFILE,
840 G_PARAM_READWRITE |
841 G_PARAM_CONSTRUCT_ONLY |
842 G_PARAM_STATIC_STRINGS);
843
844 g_object_class_install_properties (object_class, N_PROPERTIES, properties);
845 }
846
847 static void
book_tree_add_columns(DhBookTree * tree)848 book_tree_add_columns (DhBookTree *tree)
849 {
850 GtkCellRenderer *cell;
851 GtkTreeViewColumn *column;
852
853 column = gtk_tree_view_column_new ();
854
855 cell = gtk_cell_renderer_text_new ();
856 g_object_set (cell,
857 "ellipsize", PANGO_ELLIPSIZE_END,
858 NULL);
859 gtk_tree_view_column_pack_start (column, cell, TRUE);
860 gtk_tree_view_column_set_attributes (column, cell,
861 "text", COL_TITLE,
862 "weight", COL_WEIGHT,
863 "underline", COL_UNDERLINE,
864 NULL);
865
866 gtk_tree_view_append_column (GTK_TREE_VIEW (tree), column);
867 }
868
869 static void
dh_book_tree_init(DhBookTree * tree)870 dh_book_tree_init (DhBookTree *tree)
871 {
872 DhBookTreePrivate *priv = dh_book_tree_get_instance_private (tree);
873
874 gtk_tree_view_set_headers_visible (GTK_TREE_VIEW (tree), FALSE);
875 gtk_tree_view_set_enable_search (GTK_TREE_VIEW (tree), FALSE);
876
877 priv->store = gtk_tree_store_new (N_COLUMNS,
878 G_TYPE_STRING, /* Title */
879 DH_TYPE_LINK,
880 DH_TYPE_BOOK,
881 PANGO_TYPE_WEIGHT,
882 PANGO_TYPE_UNDERLINE);
883
884 gtk_tree_view_set_model (GTK_TREE_VIEW (tree),
885 GTK_TREE_MODEL (priv->store));
886
887 book_tree_add_columns (tree);
888 }
889
890 /**
891 * dh_book_tree_new:
892 * @profile: (nullable): a #DhProfile, or %NULL for the default profile.
893 *
894 * Returns: (transfer floating): a new #DhBookTree widget.
895 */
896 DhBookTree *
dh_book_tree_new(DhProfile * profile)897 dh_book_tree_new (DhProfile *profile)
898 {
899 g_return_val_if_fail (profile == NULL || DH_IS_PROFILE (profile), NULL);
900
901 return g_object_new (DH_TYPE_BOOK_TREE,
902 "profile", profile,
903 NULL);
904 }
905
906 /**
907 * dh_book_tree_get_profile:
908 * @tree: a #DhBookTree.
909 *
910 * Returns: (transfer none): the #DhProfile of @tree.
911 * Since: 3.30
912 */
913 DhProfile *
dh_book_tree_get_profile(DhBookTree * tree)914 dh_book_tree_get_profile (DhBookTree *tree)
915 {
916 DhBookTreePrivate *priv;
917
918 g_return_val_if_fail (DH_IS_BOOK_TREE (tree), NULL);
919
920 priv = dh_book_tree_get_instance_private (tree);
921 return priv->profile;
922 }
923
924 /**
925 * dh_book_tree_get_selected_link:
926 * @tree: a #DhBookTree.
927 *
928 * Returns: (transfer full) (nullable): the currently selected #DhLink in @tree,
929 * or %NULL if the selection is empty or if a language group row is selected.
930 * Unref with dh_link_unref() when no longer needed.
931 * Since: 3.30
932 */
933 DhLink *
dh_book_tree_get_selected_link(DhBookTree * tree)934 dh_book_tree_get_selected_link (DhBookTree *tree)
935 {
936 GtkTreeSelection *selection;
937 GtkTreeModel *model;
938 GtkTreeIter iter;
939 DhLink *link;
940
941 g_return_val_if_fail (DH_IS_BOOK_TREE (tree), NULL);
942
943 selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (tree));
944 if (!gtk_tree_selection_get_selected (selection, &model, &iter))
945 return NULL;
946
947 gtk_tree_model_get (model, &iter,
948 COL_LINK, &link,
949 -1);
950
951 return link;
952 }
953
954 static gboolean
book_tree_find_uri_foreach_func(GtkTreeModel * model,GtkTreePath * path,GtkTreeIter * iter,gpointer _data)955 book_tree_find_uri_foreach_func (GtkTreeModel *model,
956 GtkTreePath *path,
957 GtkTreeIter *iter,
958 gpointer _data)
959 {
960 FindURIData *data = _data;
961 DhLink *link;
962
963 gtk_tree_model_get (model, iter,
964 COL_LINK, &link,
965 -1);
966
967 if (link != NULL) {
968 gchar *link_uri;
969
970 link_uri = dh_link_get_uri (link);
971
972 if (link_uri != NULL &&
973 g_str_has_prefix (data->uri, link_uri)) {
974 data->found = TRUE;
975 data->iter = *iter;
976 data->path = gtk_tree_path_copy (path);
977 }
978
979 g_free (link_uri);
980 dh_link_unref (link);
981 }
982
983 return data->found;
984 }
985
986 /**
987 * dh_book_tree_select_uri:
988 * @tree: a #DhBookTree.
989 * @uri: the URI to select.
990 *
991 * Selects the row corresponding to @uri. It searches in the tree a #DhLink
992 * being at @uri (if it's an exact match), or containing @uri (if @uri contains
993 * an anchor).
994 */
995 void
dh_book_tree_select_uri(DhBookTree * tree,const gchar * uri)996 dh_book_tree_select_uri (DhBookTree *tree,
997 const gchar *uri)
998 {
999 DhBookTreePrivate *priv;
1000 GtkTreeSelection *selection;
1001 FindURIData data;
1002
1003 g_return_if_fail (DH_IS_BOOK_TREE (tree));
1004 g_return_if_fail (uri != NULL);
1005
1006 priv = dh_book_tree_get_instance_private (tree);
1007
1008 data.found = FALSE;
1009 data.uri = uri;
1010
1011 gtk_tree_model_foreach (GTK_TREE_MODEL (priv->store),
1012 book_tree_find_uri_foreach_func,
1013 &data);
1014
1015 if (!data.found)
1016 return;
1017
1018 selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (tree));
1019
1020 /* Do not re-expand/select/scroll if already there. */
1021 if (gtk_tree_selection_iter_is_selected (selection, &data.iter))
1022 goto out;
1023
1024 /* The order is important here: select_iter() doesn't work if the row is
1025 * hidden.
1026 */
1027 gtk_tree_view_expand_to_path (GTK_TREE_VIEW (tree), data.path);
1028 gtk_tree_selection_select_iter (selection, &data.iter);
1029
1030 gtk_tree_view_scroll_to_cell (GTK_TREE_VIEW (tree),
1031 data.path, NULL,
1032 FALSE, 0.0, 0.0);
1033
1034 out:
1035 gtk_tree_path_free (data.path);
1036 }
1037