1 /* gdict-sidebar.c - sidebar widget
2  *
3  * Copyright (C) 2006  Emmanuele Bassi <ebassi@gmail.com>
4  *
5  * This program is free software; you can redistribute it and/or
6  * modify it under the terms of the GNU Lesser General Public
7  * License as published by the Free Software Foundation; either
8  * version 2.1 of the License, or (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  * Lesser General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program. If not, see <http://www.gnu.org/licenses/>.
17  *
18  * Based on the equivalent widget from Evince
19  * 	by Jonathan Blandford,
20  * 	Copyright (C) 2004  Red Hat, Inc.
21  */
22 
23 #include "config.h"
24 
25 #include <stdio.h>
26 #include <stdlib.h>
27 #include <string.h>
28 #include <stdarg.h>
29 
30 #include <gdk/gdkkeysyms.h>
31 #include <gtk/gtk.h>
32 #include <glib/gi18n.h>
33 
34 #include "gdict-sidebar.h"
35 
36 typedef struct
37 {
38   guint index;
39 
40   gchar *id;
41   gchar *name;
42 
43   GtkWidget *child;
44   GtkWidget *menu_item;
45 } SidebarPage;
46 
47 struct _GdictSidebarPrivate
48 {
49   GHashTable *pages_by_id;
50   GSList *pages;
51 
52   GtkWidget *hbox;
53   GtkWidget *notebook;
54   GtkWidget *menu;
55   GtkWidget *close_button;
56   GtkWidget *label;
57   GtkWidget *select_button;
58 };
59 
60 enum
61 {
62   PAGE_CHANGED,
63   CLOSED,
64 
65   LAST_SIGNAL
66 };
67 
68 static guint sidebar_signals[LAST_SIGNAL] = { 0 };
69 static GQuark sidebar_page_id_quark = 0;
70 
G_DEFINE_TYPE_WITH_PRIVATE(GdictSidebar,gdict_sidebar,GTK_TYPE_BOX)71 G_DEFINE_TYPE_WITH_PRIVATE (GdictSidebar, gdict_sidebar, GTK_TYPE_BOX)
72 
73 static SidebarPage *
74 sidebar_page_new (const gchar *id,
75 		  const gchar *name,
76 		  GtkWidget   *widget)
77 {
78   SidebarPage *page;
79 
80   page = g_slice_new (SidebarPage);
81 
82   page->id = g_strdup (id);
83   page->name = g_strdup (name);
84   page->child = widget;
85   page->index = -1;
86   page->menu_item = NULL;
87 
88   return page;
89 }
90 
91 static void
sidebar_page_free(SidebarPage * page)92 sidebar_page_free (SidebarPage *page)
93 {
94   if (G_LIKELY (page))
95     {
96       g_free (page->name);
97       g_free (page->id);
98 
99       g_slice_free (SidebarPage, page);
100     }
101 }
102 
103 static void
gdict_sidebar_finalize(GObject * object)104 gdict_sidebar_finalize (GObject *object)
105 {
106   GdictSidebar *sidebar = GDICT_SIDEBAR (object);
107   GdictSidebarPrivate *priv = sidebar->priv;
108 
109   if (priv->pages_by_id)
110     g_hash_table_destroy (priv->pages_by_id);
111 
112   if (priv->pages)
113     {
114       g_slist_foreach (priv->pages, (GFunc) sidebar_page_free, NULL);
115       g_slist_free (priv->pages);
116     }
117 
118   G_OBJECT_CLASS (gdict_sidebar_parent_class)->finalize (object);
119 }
120 
121 static void
gdict_sidebar_dispose(GObject * object)122 gdict_sidebar_dispose (GObject *object)
123 {
124   GdictSidebar *sidebar = GDICT_SIDEBAR (object);
125 
126   if (sidebar->priv->menu)
127     {
128       gtk_menu_detach (GTK_MENU (sidebar->priv->menu));
129       sidebar->priv->menu = NULL;
130     }
131 
132   G_OBJECT_CLASS (gdict_sidebar_parent_class)->dispose (object);
133 }
134 
135 #if !GTK_CHECK_VERSION (3, 22, 0)
136 /* We only use this with older versions of GTK+ */
137 static void
gdict_sidebar_menu_position_function(GtkMenu * menu,gint * x,gint * y,gboolean * push_in,gpointer user_data)138 gdict_sidebar_menu_position_function (GtkMenu  *menu,
139 				      gint     *x,
140 				      gint     *y,
141 				      gboolean *push_in,
142 				      gpointer  user_data)
143 {
144   GtkWidget *widget;
145   GtkAllocation allocation;
146 
147   g_assert (GTK_IS_BUTTON (user_data));
148 
149   widget = GTK_WIDGET (user_data);
150 
151   gdk_window_get_origin (gtk_widget_get_window (widget), x, y);
152 
153   gtk_widget_get_allocation (widget, &allocation);
154   *x += allocation.x;
155   *y += allocation.y + allocation.height;
156 
157   *push_in = FALSE;
158 }
159 #endif /* !GTK_CHECK_VERSION (3, 22, 0) */
160 
161 static gboolean
gdict_sidebar_select_button_press_cb(GtkWidget * widget,GdkEventButton * event,gpointer user_data)162 gdict_sidebar_select_button_press_cb (GtkWidget      *widget,
163 				      GdkEventButton *event,
164 				      gpointer        user_data)
165 {
166   GdictSidebar *sidebar = GDICT_SIDEBAR (user_data);
167   GtkAllocation allocation;
168 
169   if (event->button == 1)
170     {
171       GtkRequisition req;
172       gint width;
173 
174       gtk_widget_get_allocation (widget, &allocation);
175       width = allocation.width;
176       gtk_widget_set_size_request (sidebar->priv->menu, -1, -1);
177       gtk_widget_get_preferred_size (sidebar->priv->menu, NULL, &req);
178       gtk_widget_set_size_request (sidebar->priv->menu,
179 		      		   MAX (width, req.width), -1);
180       gtk_widget_grab_focus (widget);
181 
182       gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (widget), TRUE);
183 
184 #if GTK_CHECK_VERSION (3, 22, 0)
185       gtk_menu_popup_at_widget (GTK_MENU (sidebar->priv->menu),
186                                 widget,
187                                 GDK_GRAVITY_SOUTH_WEST,
188                                 GDK_GRAVITY_NORTH_WEST,
189                                 (GdkEvent *) event);
190 #else
191       gtk_menu_popup (GTK_MENU (sidebar->priv->menu),
192 		      NULL, NULL,
193 		      gdict_sidebar_menu_position_function, widget,
194 		      event->button, event->time);
195 #endif
196 
197       return TRUE;
198     }
199 
200   return FALSE;
201 }
202 
203 static gboolean
gdict_sidebar_select_key_press_cb(GtkWidget * widget,GdkEventKey * event,gpointer user_data)204 gdict_sidebar_select_key_press_cb (GtkWidget   *widget,
205 				   GdkEventKey *event,
206 				   gpointer     user_data)
207 {
208   GdictSidebar *sidebar = GDICT_SIDEBAR (user_data);
209 
210   if (event->keyval == GDK_KEY_space ||
211       event->keyval == GDK_KEY_KP_Space ||
212       event->keyval == GDK_KEY_Return ||
213       event->keyval == GDK_KEY_KP_Enter)
214     {
215       gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (widget), TRUE);
216 
217 #if GTK_CHECK_VERSION (3, 22, 0)
218       gtk_menu_popup_at_widget (GTK_MENU (sidebar->priv->menu),
219                                 widget,
220                                 GDK_GRAVITY_SOUTH_WEST,
221                                 GDK_GRAVITY_NORTH_WEST,
222                                 (GdkEvent *) event);
223 #else
224       gtk_menu_popup (GTK_MENU (sidebar->priv->menu),
225 		      NULL, NULL,
226 		      gdict_sidebar_menu_position_function, widget,
227 		      1, event->time);
228 #endif
229 
230       return TRUE;
231     }
232 
233   return FALSE;
234 }
235 
236 static void
gdict_sidebar_close_clicked_cb(GtkWidget * widget,gpointer user_data)237 gdict_sidebar_close_clicked_cb (GtkWidget *widget,
238 				gpointer   user_data)
239 {
240   GdictSidebar *sidebar = GDICT_SIDEBAR (user_data);
241 
242   g_signal_emit (sidebar, sidebar_signals[CLOSED], 0);
243 }
244 
245 static void
gdict_sidebar_menu_deactivate_cb(GtkWidget * widget,gpointer user_data)246 gdict_sidebar_menu_deactivate_cb (GtkWidget *widget,
247 				  gpointer   user_data)
248 {
249   GdictSidebar *sidebar = GDICT_SIDEBAR (user_data);
250   GdictSidebarPrivate *priv = sidebar->priv;
251   GtkToggleButton *select_button = GTK_TOGGLE_BUTTON (priv->select_button);
252 
253   gtk_toggle_button_set_active (select_button, FALSE);
254 }
255 
256 static void
gdict_sidebar_menu_detach_cb(GtkWidget * widget,GtkMenu * menu)257 gdict_sidebar_menu_detach_cb (GtkWidget *widget,
258 			      GtkMenu   *menu)
259 {
260   GdictSidebar *sidebar = GDICT_SIDEBAR (widget);
261 
262   sidebar->priv->menu = NULL;
263 }
264 
265 static void
gdict_sidebar_menu_item_activate(GtkWidget * widget,gpointer user_data)266 gdict_sidebar_menu_item_activate (GtkWidget *widget,
267 				  gpointer   user_data)
268 {
269   GdictSidebar *sidebar = GDICT_SIDEBAR (user_data);
270   GdictSidebarPrivate *priv = sidebar->priv;
271   GtkWidget *menu_item;
272   const gchar *id;
273   SidebarPage *page;
274   gint current_index;
275 
276   menu_item = gtk_menu_get_active (GTK_MENU (priv->menu));
277   id = g_object_get_qdata (G_OBJECT (menu_item), sidebar_page_id_quark);
278   g_assert (id != NULL);
279 
280   page = g_hash_table_lookup (priv->pages_by_id, id);
281   g_assert (page != NULL);
282 
283   current_index = gtk_notebook_get_current_page (GTK_NOTEBOOK (priv->notebook));
284   if (current_index == page->index)
285     return;
286 
287   gtk_notebook_set_current_page (GTK_NOTEBOOK (priv->notebook),
288 		  		 page->index);
289   gtk_label_set_text (GTK_LABEL (priv->label), page->name);
290 
291   g_signal_emit (sidebar, sidebar_signals[PAGE_CHANGED], 0);
292 }
293 
294 static void
gdict_sidebar_class_init(GdictSidebarClass * klass)295 gdict_sidebar_class_init (GdictSidebarClass *klass)
296 {
297   GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
298 
299   sidebar_page_id_quark = g_quark_from_static_string ("gdict-sidebar-page-id");
300 
301   gobject_class->finalize = gdict_sidebar_finalize;
302   gobject_class->dispose = gdict_sidebar_dispose;
303 
304   sidebar_signals[PAGE_CHANGED] =
305     g_signal_new ("page-changed",
306 		  G_TYPE_FROM_CLASS (gobject_class),
307 		  G_SIGNAL_RUN_LAST,
308 		  G_STRUCT_OFFSET (GdictSidebarClass, page_changed),
309 		  NULL, NULL,
310 		  g_cclosure_marshal_VOID__VOID,
311 		  G_TYPE_NONE, 0);
312   sidebar_signals[CLOSED] =
313     g_signal_new ("closed",
314 		  G_TYPE_FROM_CLASS (gobject_class),
315 		  G_SIGNAL_RUN_LAST,
316 		  G_STRUCT_OFFSET (GdictSidebarClass, closed),
317 		  NULL, NULL,
318 		  g_cclosure_marshal_VOID__VOID,
319 		  G_TYPE_NONE, 0);
320 }
321 
322 static void
gdict_sidebar_init(GdictSidebar * sidebar)323 gdict_sidebar_init (GdictSidebar *sidebar)
324 {
325   GdictSidebarPrivate *priv;
326   GtkWidget *hbox;
327   GtkWidget *select_hbox;
328   GtkWidget *select_button;
329   GtkWidget *close_button;
330   GtkWidget *arrow;
331 
332   sidebar->priv = priv = gdict_sidebar_get_instance_private (sidebar);
333 
334   gtk_orientable_set_orientation (GTK_ORIENTABLE (sidebar),
335                                   GTK_ORIENTATION_VERTICAL);
336 
337   /* we store all the pages inside the list, but we keep
338    * a pointer inside the hash table for faster look up
339    * times; what's inside the table will be destroyed with
340    * the list, so there's no need to supply the destroy
341    * functions for keys and values.
342    */
343   priv->pages = NULL;
344   priv->pages_by_id = g_hash_table_new (g_str_hash, g_str_equal);
345 
346   gtk_widget_set_vexpand (GTK_WIDGET (sidebar), TRUE);
347 
348   /* top option menu */
349   hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
350   gtk_box_pack_start (GTK_BOX (sidebar), hbox, FALSE, FALSE, 0);
351   gtk_widget_show (hbox);
352   priv->hbox = hbox;
353 
354   select_button = gtk_toggle_button_new ();
355   gtk_button_set_relief (GTK_BUTTON (select_button), GTK_RELIEF_NONE);
356   g_signal_connect (select_button, "button-press-event",
357 		    G_CALLBACK (gdict_sidebar_select_button_press_cb),
358 		    sidebar);
359   g_signal_connect (select_button, "key-press-event",
360 		    G_CALLBACK (gdict_sidebar_select_key_press_cb),
361 		    sidebar);
362   priv->select_button = select_button;
363 
364   select_hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6);
365 
366   priv->label = g_object_new (GTK_TYPE_LABEL,
367                               "xalign", 0.0,
368                               "yalign", 0.5,
369                               NULL);
370   gtk_box_pack_start (GTK_BOX (select_hbox), priv->label, FALSE, FALSE, 0);
371   gtk_widget_show (priv->label);
372 
373   arrow = gtk_image_new_from_icon_name ("go-down-symbolic", GTK_ICON_SIZE_BUTTON);
374   gtk_box_pack_end (GTK_BOX (select_hbox), arrow, FALSE, FALSE, 0);
375   gtk_widget_show (arrow);
376 
377   gtk_container_add (GTK_CONTAINER (select_button), select_hbox);
378   gtk_widget_show (select_hbox);
379 
380   gtk_box_pack_start (GTK_BOX (hbox), select_button, TRUE, TRUE, 0);
381   gtk_widget_show (select_button);
382 
383   close_button = gtk_button_new ();
384   gtk_button_set_relief (GTK_BUTTON (close_button), GTK_RELIEF_NONE);
385   gtk_button_set_image (GTK_BUTTON (close_button),
386 		        gtk_image_new_from_icon_name ("window-close-symbolic", GTK_ICON_SIZE_SMALL_TOOLBAR));
387   g_signal_connect (close_button, "clicked",
388 		    G_CALLBACK (gdict_sidebar_close_clicked_cb),
389 		    sidebar);
390   gtk_box_pack_end (GTK_BOX (hbox), close_button, FALSE, FALSE, 0);
391   gtk_widget_show (close_button);
392   priv->close_button = close_button;
393 
394   sidebar->priv->menu = gtk_menu_new ();
395   g_signal_connect (sidebar->priv->menu, "deactivate",
396 		    G_CALLBACK (gdict_sidebar_menu_deactivate_cb),
397 		    sidebar);
398   gtk_menu_attach_to_widget (GTK_MENU (sidebar->priv->menu),
399 		  	     GTK_WIDGET (sidebar),
400 			     gdict_sidebar_menu_detach_cb);
401   gtk_widget_show (sidebar->priv->menu);
402 
403   sidebar->priv->notebook = gtk_notebook_new ();
404   gtk_notebook_set_show_border (GTK_NOTEBOOK (sidebar->priv->notebook), FALSE);
405   gtk_notebook_set_show_tabs (GTK_NOTEBOOK (sidebar->priv->notebook), FALSE);
406   gtk_box_pack_start (GTK_BOX (sidebar), sidebar->priv->notebook, TRUE, TRUE, 6);
407   gtk_widget_show (sidebar->priv->notebook);
408 }
409 
410 /*
411  * Public API
412  */
413 
414 GtkWidget *
gdict_sidebar_new(void)415 gdict_sidebar_new (void)
416 {
417   return g_object_new (GDICT_TYPE_SIDEBAR, NULL);
418 }
419 
420 void
gdict_sidebar_add_page(GdictSidebar * sidebar,const gchar * page_id,const gchar * page_name,GtkWidget * page_widget)421 gdict_sidebar_add_page (GdictSidebar *sidebar,
422 			const gchar  *page_id,
423 			const gchar  *page_name,
424 			GtkWidget    *page_widget)
425 {
426   GdictSidebarPrivate *priv;
427   SidebarPage *page;
428   GtkWidget *menu_item;
429 
430   g_return_if_fail (GDICT_IS_SIDEBAR (sidebar));
431   g_return_if_fail (page_id != NULL);
432   g_return_if_fail (page_name != NULL);
433   g_return_if_fail (GTK_IS_WIDGET (page_widget));
434 
435   priv = sidebar->priv;
436 
437   if (g_hash_table_lookup (priv->pages_by_id, page_id))
438     {
439       g_warning ("Attempting to add a page to the sidebar with "
440 		 "id `%s', but there already is a page with the "
441 		 "same id.  Aborting...",
442 		 page_id);
443       return;
444     }
445 
446   /* add the page inside the page list */
447   page = sidebar_page_new (page_id, page_name, page_widget);
448 
449   priv->pages = g_slist_append (priv->pages, page);
450   g_hash_table_insert (priv->pages_by_id, page->id, page);
451 
452   page->index = gtk_notebook_append_page (GTK_NOTEBOOK (priv->notebook),
453 		  			  page_widget,
454 					  NULL);
455 
456   /* add the menu item for the page */
457   menu_item = gtk_menu_item_new_with_label (page_name);
458   g_object_set_qdata_full (G_OBJECT (menu_item),
459 			   sidebar_page_id_quark,
460                            g_strdup (page_id),
461 			   (GDestroyNotify) g_free);
462   g_signal_connect (menu_item, "activate",
463 		    G_CALLBACK (gdict_sidebar_menu_item_activate),
464 		    sidebar);
465   gtk_menu_shell_append (GTK_MENU_SHELL (priv->menu), menu_item);
466   gtk_widget_show (menu_item);
467   page->menu_item = menu_item;
468 
469   if (gtk_widget_get_realized (priv->menu))
470     gtk_menu_shell_select_item (GTK_MENU_SHELL (priv->menu), menu_item);
471   gtk_label_set_text (GTK_LABEL (priv->label), page_name);
472   gtk_notebook_set_current_page (GTK_NOTEBOOK (priv->notebook), page->index);
473 }
474 
475 void
gdict_sidebar_remove_page(GdictSidebar * sidebar,const gchar * page_id)476 gdict_sidebar_remove_page (GdictSidebar *sidebar,
477 			   const gchar  *page_id)
478 {
479   GdictSidebarPrivate *priv;
480   SidebarPage *page;
481   GList *children, *l;
482 
483   g_return_if_fail (GDICT_IS_SIDEBAR (sidebar));
484   g_return_if_fail (page_id != NULL);
485 
486   priv = sidebar->priv;
487 
488   if ((page = g_hash_table_lookup (priv->pages_by_id, page_id)) == NULL)
489     {
490       g_warning ("Attempting to remove a page from the sidebar with "
491 		 "id `%s', but there is no page with this id. Aborting...",
492 		 page_id);
493       return;
494     }
495 
496   children = gtk_container_get_children (GTK_CONTAINER (priv->menu));
497   for (l = children; l != NULL; l = l->next)
498     {
499       GtkWidget *menu_item = l->data;
500 
501       if (menu_item == page->menu_item)
502         {
503           gtk_container_remove (GTK_CONTAINER (priv->menu), menu_item);
504 	  break;
505 	}
506     }
507   g_list_free (children);
508 
509   gtk_notebook_remove_page (GTK_NOTEBOOK (priv->notebook), page->index);
510 
511   g_hash_table_remove (priv->pages_by_id, page->id);
512   priv->pages = g_slist_remove (priv->pages, page);
513 
514   sidebar_page_free (page);
515 
516   /* select the first page, if present */
517   page = priv->pages->data;
518   if (page)
519     {
520       if (gtk_widget_get_realized (priv->menu))
521         gtk_menu_shell_select_item (GTK_MENU_SHELL (priv->menu),
522                                     page->menu_item);
523       gtk_label_set_text (GTK_LABEL (priv->label), page->name);
524       gtk_notebook_set_current_page (GTK_NOTEBOOK (priv->notebook), page->index);
525     }
526   else
527     gtk_widget_hide (GTK_WIDGET (sidebar));
528 }
529 
530 void
gdict_sidebar_view_page(GdictSidebar * sidebar,const gchar * page_id)531 gdict_sidebar_view_page (GdictSidebar *sidebar,
532 			 const gchar  *page_id)
533 {
534   GdictSidebarPrivate *priv;
535   SidebarPage *page;
536 
537   g_return_if_fail (GDICT_IS_SIDEBAR (sidebar));
538   g_return_if_fail (page_id != NULL);
539 
540   priv = sidebar->priv;
541   page = g_hash_table_lookup (priv->pages_by_id, page_id);
542   if (!page)
543     return;
544 
545   gtk_notebook_set_current_page (GTK_NOTEBOOK (priv->notebook), page->index);
546   gtk_label_set_text (GTK_LABEL (priv->label), page->name);
547   if (gtk_widget_get_realized (priv->menu))
548     gtk_menu_shell_select_item (GTK_MENU_SHELL (priv->menu), page->menu_item);
549 }
550 
551 const gchar *
gdict_sidebar_current_page(GdictSidebar * sidebar)552 gdict_sidebar_current_page (GdictSidebar *sidebar)
553 {
554   GdictSidebarPrivate *priv;
555   gint index;
556   SidebarPage *page;
557 
558   g_return_val_if_fail (GDICT_IS_SIDEBAR (sidebar), NULL);
559 
560   priv = sidebar->priv;
561 
562   index = gtk_notebook_get_current_page (GTK_NOTEBOOK (priv->notebook));
563   page = g_slist_nth_data (priv->pages, index);
564   if (page == NULL)
565     return NULL;
566 
567   return page->id;
568 }
569 
570 gchar **
gdict_sidebar_list_pages(GdictSidebar * sidebar,gsize * length)571 gdict_sidebar_list_pages (GdictSidebar *sidebar,
572                           gsize        *length)
573 {
574   GdictSidebarPrivate *priv;
575   gchar **retval;
576   gint i;
577   GSList *l;
578 
579   g_return_val_if_fail (GDICT_IS_SIDEBAR (sidebar), NULL);
580 
581   priv = sidebar->priv;
582 
583   retval = g_new (gchar*, g_slist_length (priv->pages) + 1);
584   for (l = priv->pages, i = 0; l; l = l->next, i++)
585     retval[i++] = g_strdup (l->data);
586 
587   retval[i] = NULL;
588 
589   if (length)
590     *length = i;
591 
592   return retval;
593 }
594