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