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