1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- */
2 /* SPDX-FileCopyrightText: 2001-2003 CodeFactory AB
3  * SPDX-FileCopyrightText: 2001-2003 Mikael Hallendal <micke@imendio.com>
4  * SPDX-FileCopyrightText: 2005-2008 Imendio AB
5  * SPDX-FileCopyrightText: 2010 Lanedo GmbH
6  * SPDX-FileCopyrightText: 2013 Aleksander Morgado <aleksander@gnu.org>
7  * SPDX-FileCopyrightText: 2015, 2017, 2018 Sébastien Wilmet <swilmet@gnome.org>
8  * SPDX-License-Identifier: GPL-3.0-or-later
9  */
10 
11 #include "dh-sidebar.h"
12 #include "dh-book.h"
13 #include "dh-book-tree.h"
14 #include "dh-keyword-model.h"
15 
16 /**
17  * SECTION:dh-sidebar
18  * @Title: DhSidebar
19  * @Short_description: The sidebar
20  *
21  * In the Devhelp application, there is one #DhSidebar per main window,
22  * displayed in the left side panel.
23  *
24  * A #DhSidebar contains:
25  * - a #GtkSearchEntry at the top;
26  * - a #DhBookTree (a subclass of #GtkTreeView);
27  * - another #GtkTreeView (displaying a list, not a tree) with a #DhKeywordModel
28  *   as its model.
29  *
30  * When the #GtkSearchEntry is empty, the #DhBookTree is shown. When the
31  * #GtkSearchEntry is not empty, it shows the search results in the other
32  * #GtkTreeView. The two #GtkTreeView's cannot be both visible at the same time,
33  * it's either one or the other.
34  *
35  * #DhSidebar emits the #DhSidebar::link-selected signal. When that happens, the
36  * Devhelp application opens the #DhLink in a #WebKitWebView shown at the right
37  * side of the main window.
38  */
39 
40 typedef struct {
41         DhProfile *profile;
42 
43         /* A GtkSearchEntry. */
44         GtkEntry *entry;
45 
46         DhBookTree *book_tree;
47         GtkScrolledWindow *sw_book_tree;
48 
49         DhKeywordModel *hitlist_model;
50         GtkTreeView *hitlist_view;
51         GtkScrolledWindow *sw_hitlist;
52 
53         guint idle_complete_id;
54         guint idle_search_id;
55 } DhSidebarPrivate;
56 
57 enum {
58         SIGNAL_LINK_SELECTED,
59         N_SIGNALS
60 };
61 
62 enum {
63         PROP_0,
64         PROP_PROFILE,
65         N_PROPERTIES
66 };
67 
68 static guint signals[N_SIGNALS] = { 0 };
69 static GParamSpec *properties[N_PROPERTIES];
70 
G_DEFINE_TYPE_WITH_PRIVATE(DhSidebar,dh_sidebar,GTK_TYPE_GRID)71 G_DEFINE_TYPE_WITH_PRIVATE (DhSidebar, dh_sidebar, GTK_TYPE_GRID)
72 
73 static void
74 set_profile (DhSidebar *sidebar,
75              DhProfile *profile)
76 {
77         DhSidebarPrivate *priv = dh_sidebar_get_instance_private (sidebar);
78 
79         g_return_if_fail (profile == NULL || DH_IS_PROFILE (profile));
80 
81         g_assert (priv->profile == NULL);
82         g_set_object (&priv->profile, profile);
83 }
84 
85 static void
dh_sidebar_get_property(GObject * object,guint prop_id,GValue * value,GParamSpec * pspec)86 dh_sidebar_get_property (GObject    *object,
87                          guint       prop_id,
88                          GValue     *value,
89                          GParamSpec *pspec)
90 {
91         DhSidebar *sidebar = DH_SIDEBAR (object);
92 
93         switch (prop_id) {
94                 case PROP_PROFILE:
95                         g_value_set_object (value, dh_sidebar_get_profile (sidebar));
96                         break;
97 
98                 default:
99                         G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
100                         break;
101         }
102 }
103 
104 static void
dh_sidebar_set_property(GObject * object,guint prop_id,const GValue * value,GParamSpec * pspec)105 dh_sidebar_set_property (GObject      *object,
106                          guint         prop_id,
107                          const GValue *value,
108                          GParamSpec   *pspec)
109 {
110         DhSidebar *sidebar = DH_SIDEBAR (object);
111 
112         switch (prop_id) {
113                 case PROP_PROFILE:
114                         set_profile (sidebar, g_value_get_object (value));
115                         break;
116 
117                 default:
118                         G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
119                         break;
120         }
121 }
122 
123 /******************************************************************************/
124 
125 static gboolean
search_idle_cb(gpointer user_data)126 search_idle_cb (gpointer user_data)
127 {
128         DhSidebar *sidebar = DH_SIDEBAR (user_data);
129         DhSidebarPrivate *priv = dh_sidebar_get_instance_private (sidebar);
130         const gchar *search_text;
131         const gchar *book_id;
132         DhLink *selected_link;
133         DhLink *exact_link;
134 
135         priv->idle_search_id = 0;
136 
137         search_text = gtk_entry_get_text (priv->entry);
138 
139         selected_link = dh_book_tree_get_selected_link (priv->book_tree);
140         book_id = selected_link != NULL ? dh_link_get_book_id (selected_link) : NULL;
141 
142         /* Disconnect the model, see the doc of dh_keyword_model_filter(). */
143         gtk_tree_view_set_model (priv->hitlist_view, NULL);
144 
145         exact_link = dh_keyword_model_filter (priv->hitlist_model,
146                                               search_text,
147                                               book_id,
148                                               priv->profile);
149 
150         gtk_tree_view_set_model (priv->hitlist_view,
151                                  GTK_TREE_MODEL (priv->hitlist_model));
152 
153         if (exact_link != NULL)
154                 g_signal_emit (sidebar, signals[SIGNAL_LINK_SELECTED], 0, exact_link);
155 
156         if (selected_link != NULL)
157                 dh_link_unref (selected_link);
158 
159         return G_SOURCE_REMOVE;
160 }
161 
162 static void
setup_search_idle(DhSidebar * sidebar)163 setup_search_idle (DhSidebar *sidebar)
164 {
165         DhSidebarPrivate *priv = dh_sidebar_get_instance_private (sidebar);
166 
167         if (priv->idle_search_id == 0)
168                 priv->idle_search_id = g_idle_add (search_idle_cb, sidebar);
169 }
170 
171 /******************************************************************************/
172 
173 /* Create DhCompletion objects, because if all the DhCompletion objects need to
174  * be created (synchronously) at the time of the first completion, it can make
175  * the GUI not responsive (measured time was for example 40ms to create the
176  * DhCompletion's for 17 books, which is not a lot of books). On application
177  * startup it is less a problem.
178  */
179 static void
create_completion_objects(DhBookList * book_list)180 create_completion_objects (DhBookList *book_list)
181 {
182         GList *books;
183         GList *l;
184 
185         books = dh_book_list_get_books (book_list);
186 
187         for (l = books; l != NULL; l = l->next) {
188                 DhBook *cur_book = DH_BOOK (l->data);
189                 dh_book_get_completion (cur_book);
190         }
191 }
192 
193 static void
add_book_cb(DhBookList * book_list,DhBook * book,DhSidebar * sidebar)194 add_book_cb (DhBookList *book_list,
195              DhBook     *book,
196              DhSidebar  *sidebar)
197 {
198         /* See comment of create_completion_objects(). */
199         dh_book_get_completion (book);
200 
201         /* Update current search if any. */
202         setup_search_idle (sidebar);
203 }
204 
205 static void
remove_book_cb(DhBookList * book_list,DhBook * book,DhSidebar * sidebar)206 remove_book_cb (DhBookList *book_list,
207                 DhBook     *book,
208                 DhSidebar  *sidebar)
209 {
210         /* Update current search if any. */
211         setup_search_idle (sidebar);
212 }
213 
214 /******************************************************************************/
215 
216 /* Returns: (transfer full) (nullable): */
217 static DhLink *
hitlist_get_selected_link(DhSidebar * sidebar)218 hitlist_get_selected_link (DhSidebar *sidebar)
219 {
220         DhSidebarPrivate *priv = dh_sidebar_get_instance_private (sidebar);
221         GtkTreeSelection *selection;
222         GtkTreeModel *model;
223         GtkTreeIter iter;
224         DhLink *link;
225 
226         selection = gtk_tree_view_get_selection (priv->hitlist_view);
227         if (!gtk_tree_selection_get_selected (selection, &model, &iter))
228                 return NULL;
229 
230         gtk_tree_model_get (model, &iter,
231                             DH_KEYWORD_MODEL_COL_LINK, &link,
232                             -1);
233 
234         return link;
235 }
236 
237 static void
hitlist_selection_changed_cb(GtkTreeSelection * selection,DhSidebar * sidebar)238 hitlist_selection_changed_cb (GtkTreeSelection *selection,
239                               DhSidebar        *sidebar)
240 {
241         DhLink *link;
242 
243         link = hitlist_get_selected_link (sidebar);
244 
245         if (link != NULL) {
246                 g_signal_emit (sidebar, signals[SIGNAL_LINK_SELECTED], 0, link);
247                 dh_link_unref (link);
248         }
249 }
250 
251 static gboolean
entry_key_press_event_cb(GtkEntry * entry,GdkEventKey * event,DhSidebar * sidebar)252 entry_key_press_event_cb (GtkEntry    *entry,
253                           GdkEventKey *event,
254                           DhSidebar   *sidebar)
255 {
256         DhSidebarPrivate *priv = dh_sidebar_get_instance_private (sidebar);
257 
258         if (event->keyval == GDK_KEY_Tab) {
259                 if (event->state & GDK_CONTROL_MASK) {
260                         if (gtk_widget_is_visible (GTK_WIDGET (priv->hitlist_view)))
261                                 gtk_widget_grab_focus (GTK_WIDGET (priv->hitlist_view));
262                 } else {
263                         gtk_editable_select_region (GTK_EDITABLE (entry), 0, 0);
264                         gtk_editable_set_position (GTK_EDITABLE (entry), -1);
265                 }
266 
267                 return GDK_EVENT_STOP;
268         }
269 
270         return GDK_EVENT_PROPAGATE;
271 }
272 
273 static void
entry_changed_cb(GtkEntry * entry,DhSidebar * sidebar)274 entry_changed_cb (GtkEntry  *entry,
275                   DhSidebar *sidebar)
276 {
277         DhSidebarPrivate *priv = dh_sidebar_get_instance_private (sidebar);
278         const gchar *search_text;
279 
280         search_text = gtk_entry_get_text (entry);
281 
282         /* We don't want a delay when the search text becomes empty, to show the
283          * book tree. So do it here and not in entry_search_changed_cb().
284          */
285         if (search_text == NULL || search_text[0] == '\0') {
286                 gtk_widget_hide (GTK_WIDGET (priv->sw_hitlist));
287                 gtk_widget_show (GTK_WIDGET (priv->sw_book_tree));
288         }
289 }
290 
291 static void
entry_search_changed_cb(GtkSearchEntry * search_entry,DhSidebar * sidebar)292 entry_search_changed_cb (GtkSearchEntry *search_entry,
293                          DhSidebar      *sidebar)
294 {
295         DhSidebarPrivate *priv = dh_sidebar_get_instance_private (sidebar);
296         const gchar *search_text;
297 
298         search_text = gtk_entry_get_text (GTK_ENTRY (search_entry));
299 
300         if (search_text != NULL && search_text[0] != '\0') {
301                 gtk_widget_hide (GTK_WIDGET (priv->sw_book_tree));
302                 gtk_widget_show (GTK_WIDGET (priv->sw_hitlist));
303                 setup_search_idle (sidebar);
304         }
305 }
306 
307 static gboolean
complete_idle_cb(gpointer user_data)308 complete_idle_cb (gpointer user_data)
309 {
310         DhSidebar *sidebar = DH_SIDEBAR (user_data);
311         DhSidebarPrivate *priv = dh_sidebar_get_instance_private (sidebar);
312         GList *books;
313         GList *l;
314         GList *completion_objects = NULL;
315         const gchar *search_text;
316         gchar *completed;
317 
318         books = dh_book_list_get_books (dh_profile_get_book_list (priv->profile));
319         for (l = books; l != NULL; l = l->next) {
320                 DhBook *cur_book = DH_BOOK (l->data);
321                 DhCompletion *completion;
322 
323                 completion = dh_book_get_completion (cur_book);
324                 completion_objects = g_list_prepend (completion_objects, completion);
325         }
326 
327         search_text = gtk_entry_get_text (priv->entry);
328         completed = dh_completion_aggregate_complete (completion_objects, search_text);
329 
330         if (completed != NULL) {
331                 guint16 n_chars_before;
332 
333                 n_chars_before = gtk_entry_get_text_length (priv->entry);
334 
335                 gtk_entry_set_text (priv->entry, completed);
336                 gtk_editable_set_position (GTK_EDITABLE (priv->entry), n_chars_before);
337                 gtk_editable_select_region (GTK_EDITABLE (priv->entry),
338                                             n_chars_before, -1);
339         }
340 
341         g_list_free (completion_objects);
342         g_free (completed);
343 
344         priv->idle_complete_id = 0;
345         return G_SOURCE_REMOVE;
346 }
347 
348 static void
entry_insert_text_cb(GtkEntry * entry,const gchar * text,gint length,gint * position,DhSidebar * sidebar)349 entry_insert_text_cb (GtkEntry    *entry,
350                       const gchar *text,
351                       gint         length,
352                       gint        *position,
353                       DhSidebar   *sidebar)
354 {
355         DhSidebarPrivate *priv = dh_sidebar_get_instance_private (sidebar);
356 
357         if (priv->idle_complete_id == 0)
358                 priv->idle_complete_id = g_idle_add (complete_idle_cb, sidebar);
359 }
360 
361 static void
entry_stop_search_cb(GtkSearchEntry * entry,gpointer user_data)362 entry_stop_search_cb (GtkSearchEntry *entry,
363                       gpointer        user_data)
364 {
365         gtk_entry_set_text (GTK_ENTRY (entry), "");
366 }
367 
368 static void
hitlist_cell_data_func(GtkTreeViewColumn * tree_column,GtkCellRenderer * cell,GtkTreeModel * hitlist_model,GtkTreeIter * iter,gpointer data)369 hitlist_cell_data_func (GtkTreeViewColumn *tree_column,
370                         GtkCellRenderer   *cell,
371                         GtkTreeModel      *hitlist_model,
372                         GtkTreeIter       *iter,
373                         gpointer           data)
374 {
375         DhLink *link;
376         DhLinkType link_type;
377         PangoStyle style;
378         PangoWeight weight;
379         gboolean current_book_flag;
380         gchar *name;
381 
382         gtk_tree_model_get (hitlist_model, iter,
383                             DH_KEYWORD_MODEL_COL_LINK, &link,
384                             DH_KEYWORD_MODEL_COL_CURRENT_BOOK_FLAG, &current_book_flag,
385                             -1);
386 
387         if (dh_link_get_flags (link) & DH_LINK_FLAGS_DEPRECATED)
388                 style = PANGO_STYLE_ITALIC;
389         else
390                 style = PANGO_STYLE_NORMAL;
391 
392         /* Matches on the current book are given in bold. Note that we check the
393          * current book as it was given to the DhKeywordModel. Do *not* rely on
394          * the current book as given by the DhSidebar, as that will change
395          * whenever a hit is clicked.
396          */
397         if (current_book_flag)
398                 weight = PANGO_WEIGHT_BOLD;
399         else
400                 weight = PANGO_WEIGHT_NORMAL;
401 
402         link_type = dh_link_get_link_type (link);
403 
404         if (link_type == DH_LINK_TYPE_STRUCT ||
405             link_type == DH_LINK_TYPE_PROPERTY ||
406             link_type == DH_LINK_TYPE_SIGNAL) {
407                 name = g_markup_printf_escaped ("%s <i><small><span weight=\"normal\">(%s)</span></small></i>",
408                                                 dh_link_get_name (link),
409                                                 dh_link_type_to_string (link_type));
410         } else {
411                 name = g_markup_printf_escaped ("%s", dh_link_get_name (link));
412         }
413 
414         g_object_set (cell,
415                       "markup", name,
416                       "style", style,
417                       "weight", weight,
418                       NULL);
419 
420         dh_link_unref (link);
421         g_free (name);
422 }
423 
424 static void
book_tree_link_selected_cb(DhBookTree * book_tree,DhLink * link,DhSidebar * sidebar)425 book_tree_link_selected_cb (DhBookTree *book_tree,
426                             DhLink     *link,
427                             DhSidebar  *sidebar)
428 {
429         g_signal_emit (sidebar, signals[SIGNAL_LINK_SELECTED], 0, link);
430 }
431 
432 static void
dh_sidebar_constructed(GObject * object)433 dh_sidebar_constructed (GObject *object)
434 {
435         DhSidebar *sidebar = DH_SIDEBAR (object);
436         DhSidebarPrivate *priv = dh_sidebar_get_instance_private (sidebar);
437         GtkTreeSelection *selection;
438         GtkCellRenderer *cell;
439         DhBookList *book_list;
440 
441         if (G_OBJECT_CLASS (dh_sidebar_parent_class)->constructed != NULL)
442                 G_OBJECT_CLASS (dh_sidebar_parent_class)->constructed (object);
443 
444         if (priv->profile == NULL)
445                 priv->profile = g_object_ref (dh_profile_get_default ());
446 
447         /* Setup the search entry */
448         priv->entry = GTK_ENTRY (gtk_search_entry_new ());
449         gtk_widget_set_hexpand (GTK_WIDGET (priv->entry), TRUE);
450         g_object_set (priv->entry,
451                       "margin", 6,
452                       NULL);
453         gtk_container_add (GTK_CONTAINER (sidebar), GTK_WIDGET (priv->entry));
454 
455         g_signal_connect (priv->entry,
456                           "key-press-event",
457                           G_CALLBACK (entry_key_press_event_cb),
458                           sidebar);
459 
460         g_signal_connect (priv->entry,
461                           "changed",
462                           G_CALLBACK (entry_changed_cb),
463                           sidebar);
464 
465         g_signal_connect (priv->entry,
466                           "search-changed",
467                           G_CALLBACK (entry_search_changed_cb),
468                           sidebar);
469 
470         g_signal_connect (priv->entry,
471                           "insert-text",
472                           G_CALLBACK (entry_insert_text_cb),
473                           sidebar);
474 
475         g_signal_connect (priv->entry,
476                           "stop-search",
477                           G_CALLBACK (entry_stop_search_cb),
478                           NULL);
479 
480         /* Setup hitlist */
481         priv->hitlist_model = dh_keyword_model_new ();
482         priv->hitlist_view = GTK_TREE_VIEW (gtk_tree_view_new ());
483         gtk_tree_view_set_model (priv->hitlist_view, GTK_TREE_MODEL (priv->hitlist_model));
484         gtk_tree_view_set_headers_visible (priv->hitlist_view, FALSE);
485         gtk_tree_view_set_enable_search (priv->hitlist_view, FALSE);
486         gtk_widget_show (GTK_WIDGET (priv->hitlist_view));
487 
488         selection = gtk_tree_view_get_selection (priv->hitlist_view);
489 
490         /* Set BROWSE mode. When clicking again on the same (already selected)
491          * row, it re-emits the ::changed signal, which is convenient to come
492          * back to that symbol when the HTML view has been scrolled away.
493          */
494         gtk_tree_selection_set_mode (selection, GTK_SELECTION_BROWSE);
495 
496         g_signal_connect (selection,
497                           "changed",
498                           G_CALLBACK (hitlist_selection_changed_cb),
499                           sidebar);
500 
501         cell = gtk_cell_renderer_text_new ();
502         g_object_set (cell,
503                       "ellipsize", PANGO_ELLIPSIZE_END,
504                       NULL);
505         gtk_tree_view_insert_column_with_data_func (priv->hitlist_view,
506                                                     -1,
507                                                     NULL,
508                                                     cell,
509                                                     hitlist_cell_data_func,
510                                                     sidebar,
511                                                     NULL);
512 
513         /* Hitlist packing */
514         priv->sw_hitlist = GTK_SCROLLED_WINDOW (gtk_scrolled_window_new (NULL, NULL));
515         gtk_widget_set_no_show_all (GTK_WIDGET (priv->sw_hitlist), TRUE);
516         gtk_scrolled_window_set_policy (priv->sw_hitlist,
517                                         GTK_POLICY_NEVER,
518                                         GTK_POLICY_AUTOMATIC);
519         gtk_container_add (GTK_CONTAINER (priv->sw_hitlist),
520                            GTK_WIDGET (priv->hitlist_view));
521         gtk_widget_set_hexpand (GTK_WIDGET (priv->sw_hitlist), TRUE);
522         gtk_widget_set_vexpand (GTK_WIDGET (priv->sw_hitlist), TRUE);
523         gtk_container_add (GTK_CONTAINER (sidebar), GTK_WIDGET (priv->sw_hitlist));
524 
525         /* DhBookList */
526         book_list = dh_profile_get_book_list (priv->profile);
527         create_completion_objects (book_list);
528 
529         g_signal_connect_object (book_list,
530                                  "add-book",
531                                  G_CALLBACK (add_book_cb),
532                                  sidebar,
533                                  G_CONNECT_AFTER);
534 
535         g_signal_connect_object (book_list,
536                                  "remove-book",
537                                  G_CALLBACK (remove_book_cb),
538                                  sidebar,
539                                  G_CONNECT_AFTER);
540 
541         /* Setup the book tree */
542         priv->sw_book_tree = GTK_SCROLLED_WINDOW (gtk_scrolled_window_new (NULL, NULL));
543         gtk_widget_show (GTK_WIDGET (priv->sw_book_tree));
544         gtk_widget_set_no_show_all (GTK_WIDGET (priv->sw_book_tree), TRUE);
545         gtk_scrolled_window_set_policy (priv->sw_book_tree,
546                                         GTK_POLICY_NEVER,
547                                         GTK_POLICY_AUTOMATIC);
548 
549         priv->book_tree = dh_book_tree_new (priv->profile);
550         gtk_widget_show (GTK_WIDGET (priv->book_tree));
551         g_signal_connect (priv->book_tree,
552                           "link-selected",
553                           G_CALLBACK (book_tree_link_selected_cb),
554                           sidebar);
555         gtk_container_add (GTK_CONTAINER (priv->sw_book_tree), GTK_WIDGET (priv->book_tree));
556         gtk_widget_set_hexpand (GTK_WIDGET (priv->sw_book_tree), TRUE);
557         gtk_widget_set_vexpand (GTK_WIDGET (priv->sw_book_tree), TRUE);
558         gtk_container_add (GTK_CONTAINER (sidebar), GTK_WIDGET (priv->sw_book_tree));
559 
560         gtk_widget_show_all (GTK_WIDGET (sidebar));
561 }
562 
563 static void
dh_sidebar_dispose(GObject * object)564 dh_sidebar_dispose (GObject *object)
565 {
566         DhSidebarPrivate *priv = dh_sidebar_get_instance_private (DH_SIDEBAR (object));
567 
568         g_clear_object (&priv->profile);
569         g_clear_object (&priv->hitlist_model);
570 
571         if (priv->idle_complete_id != 0) {
572                 g_source_remove (priv->idle_complete_id);
573                 priv->idle_complete_id = 0;
574         }
575 
576         if (priv->idle_search_id != 0) {
577                 g_source_remove (priv->idle_search_id);
578                 priv->idle_search_id = 0;
579         }
580 
581         G_OBJECT_CLASS (dh_sidebar_parent_class)->dispose (object);
582 }
583 
584 static void
dh_sidebar_class_init(DhSidebarClass * klass)585 dh_sidebar_class_init (DhSidebarClass *klass)
586 {
587         GObjectClass *object_class = G_OBJECT_CLASS (klass);
588 
589         object_class->get_property = dh_sidebar_get_property;
590         object_class->set_property = dh_sidebar_set_property;
591         object_class->constructed = dh_sidebar_constructed;
592         object_class->dispose = dh_sidebar_dispose;
593 
594         /**
595          * DhSidebar::link-selected:
596          * @sidebar: a #DhSidebar.
597          * @link: the selected #DhLink.
598          *
599          * The ::link-selected signal is emitted when:
600          * 1. One row in one of the #GtkTreeView's is selected and contains a
601          *    #DhLink (i.e. when the row is not a language group);
602          * 2. Or if there is an exact match returned by
603          *    dh_keyword_model_filter() when a search occurs.
604          *
605          * Note that dh_sidebar_get_selected_link() takes into account only the
606          * former, not the latter. So the last @link emitted with this signal is
607          * not necessarily the same as the current return value of
608          * dh_sidebar_get_selected_link().
609          */
610         signals[SIGNAL_LINK_SELECTED] =
611                 g_signal_new ("link-selected",
612                               G_TYPE_FROM_CLASS (klass),
613                               G_SIGNAL_RUN_LAST,
614                               G_STRUCT_OFFSET (DhSidebarClass, link_selected),
615                               NULL, NULL, NULL,
616                               G_TYPE_NONE,
617                               1, DH_TYPE_LINK);
618 
619         /**
620          * DhSidebar:profile:
621          *
622          * The #DhProfile. If set to %NULL, the default profile as returned by
623          * dh_profile_get_default() is used.
624          *
625          * Since: 3.30
626          */
627         properties[PROP_PROFILE] =
628                 g_param_spec_object ("profile",
629                                      "Profile",
630                                      "",
631                                      DH_TYPE_PROFILE,
632                                      G_PARAM_READWRITE |
633                                      G_PARAM_CONSTRUCT_ONLY |
634                                      G_PARAM_STATIC_STRINGS);
635 
636         g_object_class_install_properties (object_class, N_PROPERTIES, properties);
637 }
638 
639 static void
dh_sidebar_init(DhSidebar * sidebar)640 dh_sidebar_init (DhSidebar *sidebar)
641 {
642         gtk_orientable_set_orientation (GTK_ORIENTABLE (sidebar),
643                                         GTK_ORIENTATION_VERTICAL);
644 
645         gtk_widget_set_hexpand (GTK_WIDGET (sidebar), TRUE);
646         gtk_widget_set_vexpand (GTK_WIDGET (sidebar), TRUE);
647 }
648 
649 /**
650  * dh_sidebar_new:
651  * @book_manager: (nullable): a #DhBookManager. This parameter is deprecated,
652  * you should just pass %NULL.
653  *
654  * Returns: (transfer floating): a new #DhSidebar widget.
655  * Deprecated: 3.30: Use dh_sidebar_new2() instead.
656  */
657 GtkWidget *
dh_sidebar_new(DhBookManager * book_manager)658 dh_sidebar_new (DhBookManager *book_manager)
659 {
660         return g_object_new (DH_TYPE_SIDEBAR, NULL);
661 }
662 
663 /**
664  * dh_sidebar_new2:
665  * @profile: (nullable): a #DhProfile, or %NULL for the default profile.
666  *
667  * Returns: (transfer floating): a new #DhSidebar widget.
668  * Since: 3.30
669  */
670 DhSidebar *
dh_sidebar_new2(DhProfile * profile)671 dh_sidebar_new2 (DhProfile *profile)
672 {
673         g_return_val_if_fail (profile == NULL || DH_IS_PROFILE (profile), NULL);
674 
675         return g_object_new (DH_TYPE_SIDEBAR,
676                              "profile", profile,
677                              NULL);
678 }
679 
680 /**
681  * dh_sidebar_get_profile:
682  * @sidebar: a #DhSidebar.
683  *
684  * Returns: (transfer none): the #DhProfile of @sidebar.
685  * Since: 3.30
686  */
687 DhProfile *
dh_sidebar_get_profile(DhSidebar * sidebar)688 dh_sidebar_get_profile (DhSidebar *sidebar)
689 {
690         DhSidebarPrivate *priv;
691 
692         g_return_val_if_fail (DH_IS_SIDEBAR (sidebar), NULL);
693 
694         priv = dh_sidebar_get_instance_private (sidebar);
695         return priv->profile;
696 }
697 
698 /**
699  * dh_sidebar_get_selected_link:
700  * @sidebar: a #DhSidebar.
701  *
702  * Note: the return value of this function is not necessarily the same as the
703  * last #DhLink emitted by the #DhSidebar::link-selected signal. See the
704  * documentation of #DhSidebar::link-selected.
705  *
706  * Returns: (transfer full) (nullable): the currently selected #DhLink in the
707  * visible #GtkTreeView of @sidebar, or %NULL if the selection is empty or if a
708  * language group row is selected. Unref with dh_link_unref() when no longer
709  * needed.
710  * Since: 3.30
711  */
712 DhLink *
dh_sidebar_get_selected_link(DhSidebar * sidebar)713 dh_sidebar_get_selected_link (DhSidebar *sidebar)
714 {
715         DhSidebarPrivate *priv;
716         gboolean book_tree_visible;
717         gboolean hitlist_visible;
718 
719         g_return_val_if_fail (DH_IS_SIDEBAR (sidebar), NULL);
720 
721         priv = dh_sidebar_get_instance_private (sidebar);
722 
723         book_tree_visible = gtk_widget_get_visible (GTK_WIDGET (priv->sw_book_tree));
724         hitlist_visible = gtk_widget_get_visible (GTK_WIDGET (priv->sw_hitlist));
725 
726         g_return_val_if_fail ((book_tree_visible || hitlist_visible) &&
727                               !(book_tree_visible && hitlist_visible), NULL);
728 
729         if (book_tree_visible)
730                 return dh_book_tree_get_selected_link (priv->book_tree);
731 
732         return hitlist_get_selected_link (sidebar);
733 }
734 
735 /**
736  * dh_sidebar_select_uri:
737  * @sidebar: a #DhSidebar.
738  * @uri: the URI to select.
739  *
740  * Calls dh_book_tree_select_uri().
741  */
742 void
dh_sidebar_select_uri(DhSidebar * sidebar,const gchar * uri)743 dh_sidebar_select_uri (DhSidebar   *sidebar,
744                        const gchar *uri)
745 {
746         DhSidebarPrivate *priv;
747 
748         g_return_if_fail (DH_IS_SIDEBAR (sidebar));
749         g_return_if_fail (uri != NULL);
750 
751         priv = dh_sidebar_get_instance_private (sidebar);
752 
753         dh_book_tree_select_uri (priv->book_tree, uri);
754 }
755 
756 /**
757  * dh_sidebar_set_search_string:
758  * @sidebar: a #DhSidebar.
759  * @str: the string to search.
760  */
761 void
dh_sidebar_set_search_string(DhSidebar * sidebar,const gchar * str)762 dh_sidebar_set_search_string (DhSidebar   *sidebar,
763                               const gchar *str)
764 {
765         DhSidebarPrivate *priv;
766 
767         g_return_if_fail (DH_IS_SIDEBAR (sidebar));
768         g_return_if_fail (str != NULL);
769 
770         priv = dh_sidebar_get_instance_private (sidebar);
771 
772         gtk_entry_set_text (priv->entry, str);
773         gtk_editable_select_region (GTK_EDITABLE (priv->entry), 0, 0);
774         gtk_editable_set_position (GTK_EDITABLE (priv->entry), -1);
775 
776         /* If the GtkEntry text was already equal to @str, the
777          * GtkEditable::changed signal was not emitted, so force to emit it to
778          * call entry_changed_cb() and entry_search_changed_cb(), forcing a new
779          * search. If an exact match is found, the DhSidebar::link-selected
780          * signal will be emitted, to re-jump to that symbol (even if the
781          * GtkEntry text was equal, it doesn't mean that the WebKitWebView was
782          * showing the exact match).
783          * https://bugzilla.gnome.org/show_bug.cgi?id=776596
784          */
785         g_signal_emit_by_name (priv->entry, "changed");
786 }
787 
788 /**
789  * dh_sidebar_set_search_focus:
790  * @sidebar: a #DhSidebar.
791  *
792  * Gives the focus to the search entry.
793  */
794 void
dh_sidebar_set_search_focus(DhSidebar * sidebar)795 dh_sidebar_set_search_focus (DhSidebar *sidebar)
796 {
797         DhSidebarPrivate *priv;
798 
799         g_return_if_fail (DH_IS_SIDEBAR (sidebar));
800 
801         priv = dh_sidebar_get_instance_private (sidebar);
802 
803         gtk_widget_grab_focus (GTK_WIDGET (priv->entry));
804 }
805