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