From 6dcdefc18547ee5ce4a56f78caa316689d86b309 Mon Sep 17 00:00:00 2001 From: Thomas Martitz Date: Sat, 16 Apr 2022 22:41:00 +0200 Subject: [PATCH 1/4] Add (and default to) tree view of open documents in the sidebar Currently you can chose between a flat document list and a two-level tree that groups the files by their directory. With the new tree view the latter mode is extended to a true tree where directory rows only represent the direct parent directory of open documents. Except that single-child subtrees are collapsed to a single row. See the manual for glory details. The new mode allows to quickly close entire subtrees and is visually close to the file system layout. As with the two-level tree, the home directory and project base directory (if any project is open) are treated specially and usually get a distict row - home directory appears as "~" - project base directory path is replaced by the project name The new mode is the default. Existing installations will use the new mode because a new pref is introduced. Closes #259 Based on work by RPG --- doc/Makefile.am | 5 +- doc/geany.txt | 44 +- doc/images/sidebar_documents_only.png | Bin 0 -> 1626 bytes doc/images/sidebar_show_paths.png | Bin 0 -> 2417 bytes doc/images/sidebar_show_tree.png | Bin 0 -> 2400 bytes doc/meson.build | 3 + src/keyfile.c | 2 +- src/sidebar.c | 820 +++++++++++++++++++++----- src/sidebar.h | 6 - src/ui_utils.h | 2 +- src/utils.c | 6 +- src/utils.h | 2 + 12 files changed, 718 insertions(+), 172 deletions(-) create mode 100644 doc/images/sidebar_documents_only.png create mode 100644 doc/images/sidebar_show_paths.png create mode 100644 doc/images/sidebar_show_tree.png diff --git a/doc/Makefile.am b/doc/Makefile.am index b8d0532090..ea0e0f697b 100644 --- a/doc/Makefile.am +++ b/doc/Makefile.am @@ -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 = \ diff --git a/doc/geany.txt b/doc/geany.txt index 4c2b006a78..387c7735f5 100644 --- a/doc/geany.txt +++ b/doc/geany.txt @@ -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, diff --git a/doc/images/sidebar_documents_only.png b/doc/images/sidebar_documents_only.png new file mode 100644 index 0000000000000000000000000000000000000000..7dd3eeb7e169ddab6c05c07fa45e1d6cd3b43fa6 GIT binary patch literal 1626 zcmV-g2BrClP)004R>004l5008;`004mK004C`008P>0026e000+ooVrmw0001x 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 LJ6knaT)OcO=@x>o literal 0 HcmV?d00001 diff --git a/doc/images/sidebar_show_tree.png b/doc/images/sidebar_show_tree.png new file mode 100644 index 0000000000000000000000000000000000000000..78d1f83ba65d3a9b562e5e5fd7b551ca2e9da90b GIT binary patch literal 2400 zcmZ`)c|6o>7ypS;vfM5ylWWTzM#WfWFw~`tWg>)}*+lpi(7OJ_^`~LBM-apgwwFe129|7K6dqy?eK@u`z{0QB+j4wY9CRtaNvG@9OHB z<1W!|9~+=w9~>N9TwF{{Ozf`+>w1b8Ub!MnO%=l7U*Go$B_xDSPC^<@$mI(6?W=E} z4Ne@(DxGvB4E7FiMYpO&+8K#vi(NcvV`X5$wr7a~$uK(ywB&@O{GNkmJrR+nBBs^O z%F;DTY;kxrr5ve{utvbjG8=wxz87TQy|`^@tI;ogQQZ1q2G`$;*yLzy%UekAx8K0> zg`Df7-q)x0EEVV$MD`iWWqe)Vw3!sdb*ya|@LTh$zE8K>m$)tZU_W>Np%a4|!i1Ag z`SePDQ_q&8w#6Se&nS68;9KNNtJ=AQWbYrTekL#fCu3$;)`_>WwNa$T*u2pTN2|7# zeJ!miy}xp!_qwFX8THSPcM#q^8vNi1;aZV2W-W;lo~21jnvkAMmu+m8)`+Z&QXijz z1qu_O`9{}d_cAwvv)T7tv}E%(ypk3DCG=2tOZIhON2Fawd<9B0GX%h}!D(raP4tuUr6 z1JhLSYGOP+=Ml3pC^lk0Jx=+Wpo!RcO{+Rh#roYT)O?$XeuZC%K_R+iyp%n#-j2zH z68XA4Rhe;}URkOP`Ki`wky`+~m*2{fAO1M4K41+LR7wo_zx~`p(&)`Y+=GuIo`@&tI(rO_y8X((P+2V@Yap z7U$%YSaeO^k3s@Hm-(^iwq^5ACmZ!F)yp2b{3xTKg%u+hKPvELkms-$$Hr%@X1u@r z$t(s}>>D(eT3(Acxg$mkODW61;TEFJ^N_|k1#843wIP-Sz6iSGnJe957vM}QgasU! zs;V5#+!~pv8B;SDmZF^=+`))R-z&!0NEjGM>x=M6c`i^M89PM@d6lB9y9Z*CvTCG6 z+5Z~v^|;<2g7L%^9>IGaW}Xt6cKIwGoq$@3hyiv(3vk zl^9#mr0*6h4VDK3%AQE1S)(7$lX7sM;BVID$Fv;|4|LqdXQ|3Sz6<7xPSTHhYw!HOO%IBrz zVt%``Rg$o$RlKFG{o$n8ebW_TsMZOukm{VfUF-vu160;Sie;Guo=PZ}($>kqP*1}& z4r%b01 zWi!n6aB~eaaAeuT>{9YzmGjHxz-6yx!aL>KZf#KFHxB36(@yNAWM-uE%hw67VLu+b z%Q{5*e5(UA%slNQGtNzg2fALkMxeiSKAa#cynlXMT7HAQsv)?MfC$a1KzbiefYxr7 zOK6ylQdHnSBtKUlkOFmg*+lQ{D&j7B-cYpg$M%c|AhxRt2AR~E@Id#vfjDAZP$~W= zt03--U=0SjYQZDQHqoI-;@XAXQpU>os41@S!0g+C7kFX z+4G=bCnHC-1#V7Qc~Yh5$<&JROXQ01fD!1I z=0^-gaw+~5qUni%>v4_1zJf3!f^;-UT5?EhY!q#Rp~C3Jto4x$jgu$U(3!)<3Y;^= zVtczlmwSs2AK(5}4m#emKX%Zmsvb#YO%snlI{d-`X7EkNJ$>%>5^UXH#`PBwN`ne3 zo_oVW;SxHE4~kh6XMZWP70#&+$4QmeE4obIMbyh-ymrGvTOo8m(vf1;GaHI-@)yg= z*x}fx=UNX_qVCb-jE^M zY`!h|sV}NPgQDWyR{3L_9zHvzM5I_aXRL6-&pkbk`%7)3{MP_qocTEr;`^ThitdlA zstWP#G^osbvPQY9Pl-^Ba3J`#NqkbX=!^d|4DBA~?;93?@(T?R0bl^vM;w70AJI2; w)i*{NB2Wm^LvTYB93FzV<^4}X5WydRiS+*)0Hf8jq6WYYeah /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; } +static void 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; +} + + +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 +426,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 +463,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 +480,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 +490,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,9 +508,9 @@ 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); @@ -394,10 +518,22 @@ static gchar *get_doc_folder(const gchar *path) } 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,52 +545,363 @@ 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 (!interface_prefs.documents_show_paths) - return 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); + } + } - 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); } @@ -463,45 +910,87 @@ static GtkTreeIter *get_doc_parent(GeanyDocument *doc) 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 */ + if (expand) + 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 +1108,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 +1170,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 +1239,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 +1261,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 +1317,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 +1532,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,9 +1548,9 @@ 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); } @@ -1058,6 +1558,10 @@ 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 +1596,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 +1609,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) diff --git a/src/sidebar.h b/src/sidebar.h index 3fa006adf4..516b635395 100644 --- a/src/sidebar.h +++ b/src/sidebar.h @@ -48,12 +48,6 @@ enum SYMBOLS_N_COLUMNS }; -enum -{ - SHOW_PATHS_NONE, - SHOW_PATHS_LIST -}; - void sidebar_init(void); void sidebar_finalize(void); diff --git a/src/ui_utils.h b/src/ui_utils.h index 84a74de945..a2268fccd2 100644 --- a/src/ui_utils.h +++ b/src/ui_utils.h @@ -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; diff --git a/src/utils.c b/src/utils.c index e2f9e56c34..2a8cccddb5 100644 --- a/src/utils.c +++ b/src/utils.c @@ -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); diff --git a/src/utils.h b/src/utils.h index 786786d4ef..511cd456e9 100644 --- a/src/utils.h +++ b/src/utils.h @@ -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); From 9e1c79079bc5cf22cb417831697a6a65c7e3d7e5 Mon Sep 17 00:00:00 2001 From: Thomas Martitz Date: Sun, 17 Apr 2022 13:15:02 +0200 Subject: [PATCH 2/4] Refactor GTK-related autoconf checks - Move to separate file geany-gtk.m4 - Merge gthread checks into the main gtk ones --- configure.ac | 19 +------------------ m4/geany-gtk.m4 | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 18 deletions(-) create mode 100644 m4/geany-gtk.m4 diff --git a/configure.ac b/configure.ac index 716dacc11f..be2fc1dbe1 100644 --- a/configure.ac +++ b/configure.ac @@ -88,24 +88,7 @@ AC_CHECK_DECLS([_NSGetEnviron],,,[[#include ]]) 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 # --disable-deprecated switch for GTK purification AC_ARG_ENABLE([deprecated], diff --git a/m4/geany-gtk.m4 b/m4/geany-gtk.m4 new file mode 100644 index 0000000000..4b9f37a29b --- /dev/null +++ b/m4/geany-gtk.m4 @@ -0,0 +1,18 @@ +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}]) +]) From 20bd9c44ea638fb7c29dc6df374f39df4947cd3f Mon Sep 17 00:00:00 2001 From: Thomas Martitz Date: Sun, 17 Apr 2022 13:15:02 +0200 Subject: [PATCH 3/4] 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. --- configure.ac | 1 + m4/geany-gtk.m4 | 15 +++++ meson.build | 6 +- src/Makefile.am | 5 +- src/libmain.c | 25 ++++--- src/main.h | 2 + src/sidebar.c | 32 ++------- src/sidebar.h | 25 +++++++ src/ui_utils.c | 2 +- tests/Makefile.am | 12 ++-- tests/meson.build | 4 +- tests/test_sidebar.c | 154 +++++++++++++++++++++++++++++++++++++++++++ 12 files changed, 236 insertions(+), 47 deletions(-) create mode 100644 tests/test_sidebar.c diff --git a/configure.ac b/configure.ac index be2fc1dbe1..2da72f2160 100644 --- a/configure.ac +++ b/configure.ac @@ -89,6 +89,7 @@ GEANY_CHECK_REVISION([dnl force debug mode for a VCS working copy CFLAGS="-g -DGEANY_DEBUG $CFLAGS"]) GEANY_CHECK_GTK +GEANY_CHECK_GTK_FUNCS([g_strv_equal]) # --disable-deprecated switch for GTK purification AC_ARG_ENABLE([deprecated], diff --git a/m4/geany-gtk.m4 b/m4/geany-gtk.m4 index 4b9f37a29b..051cd259a3 100644 --- a/m4/geany-gtk.m4 +++ b/m4/geany-gtk.m4 @@ -16,3 +16,18 @@ AC_DEFUN([GEANY_CHECK_GTK], 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 +]) diff --git a/meson.build b/meson.build index bbbaa7b33c..4272c7e671 100644 --- a/meson.build +++ b/meson.build @@ -85,7 +85,7 @@ check_functions = [ ['truncate', '#include '], ['wcrtomb', '#include '], ['wcscoll', '#include '], - + ['g_strv_equal', '#include '] ] foreach h : check_headers @@ -100,7 +100,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) @@ -846,7 +846,7 @@ libgeany = shared_library('geany', ) dep_libgeany = declare_dependency( link_with: libgeany, - include_directories: [igeany] + include_directories: [iscintilla, itagmanager, igeany] ) executable('geany', diff --git a/src/Makefile.am b/src/Makefile.am index d6bb450200..1a91a13bd5 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -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) diff --git a/src/libmain.c b/src/libmain.c index 6da6469e95..2012a21686 100644 --- a/src/libmain.c +++ b/src/libmain.c @@ -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(); diff --git a/src/main.h b/src/main.h index 1a5837c625..e4bf3c77f2 100644 --- a/src/main.h +++ b/src/main.h @@ -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 diff --git a/src/sidebar.c b/src/sidebar.c index 7f0af540f1..d27f73a9d1 100644 --- a/src/sidebar.c +++ b/src/sidebar.c @@ -51,14 +51,6 @@ SidebarTreeviews tv = {NULL, NULL, NULL}; /* while typeahead searching, editor should not get focus */ static gboolean may_steal_focus = FALSE; -enum -{ - OPENFILES_PATHS_NONE, - OPENFILES_PATHS_LIST, - OPENFILES_PATHS_TREE, - OPENFILES_PATHS_COUNT -}; - static struct { GtkWidget *close; @@ -84,19 +76,6 @@ 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 */ - DOCUMENTS_FOLD, /* fold state stored when folding parent rows */ -}; - - static GtkTreeStore *store_openfiles; static GtkWidget *openfiles_popup_menu; static GtkWidget *tag_window; /* scrolled window that holds the symbol list GtkTreeView */ @@ -300,8 +279,8 @@ static gint documents_sort_func(GtkTreeModel *model, GtkTreeIter *iter_a, return cmp; } - -static void sidebar_create_store_openfiles(void) +GEANY_EXPORT_SYMBOL +GtkTreeStore *sidebar_create_store_openfiles(void) { GtkTreeSortable *sortable; GtkTreeStore *store; @@ -316,6 +295,7 @@ static void sidebar_create_store_openfiles(void) gtk_tree_sortable_set_sort_column_id(sortable, DOCUMENTS_SHORTNAME, GTK_SORT_ASCENDING); store_openfiles = store; + return store; } @@ -907,6 +887,7 @@ static void expand_iter(GtkTreeIter *iter) /* 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; @@ -934,8 +915,9 @@ void sidebar_openfiles_add(GeanyDocument *doc) -1); g_free(basename); - /* expand new parent if necessary */ - if (expand) + /* 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); } diff --git a/src/sidebar.h b/src/sidebar.h index 516b635395..83b723f669 100644 --- a/src/sidebar.h +++ b/src/sidebar.h @@ -27,6 +27,8 @@ #include "gtkcompat.h" +#ifdef GEANY_PRIVATE + G_BEGIN_DECLS typedef struct SidebarTreeviews @@ -48,6 +50,25 @@ enum SYMBOLS_N_COLUMNS }; +enum +{ + 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); void sidebar_finalize(void); @@ -70,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 */ diff --git a/src/ui_utils.c b/src/ui_utils.c index 874c2dcff6..749195fd66 100644 --- a/src/ui_utils.c +++ b/src/ui_utils.c @@ -68,7 +68,7 @@ "filetype: %f " \ "scope: %S") -GeanyInterfacePrefs interface_prefs; +GEANY_EXPORT_SYMBOL GeanyInterfacePrefs interface_prefs; GeanyMainWidgets main_widgets; UIPrefs ui_prefs; diff --git a/tests/Makefile.am b/tests/Makefile.am index 484e3d77cc..12bf58799c 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -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) diff --git a/tests/meson.build b/tests/meson.build index 7125f330d0..f8f81101d6 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -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', @@ -352,3 +353,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)) diff --git a/tests/test_sidebar.c b/tests/test_sidebar.c new file mode 100644 index 0000000000..9659dc72e7 --- /dev/null +++ b/tests/test_sidebar.c @@ -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(); +} From 8932304f678f063b489619cd4dccbb9c1a4a6789 Mon Sep 17 00:00:00 2001 From: Thomas Martitz Date: Thu, 26 May 2022 16:16:23 +0200 Subject: [PATCH 4/4] Add item to NEWS for tree view of open documents --- NEWS | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/NEWS b/NEWS index cbd16b2c27..dda9f066a4 100644 --- a/NEWS +++ b/NEWS @@ -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)