1 /* -*-mode:c; c-style:k&r; c-basic-offset:4; -*- */
2 /* Balsa E-Mail Client
3  * Copyright (C) 1997-2013 Stuart Parmenter and others,
4  *                         See the file AUTHORS for a list.
5  *
6  * This program is free software; you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation; either version 2, or (at your option)
9  * any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program; if not, write to the Free Software
18  * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
19  * 02111-1307, USA.
20  */
21 
22 #if defined(HAVE_CONFIG_H) && HAVE_CONFIG_H
23 # include "config.h"
24 #endif                          /* HAVE_CONFIG_H */
25 #include "toolbar-factory.h"
26 
27 #include <string.h>
28 #include <glib/gi18n.h>
29 
30 #include "balsa-app.h"
31 #include "balsa-icons.h"
32 #include "main-window.h"
33 #include "message-window.h"
34 #include "sendmsg-window.h"
35 #include "libbalsa-conf.h"
36 
37 #include "toolbar-prefs.h"
38 
39 /* Must be consistent with BalsaToolbarType enum: */
40 static const gchar *const balsa_toolbar_names[] =
41     { "MainWindow", "ComposeWindow", "MessageWindow" };
42 
43 /*
44  * The BalsaToolbarModel class.
45  */
46 
47 struct BalsaToolbarModel_ {
48     GObject object;
49 
50     GHashTable      *legal;
51     GSList          *standard;
52     GSList          *current;
53     BalsaToolbarType type;
54     GtkToolbarStyle  style;
55 #if HAVE_GNOME
56     GSettings       *settings;
57 #endif /* HAVE_GNOME */
58 };
59 
60 enum {
61     CHANGED,
62     LAST_SIGNAL
63 };
64 
65 static GObjectClass* parent_class;
66 static guint model_signals[LAST_SIGNAL] = { 0 };
67 
68 static void
balsa_toolbar_model_finalize(GObject * object)69 balsa_toolbar_model_finalize(GObject * object)
70 {
71     BalsaToolbarModel *model = BALSA_TOOLBAR_MODEL(object);
72     g_hash_table_destroy(model->legal);
73 #if HAVE_GNOME
74     g_object_unref(model->settings);
75 #endif /* HAVE_GNOME */
76     G_OBJECT_CLASS(parent_class)->finalize(object);
77 }
78 
79 static void
balsa_toolbar_model_class_init(BalsaToolbarModelClass * klass)80 balsa_toolbar_model_class_init(BalsaToolbarModelClass* klass)
81 {
82     GObjectClass *object_class = (GObjectClass *) klass;
83 
84     parent_class = g_type_class_peek_parent(klass);
85 
86     model_signals[CHANGED] =
87         g_signal_new("changed", G_TYPE_FROM_CLASS(object_class),
88                      G_SIGNAL_RUN_FIRST,
89                      0,
90                      NULL, NULL,
91                      g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0);
92 
93     object_class = G_OBJECT_CLASS(klass);
94     object_class->finalize = balsa_toolbar_model_finalize;
95 }
96 
97 static void
balsa_toolbar_model_init(BalsaToolbarModel * model)98 balsa_toolbar_model_init(BalsaToolbarModel * model)
99 {
100     model->legal =
101         g_hash_table_new_full(g_str_hash, g_str_equal, NULL, g_free);
102 }
103 
104 GType
balsa_toolbar_model_get_type()105 balsa_toolbar_model_get_type()
106 {
107     static GType balsa_toolbar_model_type = 0;
108 
109     if (!balsa_toolbar_model_type) {
110         static const GTypeInfo balsa_toolbar_model_info = {
111             sizeof(BalsaToolbarModelClass),
112             NULL,               /* base_init */
113             NULL,               /* base_finalize */
114             (GClassInitFunc) balsa_toolbar_model_class_init,
115             NULL,               /* class_finalize */
116             NULL,               /* class_data */
117             sizeof(BalsaToolbarModel),
118             0,                  /* n_preallocs */
119             (GInstanceInitFunc) balsa_toolbar_model_init,
120         };
121 
122         balsa_toolbar_model_type =
123             g_type_register_static(G_TYPE_OBJECT,
124                                    "BalsaToolbarModel",
125                                    &balsa_toolbar_model_info, 0);
126     }
127 
128     return balsa_toolbar_model_type;
129 }
130 
131 /* End of class boilerplate */
132 
133 /* The descriptions must be SHORT */
134 button_data toolbar_buttons[]={
135     {"",                         N_("Separator"),       FALSE},
136     {GTK_STOCK_QUIT,             N_("Quit"),            FALSE},
137     {BALSA_PIXMAP_RECEIVE,       N_("Check"),           TRUE},
138     {BALSA_PIXMAP_COMPOSE,       N_("Compose"),         TRUE},
139     {BALSA_PIXMAP_CONTINUE,      N_("Continue"),        FALSE},
140     {BALSA_PIXMAP_REPLY,         N_("Reply"),           TRUE},
141     {BALSA_PIXMAP_REPLY_ALL,     N_("Reply\nto all"),   FALSE},
142     {BALSA_PIXMAP_REPLY_GROUP,   N_("Reply\nto group"), FALSE},
143     {BALSA_PIXMAP_FORWARD,       N_("Forward"),         FALSE},
144     {BALSA_PIXMAP_PREVIOUS,      N_("Previous"),        FALSE},
145     {BALSA_PIXMAP_NEXT,          N_("Next"),            FALSE},
146     {BALSA_PIXMAP_NEXT_UNREAD,   N_("Next\nunread"),    TRUE},
147     {BALSA_PIXMAP_NEXT_FLAGGED,  N_("Next\nflagged"),   FALSE},
148     {BALSA_PIXMAP_PREVIOUS_PART, N_("Previous\npart"),  FALSE},
149     {BALSA_PIXMAP_NEXT_PART,     N_("Next\npart"),      FALSE},
150     {GTK_STOCK_DELETE,           N_("Trash /\nDelete"), FALSE},
151     {BALSA_PIXMAP_POSTPONE,      N_("Postpone"),        FALSE},
152     {GTK_STOCK_PRINT,            N_("Print"),           FALSE},
153     {BALSA_PIXMAP_REQUEST_MDN,   N_("Request\nMDN"),    FALSE},
154     {BALSA_PIXMAP_SEND,          N_("Send"),            TRUE},
155     {BALSA_PIXMAP_SEND_RECEIVE,  N_("Exchange"),        FALSE},
156     {BALSA_PIXMAP_ATTACHMENT,    N_("Attach"),          TRUE},
157     {GTK_STOCK_SAVE,             N_("Save"),            TRUE},
158     {BALSA_PIXMAP_IDENTITY,      N_("Identity"),        FALSE},
159     {GTK_STOCK_SPELL_CHECK,      N_("Spelling"),        TRUE},
160     {GTK_STOCK_CLOSE,            N_("Close"),           FALSE},
161     {BALSA_PIXMAP_MARKED_NEW,    N_("Toggle\nnew"),     FALSE},
162     {BALSA_PIXMAP_MARK_ALL,      N_("Mark all"),        FALSE},
163     {BALSA_PIXMAP_SHOW_HEADERS,  N_("All\nheaders"),    FALSE},
164     {GTK_STOCK_CANCEL,           N_("Reset\nFilter"),   FALSE},
165     {BALSA_PIXMAP_SHOW_PREVIEW,  N_("Msg Preview"),     FALSE},
166 #ifdef HAVE_GPGME
167     {BALSA_PIXMAP_GPG_SIGN,      N_("Sign"),            FALSE},
168     {BALSA_PIXMAP_GPG_ENCRYPT,   N_("Encrypt"),         FALSE},
169 #endif
170     {GTK_STOCK_UNDO,             N_("Undo"),            FALSE},
171     {GTK_STOCK_REDO,             N_("Redo"),            FALSE},
172     {GTK_STOCK_CLEAR,            N_("Expunge"),         FALSE},
173     {GTK_STOCK_REMOVE,           N_("Empty\nTrash"),    FALSE},
174     {GTK_STOCK_EDIT,             N_("Edit"),            FALSE},
175 };
176 
177 const int toolbar_button_count =
178     sizeof(toolbar_buttons) / sizeof(button_data);
179 
180 /* Public methods. */
181 const gchar *
balsa_toolbar_button_text(gint button)182 balsa_toolbar_button_text(gint button)
183 {
184     return _(strcmp(toolbar_buttons[button].pixmap_id,
185                     BALSA_PIXMAP_SEND) == 0
186              && balsa_app.always_queue_sent_mail ?
187              N_("Queue") : toolbar_buttons[button].button_text);
188 }
189 
190 const gchar *
balsa_toolbar_sanitize_id(const gchar * id)191 balsa_toolbar_sanitize_id(const gchar *id)
192 {
193     gint button = get_toolbar_button_index(id);
194 
195     if (button >= 0)
196 	return toolbar_buttons[button].pixmap_id;
197     else
198 	return NULL;
199 }
200 
201 /* this should go to GTK because it modifies its internal structures. */
202 void
balsa_toolbar_remove_all(GtkWidget * widget)203 balsa_toolbar_remove_all(GtkWidget * widget)
204 {
205     GList *child, *children;
206 
207     children = gtk_container_get_children(GTK_CONTAINER(widget));
208     for (child = children; child; child = child->next)
209         gtk_widget_destroy(child->data);
210     g_list_free(children);
211 }
212 
213 /* Load and save config
214  */
215 
216 static void
tm_load_model(BalsaToolbarModel * model)217 tm_load_model(BalsaToolbarModel * model)
218 {
219     gchar *key;
220     guint j;
221 
222     key = g_strconcat("toolbar-", balsa_toolbar_names[model->type], NULL);
223     libbalsa_conf_push_group(key);
224     g_free(key);
225 
226     model->style = libbalsa_conf_get_int_with_default("Style=-1", NULL);
227 
228     model->current = NULL;
229     for (j = 0;; j++) {
230         gchar *item;
231 
232         key = g_strdup_printf("Item%d", j);
233         item = libbalsa_conf_get_string(key);
234         g_free(key);
235 
236         if (!item)
237             break;
238 
239         model->current = g_slist_prepend(model->current, item);
240     }
241     model->current = g_slist_reverse(model->current);
242 
243     libbalsa_conf_pop_group();
244 }
245 
246 static void
tm_save_model(BalsaToolbarModel * model)247 tm_save_model(BalsaToolbarModel * model)
248 {
249     gchar *key;
250     guint j;
251     GSList *list;
252 
253     key = g_strconcat("toolbar-", balsa_toolbar_names[model->type], NULL);
254     libbalsa_conf_remove_group(key);
255     libbalsa_conf_push_group(key);
256     g_free(key);
257 
258     if (model->style != (GtkToolbarStyle) (-1))
259         libbalsa_conf_set_int("Style", model->style);
260 
261     for (j = 0, list = model->current;
262          list;
263          j++, list = list->next) {
264         key = g_strdup_printf("Item%d", j);
265         libbalsa_conf_set_string(key, list->data);
266         g_free(key);
267     }
268 
269     libbalsa_conf_pop_group();
270 }
271 
272 #if HAVE_GNOME
273 /* GSettings change_cb
274  */
275 static void
tm_gsettings_change_cb(GSettings * settings,const gchar * key,gpointer user_data)276 tm_gsettings_change_cb(GSettings   * settings,
277                        const gchar * key,
278                        gpointer      user_data)
279 {
280     BalsaToolbarModel *model = user_data;
281 
282     if (!strcmp(key, "toolbar-style") &&
283         model->style == (GtkToolbarStyle) (-1))
284         balsa_toolbar_model_changed(model);
285 }
286 #endif /* HAVE_GNOME */
287 
288 /* Create a BalsaToolbarModel structure.
289  */
290 BalsaToolbarModel *
balsa_toolbar_model_new(BalsaToolbarType type,GSList * standard)291 balsa_toolbar_model_new(BalsaToolbarType type, GSList * standard)
292 {
293     BalsaToolbarModel *model =
294         g_object_new(BALSA_TYPE_TOOLBAR_MODEL, NULL);
295 
296     model->type = type;
297     model->standard = standard;
298     tm_load_model(model);
299 
300 #if HAVE_GNOME
301     model->settings = g_settings_new("org.gnome.desktop.interface");
302     g_signal_connect(model->settings, "changed",
303                      G_CALLBACK(tm_gsettings_change_cb), model);
304 #endif /* HAVE_GNOME */
305 
306     return model;
307 }
308 
309 /* balsa_toolbar_model_changed:
310    Update all toolbars derived from the model.
311 
312    Called from toolbar-prefs.c after a change has been made to a toolbar
313    layout.
314 */
315 void
balsa_toolbar_model_changed(BalsaToolbarModel * model)316 balsa_toolbar_model_changed(BalsaToolbarModel * model)
317 {
318     g_signal_emit(model, model_signals[CHANGED], 0);
319 }
320 
321 static void
tm_add_action(BalsaToolbarModel * model,const gchar * stock_id,const gchar * name)322 tm_add_action(BalsaToolbarModel * model, const gchar * stock_id,
323               const gchar * name)
324 {
325     /* Check whether we have already seen this icon: */
326     if (stock_id && !g_hash_table_lookup(model->legal, stock_id))
327         g_hash_table_insert(model->legal, (gchar *) stock_id,
328                             (gchar *) name);
329 }
330 
331 void
balsa_toolbar_model_add_actions(BalsaToolbarModel * model,const GtkActionEntry * entries,guint n_entries)332 balsa_toolbar_model_add_actions(BalsaToolbarModel * model,
333                                 const GtkActionEntry * entries,
334                                 guint n_entries)
335 {
336     guint i;
337 
338     for (i = 0; i < n_entries; i++)
339         tm_add_action(model, entries[i].stock_id, entries[i].name);
340 }
341 
342 void
balsa_toolbar_model_add_toggle_actions(BalsaToolbarModel * model,const GtkToggleActionEntry * entries,guint n_entries)343 balsa_toolbar_model_add_toggle_actions(BalsaToolbarModel * model,
344                                        const GtkToggleActionEntry *
345                                        entries, guint n_entries)
346 {
347     guint i;
348 
349     for (i = 0; i < n_entries; i++)
350         tm_add_action(model, entries[i].stock_id, entries[i].name);
351 }
352 
353 /* Return the legal icons.
354  */
355 GHashTable *
balsa_toolbar_model_get_legal(BalsaToolbarModel * model)356 balsa_toolbar_model_get_legal(BalsaToolbarModel * model)
357 {
358     return model->legal;
359 }
360 
361 /* Return the current icons.
362  */
363 GSList *
balsa_toolbar_model_get_current(BalsaToolbarModel * model)364 balsa_toolbar_model_get_current(BalsaToolbarModel * model)
365 {
366     return model->current ? model->current : model->standard;
367 }
368 
369 gboolean
balsa_toolbar_model_is_standard(BalsaToolbarModel * model)370 balsa_toolbar_model_is_standard(BalsaToolbarModel * model)
371 {
372     return model->current == NULL;
373 }
374 
375 /* Add an icon to the list of current icons in a BalsaToolbarModel.
376  */
377 void
balsa_toolbar_model_insert_icon(BalsaToolbarModel * model,gchar * icon,gint position)378 balsa_toolbar_model_insert_icon(BalsaToolbarModel * model, gchar * icon,
379                                 gint position)
380 {
381     const gchar *real_button = balsa_toolbar_sanitize_id(icon);
382 
383     if (real_button)
384         model->current =
385             g_slist_insert(model->current, g_strdup(real_button),
386                            position);
387     else
388         g_warning(_("Unknown toolbar icon \"%s\""), icon);
389 }
390 
391 /* Remove all icons from the BalsaToolbarModel.
392  */
393 void
balsa_toolbar_model_clear(BalsaToolbarModel * model)394 balsa_toolbar_model_clear(BalsaToolbarModel * model)
395 {
396     g_slist_foreach(model->current, (GFunc) g_free, NULL);
397     g_slist_free(model->current);
398     model->current = NULL;
399 }
400 
401 /* Create a new instance of a toolbar
402  */
403 
404 static gboolean
tm_has_second_line(BalsaToolbarModel * model)405 tm_has_second_line(BalsaToolbarModel * model)
406 {
407     GSList *list;
408 
409     /* Find out whether any button has 2 lines of text. */
410     for (list = balsa_toolbar_model_get_current(model); list;
411          list = list->next) {
412         const gchar *icon = list->data;
413         gint button = get_toolbar_button_index(icon);
414 
415         if (button < 0) {
416             g_warning("button '%s' not found. ABORT!\n", icon);
417             continue;
418         }
419 
420         if (strchr(balsa_toolbar_button_text(button), '\n'))
421             return TRUE;
422     }
423 
424     return FALSE;
425 }
426 
427 static gint
tm_set_tool_item_label(GtkToolItem * tool_item,const gchar * stock_id,gboolean make_two_line)428 tm_set_tool_item_label(GtkToolItem * tool_item, const gchar * stock_id,
429                        gboolean make_two_line)
430 {
431     gint button = get_toolbar_button_index(stock_id);
432     const gchar *text;
433     gchar *label;
434 
435     if (button < 0)
436         return button;
437 
438     text = balsa_toolbar_button_text(button);
439     if (balsa_app.toolbar_wrap_button_text) {
440         /* Make sure all buttons have the same number of lines of
441          * text (1 or 2), to keep icons aligned */
442         label = make_two_line && !strchr(text, '\n') ?
443             g_strconcat(text, "\n", NULL) : g_strdup(text);
444     } else {
445         gchar *p = label = g_strdup(text);
446         while ((p = strchr(p, '\n')))
447             *p++ = ' ';
448     }
449 
450     gtk_tool_button_set_label(GTK_TOOL_BUTTON(tool_item), label);
451     g_free(label);
452 
453     gtk_tool_item_set_is_important(tool_item,
454                                    toolbar_buttons[button].is_important);
455 
456     if (strcmp(toolbar_buttons[button].pixmap_id, BALSA_PIXMAP_SEND) == 0
457         && balsa_app.always_queue_sent_mail)
458         gtk_tool_item_set_tooltip_text(tool_item,
459                                        _("Queue this message for sending"));
460 
461     return button;
462 }
463 
464 static GtkToolbarStyle tm_default_style(void);
465 
466 static void
tm_populate(BalsaToolbarModel * model,GtkUIManager * ui_manager,GArray * merge_ids)467 tm_populate(BalsaToolbarModel * model, GtkUIManager * ui_manager,
468             GArray * merge_ids)
469 {
470     gboolean style_is_both;
471     gboolean make_two_line;
472     GSList *list;
473 
474     style_is_both = (model->style == GTK_TOOLBAR_BOTH
475                      || (model->style == (GtkToolbarStyle) -1
476                          && tm_default_style() == GTK_TOOLBAR_BOTH));
477     make_two_line = style_is_both && tm_has_second_line(model);
478 
479     for (list = balsa_toolbar_model_get_current(model); list;
480          list = list->next) {
481         const gchar *stock_id = list->data;
482         guint merge_id = gtk_ui_manager_new_merge_id(ui_manager);
483 
484         g_array_append_val(merge_ids, merge_id);
485 
486         if (!*stock_id)
487             gtk_ui_manager_add_ui(ui_manager, merge_id, "/Toolbar",
488                                   NULL, NULL, GTK_UI_MANAGER_SEPARATOR,
489                                   FALSE);
490         else {
491             gchar *path, *name;
492             GtkWidget *tool_item;
493 
494             name = g_hash_table_lookup(model->legal, stock_id);
495             if (!name) {
496                 g_warning("no name for stock_id \"%s\"", stock_id);
497                 continue;
498             }
499             gtk_ui_manager_add_ui(ui_manager, merge_id, "/Toolbar",
500                                   name, name, GTK_UI_MANAGER_AUTO, FALSE);
501             /* Replace the long menu-item label with the short
502              * tool-button label: */
503             path = g_strconcat("/Toolbar/", name, NULL);
504             tool_item = gtk_ui_manager_get_widget(ui_manager, path);
505             g_free(path);
506             tm_set_tool_item_label(GTK_TOOL_ITEM(tool_item), stock_id,
507                                    make_two_line);
508         }
509     }
510 }
511 
512 #define BALSA_TOOLBAR_MERGE_IDS "balsa-toolbar-merge-ids"
513 static void
bt_free_merge_ids(GArray * merge_ids)514 bt_free_merge_ids(GArray * merge_ids)
515 {
516     g_array_free(merge_ids, TRUE);
517 }
518 
519 static const struct {
520     const gchar *text;
521     const gchar *config_name;
522     GtkToolbarStyle style;
523 } tm_toolbar_options[] = {
524     {N_("Text Be_low Icons"),           "both",       GTK_TOOLBAR_BOTH},
525     {N_("Priority Text Be_side Icons"), "both-horiz", GTK_TOOLBAR_BOTH_HORIZ},
526     {NULL,                              "both_horiz", GTK_TOOLBAR_BOTH_HORIZ},
527     {N_("_Icons Only"),                 "icons",      GTK_TOOLBAR_ICONS},
528     {N_("_Text Only"),                  "text",       GTK_TOOLBAR_TEXT}
529 };
530 
531 static GtkToolbarStyle
tm_default_style(void)532 tm_default_style(void)
533 {
534     GtkToolbarStyle default_style = GTK_TOOLBAR_BOTH;
535 #if HAVE_GNOME
536     GSettings *settings;
537     gchar *str;
538 
539     /* Get global setting */
540     settings = g_settings_new("org.gnome.desktop.interface");
541     str  = g_settings_get_string(settings, "toolbar-style");
542     if (str) {
543         guint i;
544 
545         for (i = 0; i < G_N_ELEMENTS(tm_toolbar_options); i++)
546             if (strcmp(tm_toolbar_options[i].config_name, str) == 0) {
547                 default_style = tm_toolbar_options[i].style;
548                 break;
549             }
550         g_free(str);
551     }
552     g_object_unref(settings);
553 #endif /* HAVE_GNOME */
554 
555     return default_style;
556 }
557 
558 static void
tm_set_style(GtkWidget * toolbar,BalsaToolbarModel * model)559 tm_set_style(GtkWidget * toolbar, BalsaToolbarModel * model)
560 {
561     gtk_toolbar_set_style(GTK_TOOLBAR(toolbar),
562                           model->style != (GtkToolbarStyle) (-1) ?
563                           model->style : tm_default_style());
564 }
565 
566 /* Update a real toolbar when the model has changed.
567  */
568 static void
tm_changed_cb(BalsaToolbarModel * model,GtkUIManager * ui_manager)569 tm_changed_cb(BalsaToolbarModel * model, GtkUIManager * ui_manager)
570 {
571     GArray *merge_ids =
572         g_object_get_data(G_OBJECT(ui_manager), BALSA_TOOLBAR_MERGE_IDS);
573     guint i;
574     GtkWidget *toolbar;
575 
576     for (i = 0; i < merge_ids->len; i++) {
577         guint merge_id = g_array_index(merge_ids, guint, i);
578         gtk_ui_manager_remove_ui(ui_manager, merge_id);
579     }
580     merge_ids->len = 0;
581 
582     tm_populate(model, ui_manager, merge_ids);
583 
584     toolbar = gtk_ui_manager_get_widget(ui_manager, "/Toolbar");
585     tm_set_style(toolbar, model);
586 
587     tm_save_model(model);
588 }
589 
590 typedef struct {
591     BalsaToolbarModel *model;
592     GtkUIManager      *ui_manager;
593     GtkWidget         *menu;
594 } toolbar_info;
595 
596 static void
tm_toolbar_weak_notify(toolbar_info * info,GtkWidget * toolbar)597 tm_toolbar_weak_notify(toolbar_info * info, GtkWidget * toolbar)
598 {
599     g_signal_handlers_disconnect_by_func(info->model, tm_changed_cb,
600                                          info->ui_manager);
601     g_object_unref(info->ui_manager);
602     g_free(info);
603 }
604 
605 #define BALSA_TOOLBAR_STYLE "balsa-toolbar-style"
606 static void
menu_item_toggled_cb(GtkCheckMenuItem * item,toolbar_info * info)607 menu_item_toggled_cb(GtkCheckMenuItem * item, toolbar_info * info)
608 {
609     if (gtk_check_menu_item_get_active(item)) {
610         info->model->style =
611             GPOINTER_TO_INT(g_object_get_data
612                             (G_OBJECT(item), BALSA_TOOLBAR_STYLE));
613         balsa_toolbar_model_changed(info->model);
614         if (info->menu)
615             gtk_menu_shell_deactivate(GTK_MENU_SHELL(info->menu));
616     }
617 }
618 
619 /* We want to destroy the popup menu after handling the "toggled"
620  * signal; the "deactivate" signal is apparently emitted before
621  * "toggled", so we have to use an idle callback. */
622 static gboolean
tm_popup_idle_cb(GtkWidget * menu)623 tm_popup_idle_cb(GtkWidget *menu)
624 {
625     gtk_widget_destroy(menu);
626     return FALSE;
627 }
628 
629 static void
tm_popup_deactivated_cb(GtkWidget * menu,toolbar_info * info)630 tm_popup_deactivated_cb(GtkWidget * menu, toolbar_info * info)
631 {
632     if (info->menu) {
633         g_idle_add((GSourceFunc) tm_popup_idle_cb, menu);
634         info->menu = NULL;
635     }
636 }
637 
638 static gchar *
tm_remove_underscore(const gchar * text)639 tm_remove_underscore(const gchar * text)
640 {
641     gchar *p, *q, *r = g_strdup(text);
642 
643     for (p = q = r; *p; p++)
644         if (*p != '_')
645             *q++ = *p;
646     *q = '\0';
647 
648     return r;
649 }
650 
651 /* If the menu is popped up in response to a keystroke, center it
652  * immediately below the toolbar.
653  */
654 static void
tm_popup_position_func(GtkMenu * menu,gint * x,gint * y,gboolean * push_in,gpointer user_data)655 tm_popup_position_func(GtkMenu * menu, gint * x, gint * y,
656                        gboolean * push_in, gpointer user_data)
657 {
658     GtkWidget *toolbar = GTK_WIDGET(user_data);
659     GdkScreen *screen = gtk_widget_get_screen(toolbar);
660     GtkRequisition req;
661     gint monitor_num;
662     GdkRectangle monitor;
663     GtkAllocation allocation;
664 
665     g_return_if_fail(gtk_widget_get_window(toolbar));
666 
667     gdk_window_get_origin(gtk_widget_get_window(toolbar), x, y);
668 
669     gtk_widget_get_preferred_size(GTK_WIDGET(menu), NULL, &req);
670 
671     gtk_widget_get_allocation(toolbar, &allocation);
672     *x += (allocation.width - req.width) / 2;
673     *y += allocation.height;
674 
675     monitor_num = gdk_screen_get_monitor_at_point(screen, *x, *y);
676     gtk_menu_set_monitor(menu, monitor_num);
677     gdk_screen_get_monitor_geometry(screen, monitor_num, &monitor);
678 
679     *x = CLAMP(*x, monitor.x,
680                monitor.x + MAX(0, monitor.width - req.width));
681     *y = CLAMP(*y, monitor.y,
682                monitor.y + MAX(0, monitor.height - req.height));
683 
684     *push_in = FALSE;
685 }
686 
687 static gboolean
tm_do_popup_menu(GtkWidget * toolbar,GdkEventButton * event,toolbar_info * info)688 tm_do_popup_menu(GtkWidget * toolbar, GdkEventButton * event,
689                  toolbar_info * info)
690 {
691     GtkWidget *menu;
692     int button, event_time;
693     guint i;
694     GSList *group = NULL;
695     GtkToolbarStyle default_style = tm_default_style();
696 
697     if (info->menu)
698         return FALSE;
699 
700     info->menu = menu = gtk_menu_new();
701     g_signal_connect(menu, "deactivate",
702                      G_CALLBACK(tm_popup_deactivated_cb), info);
703 
704     /* ... add menu items ... */
705     for (i = 0; i < G_N_ELEMENTS(tm_toolbar_options); i++) {
706         GtkWidget *item;
707 
708         if (!tm_toolbar_options[i].text)
709             continue;
710 
711         item =
712             gtk_radio_menu_item_new_with_mnemonic(group,
713                                                   _(tm_toolbar_options[i].
714                                                     text));
715         group = gtk_radio_menu_item_get_group(GTK_RADIO_MENU_ITEM(item));
716 
717         if (tm_toolbar_options[i].style == info->model->style)
718             gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(item),
719                                            TRUE);
720 
721         gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
722         g_object_set_data(G_OBJECT(item), BALSA_TOOLBAR_STYLE,
723                           GINT_TO_POINTER(tm_toolbar_options[i].style));
724         g_signal_connect(item, "toggled", G_CALLBACK(menu_item_toggled_cb),
725                          info);
726     }
727 
728     for (i = 0; i < G_N_ELEMENTS(tm_toolbar_options); i++) {
729 
730         if (!tm_toolbar_options[i].text)
731             continue;
732 
733         if (tm_toolbar_options[i].style == default_style) {
734             gchar *option_text, *text;
735             GtkWidget *item;
736 
737             gtk_menu_shell_append(GTK_MENU_SHELL(menu),
738                                   gtk_separator_menu_item_new());
739 
740             option_text =
741                 tm_remove_underscore(_(tm_toolbar_options[i].text));
742             text =
743                 g_strdup_printf(_("Use Desktop _Default (%s)"),
744                                 option_text);
745             g_free(option_text);
746 
747             item = gtk_radio_menu_item_new_with_mnemonic(group, text);
748             g_free(text);
749 
750             if (info->model->style == (GtkToolbarStyle) (-1))
751                 gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM
752                                                (item), TRUE);
753             gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
754             g_object_set_data(G_OBJECT(item), BALSA_TOOLBAR_STYLE,
755                               GINT_TO_POINTER(-1));
756             g_signal_connect(item, "toggled",
757                              G_CALLBACK(menu_item_toggled_cb), info);
758         }
759     }
760 
761     if (gtk_widget_is_sensitive(toolbar)) {
762         /* This is a real toolbar, not the template from the
763          * toolbar-prefs dialog. */
764         GtkWidget *item;
765 
766         gtk_menu_shell_append(GTK_MENU_SHELL(menu),
767                               gtk_separator_menu_item_new());
768         item =
769             gtk_menu_item_new_with_mnemonic(_("_Customize Toolbars..."));
770         g_signal_connect(item, "activate", G_CALLBACK(customize_dialog_cb),
771                          gtk_widget_get_toplevel(toolbar));
772         gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
773 
774         /* Pass the model type to the customize widget, so that it can
775          * show the appropriate notebook page. */
776         g_object_set_data(G_OBJECT(item), BALSA_TOOLBAR_MODEL_TYPE,
777                           GINT_TO_POINTER(info->model->type));
778     }
779 
780     gtk_widget_show_all(menu);
781 
782     if (event) {
783         button = event->button;
784         event_time = event->time;
785     } else {
786         button = 0;
787         event_time = gtk_get_current_event_time();
788     }
789 
790     gtk_menu_attach_to_widget(GTK_MENU(menu), toolbar, NULL);
791     if (button)
792         gtk_menu_popup(GTK_MENU(menu), NULL, NULL, NULL, NULL, button,
793                        event_time);
794     else
795         gtk_menu_popup(GTK_MENU(menu), NULL, NULL, tm_popup_position_func,
796                        toolbar, button, event_time);
797 
798     return TRUE;
799 }
800 
801 static gboolean
tm_button_press_cb(GtkWidget * toolbar,GdkEventButton * event,toolbar_info * info)802 tm_button_press_cb(GtkWidget * toolbar, GdkEventButton * event,
803                    toolbar_info * info)
804 {
805     /* Ignore double-clicks and triple-clicks */
806     if (event->button == 3 && event->type == GDK_BUTTON_PRESS)
807         return tm_do_popup_menu(toolbar, event, info);
808 
809     return FALSE;
810 }
811 
812 static gboolean
tm_popup_menu_cb(GtkWidget * toolbar,toolbar_info * info)813 tm_popup_menu_cb(GtkWidget * toolbar, toolbar_info * info)
814 {
815     return tm_do_popup_menu(toolbar, NULL, info);
816 }
817 
balsa_toolbar_new(BalsaToolbarModel * model,GtkUIManager * ui_manager)818 GtkWidget *balsa_toolbar_new(BalsaToolbarModel * model,
819                              GtkUIManager * ui_manager)
820 {
821     GtkWidget *toolbar;
822     toolbar_info *info;
823     GArray *merge_ids = g_array_new(FALSE, FALSE, sizeof(guint));
824 
825     g_object_set_data_full(G_OBJECT(ui_manager), BALSA_TOOLBAR_MERGE_IDS,
826                            merge_ids, (GDestroyNotify) bt_free_merge_ids);
827 
828     tm_populate(model, ui_manager, merge_ids);
829     g_signal_connect(model, "changed", G_CALLBACK(tm_changed_cb),
830                      ui_manager);
831 
832     info = g_new(toolbar_info, 1);
833     info->model = model;
834     info->ui_manager = g_object_ref(ui_manager);
835     info->menu = NULL;
836 
837     toolbar = gtk_ui_manager_get_widget(ui_manager, "/Toolbar");
838     tm_set_style(toolbar, model);
839     g_object_weak_ref(G_OBJECT(toolbar),
840                       (GWeakNotify) tm_toolbar_weak_notify, info);
841 
842     g_signal_connect(toolbar, "button-press-event",
843                      G_CALLBACK(tm_button_press_cb), info);
844     g_signal_connect(toolbar, "popup-menu", G_CALLBACK(tm_popup_menu_cb),
845                      info);
846 
847     return toolbar;
848 }
849