geany/src/sidebar.c
Thomas Martitz 20bd9c44ea Add a unit test program to check the new sidebar documents tree view
The test program checks if open documents are grouped correctly by
their parent directory. The older modes (plain document list and two-level
tree) also get a distinct test. For this to work some symbols
must become visible from libgeany.

The test uses g_strv_length() which is relatively new.
Autoconf and meson checks are added as needed.
2022-06-08 07:54:00 +02:00

1649 lines
48 KiB
C

/*
* sidebar.c - this file is part of Geany, a fast and lightweight IDE
*
* Copyright 2005 The Geany contributors
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
/*
* Sidebar related code for the Symbol list and Open files GtkTreeViews.
*/
#ifdef HAVE_CONFIG_H
# include "config.h"
#endif
#include "sidebar.h"
#include "app.h"
#include "callbacks.h" /* FIXME: for ignore_callback */
#include "documentprivate.h"
#include "filetypesprivate.h"
#include "geanyobject.h"
#include "keyfile.h"
#include "navqueue.h"
#include "stash.h"
#include "support.h"
#include "symbols.h"
#include "ui_utils.h"
#include "utils.h"
#include "keybindings.h"
#include <string.h>
#include <gdk/gdkkeysyms.h>
SidebarTreeviews tv = {NULL, NULL, NULL};
/* while typeahead searching, editor should not get focus */
static gboolean may_steal_focus = FALSE;
static struct
{
GtkWidget *close;
GtkWidget *save;
GtkWidget *reload;
GtkWidget *show_paths[OPENFILES_PATHS_COUNT];
GtkWidget *find_in_files;
GtkWidget *expand_all;
GtkWidget *collapse_all;
}
doc_items;
enum
{
TREEVIEW_SYMBOL = 0,
TREEVIEW_OPENFILES
};
enum
{
OPENFILES_ACTION_REMOVE = 0,
OPENFILES_ACTION_SAVE,
OPENFILES_ACTION_RELOAD
};
static GtkTreeStore *store_openfiles;
static GtkWidget *openfiles_popup_menu;
static GtkWidget *tag_window; /* scrolled window that holds the symbol list GtkTreeView */
/* callback prototypes */
static void on_openfiles_document_action(GtkMenuItem *menuitem, gpointer user_data);
static gboolean sidebar_button_press_cb(GtkWidget *widget, GdkEventButton *event,
gpointer user_data);
static gboolean sidebar_key_press_cb(GtkWidget *widget, GdkEventKey *event,
gpointer user_data);
static void on_list_document_activate(GtkCheckMenuItem *item, gpointer user_data);
static void on_list_symbol_activate(GtkCheckMenuItem *item, gpointer user_data);
static void documents_menu_update(GtkTreeSelection *selection);
static void sidebar_tabs_show_hide(GtkNotebook *notebook, GtkWidget *child,
guint page_num, gpointer data);
/* the prepare_* functions are document-related, but I think they fit better here than in document.c */
static void prepare_taglist(GtkWidget *tree, GtkTreeStore *store)
{
GtkCellRenderer *text_renderer, *icon_renderer;
GtkTreeViewColumn *column;
GtkTreeSelection *selection;
text_renderer = gtk_cell_renderer_text_new();
icon_renderer = gtk_cell_renderer_pixbuf_new();
column = gtk_tree_view_column_new();
gtk_tree_view_column_pack_start(column, icon_renderer, FALSE);
gtk_tree_view_column_set_attributes(column, icon_renderer, "pixbuf", SYMBOLS_COLUMN_ICON, NULL);
g_object_set(icon_renderer, "xalign", 0.0, NULL);
gtk_tree_view_column_pack_start(column, text_renderer, TRUE);
gtk_tree_view_column_set_attributes(column, text_renderer, "text", SYMBOLS_COLUMN_NAME, NULL);
g_object_set(text_renderer, "yalign", 0.5, NULL);
gtk_tree_view_column_set_title(column, _("Symbols"));
gtk_tree_view_append_column(GTK_TREE_VIEW(tree), column);
gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(tree), FALSE);
ui_widget_modify_font_from_string(tree, interface_prefs.tagbar_font);
gtk_tree_view_set_model(GTK_TREE_VIEW(tree), GTK_TREE_MODEL(store));
g_object_unref(store);
g_signal_connect(tree, "button-press-event",
G_CALLBACK(sidebar_button_press_cb), NULL);
g_signal_connect(tree, "key-press-event",
G_CALLBACK(sidebar_key_press_cb), NULL);
gtk_tree_view_set_show_expanders(GTK_TREE_VIEW(tree), interface_prefs.show_symbol_list_expanders);
if (! interface_prefs.show_symbol_list_expanders)
gtk_tree_view_set_level_indentation(GTK_TREE_VIEW(tree), 10);
/* Tooltips */
ui_tree_view_set_tooltip_text_column(GTK_TREE_VIEW(tree), SYMBOLS_COLUMN_TOOLTIP);
/* selection handling */
selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(tree));
gtk_tree_selection_set_mode(selection, GTK_SELECTION_SINGLE);
/* callback for changed selection not necessary, will be handled by button-press-event */
}
static gboolean
on_default_tag_tree_button_press_event(GtkWidget *widget, GdkEventButton *event,
gpointer user_data)
{
if (event->button == 3)
{
gtk_menu_popup(GTK_MENU(tv.popup_taglist), NULL, NULL, NULL, NULL,
event->button, event->time);
return TRUE;
}
return FALSE;
}
static void create_default_tag_tree(void)
{
GtkScrolledWindow *scrolled_window = GTK_SCROLLED_WINDOW(tag_window);
GtkWidget *label;
/* default_tag_tree is a GtkViewPort with a GtkLabel inside it */
tv.default_tag_tree = gtk_viewport_new(
gtk_scrolled_window_get_hadjustment(scrolled_window),
gtk_scrolled_window_get_vadjustment(scrolled_window));
gtk_viewport_set_shadow_type(GTK_VIEWPORT(tv.default_tag_tree), GTK_SHADOW_NONE);
label = gtk_label_new(_("No symbols found"));
gtk_misc_set_alignment(GTK_MISC(label), 0.1f, 0.01f);
gtk_container_add(GTK_CONTAINER(tv.default_tag_tree), label);
gtk_widget_show_all(tv.default_tag_tree);
g_signal_connect(tv.default_tag_tree, "button-press-event",
G_CALLBACK(on_default_tag_tree_button_press_event), NULL);
g_object_ref((gpointer)tv.default_tag_tree); /* to hold it after removing */
}
/* update = rescan the tags for doc->filename */
void sidebar_update_tag_list(GeanyDocument *doc, gboolean update)
{
GtkWidget *child = gtk_bin_get_child(GTK_BIN(tag_window));
g_return_if_fail(doc == NULL || doc->is_valid);
if (update && doc != NULL)
doc->priv->tag_tree_dirty = TRUE;
if (gtk_notebook_get_current_page(GTK_NOTEBOOK(main_widgets.sidebar_notebook)) != TREEVIEW_SYMBOL)
return; /* don't bother updating symbol tree if we don't see it */
/* changes the tree view to the given one, trying not to do useless changes */
#define CHANGE_TREE(new_child) \
G_STMT_START { \
/* only change the tag tree if it's actually not the same (to avoid flickering) and if \
* it's the one of the current document (to avoid problems when e.g. reloading \
* configuration files */ \
if (child != new_child && doc == document_get_current()) \
{ \
if (child) \
gtk_container_remove(GTK_CONTAINER(tag_window), child); \
gtk_container_add(GTK_CONTAINER(tag_window), new_child); \
} \
} G_STMT_END
if (tv.default_tag_tree == NULL)
create_default_tag_tree();
/* show default empty tag tree if there are no tags */
if (doc == NULL || doc->file_type == NULL || ! filetype_has_tags(doc->file_type))
{
CHANGE_TREE(tv.default_tag_tree);
return;
}
if (doc->priv->tag_tree_dirty)
{ /* updating the tag list in the left tag window */
if (doc->priv->tag_tree == NULL)
{
doc->priv->tag_store = gtk_tree_store_new(
SYMBOLS_N_COLUMNS, GDK_TYPE_PIXBUF, G_TYPE_STRING, TM_TYPE_TAG, G_TYPE_STRING);
doc->priv->tag_tree = gtk_tree_view_new();
prepare_taglist(doc->priv->tag_tree, doc->priv->tag_store);
gtk_widget_show(doc->priv->tag_tree);
g_object_ref((gpointer)doc->priv->tag_tree); /* to hold it after removing */
}
doc->has_tags = symbols_recreate_tag_list(doc, SYMBOLS_SORT_USE_PREVIOUS);
doc->priv->tag_tree_dirty = FALSE;
}
if (doc->has_tags)
{
CHANGE_TREE(doc->priv->tag_tree);
}
else
{
CHANGE_TREE(tv.default_tag_tree);
}
#undef CHANGE_TREE
}
/* cleverly sorts documents by their short name */
static gint documents_sort_func(GtkTreeModel *model, GtkTreeIter *iter_a,
GtkTreeIter *iter_b, gpointer data)
{
gchar *name_a, *name_b;
GeanyDocument *doc_a, *doc_b;
gint cmp;
gtk_tree_model_get(model, iter_a, DOCUMENTS_SHORTNAME, &name_a, DOCUMENTS_DOCUMENT, &doc_a, -1);
gtk_tree_model_get(model, iter_b, DOCUMENTS_SHORTNAME, &name_b, DOCUMENTS_DOCUMENT, &doc_b, -1);
/* sort dirs after files (within a directory node) */
if (!doc_a && doc_b)
cmp = 1;
else if (doc_a && !doc_b)
cmp = -1;
else
{
gchar *key_a, *key_b;
/* g_utf8_collate_key_for_filename() seems to ignore ~. As a result, ~/dev <=> /data
* compares as /dev > /data while ~/dev <=> /etc compares as /dev < /etc. Thus, for sorting
* purposes, treat leading ~ as . which is documented to be special-cased to sort before
* anything else. The side effect, that documents under ~ are always first is actually
* welcome.
*/
name_a[0] = name_a[0] == '~' ? '.' : name_a[0];
name_b[0] = name_b[0] == '~' ? '.' : name_b[0];
key_a = g_utf8_collate_key_for_filename(name_a, -1);
key_b = g_utf8_collate_key_for_filename(name_b, -1);
cmp = strcmp(key_a, key_b);
g_free(key_b);
g_free(key_a);
}
g_free(name_b);
g_free(name_a);
return cmp;
}
GEANY_EXPORT_SYMBOL
GtkTreeStore *sidebar_create_store_openfiles(void)
{
GtkTreeSortable *sortable;
GtkTreeStore *store;
/* store the icon and the short filename to show, and the index as reference,
* the colour (black/red/green) and the full name for the tooltip */
store = gtk_tree_store_new(6, G_TYPE_ICON, G_TYPE_STRING,
G_TYPE_POINTER, GDK_TYPE_COLOR, G_TYPE_STRING, G_TYPE_BOOLEAN);
/* sort opened filenames in the store_openfiles treeview */
sortable = GTK_TREE_SORTABLE(GTK_TREE_MODEL(store));
gtk_tree_sortable_set_sort_func(sortable, DOCUMENTS_SHORTNAME, documents_sort_func, NULL, NULL);
gtk_tree_sortable_set_sort_column_id(sortable, DOCUMENTS_SHORTNAME, GTK_SORT_ASCENDING);
store_openfiles = store;
return store;
}
static void store_fold_recurse(GtkTreeView *view,
GtkTreeIter *iter,
GtkTreeModel *model)
{
GeanyDocument *doc;
gboolean fold, valid;
GtkTreePath *path;
GtkTreeIter child_iter;
gtk_tree_model_get(model, iter, DOCUMENTS_DOCUMENT, &doc, -1);
if (doc) /* document rows are not foldable */
return;
path = gtk_tree_model_get_path(model, iter);
fold = !gtk_tree_view_row_expanded(view, path);
gtk_tree_store_set(GTK_TREE_STORE(model), iter, DOCUMENTS_FOLD, fold, -1);
gtk_tree_path_free(path);
/* After storing the fold state for *this* row, recursively do the same for its children.
* We need do do this only for expanded children because all children of folded rows are
* folded as well.
*/
if (fold)
return;
valid = gtk_tree_model_iter_children(model, &child_iter, iter);
while (valid)
{
store_fold_recurse(view, &child_iter, model);
valid = gtk_tree_model_iter_next(model, &child_iter);
}
}
static gboolean on_row_expand(GtkTreeView *view,
GtkTreeIter *iter,
GtkTreePath *path,
gpointer user_data)
{
GtkTreeModel *model;
model = gtk_tree_view_get_model(view);
gtk_tree_store_set(GTK_TREE_STORE(model), iter, DOCUMENTS_FOLD, FALSE, -1);
return FALSE;
}
static gboolean on_row_collapse(GtkTreeView *view,
GtkTreeIter *iter,
GtkTreePath *path,
gpointer user_data)
{
GtkTreeModel *model;
GtkTreeIter child_iter;
gboolean valid;
model = gtk_tree_view_get_model(view);
gtk_tree_store_set(GTK_TREE_STORE(model), iter, DOCUMENTS_FOLD, TRUE, -1);
valid = gtk_tree_model_iter_children(model, &child_iter, iter);
while (valid)
{
store_fold_recurse(view, &child_iter, model);
valid = gtk_tree_model_iter_next(model, &child_iter);
}
return FALSE;
}
static void on_row_expanded(GtkTreeView *view,
GtkTreeIter *iter,
GtkTreePath *path_,
gpointer user_data)
{
GtkTreeIter child_iter;
GtkTreeModel *model;
GtkTreePath *path;
gboolean valid;
GeanyDocument *doc;
model = gtk_tree_view_get_model(view);
valid = gtk_tree_model_iter_children(model, &child_iter, iter);
while (valid)
{
gboolean fold;
gtk_tree_model_get(model, &child_iter, DOCUMENTS_DOCUMENT, &doc, DOCUMENTS_FOLD, &fold, -1);
path = gtk_tree_model_get_path(model, &child_iter);
if (!doc && !fold)
gtk_tree_view_expand_row(view, path, FALSE);
valid = gtk_tree_model_iter_next(model, &child_iter);
gtk_tree_path_free(path);
}
}
/* does some preparing things to the open files list widget */
static void prepare_openfiles(void)
{
GtkCellRenderer *icon_renderer;
GtkCellRenderer *text_renderer;
GtkTreeViewColumn *column;
GtkTreeSelection *selection;
tv.tree_openfiles = ui_lookup_widget(main_widgets.window, "treeview6");
sidebar_create_store_openfiles();
gtk_tree_view_set_model(GTK_TREE_VIEW(tv.tree_openfiles), GTK_TREE_MODEL(store_openfiles));
/* These two implement "remember fold state of rows when their parents are folded". Normally
* GTK does not remember the fold state and can only expand all or no children when
* expanding a row. Maybe this can be useful for other tree views as well?
*/
g_signal_connect_after(GTK_TREE_VIEW(tv.tree_openfiles), "test-expand-row", G_CALLBACK(on_row_expand), NULL);
g_signal_connect_after(GTK_TREE_VIEW(tv.tree_openfiles), "test-collapse-row", G_CALLBACK(on_row_collapse), NULL);
g_signal_connect_after(GTK_TREE_VIEW(tv.tree_openfiles), "row-expanded", G_CALLBACK(on_row_expanded), NULL);
/* set policy settings for the scolledwindow around the treeview again, because glade
* doesn't keep the settings */
gtk_scrolled_window_set_policy(
GTK_SCROLLED_WINDOW(ui_lookup_widget(main_widgets.window, "scrolledwindow7")),
GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
icon_renderer = gtk_cell_renderer_pixbuf_new();
g_object_set(icon_renderer, "stock-size", GTK_ICON_SIZE_MENU, NULL);
text_renderer = gtk_cell_renderer_text_new();
g_object_set(text_renderer, "ellipsize", PANGO_ELLIPSIZE_MIDDLE, NULL);
column = gtk_tree_view_column_new();
gtk_tree_view_column_pack_start(column, icon_renderer, FALSE);
gtk_tree_view_column_set_attributes(column, icon_renderer, "gicon", DOCUMENTS_ICON, NULL);
gtk_tree_view_column_pack_start(column, text_renderer, TRUE);
gtk_tree_view_column_set_attributes(column, text_renderer, "text", DOCUMENTS_SHORTNAME,
"foreground-gdk", DOCUMENTS_COLOR, NULL);
gtk_tree_view_append_column(GTK_TREE_VIEW(tv.tree_openfiles), column);
gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(tv.tree_openfiles), FALSE);
gtk_tree_view_set_search_column(GTK_TREE_VIEW(tv.tree_openfiles),
DOCUMENTS_SHORTNAME);
ui_widget_modify_font_from_string(tv.tree_openfiles, interface_prefs.tagbar_font);
/* tooltips */
ui_tree_view_set_tooltip_text_column(GTK_TREE_VIEW(tv.tree_openfiles), DOCUMENTS_FILENAME);
/* selection handling */
selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(tv.tree_openfiles));
gtk_tree_selection_set_mode(selection, GTK_SELECTION_SINGLE);
g_object_unref(store_openfiles);
g_signal_connect(GTK_TREE_VIEW(tv.tree_openfiles), "button-press-event",
G_CALLBACK(sidebar_button_press_cb), NULL);
g_signal_connect(GTK_TREE_VIEW(tv.tree_openfiles), "key-press-event",
G_CALLBACK(sidebar_key_press_cb), NULL);
}
static gboolean utils_filename_has_prefix(const gchar *str, const gchar *prefix)
{
gchar *head = g_strndup(str, strlen(prefix));
gboolean ret = utils_filenamecmp(head, prefix) == 0;
g_free(head);
return ret;
}
static gchar *get_project_folder(const gchar *path)
{
gchar *project_base_path;
gchar *dirname = NULL;
const gchar *rest;
/* replace the project base path with the project name */
project_base_path = project_get_base_path();
if (project_base_path != NULL)
{
gsize len = strlen(project_base_path);
/* remove trailing separator so we can match base path exactly */
if (project_base_path[len-1] == G_DIR_SEPARATOR)
project_base_path[--len] = '\0';
/* check whether the dir name matches or uses the project base path */
if (utils_filename_has_prefix(path, project_base_path))
{
rest = path + len;
if (*rest == G_DIR_SEPARATOR || *rest == '\0')
{
dirname = g_strdup_printf("%s%s", app->project->name, rest);
}
}
g_free(project_base_path);
}
return dirname;
}
static gchar *get_doc_folder(const gchar *path)
{
gchar *dirname = get_project_folder(path);
const gchar *rest;
if (dirname == NULL)
{
const gchar *home_dir = g_get_home_dir();
gchar *tmp_dirname = g_strdup(path);
dirname = tmp_dirname;
/* If matches home dir, replace with tilde */
if (!EMPTY(home_dir) && utils_filename_has_prefix(dirname, home_dir))
{
rest = dirname + strlen(home_dir);
if (*rest == G_DIR_SEPARATOR || *rest == '\0')
{
dirname = g_strdup_printf("~%s", rest);
g_free(tmp_dirname);
}
}
}
return dirname;
}
static gchar *parent_dir_name(GtkTreeStore *tree, GtkTreeIter *parent, const gchar *path)
{
gsize parent_len = 0;
gchar *dirname;
gchar *pathname = NULL;
if (parent)
{
gchar *parent_dir;
GtkTreeModel *model = GTK_TREE_MODEL(tree);
gtk_tree_model_get(model, parent, DOCUMENTS_FILENAME, &parent_dir, -1);
if (parent_dir)
{
pathname = get_doc_folder(parent_dir);
parent_len = strlen(pathname) + 1;
g_free(parent_dir);
}
}
dirname = get_doc_folder(path);
if (parent_len)
{
gsize len;
dirname = get_doc_folder(path);
len = strlen(dirname);
/* Maybe parent is /home but dirname is ~ (after substitution from /home/user) */
if (pathname[0] == dirname[0])
memmove(dirname, dirname + parent_len, len - parent_len + 1);
}
g_free(pathname);
return dirname;
}
static void tree_copy_node(GtkTreeStore *tree, GtkTreeIter *new_node, GtkTreeIter *node, GtkTreeIter *parent_new)
{
GIcon *icon;
gchar *filename;
gchar *shortname;
GdkColor *color;
GeanyDocument *doc;
GtkTreeModel *model = GTK_TREE_MODEL(tree);
gboolean fold;
gtk_tree_store_append(tree, new_node, parent_new);
gtk_tree_model_get(model, node,
DOCUMENTS_ICON, &icon,
DOCUMENTS_SHORTNAME, &shortname,
DOCUMENTS_DOCUMENT, &doc,
DOCUMENTS_COLOR, &color,
DOCUMENTS_FILENAME, &filename,
DOCUMENTS_FOLD, &fold,
-1);
if (doc)
doc->priv->iter = *new_node;
else
SETPTR(shortname, parent_dir_name(tree, parent_new, filename));
gtk_tree_store_set(tree, new_node,
DOCUMENTS_ICON, icon,
DOCUMENTS_SHORTNAME, shortname,
DOCUMENTS_DOCUMENT, doc,
DOCUMENTS_COLOR, color,
DOCUMENTS_FILENAME, filename,
DOCUMENTS_FOLD, fold,
-1);
g_free(filename);
g_free(shortname);
if (color)
gdk_color_free(color);
}
/* Helper that implements the recursive part of tree_reparent() */
static void tree_reparent_recurse(GtkTreeStore *tree, GtkTreeIter *node, GtkTreeIter *parent_new, GtkTreeIter *new_node)
{
GtkTreeModel *model = GTK_TREE_MODEL(tree);
GtkTreeIter child;
/* Start by copying the node itself. It becomes parent_new for the children to be copied. */
tree_copy_node(tree, new_node, node, parent_new);
if (gtk_tree_model_iter_nth_child(model, &child, node, 0))
{
do {
GtkTreeIter new_child;
tree_reparent_recurse(tree, &child, new_node, &new_child);
}
while (gtk_tree_model_iter_next(model, &child));
}
}
/*
* Copy node and all of its children to a new parent, and then remove the old node.
*
* It is done by reparenting the node itself to the new parent, creating a copy of it,
* and then recursively reparenting all children to the copy of the node.
*
* Finally, the new location will be written back to node so it's readily available,
* e.g. to unfold it.
* */
static void tree_reparent(GtkTreeStore *tree, GtkTreeIter *node, GtkTreeIter *parent_new)
{
GtkTreeIter new_node;
tree_reparent_recurse(tree, node, parent_new, &new_node);
gtk_tree_store_remove(tree, node);
*node = new_node;
}
static void tree_add_new_dir(GtkTreeStore *tree, GtkTreeIter *child, GtkTreeIter *parent, const gchar *file)
{
static GIcon *dir_icon = NULL;
gchar *dirname = parent_dir_name(tree, parent, file);
if (!dir_icon)
dir_icon = ui_get_mime_icon("inode/directory");
gtk_tree_store_append(tree, child, parent);
gtk_tree_store_set(tree, child,
DOCUMENTS_ICON, dir_icon,
DOCUMENTS_FILENAME, file,
DOCUMENTS_SHORTNAME, dirname,
DOCUMENTS_FOLD, TRUE, /* GTK inserts folded by default, caller may expand */
-1);
g_free(dirname);
}
/*
* Returns the position of dir delimiter where paths don't match
* */
static guint pathcmp(const gchar *s1, const gchar *s2)
{
guint i = 0;
gchar *a;
gchar *b;
g_return_val_if_fail(s1 != NULL, 0);
g_return_val_if_fail(s2 != NULL, 0);
#ifdef G_OS_WIN32
a = utils_utf8_strdown(s1);
if (NULL == a)
return 0;
b = utils_utf8_strdown(s2);
if (NULL == b)
{
g_free(a);
return 0;
}
#else
a = (gchar*)s1;
b = (gchar*)s2;
#endif
while (a[i] && b[i] && a[i] == b[i])
i++;
if (a[i] == '\0' && b[i] == '\0')
return i; /* strings are equal: a/b/c == a/b/c */
if ((a[i] == '\0' && b[i] == G_DIR_SEPARATOR) ||
(b[i] == '\0' && a[i] == G_DIR_SEPARATOR))
return i; /* subdir case: a/b/c == a/b */
while (i > 0 && (a[i] != G_DIR_SEPARATOR || b[i] != G_DIR_SEPARATOR))
i--; /* last slash: a/b/boo == a/b/bar */
#ifdef G_OS_WIN32
g_free(a);
g_free(b);
#endif
return i;
}
typedef struct TreeForeachData {
gchar *needle;
gsize best_len;
gsize needle_len;
GtkTreeIter best_iter;
enum {
TREE_CASE_NONE,
TREE_CASE_EQUALS,
TREE_CASE_CHILD_OF,
TREE_CASE_PARENT_OF,
TREE_CASE_HAVE_SAME_PARENT
} best_case;
} TreeForeachData;
static gboolean tree_foreach_callback(GtkTreeModel *model,
GtkTreePath *path,
GtkTreeIter *iter,
gpointer user_data)
{
gchar *name;
gchar *dirname;
guint diff;
gsize name_len;
GeanyDocument *doc;
TreeForeachData *data = (TreeForeachData*) user_data;
gtk_tree_model_get(model, iter, DOCUMENTS_FILENAME, &name, DOCUMENTS_DOCUMENT, &doc, -1);
if (doc) /* skip documents */
goto finally;
dirname = get_doc_folder(name);
if (dirname)
SETPTR(name, dirname);
diff = pathcmp(name, data->needle);
name_len = strlen(name);
if (diff == 0)
goto finally;
if (data->best_len < diff)
{
gint best_case;
gboolean tree = interface_prefs.openfiles_path_mode == OPENFILES_PATHS_TREE;
/* there are four cases */
/* first case: exact match. File is from already opened dir */
if (name_len == diff && data->needle_len == name_len)
best_case = TREE_CASE_EQUALS;
/* second case: split current dir. File is from deeper level */
else if (name_len == diff && tree)
best_case = TREE_CASE_CHILD_OF;
/* third case: split parent dir. File is from one of existing level */
else if (data->needle_len == diff && tree)
best_case = TREE_CASE_PARENT_OF;
/* fourth case: both dirs have same parent */
else if (tree)
best_case = TREE_CASE_HAVE_SAME_PARENT;
else
goto finally;
data->best_len = diff;
data->best_case = best_case;
data->best_iter = *iter;
}
finally:
g_free(name);
return FALSE;
}
/* Returns TRUE if parent points to a newly added row,
* caller might want to expand the relevant rows in the tree view */
static gboolean get_parent_for_file(GtkTreeStore *tree, const gchar *file, GtkTreeIter *parent)
{
gchar *path;
GtkTreeIter iter;
gint name_diff = 0;
gboolean has_parent;
GtkTreeModel *model = GTK_TREE_MODEL(tree);
TreeForeachData data = {NULL, 0, 0, {0}, TREE_CASE_NONE};
gboolean new_row;
path = g_path_get_dirname(file);
/* find best opened dir */
data.needle = get_doc_folder(path);
data.needle_len = strlen(data.needle);
name_diff = strlen(path) - data.needle_len;
gtk_tree_model_foreach(model, tree_foreach_callback, (gpointer)&data);
switch (data.best_case)
{
case TREE_CASE_EQUALS:
{
*parent = data.best_iter;
/* dir already open */
new_row = FALSE;
break;
}
case TREE_CASE_CHILD_OF:
{
/* This dir is longer than existing so just add child */
tree_add_new_dir(tree, parent, &data.best_iter, path);
new_row = TRUE;
break;
}
case TREE_CASE_PARENT_OF:
{
/* More complicated logic. This dir should be a parent
* of existing, so reparent existing dir.
*/
has_parent = gtk_tree_model_iter_parent(model, &iter, &data.best_iter);
tree_add_new_dir(tree, parent, has_parent ? &iter : NULL, path);
tree_reparent(tree, &data.best_iter, parent);
new_row = TRUE;
break;
}
case TREE_CASE_HAVE_SAME_PARENT:
{
/* Even more complicated logic. Both dirs have same
* parent, so create new parent and reparent them
*/
GtkTreeIter new_parent;
gchar *newpath = g_strndup(path, data.best_len + name_diff);
has_parent = gtk_tree_model_iter_parent(model, &iter, &data.best_iter);
tree_add_new_dir(tree, &new_parent, has_parent ? &iter : NULL, newpath);
tree_reparent(tree, &data.best_iter, &new_parent);
tree_add_new_dir(tree, parent, &new_parent, path);
g_free(newpath);
new_row = TRUE;
break;
}
default:
{
tree_add_new_dir(tree, parent, NULL, path);
new_row = TRUE;
break;
}
}
g_free(data.needle);
g_free(path);
return new_row;
}
/* Returns true when parent points to a newly added row. */
static gboolean sidebar_openfiles_add_iter(GtkTreeStore *tree, const gchar *file,
GtkTreeIter *iter, GtkTreeIter *parent)
{
gboolean new_row;
/* get_parent_for_file() might add rows for parent directories */
new_row = get_parent_for_file(tree, file, parent);
/* insert row for this file */
gtk_tree_store_append(tree, iter, parent);
return new_row;
}
static void expand_iter(GtkTreeIter *iter)
{
GtkTreePath *path;
path = gtk_tree_model_get_path(GTK_TREE_MODEL(store_openfiles), iter);
gtk_tree_view_expand_to_path(GTK_TREE_VIEW(tv.tree_openfiles), path);
gtk_tree_path_free(path);
}
/* Also sets doc->priv->iter.
* This is called recursively in sidebar_openfiles_update_all(). */
GEANY_EXPORT_SYMBOL
void sidebar_openfiles_add(GeanyDocument *doc)
{
GtkTreeIter *iter = &doc->priv->iter;
GtkTreeIter parent;
const gchar *filename = DOC_FILENAME(doc);
gchar *basename;
const GdkColor *color = document_get_status_color(doc);
static GIcon *file_icon = NULL;
gboolean expand = FALSE;
if (interface_prefs.openfiles_path_mode != OPENFILES_PATHS_NONE)
expand = sidebar_openfiles_add_iter(store_openfiles, filename, iter, &parent);
else
gtk_tree_store_append(store_openfiles, iter, NULL);
if (!file_icon)
file_icon = ui_get_mime_icon("text/plain");
basename = g_path_get_basename(filename);
gtk_tree_store_set(store_openfiles, iter,
DOCUMENTS_ICON, (doc->file_type && doc->file_type->icon) ? doc->file_type->icon : file_icon,
DOCUMENTS_SHORTNAME, basename, DOCUMENTS_DOCUMENT, doc, DOCUMENTS_COLOR, color,
DOCUMENTS_FILENAME, DOC_FILENAME(doc),
DOCUMENTS_FOLD, FALSE,
-1);
g_free(basename);
/* Expand new parent if necessary. Beware: this is executed by unit tests
* which don't create the tree view. */
if (expand && G_LIKELY(tv.tree_openfiles))
expand_iter(&parent);
}
/* Returns true if new_node points to a reparented directory, as a result of merging empty
* directories.
*/
void sidebar_openfiles_remove_iter(GtkTreeStore *tree, GtkTreeIter *iter_)
{
GtkTreeIter iter = *iter_;
GtkTreeIter parent;
GtkTreeModel *model = GTK_TREE_MODEL(store_openfiles);
/* walk on top and close all orphaned parents */
while (gtk_tree_model_iter_parent(model, &parent, &iter)
&& gtk_tree_model_iter_n_children(model, &parent) == 1)
{
iter = parent;
}
gtk_tree_store_remove(store_openfiles, &iter);
/* If, after removing, there is a single silbling left and it represents
* a directory, it can be merged with the parent directory row,
* essentially to reverse the effect of TREE_CASE_PARENT_OF and TREE_CASE_HAVE_SAME_PARENT
* in get_doc_parent(). Inherit fold state from the merged child as well.
*/
if (gtk_tree_store_iter_is_valid(store_openfiles, &parent)
&& gtk_tree_model_iter_n_children(model, &parent) == 1)
{
GeanyDocument *other_doc;
GtkTreeIter child, pparent;
gboolean fold, has_parent;
gtk_tree_model_iter_nth_child(model, &child, &parent, 0);
gtk_tree_model_get(model, &child, DOCUMENTS_DOCUMENT, &other_doc, -1);
if (!other_doc)
{
has_parent = gtk_tree_model_iter_parent(model, &pparent, &parent);
tree_reparent(store_openfiles, &child, has_parent ? &pparent : NULL);
gtk_tree_store_remove(store_openfiles, &parent);
/* Expand if the child node was expanded before the merge. */
gtk_tree_model_get(model, &child, DOCUMENTS_FOLD, &fold, -1);
if (!fold)
expand_iter(&child);
}
}
}
static void openfiles_remove(GeanyDocument *doc)
{
if (interface_prefs.openfiles_path_mode != OPENFILES_PATHS_NONE)
sidebar_openfiles_remove_iter(store_openfiles, &doc->priv->iter);
else
gtk_tree_store_remove(store_openfiles, &doc->priv->iter);
}
void sidebar_openfiles_update(GeanyDocument *doc)
{
GtkTreeIter *iter = &doc->priv->iter;
gchar *fname;
gtk_tree_model_get(GTK_TREE_MODEL(store_openfiles), iter, DOCUMENTS_FILENAME, &fname, -1);
if (utils_str_equal(fname, DOC_FILENAME(doc)))
{
/* just update color and the icon */
const GdkColor *color = document_get_status_color(doc);
GIcon *icon = doc->file_type->icon;
gtk_tree_store_set(store_openfiles, iter, DOCUMENTS_COLOR, color, -1);
if (icon)
gtk_tree_store_set(store_openfiles, iter, DOCUMENTS_ICON, icon, -1);
}
else
{
/* path has changed, so remove and re-add */
GtkTreeSelection *treesel;
gboolean sel;
treesel = gtk_tree_view_get_selection(GTK_TREE_VIEW(tv.tree_openfiles));
sel = gtk_tree_selection_iter_is_selected(treesel, &doc->priv->iter);
openfiles_remove(doc);
sidebar_openfiles_add(doc);
if (sel)
gtk_tree_selection_select_iter(treesel, &doc->priv->iter);
}
g_free(fname);
}
void sidebar_openfiles_update_all(void)
{
guint i;
gtk_tree_store_clear(store_openfiles);
foreach_document (i)
{
sidebar_openfiles_add(documents[i]);
}
}
void sidebar_remove_document(GeanyDocument *doc)
{
openfiles_remove(doc);
if (GTK_IS_WIDGET(doc->priv->tag_tree))
{
gtk_widget_destroy(doc->priv->tag_tree); /* make GTK release its references, if any */
/* Because it was ref'd in sidebar_update_tag_list, it needs unref'ing */
g_object_unref(doc->priv->tag_tree);
doc->priv->tag_tree = NULL;
}
}
static void on_hide_sidebar(void)
{
ui_prefs.sidebar_visible = FALSE;
ui_sidebar_show_hide();
}
static gboolean on_sidebar_display_symbol_list_show(GtkWidget *item)
{
gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(item),
interface_prefs.sidebar_symbol_visible);
return FALSE;
}
static gboolean on_sidebar_display_open_files_show(GtkWidget *item)
{
gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(item),
interface_prefs.sidebar_openfiles_visible);
return FALSE;
}
void sidebar_add_common_menu_items(GtkMenu *menu)
{
GtkWidget *item;
item = gtk_separator_menu_item_new();
gtk_widget_show(item);
gtk_container_add(GTK_CONTAINER(menu), item);
item = gtk_check_menu_item_new_with_mnemonic(_("Show S_ymbol List"));
gtk_container_add(GTK_CONTAINER(menu), item);
g_signal_connect(item, "draw", G_CALLBACK(on_sidebar_display_symbol_list_show), NULL);
gtk_widget_show(item);
g_signal_connect(item, "activate",
G_CALLBACK(on_list_symbol_activate), NULL);
item = gtk_check_menu_item_new_with_mnemonic(_("Show _Document List"));
gtk_container_add(GTK_CONTAINER(menu), item);
g_signal_connect(item, "draw", G_CALLBACK(on_sidebar_display_open_files_show), NULL);
gtk_widget_show(item);
g_signal_connect(item, "activate",
G_CALLBACK(on_list_document_activate), NULL);
item = gtk_image_menu_item_new_with_mnemonic(_("H_ide Sidebar"));
gtk_image_menu_item_set_image(GTK_IMAGE_MENU_ITEM(item),
gtk_image_new_from_stock(GTK_STOCK_CLOSE, GTK_ICON_SIZE_MENU));
gtk_widget_show(item);
gtk_container_add(GTK_CONTAINER(menu), item);
g_signal_connect(item, "activate", G_CALLBACK(on_hide_sidebar), NULL);
}
static void on_openfiles_show_paths_activate(GtkCheckMenuItem *item, gpointer user_data)
{
interface_prefs.openfiles_path_mode = GPOINTER_TO_INT(user_data);
sidebar_openfiles_update_all();
gtk_tree_view_expand_all(GTK_TREE_VIEW(tv.tree_openfiles));
sidebar_select_openfiles_item(document_get_current());
}
static void on_list_document_activate(GtkCheckMenuItem *item, gpointer user_data)
{
interface_prefs.sidebar_openfiles_visible = gtk_check_menu_item_get_active(item);
ui_sidebar_show_hide();
sidebar_tabs_show_hide(GTK_NOTEBOOK(main_widgets.sidebar_notebook), NULL, 0, NULL);
}
static void on_list_symbol_activate(GtkCheckMenuItem *item, gpointer user_data)
{
interface_prefs.sidebar_symbol_visible = gtk_check_menu_item_get_active(item);
ui_sidebar_show_hide();
sidebar_tabs_show_hide(GTK_NOTEBOOK(main_widgets.sidebar_notebook), NULL, 0, NULL);
}
static void on_find_in_files(GtkMenuItem *menuitem, gpointer user_data)
{
GtkTreeSelection *treesel;
GtkTreeIter iter;
GtkTreeModel *model;
GeanyDocument *doc;
gchar *dir;
treesel = gtk_tree_view_get_selection(GTK_TREE_VIEW(tv.tree_openfiles));
if (!gtk_tree_selection_get_selected(treesel, &model, &iter))
return;
gtk_tree_model_get(model, &iter, DOCUMENTS_DOCUMENT, &doc, -1);
if (!doc)
{
gtk_tree_model_get(model, &iter, DOCUMENTS_FILENAME, &dir, -1);
}
else
dir = g_path_get_dirname(DOC_FILENAME(doc));
search_show_find_in_files_dialog(dir);
g_free(dir);
}
static void on_openfiles_expand_collapse(GtkMenuItem *menuitem, gpointer user_data)
{
gboolean expand = GPOINTER_TO_INT(user_data);
if (expand)
gtk_tree_view_expand_all(GTK_TREE_VIEW(tv.tree_openfiles));
else
gtk_tree_view_collapse_all(GTK_TREE_VIEW(tv.tree_openfiles));
}
static void create_show_paths_popup_menu(void)
{
GSList *group = NULL;
const gchar *items[OPENFILES_PATHS_COUNT] = {
[OPENFILES_PATHS_NONE] = _("D_ocuments Only"),
[OPENFILES_PATHS_LIST] = _("Show _Paths"),
[OPENFILES_PATHS_TREE] = _("Show _Tree")
};
for (guint i = 0; i < G_N_ELEMENTS(items); i++)
{
GtkWidget *w = gtk_radio_menu_item_new_with_mnemonic(group, items[i]);
group = gtk_radio_menu_item_get_group(GTK_RADIO_MENU_ITEM(w));
gtk_widget_show(w);
gtk_container_add(GTK_CONTAINER(openfiles_popup_menu), w);
g_signal_connect(w, "activate",
G_CALLBACK(on_openfiles_show_paths_activate), GINT_TO_POINTER(i));
doc_items.show_paths[i] = w;
}
}
static void create_openfiles_popup_menu(void)
{
GtkWidget *item;
openfiles_popup_menu = gtk_menu_new();
item = gtk_image_menu_item_new_from_stock(GTK_STOCK_CLOSE, NULL);
gtk_widget_show(item);
gtk_container_add(GTK_CONTAINER(openfiles_popup_menu), item);
g_signal_connect(item, "activate",
G_CALLBACK(on_openfiles_document_action), GINT_TO_POINTER(OPENFILES_ACTION_REMOVE));
doc_items.close = item;
item = gtk_separator_menu_item_new();
gtk_widget_show(item);
gtk_container_add(GTK_CONTAINER(openfiles_popup_menu), item);
item = gtk_image_menu_item_new_from_stock(GTK_STOCK_SAVE, NULL);
gtk_widget_show(item);
gtk_container_add(GTK_CONTAINER(openfiles_popup_menu), item);
g_signal_connect(item, "activate",
G_CALLBACK(on_openfiles_document_action), GINT_TO_POINTER(OPENFILES_ACTION_SAVE));
doc_items.save = item;
item = gtk_image_menu_item_new_with_mnemonic(_("_Reload"));
gtk_image_menu_item_set_image(GTK_IMAGE_MENU_ITEM(item),
gtk_image_new_from_stock(GTK_STOCK_REVERT_TO_SAVED, GTK_ICON_SIZE_MENU));
gtk_widget_show(item);
gtk_container_add(GTK_CONTAINER(openfiles_popup_menu), item);
g_signal_connect(item, "activate",
G_CALLBACK(on_openfiles_document_action), GINT_TO_POINTER(OPENFILES_ACTION_RELOAD));
doc_items.reload = item;
item = gtk_separator_menu_item_new();
gtk_widget_show(item);
gtk_container_add(GTK_CONTAINER(openfiles_popup_menu), item);
item = ui_image_menu_item_new(GTK_STOCK_FIND, _("_Find in Files..."));
gtk_widget_show(item);
gtk_container_add(GTK_CONTAINER(openfiles_popup_menu), item);
g_signal_connect(item, "activate", G_CALLBACK(on_find_in_files), NULL);
doc_items.find_in_files = item;
item = gtk_separator_menu_item_new();
gtk_widget_show(item);
gtk_container_add(GTK_CONTAINER(openfiles_popup_menu), item);
create_show_paths_popup_menu();
item = gtk_separator_menu_item_new();
gtk_widget_show(item);
gtk_container_add(GTK_CONTAINER(openfiles_popup_menu), item);
doc_items.expand_all = ui_image_menu_item_new(GTK_STOCK_ADD, _("_Expand All"));
gtk_widget_show(doc_items.expand_all);
gtk_container_add(GTK_CONTAINER(openfiles_popup_menu), doc_items.expand_all);
g_signal_connect(doc_items.expand_all, "activate",
G_CALLBACK(on_openfiles_expand_collapse), GINT_TO_POINTER(TRUE));
doc_items.collapse_all = ui_image_menu_item_new(GTK_STOCK_REMOVE, _("_Collapse All"));
gtk_widget_show(doc_items.collapse_all);
gtk_container_add(GTK_CONTAINER(openfiles_popup_menu), doc_items.collapse_all);
g_signal_connect(doc_items.collapse_all, "activate",
G_CALLBACK(on_openfiles_expand_collapse), GINT_TO_POINTER(FALSE));
sidebar_add_common_menu_items(GTK_MENU(openfiles_popup_menu));
}
/* compares the given data with the doc pointer from the selected row of openfiles
* treeview, in case of a match the row is selected and TRUE is returned
* (called indirectly from gtk_tree_model_foreach()) */
static gboolean tree_model_find_node(GtkTreeModel *model,
GtkTreePath *path,
GtkTreeIter *iter,
gpointer data)
{
GeanyDocument *doc;
gtk_tree_model_get(model, iter, DOCUMENTS_DOCUMENT, &doc, -1);
if (doc == data)
{
gtk_tree_view_expand_to_path(GTK_TREE_VIEW(tv.tree_openfiles), path);
gtk_tree_view_set_cursor(GTK_TREE_VIEW(tv.tree_openfiles), path, NULL, FALSE);
return TRUE;
}
return FALSE;
}
void sidebar_select_openfiles_item(GeanyDocument *doc)
{
gtk_tree_model_foreach(GTK_TREE_MODEL(store_openfiles), tree_model_find_node, doc);
}
/* callbacks */
static void document_action(GeanyDocument *doc, gint action)
{
if (! DOC_VALID(doc))
return;
switch (action)
{
case OPENFILES_ACTION_REMOVE:
{
document_close(doc);
break;
}
case OPENFILES_ACTION_SAVE:
{
document_save_file(doc, FALSE);
break;
}
case OPENFILES_ACTION_RELOAD:
{
document_reload_prompt(doc, NULL);
break;
}
}
}
static void on_openfiles_document_action_apply(GtkTreeModel *model, GtkTreeIter iter, gint action)
{
GeanyDocument *doc;
gtk_tree_model_get(model, &iter, DOCUMENTS_DOCUMENT, &doc, -1);
if (doc)
{
document_action(doc, action);
}
else
{
/* parent item selected */
GtkTreeIter child;
gint i = gtk_tree_model_iter_n_children(model, &iter) - 1;
while (i >= 0 && gtk_tree_model_iter_nth_child(model, &child, &iter, i))
{
on_openfiles_document_action_apply(model, child, action);
i--;
}
}
}
static void on_openfiles_document_action(GtkMenuItem *menuitem, gpointer user_data)
{
GtkTreeIter iter;
GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(tv.tree_openfiles));
GtkTreeModel *model;
gint action = GPOINTER_TO_INT(user_data);
if (gtk_tree_selection_get_selected(selection, &model, &iter))
on_openfiles_document_action_apply(model, iter, action);
}
static void change_focus_to_editor(GeanyDocument *doc, GtkWidget *source_widget)
{
if (may_steal_focus)
document_try_focus(doc, source_widget);
may_steal_focus = FALSE;
}
static gboolean openfiles_go_to_selection(GtkTreeSelection *selection, guint keyval)
{
GtkTreeIter iter;
GtkTreeModel *model;
GeanyDocument *doc = NULL;
/* use switch_notebook_page to ignore changing the notebook page because it is already done */
if (gtk_tree_selection_get_selected(selection, &model, &iter) && ! ignore_callback)
{
gtk_tree_model_get(model, &iter, DOCUMENTS_DOCUMENT, &doc, -1);
if (! doc)
return FALSE; /* parent */
/* switch to the doc and grab the focus */
document_show_tab(doc);
if (keyval != GDK_KEY_space)
change_focus_to_editor(doc, tv.tree_openfiles);
}
return FALSE;
}
static gboolean taglist_go_to_selection(GtkTreeSelection *selection, guint keyval, guint state)
{
GtkTreeIter iter;
GtkTreeModel *model;
gint line = 0;
gboolean handled = TRUE;
if (gtk_tree_selection_get_selected(selection, &model, &iter))
{
TMTag *tag;
gtk_tree_model_get(model, &iter, SYMBOLS_COLUMN_TAG, &tag, -1);
if (! tag)
return FALSE;
line = tag->line;
if (line > 0)
{
GeanyDocument *doc = document_get_current();
if (doc != NULL)
{
navqueue_goto_line(doc, doc, line);
state = keybindings_get_modifiers(state);
if (keyval != GDK_KEY_space && ! (state & GEANY_PRIMARY_MOD_MASK))
change_focus_to_editor(doc, NULL);
else
handled = FALSE;
}
}
tm_tag_unref(tag);
}
return handled;
}
static gboolean sidebar_key_press_cb(GtkWidget *widget, GdkEventKey *event,
gpointer user_data)
{
may_steal_focus = FALSE;
if (ui_is_keyval_enter_or_return(event->keyval) || event->keyval == GDK_KEY_space)
{
GtkWidgetClass *widget_class = GTK_WIDGET_GET_CLASS(widget);
GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(widget));
may_steal_focus = TRUE;
/* force the TreeView handler to run before us for it to do its job (selection & stuff).
* doing so will prevent further handlers to be run in most cases, but the only one is our
* own, so guess it's fine. */
if (widget_class->key_press_event)
widget_class->key_press_event(widget, event);
if (widget == tv.tree_openfiles) /* tag and doc list have separate handlers */
openfiles_go_to_selection(selection, event->keyval);
else
taglist_go_to_selection(selection, event->keyval, event->state);
return TRUE;
}
return FALSE;
}
static gboolean sidebar_button_press_cb(GtkWidget *widget, GdkEventButton *event,
G_GNUC_UNUSED gpointer user_data)
{
GtkTreeSelection *selection;
GtkWidgetClass *widget_class = GTK_WIDGET_GET_CLASS(widget);
gboolean handled = FALSE;
/* force the TreeView handler to run before us for it to do its job (selection & stuff).
* doing so will prevent further handlers to be run in most cases, but the only one is our own,
* so guess it's fine. */
if (widget_class->button_press_event)
handled = widget_class->button_press_event(widget, event);
selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(widget));
may_steal_focus = TRUE;
if (event->type == GDK_2BUTTON_PRESS)
{ /* double click on parent node(section) expands/collapses it */
GtkTreeModel *model;
GtkTreeIter iter;
if (gtk_tree_selection_get_selected(selection, &model, &iter))
{
if (gtk_tree_model_iter_has_child(model, &iter))
{
GtkTreePath *path = gtk_tree_model_get_path(model, &iter);
if (gtk_tree_view_row_expanded(GTK_TREE_VIEW(widget), path))
gtk_tree_view_collapse_row(GTK_TREE_VIEW(widget), path);
else
gtk_tree_view_expand_row(GTK_TREE_VIEW(widget), path, FALSE);
gtk_tree_path_free(path);
return TRUE;
}
}
}
else if (event->button == 1)
{ /* allow reclicking of taglist treeview item */
if (widget == tv.tree_openfiles)
{
openfiles_go_to_selection(selection, 0);
handled = TRUE;
}
else
handled = taglist_go_to_selection(selection, 0, event->state);
}
else if (event->button == 2)
{
if (widget == tv.tree_openfiles)
on_openfiles_document_action(NULL, GINT_TO_POINTER(OPENFILES_ACTION_REMOVE));
}
else if (event->button == 3)
{
if (widget == tv.tree_openfiles)
{
if (!openfiles_popup_menu)
create_openfiles_popup_menu();
/* update menu item sensitivity */
documents_menu_update(selection);
gtk_menu_popup(GTK_MENU(openfiles_popup_menu), NULL, NULL, NULL, NULL,
event->button, event->time);
}
else
{
gtk_menu_popup(GTK_MENU(tv.popup_taglist), NULL, NULL, NULL, NULL,
event->button, event->time);
}
handled = TRUE;
}
return handled;
}
static void documents_menu_update(GtkTreeSelection *selection)
{
GtkTreeModel *model;
GtkTreeIter iter;
gboolean sel, path;
gchar *shortname = NULL;
GeanyDocument *doc = NULL;
/* maybe no selection e.g. if ctrl-click deselected */
sel = gtk_tree_selection_get_selected(selection, &model, &iter);
if (sel)
{
gtk_tree_model_get(model, &iter,
DOCUMENTS_DOCUMENT, &doc,
DOCUMENTS_SHORTNAME, &shortname,
-1);
}
path = !EMPTY(shortname) &&
(g_path_is_absolute(shortname) ||
(app->project && g_str_has_prefix(shortname, app->project->name)));
/* can close all, save all (except shortname), but only reload individually ATM */
gtk_widget_set_sensitive(doc_items.close, sel);
gtk_widget_set_sensitive(doc_items.save, (doc && doc->real_path) || path);
gtk_widget_set_sensitive(doc_items.reload, doc && doc->real_path);
gtk_widget_set_sensitive(doc_items.find_in_files, sel);
g_free(shortname);
gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(doc_items.show_paths[interface_prefs.openfiles_path_mode]), TRUE);
gtk_widget_set_sensitive(doc_items.expand_all, interface_prefs.openfiles_path_mode);
gtk_widget_set_sensitive(doc_items.collapse_all, interface_prefs.openfiles_path_mode);
}
static StashGroup *stash_group = NULL;
static void on_load_settings(void)
{
if (interface_prefs.openfiles_path_mode < 0
|| interface_prefs.openfiles_path_mode >= OPENFILES_PATHS_COUNT)
interface_prefs.openfiles_path_mode = OPENFILES_PATHS_TREE;
tag_window = ui_lookup_widget(main_widgets.window, "scrolledwindow2");
prepare_openfiles();
/* note: ui_prefs.sidebar_page is reapplied after plugins are loaded */
stash_group_display(stash_group, NULL);
sidebar_tabs_show_hide(GTK_NOTEBOOK(main_widgets.sidebar_notebook), NULL, 0, NULL);
}
static void on_save_settings(void)
{
stash_group_update(stash_group, NULL);
sidebar_tabs_show_hide(GTK_NOTEBOOK(main_widgets.sidebar_notebook), NULL, 0, NULL);
}
static void on_sidebar_switch_page(GtkNotebook *notebook,
gpointer page, guint page_num, gpointer user_data)
{
if (page_num == TREEVIEW_SYMBOL)
sidebar_update_tag_list(document_get_current(), FALSE);
}
void sidebar_init(void)
{
StashGroup *group;
group = stash_group_new(PACKAGE);
stash_group_add_widget_property(group, &ui_prefs.sidebar_page, "sidebar_page", GINT_TO_POINTER(0),
main_widgets.sidebar_notebook, "page", 0);
configuration_add_session_group(group, FALSE);
stash_group = group;
/* Delay building documents treeview until sidebar font has been read and prefs are sanitized */
g_signal_connect(geany_object, "load-settings", on_load_settings, NULL);
g_signal_connect(geany_object, "save-settings", on_save_settings, NULL);
g_signal_connect(main_widgets.sidebar_notebook, "page-added",
G_CALLBACK(sidebar_tabs_show_hide), NULL);
g_signal_connect(main_widgets.sidebar_notebook, "page-removed",
G_CALLBACK(sidebar_tabs_show_hide), NULL);
/* tabs may have changed when sidebar is reshown */
g_signal_connect(main_widgets.sidebar_notebook, "show",
G_CALLBACK(sidebar_tabs_show_hide), NULL);
g_signal_connect_after(main_widgets.sidebar_notebook, "switch-page",
G_CALLBACK(on_sidebar_switch_page), NULL);
}
#define WIDGET(w) w && GTK_IS_WIDGET(w)
void sidebar_finalize(void)
{
if (WIDGET(tv.default_tag_tree))
{
gtk_widget_destroy(tv.default_tag_tree); /* make GTK release its references, if any... */
g_object_unref(tv.default_tag_tree); /* ...and release our own */
}
if (WIDGET(tv.popup_taglist))
gtk_widget_destroy(tv.popup_taglist);
if (WIDGET(openfiles_popup_menu))
gtk_widget_destroy(openfiles_popup_menu);
}
void sidebar_focus_openfiles_tab(void)
{
if (ui_prefs.sidebar_visible && interface_prefs.sidebar_openfiles_visible)
{
GtkNotebook *notebook = GTK_NOTEBOOK(main_widgets.sidebar_notebook);
gtk_notebook_set_current_page(notebook, TREEVIEW_OPENFILES);
gtk_widget_grab_focus(tv.tree_openfiles);
}
}
void sidebar_focus_symbols_tab(void)
{
if (ui_prefs.sidebar_visible && interface_prefs.sidebar_symbol_visible)
{
GtkNotebook *notebook = GTK_NOTEBOOK(main_widgets.sidebar_notebook);
GtkWidget *symbol_list_scrollwin = gtk_notebook_get_nth_page(notebook, TREEVIEW_SYMBOL);
gtk_notebook_set_current_page(notebook, TREEVIEW_SYMBOL);
gtk_widget_grab_focus(gtk_bin_get_child(GTK_BIN(symbol_list_scrollwin)));
}
}
static void sidebar_tabs_show_hide(GtkNotebook *notebook, GtkWidget *child,
guint page_num, gpointer data)
{
gint tabs = gtk_notebook_get_n_pages(notebook);
if (interface_prefs.sidebar_symbol_visible == FALSE)
tabs--;
if (interface_prefs.sidebar_openfiles_visible == FALSE)
tabs--;
gtk_notebook_set_show_tabs(notebook, (tabs > 1));
}