Merge pull request #3823 from ntrel/msg-tree-search
[geany-mirror.git] / src / sidebar.c
blob435e313947f12b74b62ee59cc9ce10738fe709d3
1 /*
2 * sidebar.c - this file is part of Geany, a fast and lightweight IDE
4 * Copyright 2005 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.
22 * Sidebar related code for the Symbol list and Open files GtkTreeViews.
25 #ifdef HAVE_CONFIG_H
26 # include "config.h"
27 #endif
29 #include "sidebar.h"
31 #include "app.h"
32 #include "callbacks.h" /* FIXME: for ignore_callback */
33 #include "documentprivate.h"
34 #include "filetypesprivate.h"
35 #include "geanyobject.h"
36 #include "keyfile.h"
37 #include "navqueue.h"
38 #include "stash.h"
39 #include "support.h"
40 #include "symbols.h"
41 #include "ui_utils.h"
42 #include "utils.h"
43 #include "keybindings.h"
45 #include <string.h>
47 #include <gdk/gdkkeysyms.h>
50 SidebarTreeviews tv = {NULL, NULL, NULL};
51 /* while typeahead searching, editor should not get focus */
52 static gboolean may_steal_focus = FALSE;
54 static struct
56 GtkWidget *close;
57 GtkWidget *save;
58 GtkWidget *reload;
59 GtkWidget *show_paths[OPENFILES_PATHS_COUNT];
60 GtkWidget *find_in_files;
61 GtkWidget *expand_all;
62 GtkWidget *collapse_all;
64 doc_items;
66 enum
68 TREEVIEW_SYMBOL = 0,
69 TREEVIEW_OPENFILES
72 enum
74 OPENFILES_ACTION_REMOVE = 0,
75 OPENFILES_ACTION_SAVE,
76 OPENFILES_ACTION_RELOAD
79 static GtkTreeStore *store_openfiles;
80 static GtkWidget *openfiles_popup_menu;
81 static GtkWidget *tag_window; /* scrolled window that holds the symbol list GtkTreeView */
83 /* callback prototypes */
84 static void on_openfiles_document_action(GtkMenuItem *menuitem, gpointer user_data);
85 static gboolean sidebar_button_press_cb(GtkWidget *widget, GdkEventButton *event,
86 gpointer user_data);
87 static gboolean sidebar_key_press_cb(GtkWidget *widget, GdkEventKey *event,
88 gpointer user_data);
89 static void on_list_document_activate(GtkCheckMenuItem *item, gpointer user_data);
90 static void on_list_symbol_activate(GtkCheckMenuItem *item, gpointer user_data);
91 static void documents_menu_update(GtkTreeSelection *selection);
92 static void sidebar_tabs_show_hide(GtkNotebook *notebook, GtkWidget *child,
93 guint page_num, gpointer data);
96 /* the prepare_* functions are document-related, but I think they fit better here than in document.c */
97 static void prepare_taglist(GtkWidget *tree, GtkTreeStore *store)
99 GtkCellRenderer *text_renderer, *icon_renderer;
100 GtkTreeViewColumn *column;
101 GtkTreeSelection *selection;
103 text_renderer = gtk_cell_renderer_text_new();
104 icon_renderer = gtk_cell_renderer_pixbuf_new();
105 column = gtk_tree_view_column_new();
107 gtk_tree_view_column_pack_start(column, icon_renderer, FALSE);
108 gtk_tree_view_column_set_attributes(column, icon_renderer, "pixbuf", SYMBOLS_COLUMN_ICON, NULL);
109 g_object_set(icon_renderer, "xalign", 0.0, NULL);
111 gtk_tree_view_column_pack_start(column, text_renderer, TRUE);
112 gtk_tree_view_column_set_attributes(column, text_renderer, "text", SYMBOLS_COLUMN_NAME, NULL);
113 g_object_set(text_renderer, "yalign", 0.5, NULL);
114 gtk_tree_view_column_set_title(column, _("Symbols"));
116 gtk_tree_view_append_column(GTK_TREE_VIEW(tree), column);
117 gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(tree), FALSE);
119 ui_widget_modify_font_from_string(tree, interface_prefs.tagbar_font);
121 gtk_tree_view_set_model(GTK_TREE_VIEW(tree), GTK_TREE_MODEL(store));
122 g_object_unref(store);
124 g_signal_connect(tree, "button-press-event",
125 G_CALLBACK(sidebar_button_press_cb), NULL);
126 g_signal_connect(tree, "key-press-event",
127 G_CALLBACK(sidebar_key_press_cb), NULL);
129 gtk_tree_view_set_show_expanders(GTK_TREE_VIEW(tree), interface_prefs.show_symbol_list_expanders);
130 if (! interface_prefs.show_symbol_list_expanders)
131 gtk_tree_view_set_level_indentation(GTK_TREE_VIEW(tree), 10);
132 /* Tooltips */
133 ui_tree_view_set_tooltip_text_column(GTK_TREE_VIEW(tree), SYMBOLS_COLUMN_TOOLTIP);
135 /* selection handling */
136 selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(tree));
137 gtk_tree_selection_set_mode(selection, GTK_SELECTION_SINGLE);
138 /* callback for changed selection not necessary, will be handled by button-press-event */
142 static gboolean
143 on_default_tag_tree_button_press_event(GtkWidget *widget, GdkEventButton *event,
144 gpointer user_data)
146 if (event->button == 3)
148 gtk_menu_popup_at_pointer(GTK_MENU(tv.popup_taglist), (GdkEvent *) event);
149 return TRUE;
151 return FALSE;
155 static void create_default_tag_tree(void)
157 GtkScrolledWindow *scrolled_window = GTK_SCROLLED_WINDOW(tag_window);
158 GtkWidget *label;
160 /* default_tag_tree is a GtkViewPort with a GtkLabel inside it */
161 tv.default_tag_tree = gtk_viewport_new(
162 gtk_scrolled_window_get_hadjustment(scrolled_window),
163 gtk_scrolled_window_get_vadjustment(scrolled_window));
164 gtk_viewport_set_shadow_type(GTK_VIEWPORT(tv.default_tag_tree), GTK_SHADOW_NONE);
165 label = gtk_label_new(_("No symbols found"));
166 gtk_misc_set_alignment(GTK_MISC(label), 0.1f, 0.01f);
167 gtk_container_add(GTK_CONTAINER(tv.default_tag_tree), label);
168 gtk_widget_show_all(tv.default_tag_tree);
169 g_signal_connect(tv.default_tag_tree, "button-press-event",
170 G_CALLBACK(on_default_tag_tree_button_press_event), NULL);
171 g_object_ref((gpointer)tv.default_tag_tree); /* to hold it after removing */
175 /* update = rescan the tags for doc->filename */
176 void sidebar_update_tag_list(GeanyDocument *doc, gboolean update)
178 GtkWidget *child = gtk_bin_get_child(GTK_BIN(tag_window));
180 g_return_if_fail(doc == NULL || doc->is_valid);
182 if (update && doc != NULL)
183 doc->priv->tag_tree_dirty = TRUE;
185 if (gtk_notebook_get_current_page(GTK_NOTEBOOK(main_widgets.sidebar_notebook)) != TREEVIEW_SYMBOL)
186 return; /* don't bother updating symbol tree if we don't see it */
188 /* changes the tree view to the given one, trying not to do useless changes */
189 #define CHANGE_TREE(new_child) \
190 G_STMT_START { \
191 /* only change the tag tree if it's actually not the same (to avoid flickering) and if \
192 * it's the one of the current document (to avoid problems when e.g. reloading \
193 * configuration files */ \
194 if (child != new_child && doc == document_get_current()) \
196 if (child) \
197 gtk_container_remove(GTK_CONTAINER(tag_window), child); \
198 gtk_container_add(GTK_CONTAINER(tag_window), new_child); \
200 } G_STMT_END
202 if (tv.default_tag_tree == NULL)
203 create_default_tag_tree();
205 /* show default empty tag tree if there are no tags */
206 if (doc == NULL || doc->file_type == NULL || ! filetype_has_tags(doc->file_type))
208 CHANGE_TREE(tv.default_tag_tree);
209 return;
212 if (doc->priv->tag_tree_dirty)
213 { /* updating the tag list in the left tag window */
214 if (doc->priv->tag_tree == NULL)
216 doc->priv->tag_store = gtk_tree_store_new(
217 SYMBOLS_N_COLUMNS, GDK_TYPE_PIXBUF, G_TYPE_STRING, TM_TYPE_TAG, G_TYPE_STRING);
218 doc->priv->tag_tree = gtk_tree_view_new();
219 prepare_taglist(doc->priv->tag_tree, doc->priv->tag_store);
220 gtk_widget_show(doc->priv->tag_tree);
221 g_object_ref((gpointer)doc->priv->tag_tree); /* to hold it after removing */
224 doc->has_tags = symbols_recreate_tag_list(doc, SYMBOLS_SORT_USE_PREVIOUS);
225 doc->priv->tag_tree_dirty = FALSE;
228 if (doc->has_tags)
230 CHANGE_TREE(doc->priv->tag_tree);
232 else
234 CHANGE_TREE(tv.default_tag_tree);
237 #undef CHANGE_TREE
241 /* cleverly sorts documents by their short name */
242 static gint documents_sort_func(GtkTreeModel *model, GtkTreeIter *iter_a,
243 GtkTreeIter *iter_b, gpointer data)
245 gchar *name_a, *name_b;
246 GeanyDocument *doc_a, *doc_b;
247 gint cmp;
249 gtk_tree_model_get(model, iter_a, DOCUMENTS_SHORTNAME, &name_a, DOCUMENTS_DOCUMENT, &doc_a, -1);
250 gtk_tree_model_get(model, iter_b, DOCUMENTS_SHORTNAME, &name_b, DOCUMENTS_DOCUMENT, &doc_b, -1);
252 /* sort dirs after files (within a directory node) */
253 if (!doc_a && doc_b)
254 cmp = 1;
255 else if (doc_a && !doc_b)
256 cmp = -1;
257 else
259 gchar *key_a, *key_b;
260 /* g_utf8_collate_key_for_filename() seems to ignore ~. As a result, ~/dev <=> /data
261 * compares as /dev > /data while ~/dev <=> /etc compares as /dev < /etc. Thus, for sorting
262 * purposes, treat leading ~ as . which is documented to be special-cased to sort before
263 * anything else. The side effect, that documents under ~ are always first is actually
264 * welcome.
266 name_a[0] = name_a[0] == '~' ? '.' : name_a[0];
267 name_b[0] = name_b[0] == '~' ? '.' : name_b[0];
268 key_a = g_utf8_collate_key_for_filename(name_a, -1);
269 key_b = g_utf8_collate_key_for_filename(name_b, -1);
270 cmp = strcmp(key_a, key_b);
271 g_free(key_b);
272 g_free(key_a);
275 g_free(name_b);
276 g_free(name_a);
278 return cmp;
281 GEANY_EXPORT_SYMBOL
282 GtkTreeStore *sidebar_create_store_openfiles(void)
284 GtkTreeSortable *sortable;
285 GtkTreeStore *store;
286 /* store the icon and the short filename to show, and the index as reference,
287 * the colour (black/red/green) and the full name for the tooltip */
288 store = gtk_tree_store_new(6, G_TYPE_ICON, G_TYPE_STRING,
289 G_TYPE_POINTER, GDK_TYPE_COLOR, G_TYPE_STRING, G_TYPE_BOOLEAN);
291 /* sort opened filenames in the store_openfiles treeview */
292 sortable = GTK_TREE_SORTABLE(GTK_TREE_MODEL(store));
293 gtk_tree_sortable_set_sort_func(sortable, DOCUMENTS_SHORTNAME, documents_sort_func, NULL, NULL);
294 gtk_tree_sortable_set_sort_column_id(sortable, DOCUMENTS_SHORTNAME, GTK_SORT_ASCENDING);
296 store_openfiles = store;
297 return store;
301 static void store_fold_recurse(GtkTreeView *view,
302 GtkTreeIter *iter,
303 GtkTreeModel *model)
305 GeanyDocument *doc;
306 gboolean fold, valid;
307 GtkTreePath *path;
308 GtkTreeIter child_iter;
310 gtk_tree_model_get(model, iter, DOCUMENTS_DOCUMENT, &doc, -1);
311 if (doc) /* document rows are not foldable */
312 return;
314 path = gtk_tree_model_get_path(model, iter);
315 fold = !gtk_tree_view_row_expanded(view, path);
316 gtk_tree_store_set(GTK_TREE_STORE(model), iter, DOCUMENTS_FOLD, fold, -1);
317 gtk_tree_path_free(path);
319 /* After storing the fold state for *this* row, recursively do the same for its children.
320 * We need do do this only for expanded children because all children of folded rows are
321 * folded as well.
323 if (fold)
324 return;
326 valid = gtk_tree_model_iter_children(model, &child_iter, iter);
327 while (valid)
329 store_fold_recurse(view, &child_iter, model);
330 valid = gtk_tree_model_iter_next(model, &child_iter);
335 static gboolean on_row_expand(GtkTreeView *view,
336 GtkTreeIter *iter,
337 GtkTreePath *path,
338 gpointer user_data)
340 GtkTreeModel *model;
342 model = gtk_tree_view_get_model(view);
343 gtk_tree_store_set(GTK_TREE_STORE(model), iter, DOCUMENTS_FOLD, FALSE, -1);
344 return FALSE;
348 static gboolean on_row_collapse(GtkTreeView *view,
349 GtkTreeIter *iter,
350 GtkTreePath *path,
351 gpointer user_data)
353 GtkTreeModel *model;
354 GtkTreeIter child_iter;
355 gboolean valid;
357 model = gtk_tree_view_get_model(view);
358 gtk_tree_store_set(GTK_TREE_STORE(model), iter, DOCUMENTS_FOLD, TRUE, -1);
360 valid = gtk_tree_model_iter_children(model, &child_iter, iter);
361 while (valid)
363 store_fold_recurse(view, &child_iter, model);
364 valid = gtk_tree_model_iter_next(model, &child_iter);
367 return FALSE;
371 static void on_row_expanded(GtkTreeView *view,
372 GtkTreeIter *iter,
373 GtkTreePath *path_,
374 gpointer user_data)
376 GtkTreeIter child_iter;
377 GtkTreeModel *model;
378 GtkTreePath *path;
379 gboolean valid;
380 GeanyDocument *doc;
382 model = gtk_tree_view_get_model(view);
384 valid = gtk_tree_model_iter_children(model, &child_iter, iter);
385 while (valid)
387 gboolean fold;
389 gtk_tree_model_get(model, &child_iter, DOCUMENTS_DOCUMENT, &doc, DOCUMENTS_FOLD, &fold, -1);
390 path = gtk_tree_model_get_path(model, &child_iter);
392 if (!doc && !fold)
393 gtk_tree_view_expand_row(view, path, FALSE);
395 valid = gtk_tree_model_iter_next(model, &child_iter);
396 gtk_tree_path_free(path);
401 /* does some preparing things to the open files list widget */
402 static void prepare_openfiles(void)
404 GtkCellRenderer *icon_renderer;
405 GtkCellRenderer *text_renderer;
406 GtkTreeViewColumn *column;
407 GtkTreeSelection *selection;
409 tv.tree_openfiles = ui_lookup_widget(main_widgets.window, "treeview6");
411 sidebar_create_store_openfiles();
413 gtk_tree_view_set_model(GTK_TREE_VIEW(tv.tree_openfiles), GTK_TREE_MODEL(store_openfiles));
415 /* These two implement "remember fold state of rows when their parents are folded". Normally
416 * GTK does not remember the fold state and can only expand all or no children when
417 * expanding a row. Maybe this can be useful for other tree views as well?
419 g_signal_connect_after(GTK_TREE_VIEW(tv.tree_openfiles), "test-expand-row", G_CALLBACK(on_row_expand), NULL);
420 g_signal_connect_after(GTK_TREE_VIEW(tv.tree_openfiles), "test-collapse-row", G_CALLBACK(on_row_collapse), NULL);
421 g_signal_connect_after(GTK_TREE_VIEW(tv.tree_openfiles), "row-expanded", G_CALLBACK(on_row_expanded), NULL);
423 /* set policy settings for the scolledwindow around the treeview again, because glade
424 * doesn't keep the settings */
425 gtk_scrolled_window_set_policy(
426 GTK_SCROLLED_WINDOW(ui_lookup_widget(main_widgets.window, "scrolledwindow7")),
427 GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
429 icon_renderer = gtk_cell_renderer_pixbuf_new();
430 g_object_set(icon_renderer, "stock-size", GTK_ICON_SIZE_MENU, NULL);
431 text_renderer = gtk_cell_renderer_text_new();
432 g_object_set(text_renderer, "ellipsize", PANGO_ELLIPSIZE_MIDDLE, NULL);
433 column = gtk_tree_view_column_new();
434 gtk_tree_view_column_pack_start(column, icon_renderer, FALSE);
435 gtk_tree_view_column_set_attributes(column, icon_renderer, "gicon", DOCUMENTS_ICON, NULL);
436 gtk_tree_view_column_pack_start(column, text_renderer, TRUE);
437 gtk_tree_view_column_set_attributes(column, text_renderer, "text", DOCUMENTS_SHORTNAME,
438 "foreground-gdk", DOCUMENTS_COLOR, NULL);
439 gtk_tree_view_append_column(GTK_TREE_VIEW(tv.tree_openfiles), column);
440 gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(tv.tree_openfiles), FALSE);
442 gtk_tree_view_set_search_column(GTK_TREE_VIEW(tv.tree_openfiles),
443 DOCUMENTS_SHORTNAME);
445 ui_widget_modify_font_from_string(tv.tree_openfiles, interface_prefs.tagbar_font);
447 /* tooltips */
448 ui_tree_view_set_tooltip_text_column(GTK_TREE_VIEW(tv.tree_openfiles), DOCUMENTS_FILENAME);
450 /* selection handling */
451 selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(tv.tree_openfiles));
452 gtk_tree_selection_set_mode(selection, GTK_SELECTION_SINGLE);
453 g_object_unref(store_openfiles);
455 g_signal_connect(GTK_TREE_VIEW(tv.tree_openfiles), "button-press-event",
456 G_CALLBACK(sidebar_button_press_cb), NULL);
457 g_signal_connect(GTK_TREE_VIEW(tv.tree_openfiles), "key-press-event",
458 G_CALLBACK(sidebar_key_press_cb), NULL);
462 static gboolean utils_filename_has_prefix(const gchar *str, const gchar *prefix)
464 gchar *head = g_strndup(str, strlen(prefix));
465 gboolean ret = utils_filenamecmp(head, prefix) == 0;
467 g_free(head);
468 return ret;
472 static gchar *get_project_folder(const gchar *path)
474 gchar *project_base_path;
475 gchar *dirname = NULL;
476 const gchar *rest;
478 /* replace the project base path with the project name */
479 project_base_path = project_get_base_path();
481 if (project_base_path != NULL)
483 gsize len = strlen(project_base_path);
485 /* remove trailing separator so we can match base path exactly */
486 if (project_base_path[len-1] == G_DIR_SEPARATOR)
487 project_base_path[--len] = '\0';
489 /* check whether the dir name matches or uses the project base path */
490 if (utils_filename_has_prefix(path, project_base_path))
492 rest = path + len;
493 if (*rest == G_DIR_SEPARATOR || *rest == '\0')
495 dirname = g_strdup_printf("%s%s", app->project->name, rest);
498 g_free(project_base_path);
501 return dirname;
505 static gchar *get_doc_folder(const gchar *path)
507 gchar *dirname = get_project_folder(path);
508 const gchar *rest;
510 if (dirname == NULL)
512 const gchar *home_dir = g_get_home_dir();
513 gchar *tmp_dirname = g_strdup(path);
515 dirname = tmp_dirname;
516 /* If matches home dir, replace with tilde */
517 if (!EMPTY(home_dir) && utils_filename_has_prefix(dirname, home_dir))
519 rest = dirname + strlen(home_dir);
520 if (*rest == G_DIR_SEPARATOR || *rest == '\0')
522 dirname = g_strdup_printf("~%s", rest);
523 g_free(tmp_dirname);
528 return dirname;
532 static gchar *parent_dir_name(GtkTreeStore *tree, GtkTreeIter *parent, const gchar *path)
534 gsize parent_len = 0;
535 gchar *dirname;
536 gchar *pathname = NULL;
538 if (parent)
540 gchar *parent_dir;
541 GtkTreeModel *model = GTK_TREE_MODEL(tree);
543 gtk_tree_model_get(model, parent, DOCUMENTS_FILENAME, &parent_dir, -1);
544 if (parent_dir)
546 pathname = get_doc_folder(parent_dir);
547 parent_len = strlen(pathname) + 1;
548 g_free(parent_dir);
552 dirname = get_doc_folder(path);
553 if (parent_len)
555 gsize len;
556 dirname = get_doc_folder(path);
557 len = strlen(dirname);
558 /* Maybe parent is /home but dirname is ~ (after substitution from /home/user) */
559 if (pathname[0] == dirname[0])
560 memmove(dirname, dirname + parent_len, len - parent_len + 1);
563 g_free(pathname);
565 return dirname;
569 static void tree_copy_node(GtkTreeStore *tree, GtkTreeIter *new_node, GtkTreeIter *node, GtkTreeIter *parent_new)
571 GIcon *icon;
572 gchar *filename;
573 gchar *shortname;
574 GdkColor *color;
575 GeanyDocument *doc;
576 GtkTreeModel *model = GTK_TREE_MODEL(tree);
577 gboolean fold;
579 gtk_tree_store_append(tree, new_node, parent_new);
580 gtk_tree_model_get(model, node,
581 DOCUMENTS_ICON, &icon,
582 DOCUMENTS_SHORTNAME, &shortname,
583 DOCUMENTS_DOCUMENT, &doc,
584 DOCUMENTS_COLOR, &color,
585 DOCUMENTS_FILENAME, &filename,
586 DOCUMENTS_FOLD, &fold,
587 -1);
589 if (doc)
590 doc->priv->iter = *new_node;
591 else
592 SETPTR(shortname, parent_dir_name(tree, parent_new, filename));
594 gtk_tree_store_set(tree, new_node,
595 DOCUMENTS_ICON, icon,
596 DOCUMENTS_SHORTNAME, shortname,
597 DOCUMENTS_DOCUMENT, doc,
598 DOCUMENTS_COLOR, color,
599 DOCUMENTS_FILENAME, filename,
600 DOCUMENTS_FOLD, fold,
601 -1);
602 g_free(filename);
603 g_free(shortname);
604 if (color)
605 gdk_color_free(color);
609 /* Helper that implements the recursive part of tree_reparent() */
610 static void tree_reparent_recurse(GtkTreeStore *tree, GtkTreeIter *node, GtkTreeIter *parent_new, GtkTreeIter *new_node)
612 GtkTreeModel *model = GTK_TREE_MODEL(tree);
613 GtkTreeIter child;
615 /* Start by copying the node itself. It becomes parent_new for the children to be copied. */
616 tree_copy_node(tree, new_node, node, parent_new);
617 if (gtk_tree_model_iter_nth_child(model, &child, node, 0))
619 do {
620 GtkTreeIter new_child;
621 tree_reparent_recurse(tree, &child, new_node, &new_child);
623 while (gtk_tree_model_iter_next(model, &child));
629 * Copy node and all of its children to a new parent, and then remove the old node.
631 * It is done by reparenting the node itself to the new parent, creating a copy of it,
632 * and then recursively reparenting all children to the copy of the node.
634 * Finally, the new location will be written back to node so it's readily available,
635 * e.g. to unfold it.
636 * */
637 static void tree_reparent(GtkTreeStore *tree, GtkTreeIter *node, GtkTreeIter *parent_new)
639 GtkTreeIter new_node;
640 tree_reparent_recurse(tree, node, parent_new, &new_node);
641 gtk_tree_store_remove(tree, node);
642 *node = new_node;
646 static void tree_add_new_dir(GtkTreeStore *tree, GtkTreeIter *child, GtkTreeIter *parent, const gchar *file)
648 static GIcon *dir_icon = NULL;
649 gchar *dirname = parent_dir_name(tree, parent, file);
651 if (!dir_icon)
652 dir_icon = ui_get_mime_icon("inode/directory");
654 gtk_tree_store_append(tree, child, parent);
655 gtk_tree_store_set(tree, child,
656 DOCUMENTS_ICON, dir_icon,
657 DOCUMENTS_FILENAME, file,
658 DOCUMENTS_SHORTNAME, dirname,
659 DOCUMENTS_FOLD, TRUE, /* GTK inserts folded by default, caller may expand */
660 -1);
662 g_free(dirname);
667 * Returns the position of dir delimiter where paths don't match
668 * */
669 static guint pathcmp(const gchar *s1, const gchar *s2)
671 guint i = 0;
672 gchar *a;
673 gchar *b;
675 g_return_val_if_fail(s1 != NULL, 0);
676 g_return_val_if_fail(s2 != NULL, 0);
678 #ifdef G_OS_WIN32
679 a = utils_utf8_strdown(s1);
680 if (NULL == a)
681 return 0;
682 b = utils_utf8_strdown(s2);
683 if (NULL == b)
685 g_free(a);
686 return 0;
688 #else
689 a = (gchar*)s1;
690 b = (gchar*)s2;
691 #endif
693 while (a[i] && b[i] && a[i] == b[i])
694 i++;
695 if (a[i] == '\0' && b[i] == '\0')
696 return i; /* strings are equal: a/b/c == a/b/c */
697 if ((a[i] == '\0' && b[i] == G_DIR_SEPARATOR) ||
698 (b[i] == '\0' && a[i] == G_DIR_SEPARATOR))
699 return i; /* subdir case: a/b/c == a/b */
700 while (i > 0 && (a[i] != G_DIR_SEPARATOR || b[i] != G_DIR_SEPARATOR))
701 i--; /* last slash: a/b/boo == a/b/bar */
703 #ifdef G_OS_WIN32
704 g_free(a);
705 g_free(b);
706 #endif
708 return i;
712 typedef struct TreeForeachData {
713 gchar *needle;
714 gsize best_len;
715 gsize needle_len;
716 GtkTreeIter best_iter;
717 enum {
718 TREE_CASE_NONE,
719 TREE_CASE_EQUALS,
720 TREE_CASE_CHILD_OF,
721 TREE_CASE_PARENT_OF,
722 TREE_CASE_HAVE_SAME_PARENT
723 } best_case;
724 } TreeForeachData;
727 static gboolean tree_foreach_callback(GtkTreeModel *model,
728 GtkTreePath *path,
729 GtkTreeIter *iter,
730 gpointer user_data)
732 gchar *name;
733 gchar *dirname;
734 guint diff;
735 gsize name_len;
736 GeanyDocument *doc;
737 TreeForeachData *data = (TreeForeachData*) user_data;
739 gtk_tree_model_get(model, iter, DOCUMENTS_FILENAME, &name, DOCUMENTS_DOCUMENT, &doc, -1);
741 if (doc) /* skip documents */
742 goto finally;
744 dirname = get_doc_folder(name);
745 if (dirname)
746 SETPTR(name, dirname);
748 diff = pathcmp(name, data->needle);
749 name_len = strlen(name);
751 if (diff == 0)
752 goto finally;
754 if (data->best_len < diff)
756 gint best_case;
757 gboolean tree = interface_prefs.openfiles_path_mode == OPENFILES_PATHS_TREE;
759 /* there are four cases */
760 /* first case: exact match. File is from already opened dir */
761 if (name_len == diff && data->needle_len == name_len)
762 best_case = TREE_CASE_EQUALS;
763 /* second case: split current dir. File is from deeper level */
764 else if (name_len == diff && tree)
765 best_case = TREE_CASE_CHILD_OF;
766 /* third case: split parent dir. File is from one of existing level */
767 else if (data->needle_len == diff && tree)
768 best_case = TREE_CASE_PARENT_OF;
769 /* fourth case: both dirs have same parent */
770 else if (tree)
771 best_case = TREE_CASE_HAVE_SAME_PARENT;
772 else
773 goto finally;
774 data->best_len = diff;
775 data->best_case = best_case;
776 data->best_iter = *iter;
778 finally:
779 g_free(name);
780 return FALSE;
784 /* Returns TRUE if parent points to a newly added row,
785 * caller might want to expand the relevant rows in the tree view */
786 static gboolean get_parent_for_file(GtkTreeStore *tree, const gchar *file, GtkTreeIter *parent)
788 gchar *path;
789 GtkTreeIter iter;
790 gint name_diff = 0;
791 gboolean has_parent;
792 GtkTreeModel *model = GTK_TREE_MODEL(tree);
793 TreeForeachData data = {NULL, 0, 0, {0}, TREE_CASE_NONE};
794 gboolean new_row;
796 path = g_path_get_dirname(file);
798 /* find best opened dir */
799 data.needle = get_doc_folder(path);
800 data.needle_len = strlen(data.needle);
801 name_diff = strlen(path) - data.needle_len;
802 gtk_tree_model_foreach(model, tree_foreach_callback, (gpointer)&data);
804 switch (data.best_case)
806 case TREE_CASE_EQUALS:
808 *parent = data.best_iter;
809 /* dir already open */
810 new_row = FALSE;
811 break;
813 case TREE_CASE_CHILD_OF:
815 /* This dir is longer than existing so just add child */
816 tree_add_new_dir(tree, parent, &data.best_iter, path);
817 new_row = TRUE;
818 break;
820 case TREE_CASE_PARENT_OF:
822 /* More complicated logic. This dir should be a parent
823 * of existing, so reparent existing dir.
825 has_parent = gtk_tree_model_iter_parent(model, &iter, &data.best_iter);
826 tree_add_new_dir(tree, parent, has_parent ? &iter : NULL, path);
827 tree_reparent(tree, &data.best_iter, parent);
828 new_row = TRUE;
829 break;
831 case TREE_CASE_HAVE_SAME_PARENT:
833 /* Even more complicated logic. Both dirs have same
834 * parent, so create new parent and reparent them
836 GtkTreeIter new_parent;
837 gchar *newpath = g_strndup(path, data.best_len + name_diff);
839 has_parent = gtk_tree_model_iter_parent(model, &iter, &data.best_iter);
840 tree_add_new_dir(tree, &new_parent, has_parent ? &iter : NULL, newpath);
841 tree_reparent(tree, &data.best_iter, &new_parent);
842 tree_add_new_dir(tree, parent, &new_parent, path);
844 g_free(newpath);
845 new_row = TRUE;
846 break;
848 default:
850 tree_add_new_dir(tree, parent, NULL, path);
851 new_row = TRUE;
852 break;
856 g_free(data.needle);
857 g_free(path);
859 return new_row;
863 /* Returns true when parent points to a newly added row. */
864 static gboolean sidebar_openfiles_add_iter(GtkTreeStore *tree, const gchar *file,
865 GtkTreeIter *iter, GtkTreeIter *parent)
867 gboolean new_row;
868 /* get_parent_for_file() might add rows for parent directories */
869 new_row = get_parent_for_file(tree, file, parent);
870 /* insert row for this file */
871 gtk_tree_store_append(tree, iter, parent);
873 return new_row;
877 static void expand_iter(GtkTreeIter *iter)
879 GtkTreePath *path;
881 path = gtk_tree_model_get_path(GTK_TREE_MODEL(store_openfiles), iter);
882 gtk_tree_view_expand_to_path(GTK_TREE_VIEW(tv.tree_openfiles), path);
883 gtk_tree_path_free(path);
887 /* Also sets doc->priv->iter.
888 * This is called recursively in sidebar_openfiles_update_all(). */
889 GEANY_EXPORT_SYMBOL
890 void sidebar_openfiles_add(GeanyDocument *doc)
892 GtkTreeIter *iter = &doc->priv->iter;
893 GtkTreeIter parent;
894 const gchar *filename = DOC_FILENAME(doc);
895 gchar *basename;
896 const GdkColor *color = document_get_status_color(doc);
897 static GIcon *file_icon = NULL;
898 gboolean expand = FALSE;
900 if (interface_prefs.openfiles_path_mode != OPENFILES_PATHS_NONE)
901 expand = sidebar_openfiles_add_iter(store_openfiles, filename, iter, &parent);
902 else
903 gtk_tree_store_append(store_openfiles, iter, NULL);
905 if (!file_icon)
906 file_icon = ui_get_mime_icon("text/plain");
908 basename = g_path_get_basename(filename);
909 gtk_tree_store_set(store_openfiles, iter,
910 DOCUMENTS_ICON, (doc->file_type && doc->file_type->icon) ? doc->file_type->icon : file_icon,
911 DOCUMENTS_SHORTNAME, basename, DOCUMENTS_DOCUMENT, doc, DOCUMENTS_COLOR, color,
912 DOCUMENTS_FILENAME, DOC_FILENAME(doc),
913 DOCUMENTS_FOLD, FALSE,
914 -1);
915 g_free(basename);
917 /* Expand new parent if necessary. Beware: this is executed by unit tests
918 * which don't create the tree view. */
919 if (expand && G_LIKELY(tv.tree_openfiles))
920 expand_iter(&parent);
924 /* Returns true if new_node points to a reparented directory, as a result of merging empty
925 * directories.
927 static void sidebar_openfiles_remove_iter(GtkTreeStore *tree, GtkTreeIter *iter_)
929 GtkTreeIter iter = *iter_;
930 GtkTreeIter parent;
931 GtkTreeModel *model = GTK_TREE_MODEL(store_openfiles);
933 /* walk on top and close all orphaned parents */
934 while (gtk_tree_model_iter_parent(model, &parent, &iter)
935 && gtk_tree_model_iter_n_children(model, &parent) == 1)
937 iter = parent;
939 gtk_tree_store_remove(store_openfiles, &iter);
941 /* If, after removing, there is a single silbling left and it represents
942 * a directory, it can be merged with the parent directory row,
943 * essentially to reverse the effect of TREE_CASE_PARENT_OF and TREE_CASE_HAVE_SAME_PARENT
944 * in get_doc_parent(). Inherit fold state from the merged child as well.
946 if (gtk_tree_store_iter_is_valid(store_openfiles, &parent)
947 && gtk_tree_model_iter_n_children(model, &parent) == 1)
949 GeanyDocument *other_doc;
950 GtkTreeIter child, pparent;
951 gboolean fold, has_parent;
953 gtk_tree_model_iter_nth_child(model, &child, &parent, 0);
954 gtk_tree_model_get(model, &child, DOCUMENTS_DOCUMENT, &other_doc, -1);
955 if (!other_doc)
957 has_parent = gtk_tree_model_iter_parent(model, &pparent, &parent);
958 tree_reparent(store_openfiles, &child, has_parent ? &pparent : NULL);
959 gtk_tree_store_remove(store_openfiles, &parent);
960 /* Expand if the child node was expanded before the merge. */
961 gtk_tree_model_get(model, &child, DOCUMENTS_FOLD, &fold, -1);
962 if (!fold)
963 expand_iter(&child);
969 static void openfiles_remove(GeanyDocument *doc)
971 if (interface_prefs.openfiles_path_mode != OPENFILES_PATHS_NONE)
972 sidebar_openfiles_remove_iter(store_openfiles, &doc->priv->iter);
973 else
974 gtk_tree_store_remove(store_openfiles, &doc->priv->iter);
978 void sidebar_openfiles_update(GeanyDocument *doc)
980 GtkTreeIter *iter = &doc->priv->iter;
981 gchar *fname;
983 gtk_tree_model_get(GTK_TREE_MODEL(store_openfiles), iter, DOCUMENTS_FILENAME, &fname, -1);
985 if (utils_str_equal(fname, DOC_FILENAME(doc)))
987 /* just update color and the icon */
988 const GdkColor *color = document_get_status_color(doc);
989 GIcon *icon = doc->file_type->icon;
991 gtk_tree_store_set(store_openfiles, iter, DOCUMENTS_COLOR, color, -1);
992 if (icon)
993 gtk_tree_store_set(store_openfiles, iter, DOCUMENTS_ICON, icon, -1);
995 else
997 /* path has changed, so remove and re-add */
998 GtkTreeSelection *treesel;
999 gboolean sel;
1001 treesel = gtk_tree_view_get_selection(GTK_TREE_VIEW(tv.tree_openfiles));
1002 sel = gtk_tree_selection_iter_is_selected(treesel, &doc->priv->iter);
1003 openfiles_remove(doc);
1005 sidebar_openfiles_add(doc);
1006 if (sel)
1007 gtk_tree_selection_select_iter(treesel, &doc->priv->iter);
1009 g_free(fname);
1013 void sidebar_openfiles_update_all(void)
1015 guint i;
1017 gtk_tree_store_clear(store_openfiles);
1018 foreach_document (i)
1020 sidebar_openfiles_add(documents[i]);
1025 void sidebar_remove_document(GeanyDocument *doc)
1027 openfiles_remove(doc);
1029 if (GTK_IS_WIDGET(doc->priv->tag_tree))
1031 gtk_widget_destroy(doc->priv->tag_tree); /* make GTK release its references, if any */
1032 /* Because it was ref'd in sidebar_update_tag_list, it needs unref'ing */
1033 g_object_unref(doc->priv->tag_tree);
1034 doc->priv->tag_tree = NULL;
1039 static void on_hide_sidebar(void)
1041 ui_prefs.sidebar_visible = FALSE;
1042 ui_sidebar_show_hide();
1046 static gboolean on_sidebar_display_symbol_list_show(GtkWidget *item)
1048 gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(item),
1049 interface_prefs.sidebar_symbol_visible);
1050 return FALSE;
1054 static gboolean on_sidebar_display_open_files_show(GtkWidget *item)
1056 gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(item),
1057 interface_prefs.sidebar_openfiles_visible);
1058 return FALSE;
1062 void sidebar_add_common_menu_items(GtkMenu *menu)
1064 GtkWidget *item;
1066 item = gtk_separator_menu_item_new();
1067 gtk_widget_show(item);
1068 gtk_container_add(GTK_CONTAINER(menu), item);
1070 item = gtk_check_menu_item_new_with_mnemonic(_("Show S_ymbol List"));
1071 gtk_container_add(GTK_CONTAINER(menu), item);
1072 g_signal_connect(item, "draw", G_CALLBACK(on_sidebar_display_symbol_list_show), NULL);
1073 gtk_widget_show(item);
1074 g_signal_connect(item, "activate",
1075 G_CALLBACK(on_list_symbol_activate), NULL);
1077 item = gtk_check_menu_item_new_with_mnemonic(_("Show _Document List"));
1078 gtk_container_add(GTK_CONTAINER(menu), item);
1079 g_signal_connect(item, "draw", G_CALLBACK(on_sidebar_display_open_files_show), NULL);
1080 gtk_widget_show(item);
1081 g_signal_connect(item, "activate",
1082 G_CALLBACK(on_list_document_activate), NULL);
1084 item = gtk_image_menu_item_new_with_mnemonic(_("H_ide Sidebar"));
1085 gtk_image_menu_item_set_image(GTK_IMAGE_MENU_ITEM(item),
1086 gtk_image_new_from_stock(GTK_STOCK_CLOSE, GTK_ICON_SIZE_MENU));
1087 gtk_widget_show(item);
1088 gtk_container_add(GTK_CONTAINER(menu), item);
1089 g_signal_connect(item, "activate", G_CALLBACK(on_hide_sidebar), NULL);
1093 static void on_openfiles_show_paths_activate(GtkCheckMenuItem *item, gpointer user_data)
1095 gint new_mode = GPOINTER_TO_INT(user_data);
1096 /* This is also called for menu items that became inactive (in response to activating
1097 * another one in the same group).
1099 if (!gtk_check_menu_item_get_active(item))
1100 return;
1102 /* Only if the mode changes...otherwise sidebar_openfiles_update_all() recreates the
1103 * list which messes up the current selection and more.
1105 * This can happen (for example) right after startup, when no menu item was active yet.
1107 if (interface_prefs.openfiles_path_mode == new_mode)
1108 return;
1110 interface_prefs.openfiles_path_mode = new_mode;
1111 sidebar_openfiles_update_all();
1112 gtk_tree_view_expand_all(GTK_TREE_VIEW(tv.tree_openfiles));
1113 sidebar_select_openfiles_item(document_get_current());
1117 static void on_list_document_activate(GtkCheckMenuItem *item, gpointer user_data)
1119 interface_prefs.sidebar_openfiles_visible = gtk_check_menu_item_get_active(item);
1120 ui_sidebar_show_hide();
1121 sidebar_tabs_show_hide(GTK_NOTEBOOK(main_widgets.sidebar_notebook), NULL, 0, NULL);
1125 static void on_list_symbol_activate(GtkCheckMenuItem *item, gpointer user_data)
1127 interface_prefs.sidebar_symbol_visible = gtk_check_menu_item_get_active(item);
1128 ui_sidebar_show_hide();
1129 sidebar_tabs_show_hide(GTK_NOTEBOOK(main_widgets.sidebar_notebook), NULL, 0, NULL);
1133 static void on_find_in_files(GtkMenuItem *menuitem, gpointer user_data)
1135 GtkTreeSelection *treesel;
1136 GtkTreeIter iter;
1137 GtkTreeModel *model;
1138 GeanyDocument *doc;
1139 gchar *dir;
1141 treesel = gtk_tree_view_get_selection(GTK_TREE_VIEW(tv.tree_openfiles));
1142 if (!gtk_tree_selection_get_selected(treesel, &model, &iter))
1143 return;
1144 gtk_tree_model_get(model, &iter, DOCUMENTS_DOCUMENT, &doc, -1);
1146 if (!doc)
1148 gtk_tree_model_get(model, &iter, DOCUMENTS_FILENAME, &dir, -1);
1150 else
1151 dir = g_path_get_dirname(DOC_FILENAME(doc));
1153 search_show_find_in_files_dialog(dir);
1154 g_free(dir);
1158 static void on_openfiles_expand_collapse(GtkMenuItem *menuitem, gpointer user_data)
1160 gboolean expand = GPOINTER_TO_INT(user_data);
1162 if (expand)
1163 gtk_tree_view_expand_all(GTK_TREE_VIEW(tv.tree_openfiles));
1164 else
1165 gtk_tree_view_collapse_all(GTK_TREE_VIEW(tv.tree_openfiles));
1169 static void create_show_paths_popup_menu(void)
1171 GSList *group = NULL;
1172 const gchar *items[OPENFILES_PATHS_COUNT] = {
1173 [OPENFILES_PATHS_NONE] = _("D_ocuments Only"),
1174 [OPENFILES_PATHS_LIST] = _("Show _Paths"),
1175 [OPENFILES_PATHS_TREE] = _("Show _Tree")
1178 for (guint i = 0; i < G_N_ELEMENTS(items); i++)
1180 GtkWidget *w = gtk_radio_menu_item_new_with_mnemonic(group, items[i]);
1181 group = gtk_radio_menu_item_get_group(GTK_RADIO_MENU_ITEM(w));
1182 gtk_widget_show(w);
1183 gtk_container_add(GTK_CONTAINER(openfiles_popup_menu), w);
1184 g_signal_connect(w, "activate",
1185 G_CALLBACK(on_openfiles_show_paths_activate), GINT_TO_POINTER(i));
1186 doc_items.show_paths[i] = w;
1191 static void create_openfiles_popup_menu(void)
1193 GtkWidget *item;
1195 openfiles_popup_menu = gtk_menu_new();
1197 item = gtk_image_menu_item_new_from_stock(GTK_STOCK_CLOSE, NULL);
1198 gtk_widget_show(item);
1199 gtk_container_add(GTK_CONTAINER(openfiles_popup_menu), item);
1200 g_signal_connect(item, "activate",
1201 G_CALLBACK(on_openfiles_document_action), GINT_TO_POINTER(OPENFILES_ACTION_REMOVE));
1202 doc_items.close = item;
1204 item = gtk_separator_menu_item_new();
1205 gtk_widget_show(item);
1206 gtk_container_add(GTK_CONTAINER(openfiles_popup_menu), item);
1208 item = gtk_image_menu_item_new_from_stock(GTK_STOCK_SAVE, NULL);
1209 gtk_widget_show(item);
1210 gtk_container_add(GTK_CONTAINER(openfiles_popup_menu), item);
1211 g_signal_connect(item, "activate",
1212 G_CALLBACK(on_openfiles_document_action), GINT_TO_POINTER(OPENFILES_ACTION_SAVE));
1213 doc_items.save = item;
1215 item = gtk_image_menu_item_new_with_mnemonic(_("_Reload"));
1216 gtk_image_menu_item_set_image(GTK_IMAGE_MENU_ITEM(item),
1217 gtk_image_new_from_stock(GTK_STOCK_REVERT_TO_SAVED, GTK_ICON_SIZE_MENU));
1218 gtk_widget_show(item);
1219 gtk_container_add(GTK_CONTAINER(openfiles_popup_menu), item);
1220 g_signal_connect(item, "activate",
1221 G_CALLBACK(on_openfiles_document_action), GINT_TO_POINTER(OPENFILES_ACTION_RELOAD));
1222 doc_items.reload = item;
1224 item = gtk_separator_menu_item_new();
1225 gtk_widget_show(item);
1226 gtk_container_add(GTK_CONTAINER(openfiles_popup_menu), item);
1228 item = ui_image_menu_item_new(GTK_STOCK_FIND, _("_Find in Files..."));
1229 gtk_widget_show(item);
1230 gtk_container_add(GTK_CONTAINER(openfiles_popup_menu), item);
1231 g_signal_connect(item, "activate", G_CALLBACK(on_find_in_files), NULL);
1232 doc_items.find_in_files = item;
1234 item = gtk_separator_menu_item_new();
1235 gtk_widget_show(item);
1236 gtk_container_add(GTK_CONTAINER(openfiles_popup_menu), item);
1238 create_show_paths_popup_menu();
1240 item = gtk_separator_menu_item_new();
1241 gtk_widget_show(item);
1242 gtk_container_add(GTK_CONTAINER(openfiles_popup_menu), item);
1244 doc_items.expand_all = ui_image_menu_item_new(GTK_STOCK_ADD, _("_Expand All"));
1245 gtk_widget_show(doc_items.expand_all);
1246 gtk_container_add(GTK_CONTAINER(openfiles_popup_menu), doc_items.expand_all);
1247 g_signal_connect(doc_items.expand_all, "activate",
1248 G_CALLBACK(on_openfiles_expand_collapse), GINT_TO_POINTER(TRUE));
1250 doc_items.collapse_all = ui_image_menu_item_new(GTK_STOCK_REMOVE, _("_Collapse All"));
1251 gtk_widget_show(doc_items.collapse_all);
1252 gtk_container_add(GTK_CONTAINER(openfiles_popup_menu), doc_items.collapse_all);
1253 g_signal_connect(doc_items.collapse_all, "activate",
1254 G_CALLBACK(on_openfiles_expand_collapse), GINT_TO_POINTER(FALSE));
1256 sidebar_add_common_menu_items(GTK_MENU(openfiles_popup_menu));
1260 /* compares the given data with the doc pointer from the selected row of openfiles
1261 * treeview, in case of a match the row is selected and TRUE is returned
1262 * (called indirectly from gtk_tree_model_foreach()) */
1263 static gboolean tree_model_find_node(GtkTreeModel *model,
1264 GtkTreePath *path,
1265 GtkTreeIter *iter,
1266 gpointer data)
1268 GeanyDocument *doc;
1270 gtk_tree_model_get(model, iter, DOCUMENTS_DOCUMENT, &doc, -1);
1271 if (doc == data)
1273 gtk_tree_view_expand_to_path(GTK_TREE_VIEW(tv.tree_openfiles), path);
1274 gtk_tree_view_set_cursor(GTK_TREE_VIEW(tv.tree_openfiles), path, NULL, FALSE);
1275 return TRUE;
1278 return FALSE;
1282 void sidebar_select_openfiles_item(GeanyDocument *doc)
1284 gtk_tree_model_foreach(GTK_TREE_MODEL(store_openfiles), tree_model_find_node, doc);
1288 /* callbacks */
1290 static void document_action(GeanyDocument *doc, gint action)
1292 if (! DOC_VALID(doc))
1293 return;
1295 switch (action)
1297 case OPENFILES_ACTION_REMOVE:
1299 document_close(doc);
1300 break;
1302 case OPENFILES_ACTION_SAVE:
1304 document_save_file(doc, FALSE);
1305 break;
1307 case OPENFILES_ACTION_RELOAD:
1309 document_reload_prompt(doc, NULL);
1310 break;
1316 /* Collects all documents under a given iter, filling @doc_array */
1317 static void on_openfiles_document_action_collect(GtkTreeModel *model, GtkTreeIter *iter,
1318 GPtrArray *doc_array)
1320 GeanyDocument *doc;
1321 gtk_tree_model_get(model, iter, DOCUMENTS_DOCUMENT, &doc, -1);
1322 if (doc)
1324 g_ptr_array_add(doc_array, doc);
1326 else
1328 /* parent item selected */
1329 GtkTreeIter child;
1331 if (gtk_tree_model_iter_children(model, &child, iter))
1335 on_openfiles_document_action_collect(model, &child, doc_array);
1337 while (gtk_tree_model_iter_next(model, &child));
1343 static void on_openfiles_document_action(GtkMenuItem *menuitem, gpointer user_data)
1345 GtkTreeIter iter;
1346 GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(tv.tree_openfiles));
1347 GtkTreeModel *model;
1348 gint action = GPOINTER_TO_INT(user_data);
1350 if (gtk_tree_selection_get_selected(selection, &model, &iter))
1352 GPtrArray *doc_array = g_ptr_array_new();
1354 on_openfiles_document_action_collect(model, &iter, doc_array);
1355 for (guint i = 0; i < doc_array->len; i++)
1356 document_action(g_ptr_array_index(doc_array, i), action);
1357 g_ptr_array_free(doc_array, TRUE);
1362 static void change_focus_to_editor(GeanyDocument *doc, GtkWidget *source_widget)
1364 if (may_steal_focus)
1365 document_try_focus(doc, source_widget);
1366 may_steal_focus = FALSE;
1370 static gboolean openfiles_go_to_selection(GtkTreeSelection *selection, guint keyval)
1372 GtkTreeIter iter;
1373 GtkTreeModel *model;
1374 GeanyDocument *doc = NULL;
1376 /* use switch_notebook_page to ignore changing the notebook page because it is already done */
1377 if (gtk_tree_selection_get_selected(selection, &model, &iter) && ! ignore_callback)
1379 gtk_tree_model_get(model, &iter, DOCUMENTS_DOCUMENT, &doc, -1);
1380 if (! doc)
1381 return FALSE; /* parent */
1383 /* switch to the doc and grab the focus */
1384 document_show_tab(doc);
1385 if (keyval != GDK_KEY_space)
1386 change_focus_to_editor(doc, tv.tree_openfiles);
1388 return FALSE;
1392 static gboolean taglist_go_to_selection(GtkTreeSelection *selection, guint keyval, guint state)
1394 GtkTreeIter iter;
1395 GtkTreeModel *model;
1396 gint line = 0;
1397 gboolean handled = TRUE;
1399 if (gtk_tree_selection_get_selected(selection, &model, &iter))
1401 TMTag *tag;
1403 gtk_tree_model_get(model, &iter, SYMBOLS_COLUMN_TAG, &tag, -1);
1404 if (! tag)
1405 return FALSE;
1407 line = tag->line;
1408 if (line > 0)
1410 GeanyDocument *doc = document_get_current();
1412 if (doc != NULL)
1414 navqueue_goto_line(doc, doc, line);
1415 state = keybindings_get_modifiers(state);
1416 if (keyval != GDK_KEY_space && ! (state & GEANY_PRIMARY_MOD_MASK))
1417 change_focus_to_editor(doc, NULL);
1418 else
1419 handled = FALSE;
1422 tm_tag_unref(tag);
1424 return handled;
1428 static gboolean sidebar_key_press_cb(GtkWidget *widget, GdkEventKey *event,
1429 gpointer user_data)
1431 may_steal_focus = FALSE;
1432 if (ui_is_keyval_enter_or_return(event->keyval) || event->keyval == GDK_KEY_space)
1434 GtkWidgetClass *widget_class = GTK_WIDGET_GET_CLASS(widget);
1435 GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(widget));
1436 may_steal_focus = TRUE;
1438 /* force the TreeView handler to run before us for it to do its job (selection & stuff).
1439 * doing so will prevent further handlers to be run in most cases, but the only one is our
1440 * own, so guess it's fine. */
1441 if (widget_class->key_press_event)
1442 widget_class->key_press_event(widget, event);
1444 if (widget == tv.tree_openfiles) /* tag and doc list have separate handlers */
1445 openfiles_go_to_selection(selection, event->keyval);
1446 else
1447 taglist_go_to_selection(selection, event->keyval, event->state);
1449 return TRUE;
1451 return FALSE;
1455 static gboolean sidebar_button_press_cb(GtkWidget *widget, GdkEventButton *event,
1456 G_GNUC_UNUSED gpointer user_data)
1458 GtkTreeSelection *selection;
1459 GtkWidgetClass *widget_class = GTK_WIDGET_GET_CLASS(widget);
1460 gboolean handled = FALSE;
1462 /* force the TreeView handler to run before us for it to do its job (selection & stuff).
1463 * doing so will prevent further handlers to be run in most cases, but the only one is our own,
1464 * so guess it's fine. */
1465 if (widget_class->button_press_event)
1466 handled = widget_class->button_press_event(widget, event);
1468 selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(widget));
1469 may_steal_focus = TRUE;
1471 if (event->type == GDK_2BUTTON_PRESS)
1472 { /* double click on parent node(section) expands/collapses it */
1473 GtkTreeModel *model;
1474 GtkTreeIter iter;
1476 if (gtk_tree_selection_get_selected(selection, &model, &iter))
1478 if (gtk_tree_model_iter_has_child(model, &iter))
1480 GtkTreePath *path = gtk_tree_model_get_path(model, &iter);
1482 if (gtk_tree_view_row_expanded(GTK_TREE_VIEW(widget), path))
1483 gtk_tree_view_collapse_row(GTK_TREE_VIEW(widget), path);
1484 else
1485 gtk_tree_view_expand_row(GTK_TREE_VIEW(widget), path, FALSE);
1487 gtk_tree_path_free(path);
1488 return TRUE;
1492 else if (event->button == 1)
1493 { /* allow reclicking of taglist treeview item */
1494 if (widget == tv.tree_openfiles)
1496 openfiles_go_to_selection(selection, 0);
1497 handled = TRUE;
1499 else
1500 handled = taglist_go_to_selection(selection, 0, event->state);
1502 else if (event->button == 2)
1504 if (widget == tv.tree_openfiles)
1505 on_openfiles_document_action(NULL, GINT_TO_POINTER(OPENFILES_ACTION_REMOVE));
1507 else if (event->button == 3)
1509 if (widget == tv.tree_openfiles)
1511 if (!openfiles_popup_menu)
1512 create_openfiles_popup_menu();
1514 /* update menu item sensitivity */
1515 documents_menu_update(selection);
1516 gtk_menu_popup_at_pointer(GTK_MENU(openfiles_popup_menu), (GdkEvent *) event);
1518 else
1520 gtk_menu_popup_at_pointer(GTK_MENU(tv.popup_taglist), (GdkEvent *) event);
1522 handled = TRUE;
1524 return handled;
1528 static void documents_menu_update(GtkTreeSelection *selection)
1530 GtkTreeModel *model;
1531 GtkTreeIter iter;
1532 gboolean sel, path;
1533 gchar *shortname = NULL;
1534 GeanyDocument *doc = NULL;
1536 /* maybe no selection e.g. if ctrl-click deselected */
1537 sel = gtk_tree_selection_get_selected(selection, &model, &iter);
1538 if (sel)
1540 gtk_tree_model_get(model, &iter,
1541 DOCUMENTS_DOCUMENT, &doc,
1542 DOCUMENTS_SHORTNAME, &shortname,
1543 -1);
1545 path = !EMPTY(shortname) &&
1546 (g_path_is_absolute(shortname) ||
1547 (app->project && g_str_has_prefix(shortname, app->project->name)));
1549 /* can close all, save all (except shortname), but only reload individually ATM */
1550 gtk_widget_set_sensitive(doc_items.close, sel);
1551 gtk_widget_set_sensitive(doc_items.save, (doc && doc->real_path) || path);
1552 gtk_widget_set_sensitive(doc_items.reload, doc && doc->real_path);
1553 gtk_widget_set_sensitive(doc_items.find_in_files, sel);
1554 g_free(shortname);
1556 gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(doc_items.show_paths[interface_prefs.openfiles_path_mode]), TRUE);
1557 gtk_widget_set_sensitive(doc_items.expand_all, interface_prefs.openfiles_path_mode);
1558 gtk_widget_set_sensitive(doc_items.collapse_all, interface_prefs.openfiles_path_mode);
1562 static StashGroup *stash_group = NULL;
1564 static void on_load_settings(void)
1566 if (interface_prefs.openfiles_path_mode < 0
1567 || interface_prefs.openfiles_path_mode >= OPENFILES_PATHS_COUNT)
1568 interface_prefs.openfiles_path_mode = OPENFILES_PATHS_TREE;
1570 tag_window = ui_lookup_widget(main_widgets.window, "scrolledwindow2");
1572 prepare_openfiles();
1573 /* note: ui_prefs.sidebar_page is reapplied after plugins are loaded */
1574 stash_group_display(stash_group, NULL);
1575 sidebar_tabs_show_hide(GTK_NOTEBOOK(main_widgets.sidebar_notebook), NULL, 0, NULL);
1579 static void on_save_settings(void)
1581 stash_group_update(stash_group, NULL);
1582 sidebar_tabs_show_hide(GTK_NOTEBOOK(main_widgets.sidebar_notebook), NULL, 0, NULL);
1586 static void on_sidebar_switch_page(GtkNotebook *notebook,
1587 gpointer page, guint page_num, gpointer user_data)
1589 if (page_num == TREEVIEW_SYMBOL)
1590 sidebar_update_tag_list(document_get_current(), FALSE);
1594 void sidebar_init(void)
1596 StashGroup *group;
1598 group = stash_group_new(PACKAGE);
1599 stash_group_add_widget_property(group, &ui_prefs.sidebar_page, "sidebar_page", GINT_TO_POINTER(0),
1600 main_widgets.sidebar_notebook, "page", 0);
1601 configuration_add_session_group(group, FALSE);
1602 stash_group = group;
1604 /* Delay building documents treeview until sidebar font has been read and prefs are sanitized */
1605 g_signal_connect(geany_object, "load-settings", on_load_settings, NULL);
1606 g_signal_connect(geany_object, "save-settings", on_save_settings, NULL);
1608 g_signal_connect(main_widgets.sidebar_notebook, "page-added",
1609 G_CALLBACK(sidebar_tabs_show_hide), NULL);
1610 g_signal_connect(main_widgets.sidebar_notebook, "page-removed",
1611 G_CALLBACK(sidebar_tabs_show_hide), NULL);
1612 /* tabs may have changed when sidebar is reshown */
1613 g_signal_connect(main_widgets.sidebar_notebook, "show",
1614 G_CALLBACK(sidebar_tabs_show_hide), NULL);
1615 g_signal_connect_after(main_widgets.sidebar_notebook, "switch-page",
1616 G_CALLBACK(on_sidebar_switch_page), NULL);
1619 #define WIDGET(w) w && GTK_IS_WIDGET(w)
1621 void sidebar_finalize(void)
1623 if (WIDGET(tv.default_tag_tree))
1625 gtk_widget_destroy(tv.default_tag_tree); /* make GTK release its references, if any... */
1626 g_object_unref(tv.default_tag_tree); /* ...and release our own */
1628 if (WIDGET(tv.popup_taglist))
1629 gtk_widget_destroy(tv.popup_taglist);
1630 if (WIDGET(openfiles_popup_menu))
1631 gtk_widget_destroy(openfiles_popup_menu);
1635 void sidebar_focus_openfiles_tab(void)
1637 if (ui_prefs.sidebar_visible && interface_prefs.sidebar_openfiles_visible)
1639 GtkNotebook *notebook = GTK_NOTEBOOK(main_widgets.sidebar_notebook);
1641 gtk_notebook_set_current_page(notebook, TREEVIEW_OPENFILES);
1642 gtk_widget_grab_focus(tv.tree_openfiles);
1647 void sidebar_focus_symbols_tab(void)
1649 if (ui_prefs.sidebar_visible && interface_prefs.sidebar_symbol_visible)
1651 GtkNotebook *notebook = GTK_NOTEBOOK(main_widgets.sidebar_notebook);
1652 GtkWidget *symbol_list_scrollwin = ui_lookup_widget(main_widgets.window, "scrolledwindow2");
1654 gtk_notebook_set_current_page(notebook, TREEVIEW_SYMBOL);
1655 gtk_widget_grab_focus(gtk_bin_get_child(GTK_BIN(symbol_list_scrollwin)));
1660 static void sidebar_tabs_show_hide(GtkNotebook *notebook, GtkWidget *child,
1661 guint page_num, gpointer data)
1663 gint tabs = gtk_notebook_get_n_pages(notebook);
1665 if (interface_prefs.sidebar_symbol_visible == FALSE)
1666 tabs--;
1667 if (interface_prefs.sidebar_openfiles_visible == FALSE)
1668 tabs--;
1670 gtk_notebook_set_show_tabs(notebook, (tabs > 1));