Branch: refs/heads/master Author: Thomas Martitz thomas.martitz@mailbox.org Committer: GitHub noreply@github.com Date: Wed, 08 Jun 2022 12:47:23 UTC Commit: def1600a5bcdcf2729f9580b04f531179f5f5f13 https://github.com/geany/geany/commit/def1600a5bcdcf2729f9580b04f531179f5f5f...
Log Message: ----------- Merge PR #1813
This adds a tree view mode to the document list in the sidebar and enables it by default.
Based on previous work by Pavel Roschin roshin@scriptumplus.ru, see #259
Modified Paths: -------------- NEWS configure.ac doc/Makefile.am doc/geany.txt doc/images/sidebar_documents_only.png doc/images/sidebar_show_paths.png doc/images/sidebar_show_tree.png doc/meson.build m4/geany-gtk.m4 meson.build src/Makefile.am src/keyfile.c src/libmain.c src/main.h src/sidebar.c src/sidebar.h src/ui_utils.c src/ui_utils.h src/utils.c src/utils.h tests/Makefile.am tests/meson.build tests/test_sidebar.c
Modified: NEWS 4 lines changed, 3 insertions(+), 1 deletions(-) =================================================================== @@ -1,5 +1,7 @@ Geany 1.39 (unreleased) - + Interface + * The document list in the sidebar has a new tree view. This mode is the + new default and existing installations automatically use it (PR#1813).
Geany 1.38 (October 09, 2021)
Modified: configure.ac 20 lines changed, 2 insertions(+), 18 deletions(-) =================================================================== @@ -88,24 +88,8 @@ AC_CHECK_DECLS([_NSGetEnviron],,,[[#include <crt_externs.h>]]) GEANY_CHECK_REVISION([dnl force debug mode for a VCS working copy CFLAGS="-g -DGEANY_DEBUG $CFLAGS"])
-# GTK/GLib/GIO checks -gtk_modules="gtk+-3.0 >= 3.0 glib-2.0 >= 2.32" -gtk_modules_private="gio-2.0 >= 2.32 gmodule-no-export-2.0" -PKG_CHECK_MODULES([GTK], [$gtk_modules $gtk_modules_private]) -AC_SUBST([DEPENDENCIES], [$gtk_modules]) -AS_VAR_APPEND([GTK_CFLAGS], [" -DGLIB_VERSION_MIN_REQUIRED=GLIB_VERSION_2_32"]) -dnl Disable all GTK deprecations -AS_VAR_APPEND([GTK_CFLAGS], [" -DGDK_DISABLE_DEPRECATION_WARNINGS"]) -AC_SUBST([GTK_CFLAGS]) -AC_SUBST([GTK_LIBS]) -GTK_VERSION=`$PKG_CONFIG --modversion gtk+-3.0` -AC_SUBST([GTK_VERSION]) -GEANY_STATUS_ADD([Using GTK version], [${GTK_VERSION}]) -# GTHREAD checks -gthread_modules="gthread-2.0" -PKG_CHECK_MODULES([GTHREAD], [$gthread_modules]) -AC_SUBST([GTHREAD_CFLAGS]) -AC_SUBST([GTHREAD_LIBS]) +GEANY_CHECK_GTK +GEANY_CHECK_GTK_FUNCS([g_strv_equal])
# --disable-deprecated switch for GTK purification AC_ARG_ENABLE([deprecated],
Modified: doc/Makefile.am 5 lines changed, 4 insertions(+), 1 deletions(-) =================================================================== @@ -23,7 +23,10 @@ dist_htmldocimages_DATA = \ images/pref_dialog_tools.png \ images/pref_dialog_various.png \ images/pref_dialog_vte.png \ - images/replace_dialog.png + images/replace_dialog.png \ + images/sidebar_documents_only.png \ + images/sidebar_show_paths.png \ + images/sidebar_show_tree.png endif
doc_DATA = \
Modified: doc/geany.txt 44 lines changed, 43 insertions(+), 1 deletions(-) =================================================================== @@ -229,7 +229,7 @@ The workspace has the following parts: * An optional toolbar. * An optional sidebar that can show the following tabs:
- * Documents - A document list, and + * Documents - A document list, see `Document list views`_. * Symbols - A list of symbols in your code.
* The main editor window. @@ -513,6 +513,48 @@ order. It is not alphabetical as shown in the documents list See the `Notebook tab keybindings`_ section for useful shortcuts including for Most-Recently-Used document switching.
+Document list views +^^^^^^^^^^^^^^^^^^^ + +There are three different ways to display documents on the sidebar if *Show +documents list* is active. To switch between views press the right mouse button +on the documents list and select one of these items: + +Documents Only + Show only file names of open documents in sorted order. + + .. image:: ./images/sidebar_documents_only.png + +Show Paths + Show open documents as a two-level tree in which first level is the paths + of directories containing open files and the second level is the file names of + the documents open in that path. All documents with the same path are grouped + together under the same first level item. Paths are in sorted order and + documents are sorted within each group. + + .. image:: ./images/sidebar_show_paths.png + +Show Tree + Show paths as above, but as a multiple level partial tree. The tree is only + expanded at positions where two or more directory paths to open documents + share the same prefix. The common prefix is shown as a parent level, and + the remainder of those paths are shown as child levels. This applies + recursively down the paths making a tree to the file names of open documents, + which are grouped in sorted order as an additional level below the last path + segment. + + For convenience two common file locations are handled specially, open + files below the users home directory and open files below an open project + base path. Each of these is moved to its own top level tree instead of + being in place in the normal tree. The top level of these trees are each + labelled differently. For the home directory tree the path of the home + directory is shown as ``~``, and for the project tree the path to the project + base path is shown simply as the project name. + + .. image:: ./images/sidebar_show_tree.png + +In all cases paths and file names that do not fit in the width available are ellipsised. + Cloning documents ^^^^^^^^^^^^^^^^^ The `Document->Clone` menu item copies the current document's text,
Modified: doc/images/sidebar_documents_only.png 0 lines changed, 0 insertions(+), 0 deletions(-) =================================================================== No diff available, check online
Modified: doc/images/sidebar_show_paths.png 0 lines changed, 0 insertions(+), 0 deletions(-) =================================================================== No diff available, check online
Modified: doc/images/sidebar_show_tree.png 0 lines changed, 0 insertions(+), 0 deletions(-) =================================================================== No diff available, check online
Modified: doc/meson.build 3 lines changed, 3 insertions(+), 0 deletions(-) =================================================================== @@ -39,6 +39,9 @@ if tarball.returncode() == 0 or rst2html.found() 'images/pref_dialog_various.png', 'images/pref_dialog_vte.png', 'images/replace_dialog.png', + 'images/sidebar_documents_only.png', + 'images/sidebar_show_paths.png', + 'images/sidebar_show_tree.png', install_dir: join_paths(cdata.get('GEANY_DOC_DIR'), 'html', 'images') ) if tarball.returncode() == 0
Modified: m4/geany-gtk.m4 33 lines changed, 33 insertions(+), 0 deletions(-) =================================================================== @@ -0,0 +1,33 @@ +dnl GEANY_CHECK_GTK +dnl Checks whether the GTK stack is available and new enough. Sets GTK_CFLAGS and GTK_LIBS. +AC_DEFUN([GEANY_CHECK_GTK], +[ + gtk_modules="gtk+-3.0 >= 3.0 glib-2.0 >= 2.32" + gtk_modules_private="gio-2.0 >= 2.32 gmodule-no-export-2.0 gthread-2.0" + + PKG_CHECK_MODULES([GTK], [$gtk_modules $gtk_modules_private]) + AC_SUBST([DEPENDENCIES], [$gtk_modules]) + AS_VAR_APPEND([GTK_CFLAGS], [" -DGLIB_VERSION_MIN_REQUIRED=GLIB_VERSION_2_32"]) + dnl Disable all GTK deprecations + AS_VAR_APPEND([GTK_CFLAGS], [" -DGDK_DISABLE_DEPRECATION_WARNINGS"]) + AC_SUBST([GTK_CFLAGS]) + AC_SUBST([GTK_LIBS]) + AC_SUBST([GTK_VERSION],[`$PKG_CONFIG --modversion gtk+-3.0`]) + + GEANY_STATUS_ADD([Using GTK version], [${GTK_VERSION}]) +]) + +dnl GEANY_CHECK_GTK_FUNCS +dnl Like AC_CHECK_FUNCS but adds GTK flags so that tests for GLib/GTK functions may succeed. +AC_DEFUN([GEANY_CHECK_GTK_FUNCS], +[ + AC_REQUIRE([GEANY_CHECK_GTK]) + + CFLAGS_save=$CFLAGS + CFLAGS=$GTK_CFLAGS + LIBS_save=$LIBS + LIBS=$GTK_LIBS + AC_CHECK_FUNCS([$1]) + CFLAGS=$CFLAGS_save + LIBS=$LIBS_save +])
Modified: meson.build 6 lines changed, 3 insertions(+), 3 deletions(-) =================================================================== @@ -87,7 +87,7 @@ check_functions = [ ['truncate', '#include <unistd.h>'], ['wcrtomb', '#include <wchar.h>'], ['wcscoll', '#include <wchar.h>'], - + ['g_strv_equal', '#include <glib.h>'] ]
foreach h : check_headers @@ -102,7 +102,7 @@ endforeach foreach f : check_functions define = 'HAVE_' + f.get(0).underscorify().to_upper() ccprefix = '\n'.join([gnu_source ? '#define _GNU_SOURCE' : '', f.get(1)]) - if cc.has_function(f.get(0), prefix : ccprefix) + if cc.has_function(f.get(0), prefix: ccprefix, dependencies: deps) cdata.set(define, 1) else cdata.set(define, false) @@ -857,7 +857,7 @@ libgeany = shared_library('geany', ) dep_libgeany = declare_dependency( link_with: libgeany, - include_directories: [igeany] + include_directories: [iscintilla, itagmanager, igeany] )
executable('geany',
Modified: src/Makefile.am 5 lines changed, 2 insertions(+), 3 deletions(-) =================================================================== @@ -20,14 +20,14 @@ AM_CPPFLAGS = \ -DGTK \ -DGEANY_PRIVATE \ -DG_LOG_DOMAIN=""Geany"" \ - @GTK_CFLAGS@ @GTHREAD_CFLAGS@ \ + @GTK_CFLAGS@ \ $(MAC_INTEGRATION_CFLAGS)
bin_PROGRAMS = geany lib_LTLIBRARIES = libgeany.la
geany_SOURCES = main.c -geany_LDADD = libgeany.la $(GTK_LIBS) $(GTHREAD_LIBS) $(INTLLIBS) +geany_LDADD = libgeany.la $(GTK_LIBS) $(INTLLIBS) geany_LDFLAGS =
if ENABLE_BINRELOC @@ -124,7 +124,6 @@ libgeany_la_LIBADD = \ $(top_builddir)/scintilla/libscintilla.la \ $(builddir)/tagmanager/libtagmanager.la \ @GTK_LIBS@ \ - @GTHREAD_LIBS@ \ $(MAC_INTEGRATION_LIBS) \ $(INTLLIBS)
Modified: src/keyfile.c 2 lines changed, 1 insertions(+), 1 deletions(-) =================================================================== @@ -187,7 +187,7 @@ static void init_pref_groups(void) stash_group_add_toggle_button(group, &file_prefs.tab_close_switch_to_mru, "tab_close_switch_to_mru", FALSE, "check_tab_close_switch_to_mru"); stash_group_add_integer(group, &interface_prefs.tab_pos_sidebar, "tab_pos_sidebar", GTK_POS_TOP); - stash_group_add_integer(group, &interface_prefs.documents_show_paths, "documents_show_paths", SHOW_PATHS_LIST); + stash_group_add_integer(group, &interface_prefs.openfiles_path_mode, "openfiles_path_mode", -1); stash_group_add_radio_buttons(group, &interface_prefs.sidebar_pos, "sidebar_pos", GTK_POS_LEFT, "radio_sidebar_left", GTK_POS_LEFT,
Modified: src/libmain.c 25 lines changed, 16 insertions(+), 9 deletions(-) =================================================================== @@ -1018,20 +1018,12 @@ static const gchar *get_locale(void)
GEANY_EXPORT_SYMBOL -gint main_lib(gint argc, gchar **argv) +void main_init_headless(void) { - GeanyDocument *doc; - gint config_dir_result; - const gchar *locale; - gchar *utf8_configdir; - gchar *os_info; - #if ! GLIB_CHECK_VERSION(2, 36, 0) g_type_init(); #endif
- log_handlers_init(); - app = g_new0(GeanyApp, 1); memset(&main_status, 0, sizeof(GeanyStatus)); memset(&prefs, 0, sizeof(GeanyPrefs)); @@ -1043,6 +1035,21 @@ gint main_lib(gint argc, gchar **argv) memset(&template_prefs, 0, sizeof(GeanyTemplatePrefs)); memset(&ui_prefs, 0, sizeof(UIPrefs)); memset(&ui_widgets, 0, sizeof(UIWidgets)); +} + + +GEANY_EXPORT_SYMBOL +gint main_lib(gint argc, gchar **argv) +{ + GeanyDocument *doc; + gint config_dir_result; + const gchar *locale; + gchar *utf8_configdir; + gchar *os_info; + + main_init_headless(); + + log_handlers_init();
setup_paths();
Modified: src/main.h 2 lines changed, 2 insertions(+), 0 deletions(-) =================================================================== @@ -76,6 +76,8 @@ void main_load_project_from_command_line(const gchar *locale_filename, gboolean
gint main_lib(gint argc, gchar **argv);
+void main_init_headless(void); + #endif /* GEANY_PRIVATE */
G_END_DECLS
Modified: src/sidebar.c 818 lines changed, 651 insertions(+), 167 deletions(-) =================================================================== @@ -56,12 +56,12 @@ static struct GtkWidget *close; GtkWidget *save; GtkWidget *reload; - GtkWidget *show_paths; + GtkWidget *show_paths[OPENFILES_PATHS_COUNT]; GtkWidget *find_in_files; GtkWidget *expand_all; GtkWidget *collapse_all; } -doc_items = {NULL, NULL, NULL, NULL, NULL, NULL, NULL}; +doc_items;
enum { @@ -76,17 +76,7 @@ enum OPENFILES_ACTION_RELOAD };
-/* documents tree model columns */ -enum -{ - DOCUMENTS_ICON, - DOCUMENTS_SHORTNAME, /* dirname for parents, basename for children */ - DOCUMENTS_DOCUMENT, - DOCUMENTS_COLOR, - DOCUMENTS_FILENAME /* full filename */ -}; - -static GtkTreeStore *store_openfiles; +static GtkTreeStore *store_openfiles; static GtkWidget *openfiles_popup_menu; static GtkWidget *tag_window; /* scrolled window that holds the symbol list GtkTreeView */
@@ -199,8 +189,8 @@ void sidebar_update_tag_list(GeanyDocument *doc, gboolean update) /* 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 + /* 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()) \ { \ @@ -253,23 +243,161 @@ void sidebar_update_tag_list(GeanyDocument *doc, gboolean update) static gint documents_sort_func(GtkTreeModel *model, GtkTreeIter *iter_a, GtkTreeIter *iter_b, gpointer data) { - gchar *key_a, *key_b; gchar *name_a, *name_b; + GeanyDocument *doc_a, *doc_b; gint cmp;
- gtk_tree_model_get(model, iter_a, DOCUMENTS_SHORTNAME, &name_a, -1); - key_a = g_utf8_collate_key_for_filename(name_a, -1); - g_free(name_a); - gtk_tree_model_get(model, iter_b, DOCUMENTS_SHORTNAME, &name_b, -1); - key_b = g_utf8_collate_key_for_filename(name_b, -1); + 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); - cmp = strcmp(key_a, key_b); - g_free(key_b); - g_free(key_a); + 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) @@ -278,16 +406,21 @@ static void prepare_openfiles(void) GtkCellRenderer *text_renderer; GtkTreeViewColumn *column; GtkTreeSelection *selection; - GtkTreeSortable *sortable;
tv.tree_openfiles = ui_lookup_widget(main_widgets.window, "treeview6");
- /* 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_openfiles = gtk_tree_store_new(5, G_TYPE_ICON, G_TYPE_STRING, - G_TYPE_POINTER, GDK_TYPE_COLOR, G_TYPE_STRING); + 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( @@ -310,11 +443,6 @@ static void prepare_openfiles(void) gtk_tree_view_set_search_column(GTK_TREE_VIEW(tv.tree_openfiles), DOCUMENTS_SHORTNAME);
- /* sort opened filenames in the store_openfiles treeview */ - sortable = GTK_TREE_SORTABLE(GTK_TREE_MODEL(store_openfiles)); - 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); - ui_widget_modify_font_from_string(tv.tree_openfiles, interface_prefs.tagbar_font);
/* tooltips */ @@ -332,28 +460,6 @@ static void prepare_openfiles(void) }
-/* iter should be toplevel */ -static gboolean find_tree_iter_dir(GtkTreeIter *iter, const gchar *dir) -{ - GeanyDocument *doc; - gchar *name; - gboolean result; - - if (utils_str_equal(dir, ".")) - dir = GEANY_STRING_UNTITLED; - - gtk_tree_model_get(GTK_TREE_MODEL(store_openfiles), iter, DOCUMENTS_DOCUMENT, &doc, -1); - g_return_val_if_fail(!doc, FALSE); - - gtk_tree_model_get(GTK_TREE_MODEL(store_openfiles), iter, DOCUMENTS_SHORTNAME, &name, -1); - - result = utils_filenamecmp(name, dir) == 0; - g_free(name); - - return result; -} - - static gboolean utils_filename_has_prefix(const gchar *str, const gchar *prefix) { gchar *head = g_strndup(str, strlen(prefix)); @@ -364,12 +470,10 @@ static gboolean utils_filename_has_prefix(const gchar *str, const gchar *prefix) }
-static gchar *get_doc_folder(const gchar *path) +static gchar *get_project_folder(const gchar *path) { - gchar *tmp_dirname = g_strdup(path); gchar *project_base_path; gchar *dirname = NULL; - const gchar *home_dir = g_get_home_dir(); const gchar *rest;
/* replace the project base path with the project name */ @@ -384,20 +488,32 @@ static gchar *get_doc_folder(const gchar *path) project_base_path[--len] = '\0';
/* check whether the dir name matches or uses the project base path */ - if (utils_filename_has_prefix(tmp_dirname, project_base_path)) + if (utils_filename_has_prefix(path, project_base_path)) { - rest = tmp_dirname + len; + 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) { - dirname = tmp_dirname; + 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)) { @@ -409,99 +525,454 @@ static gchar *get_doc_folder(const gchar *path) } } } - else - g_free(tmp_dirname);
return dirname; }
-static GtkTreeIter *get_doc_parent(GeanyDocument *doc) +static gchar *parent_dir_name(GtkTreeStore *tree, GtkTreeIter *parent, const gchar *path) { - gchar *path; - gchar *dirname = NULL; - static GtkTreeIter parent; - GtkTreeModel *model = GTK_TREE_MODEL(store_openfiles); - static GIcon *dir_icon = NULL; + gsize parent_len = 0; + gchar *dirname; + gchar *pathname = NULL; + + if (parent) + { + gchar *parent_dir; + GtkTreeModel *model = GTK_TREE_MODEL(tree);
- if (!interface_prefs.documents_show_paths) - return NULL; + 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); + } + }
- path = g_path_get_dirname(DOC_FILENAME(doc)); 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); +} +
- if (gtk_tree_model_get_iter_first(model, &parent)) +/* 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 - { - if (find_tree_iter_dir(&parent, dirname)) - { - g_free(dirname); - g_free(path); - return &parent; - } + do { + GtkTreeIter new_child; + tree_reparent_recurse(tree, &child, new_node, &new_child); } - while (gtk_tree_model_iter_next(model, &parent)); + while (gtk_tree_model_iter_next(model, &child)); } - /* no match, add dir parent */ +} + + +/* + * 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(store_openfiles, &parent, NULL); - gtk_tree_store_set(store_openfiles, &parent, DOCUMENTS_ICON, dir_icon, - DOCUMENTS_FILENAME, path, - DOCUMENTS_SHORTNAME, doc->file_name ? dirname : GEANY_STRING_UNTITLED, -1); + 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 &parent; + + 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 = get_doc_parent(doc); + 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;
- gtk_tree_store_append(store_openfiles, iter, parent); - - /* check if new parent */ - if (parent && gtk_tree_model_iter_n_children(GTK_TREE_MODEL(store_openfiles), parent) == 1) - { - GtkTreePath *path; + 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);
- /* expand parent */ - path = gtk_tree_model_get_path(GTK_TREE_MODEL(store_openfiles), parent); - gtk_tree_view_expand_row(GTK_TREE_VIEW(tv.tree_openfiles), path, TRUE); - gtk_tree_path_free(path); - } if (!file_icon) file_icon = ui_get_mime_icon("text/plain");
- basename = g_path_get_basename(DOC_FILENAME(doc)); + 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), -1); + 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); }
-static void openfiles_remove(GeanyDocument *doc) +/* 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 = &doc->priv->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); + } + } +} +
- if (gtk_tree_model_iter_parent(GTK_TREE_MODEL(store_openfiles), &parent, iter) && - gtk_tree_model_iter_n_children(GTK_TREE_MODEL(store_openfiles), &parent) == 1) - gtk_tree_store_remove(store_openfiles, &parent); +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, iter); + gtk_tree_store_remove(store_openfiles, &doc->priv->iter); }
@@ -619,10 +1090,13 @@ void sidebar_add_common_menu_items(GtkMenu *menu) 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.documents_show_paths = gtk_check_menu_item_get_active(item); + 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()); }
@@ -678,6 +1152,28 @@ static void on_openfiles_expand_collapse(GtkMenuItem *menuitem, gpointer user_da }
+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; @@ -725,12 +1221,7 @@ static void create_openfiles_popup_menu(void) gtk_widget_show(item); gtk_container_add(GTK_CONTAINER(openfiles_popup_menu), item);
- doc_items.show_paths = gtk_check_menu_item_new_with_mnemonic(_("Show _Paths")); - gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(doc_items.show_paths), interface_prefs.documents_show_paths); - gtk_widget_show(doc_items.show_paths); - gtk_container_add(GTK_CONTAINER(openfiles_popup_menu), doc_items.show_paths); - g_signal_connect(doc_items.show_paths, "activate", - G_CALLBACK(on_openfiles_show_paths_activate), NULL); + create_show_paths_popup_menu();
item = gtk_separator_menu_item_new(); gtk_widget_show(item); @@ -752,38 +1243,25 @@ static void create_openfiles_popup_menu(void) }
-static void unfold_parent(GtkTreeIter *iter) -{ - GtkTreeIter parent; - - if (gtk_tree_model_iter_parent(GTK_TREE_MODEL(store_openfiles), &parent, iter)) - { - GtkTreePath *path = gtk_tree_model_get_path(GTK_TREE_MODEL(store_openfiles), &parent); - gtk_tree_view_expand_row(GTK_TREE_VIEW(tv.tree_openfiles), path, TRUE); - gtk_tree_path_free(path); - } -} - - /* 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) +static gboolean tree_model_find_node(GtkTreeModel *model, + GtkTreePath *path, + GtkTreeIter *iter, + gpointer data) { GeanyDocument *doc;
- gtk_tree_model_get(GTK_TREE_MODEL(store_openfiles), iter, DOCUMENTS_DOCUMENT, &doc, -1); - + gtk_tree_model_get(model, iter, DOCUMENTS_DOCUMENT, &doc, -1); if (doc == data) { - /* unfolding also prevents a strange bug where the selection gets stuck on the parent - * when it is collapsed and then switching documents */ - unfold_parent(iter); + 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; } - else return FALSE; + + return FALSE; }
@@ -821,36 +1299,38 @@ static void document_action(GeanyDocument *doc, gint action) }
+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; - GeanyDocument *doc; gint action = GPOINTER_TO_INT(user_data);
if (gtk_tree_selection_get_selected(selection, &model, &iter)) - { - 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)) - { - gtk_tree_model_get(model, &child, DOCUMENTS_DOCUMENT, &doc, -1); - - document_action(doc, action); - i--; - } - } - } + on_openfiles_document_action_apply(model, iter, action); }
@@ -1034,8 +1514,10 @@ static void documents_menu_update(GtkTreeSelection *selection) sel = gtk_tree_selection_get_selected(selection, &model, &iter); if (sel) { - gtk_tree_model_get(model, &iter, DOCUMENTS_DOCUMENT, &doc, - DOCUMENTS_SHORTNAME, &shortname, -1); + gtk_tree_model_get(model, &iter, + DOCUMENTS_DOCUMENT, &doc, + DOCUMENTS_SHORTNAME, &shortname, + -1); } path = !EMPTY(shortname) && (g_path_is_absolute(shortname) || @@ -1048,16 +1530,20 @@ static void documents_menu_update(GtkTreeSelection *selection) 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.documents_show_paths); - gtk_widget_set_sensitive(doc_items.expand_all, interface_prefs.documents_show_paths); - gtk_widget_set_sensitive(doc_items.collapse_all, interface_prefs.documents_show_paths); + 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(); @@ -1092,7 +1578,7 @@ void sidebar_init(void) configuration_add_session_group(group, FALSE); stash_group = group;
- /* delay building documents treeview until sidebar font has been read */ + /* 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);
@@ -1105,8 +1591,6 @@ void sidebar_init(void) G_CALLBACK(sidebar_tabs_show_hide), NULL); g_signal_connect_after(main_widgets.sidebar_notebook, "switch-page", G_CALLBACK(on_sidebar_switch_page), NULL); - - sidebar_tabs_show_hide(GTK_NOTEBOOK(main_widgets.sidebar_notebook), NULL, 0, NULL); }
#define WIDGET(w) w && GTK_IS_WIDGET(w)
Modified: src/sidebar.h 23 lines changed, 21 insertions(+), 2 deletions(-) =================================================================== @@ -27,6 +27,8 @@
#include "gtkcompat.h"
+#ifdef GEANY_PRIVATE + G_BEGIN_DECLS
typedef struct SidebarTreeviews @@ -50,8 +52,21 @@ enum
enum { - SHOW_PATHS_NONE, - SHOW_PATHS_LIST + OPENFILES_PATHS_NONE, + OPENFILES_PATHS_LIST, + OPENFILES_PATHS_TREE, + OPENFILES_PATHS_COUNT +}; + +/* documents tree model columns */ +enum +{ + DOCUMENTS_ICON, + DOCUMENTS_SHORTNAME, /* dirname for parents, basename for children */ + DOCUMENTS_DOCUMENT, + DOCUMENTS_COLOR, + DOCUMENTS_FILENAME, /* full filename */ + DOCUMENTS_FOLD, /* fold state stored when folding parent rows */ };
void sidebar_init(void); @@ -76,6 +91,10 @@ void sidebar_focus_openfiles_tab(void);
void sidebar_focus_symbols_tab(void);
+GtkTreeStore *sidebar_create_store_openfiles(void); +#endif + G_END_DECLS
+ #endif /* GEANY_SIDEBAR_H */
Modified: src/ui_utils.c 2 lines changed, 1 insertions(+), 1 deletions(-) =================================================================== @@ -68,7 +68,7 @@ "filetype: %f " \ "scope: %S")
-GeanyInterfacePrefs interface_prefs; +GEANY_EXPORT_SYMBOL GeanyInterfacePrefs interface_prefs; GeanyMainWidgets main_widgets;
UIPrefs ui_prefs;
Modified: src/ui_utils.h 2 lines changed, 1 insertions(+), 1 deletions(-) =================================================================== @@ -71,7 +71,7 @@ typedef struct GeanyInterfacePrefs gint symbols_sort_mode; /**< symbol list sorting mode */ /** whether to show a warning when closing a project to open a new one */ gboolean warn_on_project_close; - gint documents_show_paths; + gint openfiles_path_mode; } GeanyInterfacePrefs;
Modified: src/utils.c 6 lines changed, 3 insertions(+), 3 deletions(-) =================================================================== @@ -468,7 +468,7 @@ gdouble utils_scale_round(gdouble val, gdouble factor)
/* like g_utf8_strdown() but if @str is not valid UTF8, convert it from locale first. * returns NULL on charset conversion failure */ -static gchar *utf8_strdown(const gchar *str) +gchar *utils_utf8_strdown(const gchar *str) { gchar *down;
@@ -512,10 +512,10 @@ gint utils_str_casecmp(const gchar *s1, const gchar *s2) g_return_val_if_fail(s2 != NULL, -1);
/* ensure strings are UTF-8 and lowercase */ - tmp1 = utf8_strdown(s1); + tmp1 = utils_utf8_strdown(s1); if (! tmp1) return 1; - tmp2 = utf8_strdown(s2); + tmp2 = utils_utf8_strdown(s2); if (! tmp2) { g_free(tmp1);
Modified: src/utils.h 2 lines changed, 2 insertions(+), 0 deletions(-) =================================================================== @@ -194,6 +194,8 @@ gboolean utils_spawn_async(const gchar *dir, gchar **argv, gchar **env, GSpawnFl GSpawnChildSetupFunc child_setup, gpointer user_data, GPid *child_pid, GError **error);
+gchar *utils_utf8_strdown(const gchar *str); + gint utils_str_casecmp(const gchar *s1, const gchar *s2);
gchar *utils_get_date_time(const gchar *format, time_t *time_to_use);
Modified: tests/Makefile.am 12 lines changed, 7 insertions(+), 5 deletions(-) =================================================================== @@ -1,13 +1,15 @@
SUBDIRS = ctags
-AM_CPPFLAGS = -DGEANY_PRIVATE -DG_LOG_DOMAIN=""Geany"" @GTK_CFLAGS@ @GTHREAD_CFLAGS@ -AM_CPPFLAGS += -I$(top_srcdir)/src +AM_CPPFLAGS = -DGTK -DGEANY_PRIVATE -DG_LOG_DOMAIN=""Geany"" +AM_CPPFLAGS += -I$(top_srcdir)/scintilla/include -I$(top_srcdir)/scintilla/lexilla/include +AM_CPPFLAGS += -I$(top_srcdir)/src/tagmanager -I$(top_srcdir)/src +AM_CFLAGS = $(GTK_CFLAGS) +AM_LDFLAGS = $(GTK_LIBS) $(INTLLIBS) -no-install
-AM_LDFLAGS = $(GTK_LIBS) $(GTHREAD_LIBS) $(INTLLIBS) -no-install - -check_PROGRAMS = test_utils +check_PROGRAMS = test_utils test_sidebar
test_utils_LDADD = $(top_builddir)/src/libgeany.la +test_sidebar_LDADD = $(top_builddir)/src/libgeany.la
TESTS = $(check_PROGRAMS)
Modified: tests/meson.build 4 lines changed, 3 insertions(+), 1 deletions(-) =================================================================== @@ -1,5 +1,6 @@ test_deps = declare_dependency(compile_args: geany_cflags + [ '-DG_LOG_DOMAIN="Geany"' ], - dependencies: [deps, dep_libgeany]) + dependencies: [deps, dep_libgeany], + include_directories: '..')
ctags_tests = files([ 'ctags/1795612.js.tags', @@ -366,3 +367,4 @@ test('ctags/processing-order', runner, args: [join_paths(meson.build_root(), 'geany'), '--result', process_order_sources], env: ['top_srcdir='+meson.source_root(), 'top_builddir='+meson.build_root()]) test('utils', executable('test_utils', 'test_utils.c', dependencies: test_deps)) +test('sidebar', executable('test_sidebar', 'test_sidebar.c', dependencies: test_deps))
Modified: tests/test_sidebar.c 154 lines changed, 154 insertions(+), 0 deletions(-) =================================================================== @@ -0,0 +1,154 @@ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include "document.h" +#include "documentprivate.h" +#include "keyfile.h" +#include "main.h" +#include "sidebar.h" +#include "ui_utils.h" +#include "utils.h" + +#define SIDEBAR_TEST_ADD(path, func) g_test_add_func("/sidebar/" path, func); + +static void openfiles_add(const gchar **file_names) +{ + const gchar *file; + + while ((file = *file_names++)) + { + GeanyDocument *doc = g_new0(GeanyDocument, 1); + + doc->priv = g_new0(GeanyDocumentPrivate, 1); + doc->file_name = strdup(file); + + sidebar_openfiles_add(doc); + } +} + + +static gboolean tree_count_cb(GtkTreeModel *model, GtkTreePath *path, + GtkTreeIter *iter, gpointer data_in) +{ + gint *c = (gint *) data_in; + + *c = *c + 1; + return FALSE; +} + + +static gboolean tree_strings_cb(GtkTreeModel *model, GtkTreePath *path, + GtkTreeIter *iter, gpointer data_in) +{ + gchar **data = (gchar **) data_in; + gchar *file; + + gtk_tree_model_get(model, iter, DOCUMENTS_SHORTNAME, &file, -1); + data[g_strv_length(data)] = file; + + printf("%s\n", file); + return FALSE; +} + +void do_test_sidebar_openfiles(const gchar **test_data, const gchar **expected) +{ +#ifdef HAVE_G_STRV_EQUAL + int count = 0; + GtkTreeStore *store; + gchar **data; + + store = sidebar_create_store_openfiles(); + + openfiles_add(test_data); + + gtk_tree_model_foreach(GTK_TREE_MODEL(store), tree_count_cb, (gpointer) &count); + data = g_new0(gchar *, count + 1); + gtk_tree_model_foreach(GTK_TREE_MODEL(store), tree_strings_cb, (gpointer) data); + g_assert_true(g_strv_equal(expected, (const gchar * const *) data)); +#else + g_test_skip("Need g_strv_equal(), since GLib 2.60"); +#endif +} + +void test_sidebar_openfiles_none(void) +{ + const gchar *files[] = { + "/tmp/x", + "/tmp/b/a", + "/tmp/b/b", + NULL + }; + const gchar *expected[] = { + "a", + "b", + "x", + NULL + }; + + interface_prefs.openfiles_path_mode = OPENFILES_PATHS_NONE; + do_test_sidebar_openfiles(files, expected); +} + + +void test_sidebar_openfiles_path(void) +{ + const gchar *files[] = { + "/tmp/x", + "/tmp/b/a", + "/tmp/b/b", + NULL + }; + const gchar *expected[] = { + "/tmp", + "x", + "/tmp/b", + "a", + "b", + NULL + }; + + interface_prefs.openfiles_path_mode = OPENFILES_PATHS_LIST; + do_test_sidebar_openfiles(files, expected); +} + + +void test_sidebar_openfiles_tree(void) +{ + const gchar *files[] = { + "/tmp/x", + "/tmp/b/a", + "/tmp/b/b", + NULL + }; + const gchar *expected[] = { + "/tmp", + "x", + "b", + "a", + "b", + NULL + }; + + interface_prefs.openfiles_path_mode = OPENFILES_PATHS_TREE; + do_test_sidebar_openfiles(files, expected); +} + +int main(int argc, char **argv) +{ + g_test_init(&argc, &argv, NULL); + /* Not sure if we can really continue without DISPLAY. Fake X display perhaps? + * + * This test seems to work, at least. + */ + gtk_init_check(&argc, &argv); + + main_init_headless(); + + SIDEBAR_TEST_ADD("openfiles_none", test_sidebar_openfiles_none); + SIDEBAR_TEST_ADD("openfiles_path", test_sidebar_openfiles_path); + SIDEBAR_TEST_ADD("openfiles_tree", test_sidebar_openfiles_tree); + + return g_test_run(); +}
-------------- This E-Mail was brought to you by github_commit_mail.py (Source: https://github.com/geany/infrastructure).