Merge pull request #3757 from andy5995/iss-3752
[geany-mirror.git] / src / toolbar.c
blobb1895dd1297340ef1ffba6a02ad15fb49c47cd38
1 /*
2 * toolbar.c - this file is part of Geany, a fast and lightweight IDE
4 * Copyright 2009 The Geany contributors
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 of the License, or
9 * (at your option) any later version.
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.
16 * You should have received a copy of the GNU General Public License along
17 * with this program; if not, write to the Free Software Foundation, Inc.,
18 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
21 /**
22 * @file toolbar.h
23 * Toolbar (prefs).
25 /* Utility functions to create the toolbar */
27 #ifdef HAVE_CONFIG_H
28 # include "config.h"
29 #endif
31 #include "toolbar.h"
33 #include "app.h"
34 #include "build.h"
35 #include "callbacks.h"
36 #include "document.h"
37 #include "geanyentryaction.h"
38 #include "geanymenubuttonaction.h"
39 #include "main.h"
40 #include "support.h"
41 #include "ui_utils.h"
42 #include "utils.h"
44 #include <string.h>
45 #include <glib/gstdio.h>
48 GeanyToolbarPrefs toolbar_prefs;
49 static GtkUIManager *uim;
50 static GtkActionGroup *group;
51 static GSList *plugin_items = NULL;
53 /* Available toolbar actions
54 * Fields: name, stock_id, label, accelerator, tooltip, callback */
55 static const GtkActionEntry ui_entries[] = {
56 /* custom actions defined in toolbar_init(): "New", "Open", "SearchEntry", "GotoEntry", "Build" */
57 { "Save", GTK_STOCK_SAVE, NULL, NULL, N_("Save the current file"), G_CALLBACK(on_save1_activate) },
58 { "SaveAs", GTK_STOCK_SAVE_AS, NULL, NULL, N_("Save as"), G_CALLBACK(on_save_as1_activate) },
59 { "SaveAll", GEANY_STOCK_SAVE_ALL, NULL, NULL, N_("Save all open files"), G_CALLBACK(on_save_all1_activate) },
60 { "Reload", GTK_STOCK_REVERT_TO_SAVED, NULL, NULL, N_("Reload the current file from disk"), G_CALLBACK(on_toolbutton_reload_clicked) },
61 { "Close", GTK_STOCK_CLOSE, NULL, NULL, N_("Close the current file"), G_CALLBACK(on_close1_activate) },
62 { "CloseAll", GEANY_STOCK_CLOSE_ALL, NULL, NULL, N_("Close all open files"), G_CALLBACK(on_close_all1_activate) },
63 { "Cut", GTK_STOCK_CUT, NULL, NULL, N_("Cut the current selection"), G_CALLBACK(on_cut1_activate) },
64 { "Copy", GTK_STOCK_COPY, NULL, NULL, N_("Copy the current selection"), G_CALLBACK(on_copy1_activate) },
65 { "Paste", GTK_STOCK_PASTE, NULL, NULL, N_("Paste the contents of the clipboard"), G_CALLBACK(on_paste1_activate) },
66 { "Delete", GTK_STOCK_DELETE, NULL, NULL, N_("Delete the current selection"), G_CALLBACK(on_delete1_activate) },
67 { "Undo", GTK_STOCK_UNDO, NULL, NULL, N_("Undo the last modification"), G_CALLBACK(on_undo1_activate) },
68 { "Redo", GTK_STOCK_REDO, NULL, NULL, N_("Redo the last modification"), G_CALLBACK(on_redo1_activate) },
69 { "NavBack", GTK_STOCK_GO_BACK, NULL, NULL, N_("Navigate back a location"), G_CALLBACK(on_toolbutton_back_activate) },
70 { "NavFor", GTK_STOCK_GO_FORWARD, NULL, NULL, N_("Navigate forward a location"), G_CALLBACK(on_toolbutton_forward_activate) },
71 { "Compile", GTK_STOCK_CONVERT, N_("Compile"), NULL, N_("Compile the current file"), G_CALLBACK(on_toolbutton_compile_clicked) },
72 { "Run", GTK_STOCK_EXECUTE, NULL, NULL, N_("Run or view the current file"), G_CALLBACK(on_toolbutton_run_clicked) },
73 { "Color", GTK_STOCK_SELECT_COLOR, N_("Color Chooser"), NULL, N_("Open a color chooser dialog, to interactively pick colors from a palette"), G_CALLBACK(on_show_color_chooser1_activate) },
74 { "ZoomIn", GTK_STOCK_ZOOM_IN, NULL, NULL, N_("Zoom in the text"), G_CALLBACK(on_zoom_in1_activate) },
75 { "ZoomOut", GTK_STOCK_ZOOM_OUT, NULL, NULL, N_("Zoom out the text"), G_CALLBACK(on_zoom_out1_activate) },
76 { "UnIndent", GTK_STOCK_UNINDENT, NULL, NULL, N_("Decrease indentation"), G_CALLBACK(on_menu_decrease_indent1_activate) },
77 { "Indent", GTK_STOCK_INDENT, NULL, NULL, N_("Increase indentation"), G_CALLBACK(on_menu_increase_indent1_activate) },
78 { "Search", GTK_STOCK_FIND, NULL, NULL, N_("Find the entered text in the current file"), G_CALLBACK(on_toolbutton_search_clicked) },
79 { "Goto", GTK_STOCK_JUMP_TO, NULL, NULL, N_("Jump to the entered line number"), G_CALLBACK(on_toolbutton_goto_clicked) },
80 { "Preferences", GTK_STOCK_PREFERENCES, NULL, NULL, N_("Show the preferences dialog"), G_CALLBACK(on_preferences1_activate) },
81 { "Quit", GTK_STOCK_QUIT, NULL, NULL, N_("Quit Geany"), G_CALLBACK(on_quit1_activate) },
82 { "Print", GTK_STOCK_PRINT, NULL, NULL, N_("Print document"), G_CALLBACK(on_print1_activate) },
83 { "Replace", GTK_STOCK_FIND_AND_REPLACE, NULL, NULL, N_("Replace text in the current document"), G_CALLBACK(on_replace1_activate) }
85 static const guint ui_entries_n = G_N_ELEMENTS(ui_entries);
88 /* fallback UI definition */
89 static const gchar *toolbar_markup =
90 "<ui>"
91 "<toolbar name='GeanyToolbar'>"
92 "<toolitem action='New'/>"
93 "<toolitem action='Open'/>"
94 "<toolitem action='Save'/>"
95 "<toolitem action='SaveAll'/>"
96 "<separator/>"
97 "<toolitem action='Reload'/>"
98 "<toolitem action='Close'/>"
99 "<separator/>"
100 "<toolitem action='NavBack'/>"
101 "<toolitem action='NavFor'/>"
102 "<separator/>"
103 "<toolitem action='Compile'/>"
104 "<toolitem action='Build'/>"
105 "<toolitem action='Run'/>"
106 "<separator/>"
107 "<toolitem action='Color'/>"
108 "<separator/>"
109 "<toolitem action='SearchEntry'/>"
110 "<toolitem action='Search'/>"
111 "<separator/>"
112 "<toolitem action='GotoEntry'/>"
113 "<toolitem action='Goto'/>"
114 "<separator/>"
115 "<toolitem action='Quit'/>"
116 "</toolbar>"
117 "</ui>";
120 /* Note: The returned widget pointer is only valid until the toolbar is reloaded. So, either
121 * update the widget pointer in this case (i.e. request it again) or better use
122 * toolbar_get_action_by_name() instead. The action objects will remain the same even when the
123 * toolbar is reloaded. */
124 GtkWidget *toolbar_get_widget_by_name(const gchar *name)
126 GtkWidget *widget;
127 gchar *path;
129 g_return_val_if_fail(name != NULL, NULL);
131 path = g_strconcat("/ui/GeanyToolbar/", name, NULL);
132 widget = gtk_ui_manager_get_widget(uim, path);
134 g_free(path);
135 return widget;
139 /* Note: The returned widget pointer is only valid until the toolbar is reloaded. See
140 * toolbar_get_widget_by_name for details(). */
141 GtkWidget *toolbar_get_widget_child_by_name(const gchar *name)
143 GtkWidget *widget = toolbar_get_widget_by_name(name);
145 if (G_LIKELY(widget != NULL))
146 return gtk_bin_get_child(GTK_BIN(widget));
147 else
148 return NULL;
152 GtkAction *toolbar_get_action_by_name(const gchar *name)
154 g_return_val_if_fail(name != NULL, NULL);
156 return gtk_action_group_get_action(group, name);
160 static void toolbar_item_destroy_cb(GtkWidget *widget, G_GNUC_UNUSED gpointer data)
162 plugin_items = g_slist_remove(plugin_items, widget);
166 void toolbar_item_ref(GtkToolItem *item)
168 g_return_if_fail(item != NULL);
170 plugin_items = g_slist_append(plugin_items, item);
171 g_signal_connect(item, "destroy", G_CALLBACK(toolbar_item_destroy_cb), NULL);
175 static GtkWidget *toolbar_reload(const gchar *markup)
177 gint i;
178 GSList *l;
179 GtkWidget *entry;
180 GError *error = NULL;
181 gchar *filename;
182 static guint merge_id = 0;
183 GtkWidget *toolbar_new_file_menu = NULL;
184 GtkWidget *toolbar_recent_files_menu = NULL;
185 GtkWidget *toolbar_build_menu = NULL;
187 /* Cleanup old toolbar */
188 if (merge_id > 0)
190 /* ref plugins toolbar items to keep them after we destroyed the toolbar */
191 foreach_slist(l, plugin_items)
193 g_object_ref(l->data);
194 gtk_container_remove(GTK_CONTAINER(main_widgets.toolbar), GTK_WIDGET(l->data));
196 /* ref and hold the submenus of the New, Open and Build toolbar items */
197 toolbar_new_file_menu = geany_menu_button_action_get_menu(
198 GEANY_MENU_BUTTON_ACTION(gtk_action_group_get_action(group, "New")));
199 g_object_ref(toolbar_new_file_menu);
200 toolbar_recent_files_menu = geany_menu_button_action_get_menu(
201 GEANY_MENU_BUTTON_ACTION(gtk_action_group_get_action(group, "Open")));
202 g_object_ref(toolbar_recent_files_menu);
203 toolbar_build_menu = geany_menu_button_action_get_menu(
204 GEANY_MENU_BUTTON_ACTION(gtk_action_group_get_action(group, "Build")));
205 g_object_ref(toolbar_build_menu);
207 /* Get rid of it! */
208 gtk_widget_destroy(main_widgets.toolbar);
210 gtk_ui_manager_remove_ui(uim, merge_id);
211 gtk_ui_manager_ensure_update(uim);
214 if (markup != NULL)
216 merge_id = gtk_ui_manager_add_ui_from_string(uim, markup, -1, &error);
218 else
220 /* Load the toolbar UI XML file from disk (first from config dir, then try data dir) */
221 filename = g_build_filename(app->configdir, "ui_toolbar.xml", NULL);
222 merge_id = gtk_ui_manager_add_ui_from_file(uim, filename, &error);
223 if (merge_id == 0)
225 if (! g_error_matches(error, G_FILE_ERROR, G_FILE_ERROR_NOENT))
226 geany_debug("Loading user toolbar UI definition failed (%s).", error->message);
227 g_error_free(error);
228 error = NULL;
230 SETPTR(filename, g_build_filename(app->datadir, "ui_toolbar.xml", NULL));
231 merge_id = gtk_ui_manager_add_ui_from_file(uim, filename, &error);
233 g_free(filename);
235 if (error != NULL)
237 geany_debug("UI creation failed, using internal fallback definition. Error message: %s",
238 error->message);
239 g_error_free(error);
240 /* finally load the internally defined markup as fallback */
241 merge_id = gtk_ui_manager_add_ui_from_string(uim, toolbar_markup, -1, NULL);
243 main_widgets.toolbar = gtk_ui_manager_get_widget(uim, "/ui/GeanyToolbar");
244 ui_init_toolbar_widgets();
246 /* add the toolbar again to the main window */
247 if (toolbar_prefs.append_to_menu)
249 GtkWidget *hbox_menubar = ui_lookup_widget(main_widgets.window, "hbox_menubar");
250 gtk_box_pack_start(GTK_BOX(hbox_menubar), main_widgets.toolbar, TRUE, TRUE, 0);
251 gtk_box_reorder_child(GTK_BOX(hbox_menubar), main_widgets.toolbar, 1);
253 else
255 GtkWidget *box = ui_lookup_widget(main_widgets.window, "vbox1");
257 gtk_box_pack_start(GTK_BOX(box), main_widgets.toolbar, FALSE, FALSE, 0);
258 gtk_box_reorder_child(GTK_BOX(box), main_widgets.toolbar, 1);
260 gtk_widget_show(main_widgets.toolbar);
262 /* re-add und unref the plugin toolbar items */
263 i = toolbar_get_insert_position();
264 foreach_slist(l, plugin_items)
266 gtk_toolbar_insert(GTK_TOOLBAR(main_widgets.toolbar), l->data, i);
267 g_object_unref(l->data);
268 i++;
270 /* re-add und unref the submenus of menu toolbar items */
271 if (toolbar_new_file_menu != NULL)
273 geany_menu_button_action_set_menu(GEANY_MENU_BUTTON_ACTION(
274 gtk_action_group_get_action(group, "New")), toolbar_new_file_menu);
275 g_object_unref(toolbar_new_file_menu);
277 if (toolbar_recent_files_menu != NULL)
279 geany_menu_button_action_set_menu(GEANY_MENU_BUTTON_ACTION(
280 gtk_action_group_get_action(group, "Open")), toolbar_recent_files_menu);
281 g_object_unref(toolbar_recent_files_menu);
283 if (toolbar_build_menu != NULL)
285 geany_menu_button_action_set_menu(GEANY_MENU_BUTTON_ACTION(
286 gtk_action_group_get_action(group, "Build")), toolbar_build_menu);
287 g_object_unref(toolbar_build_menu);
290 /* update button states */
291 if (main_status.main_window_realized)
293 GeanyDocument *doc = document_get_current();
294 gboolean doc_changed = (doc != NULL) ? doc->changed : FALSE;
296 ui_document_buttons_update();
297 ui_save_buttons_toggle(doc_changed); /* update save all */
298 ui_update_popup_reundo_items(doc);
300 toolbar_apply_settings();
303 /* Signals */
304 g_signal_connect(main_widgets.toolbar, "button-press-event",
305 G_CALLBACK(toolbar_popup_menu), NULL);
306 g_signal_connect(main_widgets.toolbar, "key-press-event",
307 G_CALLBACK(on_escape_key_press_event), NULL);
309 /* We don't need to disconnect those signals as this is done automatically when the entry
310 * widgets are destroyed, happens when the toolbar itself is destroyed. */
311 entry = toolbar_get_widget_child_by_name("SearchEntry");
312 if (entry != NULL)
313 g_signal_connect(entry, "motion-notify-event", G_CALLBACK(on_motion_event), NULL);
314 entry = toolbar_get_widget_child_by_name("GotoEntry");
315 if (entry != NULL)
316 g_signal_connect(entry, "motion-notify-event", G_CALLBACK(on_motion_event), NULL);
318 return main_widgets.toolbar;
322 static void toolbar_notify_style_cb(GObject *object, GParamSpec *arg1, gpointer data)
324 const gchar *arg_name = g_param_spec_get_name(arg1);
325 gint value;
327 if (toolbar_prefs.use_gtk_default_style && utils_str_equal(arg_name, "gtk-toolbar-style"))
329 value = ui_get_gtk_settings_integer(arg_name, toolbar_prefs.icon_style);
330 gtk_toolbar_set_style(GTK_TOOLBAR(main_widgets.toolbar), value);
332 else if (toolbar_prefs.use_gtk_default_icon && utils_str_equal(arg_name, "gtk-toolbar-size"))
334 value = ui_get_gtk_settings_integer(arg_name, toolbar_prefs.icon_size);
335 gtk_toolbar_set_icon_size(GTK_TOOLBAR(main_widgets.toolbar), value);
340 GtkWidget *toolbar_init(void)
342 GtkWidget *toolbar;
343 GtkAction *action_new;
344 GtkAction *action_open;
345 GtkAction *action_build;
346 GtkAction *action_searchentry;
347 GtkAction *action_gotoentry;
348 GtkSettings *gtk_settings;
350 uim = gtk_ui_manager_new();
351 group = gtk_action_group_new("GeanyToolbar");
353 gtk_action_group_set_translation_domain(group, GETTEXT_PACKAGE);
354 gtk_action_group_add_actions(group, ui_entries, ui_entries_n, NULL);
356 /* Create our custom actions */
357 action_new = geany_menu_button_action_new(
358 "New", NULL,
359 _("Create a new file"),
360 _("Create a new file from a template"),
361 GTK_STOCK_NEW);
362 g_signal_connect(action_new, "button-clicked", G_CALLBACK(on_new1_activate), NULL);
363 gtk_action_group_add_action(group, action_new);
365 action_open = geany_menu_button_action_new(
366 "Open", NULL,
367 _("Open an existing file"),
368 _("Open a recent file"),
369 GTK_STOCK_OPEN);
370 g_signal_connect(action_open, "button-clicked", G_CALLBACK(on_open1_activate), NULL);
371 gtk_action_group_add_action(group, action_open);
373 action_build = geany_menu_button_action_new(
374 "Build", NULL,
375 _("Build the current file"),
376 _("Choose more build actions"),
377 GEANY_STOCK_BUILD);
378 g_signal_connect(action_build, "button-clicked",
379 G_CALLBACK(build_toolbutton_build_clicked), NULL);
380 gtk_action_group_add_action(group, action_build);
382 action_searchentry = geany_entry_action_new(
383 "SearchEntry", _("Search Field"), _("Find the entered text in the current file"), FALSE);
384 g_signal_connect(action_searchentry, "entry-activate",
385 G_CALLBACK(on_toolbar_search_entry_activate), GINT_TO_POINTER(FALSE));
386 g_signal_connect(action_searchentry, "entry-activate-backward",
387 G_CALLBACK(on_toolbar_search_entry_activate), GINT_TO_POINTER(TRUE));
388 g_signal_connect(action_searchentry, "entry-changed",
389 G_CALLBACK(on_toolbar_search_entry_changed), NULL);
390 gtk_action_group_add_action(group, action_searchentry);
392 action_gotoentry = geany_entry_action_new(
393 "GotoEntry", _("Goto Field"), _("Jump to the entered line number"), TRUE);
394 g_signal_connect(action_gotoentry, "entry-activate",
395 G_CALLBACK(on_toolbutton_goto_entry_activate), NULL);
396 gtk_action_group_add_action(group, action_gotoentry);
398 gtk_ui_manager_insert_action_group(uim, group, 0);
400 toolbar = toolbar_reload(NULL);
401 gtk_style_context_add_class(gtk_widget_get_style_context(toolbar), "primary-toolbar");
403 gtk_settings = gtk_widget_get_settings(GTK_WIDGET(toolbar));
404 if (gtk_settings != NULL)
406 g_signal_connect(gtk_settings, "notify::gtk-toolbar-style",
407 G_CALLBACK(toolbar_notify_style_cb), NULL);
410 return toolbar;
414 void toolbar_update_ui(void)
416 static GtkWidget *hbox_menubar = NULL;
417 static GtkWidget *menubar = NULL;
418 GtkWidget *parent;
419 GtkToolItem *first_item;
421 if (menubar == NULL)
422 { /* cache widget pointers */
423 hbox_menubar = ui_lookup_widget(main_widgets.window, "hbox_menubar");
424 menubar = ui_lookup_widget(main_widgets.window, "menubar1");
426 /* the separator between the menubar and the toolbar */
427 first_item = gtk_toolbar_get_nth_item(GTK_TOOLBAR(main_widgets.toolbar), 0);
428 if (first_item != NULL && GTK_IS_SEPARATOR_TOOL_ITEM(first_item))
430 gtk_widget_destroy(GTK_WIDGET(first_item));
433 parent = gtk_widget_get_parent(main_widgets.toolbar);
435 if (toolbar_prefs.append_to_menu)
437 GtkWidget *menubar_toolbar_separator;
439 if (parent != NULL)
441 if (parent != hbox_menubar)
442 { /* here we manually 'reparent' the toolbar, gtk_widget_reparent() doesn't
443 * like to do it */
444 g_object_ref(main_widgets.toolbar);
446 gtk_container_remove(GTK_CONTAINER(parent), main_widgets.toolbar);
447 gtk_box_pack_start(GTK_BOX(hbox_menubar), main_widgets.toolbar, TRUE, TRUE, 0);
448 gtk_box_reorder_child(GTK_BOX(hbox_menubar), main_widgets.toolbar, 1);
450 g_object_unref(main_widgets.toolbar);
453 else
454 gtk_box_pack_start(GTK_BOX(hbox_menubar), main_widgets.toolbar, TRUE, TRUE, 0);
456 /* the separator between the menubar and the toolbar */
457 menubar_toolbar_separator = GTK_WIDGET(gtk_separator_tool_item_new());
458 gtk_widget_show(menubar_toolbar_separator);
459 gtk_toolbar_insert(GTK_TOOLBAR(main_widgets.toolbar),
460 GTK_TOOL_ITEM(menubar_toolbar_separator), 0);
462 else
464 GtkWidget *box = ui_lookup_widget(main_widgets.window, "vbox1");
466 if (parent != NULL)
468 if (parent != box)
470 g_object_ref(main_widgets.toolbar);
472 gtk_container_remove(GTK_CONTAINER(parent), main_widgets.toolbar);
473 gtk_box_pack_start(GTK_BOX(box), main_widgets.toolbar, FALSE, FALSE, 0);
474 gtk_box_reorder_child(GTK_BOX(box), main_widgets.toolbar, 1);
476 g_object_unref(main_widgets.toolbar);
479 else
481 gtk_box_pack_start(GTK_BOX(box), main_widgets.toolbar, FALSE, FALSE, 0);
482 gtk_box_reorder_child(GTK_BOX(box), main_widgets.toolbar, 1);
485 /* we need to adjust the packing flags for the menubar to expand it if it is alone in the
486 * hbox and not expand it if the toolbar is appended */
487 gtk_box_set_child_packing(GTK_BOX(hbox_menubar), menubar,
488 ! (toolbar_prefs.visible && toolbar_prefs.append_to_menu), TRUE, 0, GTK_PACK_START);
492 /* Returns the position for adding new toolbar items. The returned position can be used
493 * to add new toolbar items with @c gtk_toolbar_insert(). The toolbar object can be accessed
494 * with @a geany->main_widgets->toolbar.
495 * The position is always the last one before the Quit button or the very last position if the
496 * Quit button is not the last toolbar item.
498 * @return The position for new toolbar items.
500 gint toolbar_get_insert_position(void)
502 GtkWidget *quit = toolbar_get_widget_by_name("Quit");
503 gint quit_pos = -1, pos;
505 if (quit != NULL)
506 quit_pos = gtk_toolbar_get_item_index(GTK_TOOLBAR(main_widgets.toolbar), GTK_TOOL_ITEM(quit));
508 pos = gtk_toolbar_get_n_items(GTK_TOOLBAR(main_widgets.toolbar));
509 if (quit_pos == (pos - 1))
511 /* if the toolbar item before the quit button is a separator, insert new items before */
512 if (GTK_IS_SEPARATOR_TOOL_ITEM(gtk_toolbar_get_nth_item(
513 GTK_TOOLBAR(main_widgets.toolbar), quit_pos - 1)))
515 return quit_pos - 1;
517 /* else return the position of the quit button to insert new items before */
518 return quit_pos;
521 return pos;
525 void toolbar_finalize(void)
527 GeanyMenubuttonAction *open_action = GEANY_MENU_BUTTON_ACTION(toolbar_get_action_by_name("Open"));
528 g_object_unref(geany_menu_button_action_get_menu(open_action));
529 geany_menu_button_action_set_menu(open_action, NULL);
531 /* unref'ing the GtkUIManager object will destroy all its widgets unless they were ref'ed */
532 g_object_unref(uim);
533 g_object_unref(group);
535 g_slist_free(plugin_items);
539 void toolbar_show_hide(void)
541 ignore_callback = TRUE;
542 gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(
543 ui_lookup_widget(main_widgets.window, "menu_show_toolbar1")), toolbar_prefs.visible);
544 ui_widget_show_hide(main_widgets.toolbar, toolbar_prefs.visible);
545 ignore_callback = FALSE;
549 /* sets the icon style of the toolbar */
550 static void toolbar_set_icon_style(void)
552 gint icon_style;
554 icon_style = toolbar_prefs.icon_style;
556 if (toolbar_prefs.use_gtk_default_style)
557 icon_style = ui_get_gtk_settings_integer("gtk-toolbar-style", toolbar_prefs.icon_style);
559 gtk_toolbar_set_style(GTK_TOOLBAR(main_widgets.toolbar), icon_style);
563 /* sets the icon size of the toolbar */
564 static void toolbar_set_icon_size(void)
566 gint icon_size;
568 icon_size = toolbar_prefs.icon_size;
570 if (toolbar_prefs.use_gtk_default_icon)
571 icon_size = ui_get_gtk_settings_integer("gtk-toolbar-icon-size", toolbar_prefs.icon_size);
573 gtk_toolbar_set_icon_size(GTK_TOOLBAR(main_widgets.toolbar), icon_size);
577 void toolbar_apply_settings(void)
579 toolbar_set_icon_style();
580 toolbar_set_icon_size();
584 #define TB_EDITOR_SEPARATOR _("Separator")
585 #define TB_EDITOR_SEPARATOR_LABEL _("--- Separator ---")
586 typedef struct
588 GtkWidget *dialog;
590 GtkTreeView *tree_available;
591 GtkTreeView *tree_used;
593 GtkListStore *store_available;
594 GtkListStore *store_used;
596 GtkTreePath *last_drag_path;
597 GtkTreeViewDropPosition last_drag_pos;
599 GtkWidget *drag_source;
600 } TBEditorWidget;
602 static const GtkTargetEntry tb_editor_dnd_targets[] =
604 { (gchar *) "GEANY_TB_EDITOR_ROW", 0, 0 }
606 static const gint tb_editor_dnd_targets_len = G_N_ELEMENTS(tb_editor_dnd_targets);
608 enum
610 TB_EDITOR_COL_ACTION,
611 TB_EDITOR_COL_LABEL,
612 TB_EDITOR_COL_ICON,
613 TB_EDITOR_COLS_MAX
616 static void tb_editor_handler_start_element(GMarkupParseContext *context, const gchar *element_name,
617 const gchar **attribute_names,
618 const gchar **attribute_values, gpointer data,
619 GError **error)
621 gint i;
622 GSList **actions = data;
624 /* This is very basic parsing, stripped down any error checking, requires a valid UI markup. */
625 if (utils_str_equal(element_name, "separator"))
626 *actions = g_slist_append(*actions, g_strdup(TB_EDITOR_SEPARATOR));
628 for (i = 0; attribute_names[i] != NULL; i++)
630 if (utils_str_equal(attribute_names[i], "action"))
632 *actions = g_slist_append(*actions, g_strdup(attribute_values[i]));
638 static const GMarkupParser tb_editor_xml_parser =
640 tb_editor_handler_start_element, NULL, NULL, NULL, NULL
644 static GSList *tb_editor_parse_ui(const gchar *buffer, gssize length, GError **error)
646 GMarkupParseContext *context;
647 GSList *list = NULL;
649 context = g_markup_parse_context_new(&tb_editor_xml_parser, 0, &list, NULL);
650 g_markup_parse_context_parse(context, buffer, length, error);
651 g_markup_parse_context_free(context);
653 return list;
657 static void tb_editor_set_item_values(const gchar *name, GtkListStore *store, GtkTreeIter *iter)
659 gchar *icon = NULL;
660 gchar *label = NULL;
661 gchar *label_clean = NULL;
662 GtkAction *action;
664 action = gtk_action_group_get_action(group, name);
665 if (action == NULL)
667 if (utils_str_equal(name, TB_EDITOR_SEPARATOR))
668 label_clean = g_strdup(TB_EDITOR_SEPARATOR_LABEL);
669 else
670 return;
672 else
674 g_object_get(action, "icon-name", &icon, NULL);
675 if (icon == NULL)
676 g_object_get(action, "stock-id", &icon, NULL);
678 g_object_get(action, "label", &label, NULL);
679 if (label != NULL)
680 label_clean = utils_str_remove_chars(g_strdup(label), "_");
683 gtk_list_store_set(store, iter,
684 TB_EDITOR_COL_ACTION, name,
685 TB_EDITOR_COL_LABEL, label_clean,
686 TB_EDITOR_COL_ICON, icon,
687 -1);
689 g_free(icon);
690 g_free(label);
691 g_free(label_clean);
695 static void tb_editor_scroll_to_iter(GtkTreeView *treeview, GtkTreeIter *iter)
697 GtkTreePath *path = gtk_tree_model_get_path(gtk_tree_view_get_model(treeview), iter);
698 gtk_tree_view_scroll_to_cell(treeview, path, NULL, TRUE, 0.5, 0.0);
699 gtk_tree_path_free(path);
703 static void tb_editor_free_path(TBEditorWidget *tbw)
705 if (tbw->last_drag_path != NULL)
707 gtk_tree_path_free(tbw->last_drag_path);
708 tbw->last_drag_path = NULL;
713 static void tb_editor_btn_remove_clicked_cb(GtkWidget *button, TBEditorWidget *tbw)
715 GtkTreeModel *model_used;
716 GtkTreeSelection *selection_used;
717 GtkTreeIter iter_used, iter_new;
718 gchar *action_name;
720 selection_used = gtk_tree_view_get_selection(tbw->tree_used);
721 if (gtk_tree_selection_get_selected(selection_used, &model_used, &iter_used))
723 gtk_tree_model_get(model_used, &iter_used, TB_EDITOR_COL_ACTION, &action_name, -1);
724 if (gtk_list_store_remove(tbw->store_used, &iter_used))
725 gtk_tree_selection_select_iter(selection_used, &iter_used);
727 if (! utils_str_equal(action_name, TB_EDITOR_SEPARATOR))
729 gtk_list_store_append(tbw->store_available, &iter_new);
730 tb_editor_set_item_values(action_name, tbw->store_available, &iter_new);
731 tb_editor_scroll_to_iter(tbw->tree_available, &iter_new);
734 g_free(action_name);
739 static void tb_editor_btn_add_clicked_cb(GtkWidget *button, TBEditorWidget *tbw)
741 GtkTreeModel *model_available;
742 GtkTreeSelection *selection_available, *selection_used;
743 GtkTreeIter iter_available, iter_new, iter_selected;
744 gchar *action_name;
746 selection_available = gtk_tree_view_get_selection(tbw->tree_available);
747 if (gtk_tree_selection_get_selected(selection_available, &model_available, &iter_available))
749 gtk_tree_model_get(model_available, &iter_available,
750 TB_EDITOR_COL_ACTION, &action_name, -1);
751 if (! utils_str_equal(action_name, TB_EDITOR_SEPARATOR))
753 if (gtk_list_store_remove(tbw->store_available, &iter_available))
754 gtk_tree_selection_select_iter(selection_available, &iter_available);
757 selection_used = gtk_tree_view_get_selection(tbw->tree_used);
758 if (gtk_tree_selection_get_selected(selection_used, NULL, &iter_selected))
759 gtk_list_store_insert_before(tbw->store_used, &iter_new, &iter_selected);
760 else
761 gtk_list_store_append(tbw->store_used, &iter_new);
763 tb_editor_set_item_values(action_name, tbw->store_used, &iter_new);
764 tb_editor_scroll_to_iter(tbw->tree_used, &iter_new);
766 g_free(action_name);
771 static gboolean tb_editor_drag_motion_cb(GtkWidget *widget, GdkDragContext *drag_context,
772 gint x, gint y, guint ltime, TBEditorWidget *tbw)
774 if (tbw->last_drag_path != NULL)
775 gtk_tree_path_free(tbw->last_drag_path);
776 gtk_tree_view_get_drag_dest_row(GTK_TREE_VIEW(widget),
777 &(tbw->last_drag_path), &(tbw->last_drag_pos));
779 return FALSE;
783 static void tb_editor_drag_data_get_cb(GtkWidget *widget, GdkDragContext *context,
784 GtkSelectionData *data, guint info, guint ltime,
785 TBEditorWidget *tbw)
787 GtkTreeIter iter;
788 GtkTreeSelection *selection;
789 GtkTreeModel *model;
790 GdkAtom atom;
791 gchar *name;
793 selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(widget));
794 if (! gtk_tree_selection_get_selected(selection, &model, &iter))
795 return;
797 gtk_tree_model_get(model, &iter, TB_EDITOR_COL_ACTION, &name, -1);
798 if (G_UNLIKELY(EMPTY(name)))
800 g_free(name);
801 return;
804 atom = gdk_atom_intern(tb_editor_dnd_targets[0].target, FALSE);
805 gtk_selection_data_set(data, atom, 8, (guchar*) name, strlen(name));
807 g_free(name);
809 tbw->drag_source = widget;
813 static void tb_editor_drag_data_rcvd_cb(GtkWidget *widget, GdkDragContext *context,
814 gint x, gint y, GtkSelectionData *data, guint info,
815 guint ltime, TBEditorWidget *tbw)
817 GtkTreeView *tree = GTK_TREE_VIEW(widget);
818 gboolean del = FALSE;
820 if (gtk_selection_data_get_length(data) >= 0 && gtk_selection_data_get_format(data) == 8)
822 gboolean is_sep;
823 gchar *text = NULL;
825 text = (gchar*) gtk_selection_data_get_data(data);
826 is_sep = utils_str_equal(text, TB_EDITOR_SEPARATOR);
827 /* If the source of the action is equal to the target, we do just re-order and so need
828 * to delete the separator to get it moved, not just copied. */
829 if (is_sep && widget == tbw->drag_source)
830 is_sep = FALSE;
832 if (tree != tbw->tree_available || ! is_sep)
834 GtkTreeIter iter, iter_before, *iter_before_ptr;
835 GtkListStore *store = GTK_LIST_STORE(gtk_tree_view_get_model(tree));
837 if (tbw->last_drag_path != NULL)
839 gtk_tree_model_get_iter(GTK_TREE_MODEL(store), &iter_before, tbw->last_drag_path);
841 if (gtk_list_store_iter_is_valid(store, &iter_before))
842 iter_before_ptr = &iter_before;
843 else
844 iter_before_ptr = NULL;
846 if (tbw->last_drag_pos == GTK_TREE_VIEW_DROP_BEFORE ||
847 tbw->last_drag_pos == GTK_TREE_VIEW_DROP_INTO_OR_BEFORE)
848 gtk_list_store_insert_before(store, &iter, iter_before_ptr);
849 else
850 gtk_list_store_insert_after(store, &iter, iter_before_ptr);
852 else
853 gtk_list_store_append(store, &iter);
855 tb_editor_set_item_values(text, store, &iter);
856 tb_editor_scroll_to_iter(tree, &iter);
858 if (tree != tbw->tree_used || ! is_sep)
859 del = TRUE;
862 tbw->drag_source = NULL; /* reset the value just to be sure */
863 tb_editor_free_path(tbw);
864 gtk_drag_finish(context, TRUE, del, ltime);
868 static gboolean tb_editor_foreach_used(GtkTreeModel *model, GtkTreePath *path,
869 GtkTreeIter *iter, gpointer data)
871 gchar *action_name;
873 gtk_tree_model_get(model, iter, TB_EDITOR_COL_ACTION, &action_name, -1);
875 if (utils_str_equal(action_name, TB_EDITOR_SEPARATOR))
876 g_string_append_printf(data, "\t\t<separator/>\n");
877 else if (G_LIKELY(!EMPTY(action_name)))
878 g_string_append_printf(data, "\t\t<toolitem action='%s' />\n", action_name);
880 g_free(action_name);
881 return FALSE;
885 static void tb_editor_write_markup(TBEditorWidget *tbw)
887 /* <ui> must be the first tag, otherwise gtk_ui_manager_add_ui_from_string() will fail. */
888 const gchar *template = "<ui>\n<!--\n\
889 This is Geany's toolbar UI definition.\nThe DTD can be found at \n\
890 https://docs.gtk.org/gtk3/class.UIManager.html.\n\n\
891 You can re-order all items and freely add and remove available actions.\n\
892 You cannot add new actions which are not listed in the documentation.\n\
893 Everything you add or change must be inside the /ui/toolbar/ path.\n\n\
894 For changes to take effect, you need to restart Geany. Alternatively you can use the toolbar\n\
895 editor in Geany.\n\n\
896 A list of available actions can be found in the documentation included with Geany or\n\
897 at https://www.geany.org/manual/current/index.html#customizing-the-toolbar.\n-->\n\
898 \t<toolbar name='GeanyToolbar'>\n";
899 gchar *filename;
900 GString *str = g_string_new(template);
902 gtk_tree_model_foreach(GTK_TREE_MODEL(tbw->store_used), tb_editor_foreach_used, str);
904 g_string_append(str, "\n\t</toolbar>\n</ui>\n");
906 toolbar_reload(str->str);
908 filename = g_build_filename(app->configdir, "ui_toolbar.xml", NULL);
909 utils_write_file(filename, str->str);
910 g_free(filename);
912 g_string_free(str, TRUE);
916 static void tb_editor_available_items_changed_cb(GtkTreeModel *model, GtkTreePath *arg1,
917 GtkTreeIter *arg2, TBEditorWidget *tbw)
919 tb_editor_write_markup(tbw);
923 static void tb_editor_available_items_deleted_cb(GtkTreeModel *model, GtkTreePath *arg1,
924 TBEditorWidget *tbw)
926 tb_editor_write_markup(tbw);
930 static TBEditorWidget *tb_editor_create_dialog(GtkWindow *parent)
932 GtkWidget *dialog, *vbox, *hbox, *vbox_buttons, *button_add, *button_remove;
933 GtkWidget *swin_available, *swin_used, *tree_available, *tree_used, *label;
934 GtkCellRenderer *text_renderer, *icon_renderer;
935 GtkTreeViewColumn *column;
936 TBEditorWidget *tbw = g_new(TBEditorWidget, 1);
938 if (parent == NULL)
939 parent = GTK_WINDOW(main_widgets.window);
941 dialog = gtk_dialog_new_with_buttons(_("Customize Toolbar"),
942 parent,
943 GTK_DIALOG_DESTROY_WITH_PARENT,
944 GTK_STOCK_CLOSE, GTK_RESPONSE_CLOSE, NULL);
945 vbox = ui_dialog_vbox_new(GTK_DIALOG(dialog));
946 gtk_box_set_spacing(GTK_BOX(vbox), 6);
947 gtk_widget_set_name(dialog, "GeanyDialog");
948 gtk_window_set_default_size(GTK_WINDOW(dialog), -1, 400);
949 gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_CLOSE);
951 tbw->store_available = gtk_list_store_new(TB_EDITOR_COLS_MAX,
952 G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING);
953 tbw->store_used = gtk_list_store_new(TB_EDITOR_COLS_MAX,
954 G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING);
956 label = gtk_label_new(
957 _("Select items to be displayed on the toolbar. Items can be reordered by drag and drop."));
958 gtk_misc_set_alignment(GTK_MISC(label), 0.0, 0.5);
960 tree_available = gtk_tree_view_new();
961 gtk_tree_view_set_model(GTK_TREE_VIEW(tree_available), GTK_TREE_MODEL(tbw->store_available));
962 gtk_tree_view_set_rules_hint(GTK_TREE_VIEW(tree_available), TRUE);
963 gtk_tree_sortable_set_sort_column_id(
964 GTK_TREE_SORTABLE(tbw->store_available), TB_EDITOR_COL_LABEL, GTK_SORT_ASCENDING);
966 icon_renderer = gtk_cell_renderer_pixbuf_new();
967 column = gtk_tree_view_column_new_with_attributes(
968 NULL, icon_renderer, "stock-id", TB_EDITOR_COL_ICON, NULL);
969 gtk_tree_view_append_column(GTK_TREE_VIEW(tree_available), column);
971 text_renderer = gtk_cell_renderer_text_new();
972 column = gtk_tree_view_column_new_with_attributes(
973 _("Available Items"), text_renderer, "text", TB_EDITOR_COL_LABEL, NULL);
974 gtk_tree_view_append_column(GTK_TREE_VIEW(tree_available), column);
976 swin_available = gtk_scrolled_window_new(NULL, NULL);
977 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(swin_available),
978 GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
979 gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(swin_available), GTK_SHADOW_IN);
980 gtk_container_add(GTK_CONTAINER(swin_available), tree_available);
982 tree_used = gtk_tree_view_new();
983 gtk_tree_view_set_model(GTK_TREE_VIEW(tree_used), GTK_TREE_MODEL(tbw->store_used));
984 gtk_tree_view_set_rules_hint(GTK_TREE_VIEW(tree_used), TRUE);
985 gtk_tree_view_set_reorderable(GTK_TREE_VIEW(tree_used), TRUE);
987 icon_renderer = gtk_cell_renderer_pixbuf_new();
988 column = gtk_tree_view_column_new_with_attributes(
989 NULL, icon_renderer, "stock-id", TB_EDITOR_COL_ICON, NULL);
990 gtk_tree_view_append_column(GTK_TREE_VIEW(tree_used), column);
992 text_renderer = gtk_cell_renderer_text_new();
993 column = gtk_tree_view_column_new_with_attributes(
994 _("Displayed Items"), text_renderer, "text", TB_EDITOR_COL_LABEL, NULL);
995 gtk_tree_view_append_column(GTK_TREE_VIEW(tree_used), column);
997 swin_used = gtk_scrolled_window_new(NULL, NULL);
998 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(swin_used),
999 GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
1000 gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(swin_used), GTK_SHADOW_IN);
1001 gtk_container_add(GTK_CONTAINER(swin_used), tree_used);
1003 /* drag'n'drop */
1004 gtk_tree_view_enable_model_drag_source(GTK_TREE_VIEW(tree_available), GDK_BUTTON1_MASK,
1005 tb_editor_dnd_targets, tb_editor_dnd_targets_len, GDK_ACTION_MOVE);
1006 gtk_tree_view_enable_model_drag_dest(GTK_TREE_VIEW(tree_available),
1007 tb_editor_dnd_targets, tb_editor_dnd_targets_len, GDK_ACTION_MOVE);
1008 g_signal_connect(tree_available, "drag-data-get",
1009 G_CALLBACK(tb_editor_drag_data_get_cb), tbw);
1010 g_signal_connect(tree_available, "drag-data-received",
1011 G_CALLBACK(tb_editor_drag_data_rcvd_cb), tbw);
1012 g_signal_connect(tree_available, "drag-motion",
1013 G_CALLBACK(tb_editor_drag_motion_cb), tbw);
1015 gtk_tree_view_enable_model_drag_source(GTK_TREE_VIEW(tree_used), GDK_BUTTON1_MASK,
1016 tb_editor_dnd_targets, tb_editor_dnd_targets_len, GDK_ACTION_MOVE);
1017 gtk_tree_view_enable_model_drag_dest(GTK_TREE_VIEW(tree_used),
1018 tb_editor_dnd_targets, tb_editor_dnd_targets_len, GDK_ACTION_MOVE);
1019 g_signal_connect(tree_used, "drag-data-get",
1020 G_CALLBACK(tb_editor_drag_data_get_cb), tbw);
1021 g_signal_connect(tree_used, "drag-data-received",
1022 G_CALLBACK(tb_editor_drag_data_rcvd_cb), tbw);
1023 g_signal_connect(tree_used, "drag-motion",
1024 G_CALLBACK(tb_editor_drag_motion_cb), tbw);
1027 button_add = ui_button_new_with_image(GTK_STOCK_GO_FORWARD, NULL);
1028 button_remove = ui_button_new_with_image(GTK_STOCK_GO_BACK, NULL);
1029 g_signal_connect(button_add, "clicked", G_CALLBACK(tb_editor_btn_add_clicked_cb), tbw);
1030 g_signal_connect(button_remove, "clicked", G_CALLBACK(tb_editor_btn_remove_clicked_cb), tbw);
1032 vbox_buttons = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6);
1033 /* FIXME this is a little hack'ish, any better ideas? */
1034 gtk_box_pack_start(GTK_BOX(vbox_buttons), gtk_label_new(""), TRUE, TRUE, 0);
1035 gtk_box_pack_start(GTK_BOX(vbox_buttons), button_add, FALSE, FALSE, 0);
1036 gtk_box_pack_start(GTK_BOX(vbox_buttons), button_remove, FALSE, FALSE, 0);
1037 gtk_box_pack_start(GTK_BOX(vbox_buttons), gtk_label_new(""), TRUE, TRUE, 0);
1039 hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
1040 gtk_box_pack_start(GTK_BOX(hbox), swin_available, TRUE, TRUE, 0);
1041 gtk_box_pack_start(GTK_BOX(hbox), vbox_buttons, FALSE, FALSE, 0);
1042 gtk_box_pack_start(GTK_BOX(hbox), swin_used, TRUE, TRUE, 0);
1044 gtk_box_pack_start(GTK_BOX(vbox), label, FALSE, FALSE, 6);
1045 gtk_box_pack_start(GTK_BOX(vbox), hbox, TRUE, TRUE, 0);
1047 gtk_widget_show_all(vbox);
1049 g_object_unref(tbw->store_available);
1050 g_object_unref(tbw->store_used);
1052 tbw->dialog = dialog;
1053 tbw->tree_available = GTK_TREE_VIEW(tree_available);
1054 tbw->tree_used = GTK_TREE_VIEW(tree_used);
1056 tbw->last_drag_path = NULL;
1058 return tbw;
1062 void toolbar_configure(GtkWindow *parent)
1064 gchar *markup;
1065 GSList *sl, *used_items;
1066 GList *l, *all_items;
1067 GtkTreePath *path;
1068 TBEditorWidget *tbw;
1070 /* read the current active toolbar items */
1071 markup = gtk_ui_manager_get_ui(uim);
1072 used_items = tb_editor_parse_ui(markup, -1, NULL);
1073 g_free(markup);
1075 /* get all available actions */
1076 all_items = gtk_action_group_list_actions(group);
1078 /* create the GUI */
1079 tbw = tb_editor_create_dialog(parent);
1081 /* fill the stores */
1082 gtk_list_store_insert_with_values(tbw->store_available, NULL, -1,
1083 TB_EDITOR_COL_ACTION, TB_EDITOR_SEPARATOR,
1084 TB_EDITOR_COL_LABEL, TB_EDITOR_SEPARATOR_LABEL,
1085 -1);
1086 foreach_list(l, all_items)
1088 const gchar *name = gtk_action_get_name(l->data);
1090 if (g_slist_find_custom(used_items, name, (GCompareFunc) strcmp) == NULL)
1092 GtkTreeIter iter;
1094 gtk_list_store_append(tbw->store_available, &iter);
1095 tb_editor_set_item_values(name, tbw->store_available, &iter);
1098 foreach_slist(sl, used_items)
1100 GtkTreeIter iter;
1102 gtk_list_store_append(tbw->store_used, &iter);
1103 tb_editor_set_item_values(sl->data, tbw->store_used, &iter);
1105 /* select first item */
1106 path = gtk_tree_path_new_from_string("0");
1107 gtk_tree_selection_select_path(gtk_tree_view_get_selection(tbw->tree_used), path);
1108 gtk_tree_path_free(path);
1110 /* connect the changed signals after populating the store */
1111 g_signal_connect(tbw->store_used, "row-changed",
1112 G_CALLBACK(tb_editor_available_items_changed_cb), tbw);
1113 g_signal_connect(tbw->store_used, "row-deleted",
1114 G_CALLBACK(tb_editor_available_items_deleted_cb), tbw);
1116 /* run it */
1117 gtk_dialog_run(GTK_DIALOG(tbw->dialog));
1119 gtk_widget_destroy(tbw->dialog);
1121 g_slist_free_full(used_items, g_free);
1122 g_list_free(all_items);
1123 tb_editor_free_path(tbw);
1124 g_free(tbw);