From: Omari Stephens Date: Tue, 11 Jul 2017 17:08:01 +0000 (+0000) Subject: Merge pull request #497 from xsdg/cluster X-Git-Url: http://geeqie.org/cgi-bin/gitweb.cgi?p=geeqie.git;a=commitdiff_plain;h=b9740ac8888c6d8c7dd3102629fcdec460cfb61b;hp=efc10a6b92e81d0a920dac55f792e2f2e3c5f0c1 Merge pull request #497 from xsdg/cluster Adds preliminary file clustering capability --- diff --git a/src/Makefile.am b/src/Makefile.am index f01fb199..f4f2f4cc 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -144,6 +144,8 @@ geeqie_SOURCES = \ exiv2.cc \ filecache.c \ filecache.h \ + filecluster.c \ + filecluster.h \ filedata.c \ filedata.h \ filefilter.c \ diff --git a/src/filecluster.c b/src/filecluster.c new file mode 100644 index 00000000..7c5d9d03 --- /dev/null +++ b/src/filecluster.c @@ -0,0 +1,199 @@ +/* + * Copyright (C) 2008 - 2016 The Geeqie Team + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "filecluster.h" + +#include "filedata.h" + +static gboolean check_list_contains_sublist(GList *haystack, GList *needle) +{ + // TODO(xsdg): Optimize this! Sort, then scan? + GList *h_work, *n_work; + for (n_work = needle; n_work; n_work = n_work->next) + { + gboolean found = FALSE; + for (h_work = haystack; h_work; h_work = h_work->next) + { + if (n_work == h_work) + { + found = TRUE; + break; + } + } + + if (!found) return FALSE; + } + + return TRUE; +} + +static gboolean filecluster_fd_equal(gconstpointer ptr_a, gconstpointer ptr_b) +{ + // TODO(xsdg): Is there anything we can/should do to validate inputs? + FileData *fd_a = (FileData *)ptr_a; + FileData *fd_b = (FileData *)ptr_b; + return !filelist_sort_compare_filedata(fd_a, fd_b); +} + +// TODO(xsdg): Move this into filedata.h +static guint filecluster_fd_hash(gconstpointer ptr) +{ + if (!ptr) return 1; + FileData *fd = (FileData *)ptr; + return 7 * g_str_hash(fd->original_path); +} + +FileClusterList *fileclusterlist_new() +{ + FileClusterList *fcl = g_new0(FileClusterList, 1); + fcl->clusters = g_hash_table_new(&filecluster_fd_hash, &filecluster_fd_equal); + return fcl; +} + +FileCluster *filecluster_new() +{ + FileCluster *fc = g_new0(FileCluster, 1); + fc->show_children = FALSE; + return fc; +} + +void fileclusterlist_free(FileClusterList *fcl) +{ + // TODO(xsdg): don't leak stuff + // if (fcl->fd_list) g_list_free_full(fcl->fd_list, (GDestroyNotify)&filecluster_free); + g_hash_table_destroy(fcl->clusters); + g_free(fcl); +} + +void filecluster_free(FileCluster *fc) +{ + filelist_free(fc->items); + g_free(fc); +} + +gboolean filecluster_toggle_show_children(FileCluster *fc) +{ + fc->show_children = !fc->show_children; + return fc->show_children; +} + +FileCluster *fileclusterlist_create_cluster(FileClusterList *fcl, GList *fd_items) +{ + GList *work; + + // Check preconditions. + if (!fd_items) return NULL; + for (work = fd_items; work; work = work->next) + { + FileData *fd = work->data; + if (g_hash_table_contains(fcl->clusters, fd)) + { + // TODO(xsdg): Show this warning in the UI. + g_warning("Tried to create a cluster with a file that is already clustered."); + return NULL; + } + } + + FileCluster *new_fc = filecluster_new(); + new_fc->items = filelist_copy(fd_items); + new_fc->head = new_fc->items; + + for (GList *item = new_fc->items; item; item = item->next) + { + FileData *fd = item->data; + g_hash_table_insert(fcl->clusters, fd, new_fc); + } + + return new_fc; +} + +gboolean filecluster_has_head(FileCluster *fc, FileData *fd) +{ + if (!fd) return FALSE; + return filecluster_fd_equal(fc->head->data, fd); +} + +gboolean filecluster_has_child(FileCluster *fc, FileData *fd) +{ + if (!fd) return FALSE; + return !filecluster_fd_equal(fc->head->data, fd); +} + +gboolean fileclusterlist_has_head(FileClusterList *fcl, FileData *fd) +{ + FileCluster *fc = g_hash_table_lookup(fcl->clusters, fd); + if (!fc) return FALSE; + return filecluster_has_head(fc, fd); +} + +gboolean fileclusterlist_has_child(FileClusterList *fcl, FileData *fd) +{ + FileCluster *fc = g_hash_table_lookup(fcl->clusters, fd); + if (!fc) return FALSE; + return filecluster_has_child(fc, fd); +} + +static gboolean fileclusterlist_should_hide(FileClusterList *fcl, FileData *fd) +{ + FileCluster *fc = g_hash_table_lookup(fcl->clusters, fd); + if (!fc) return FALSE; + // Only difference vs. fileclusterlist_has_child. Basically, if the node is a child, but + // we're showing children, then don't hide. + if (fc->show_children) return FALSE; + return filecluster_has_child(fc, fd); +} + +// TODO(xsdg): pick a better name for this function +GList *fileclusterlist_next_non_child(FileClusterList *fcl, GList *list) +{ + // Check for no-ops + if (!list || !g_hash_table_size(fcl->clusters)) return list; + + // Clusters are being used, so we have to actually check things. + for (; list; list = list->next) + { + FileData *fd = list->data; + if (!fileclusterlist_has_child(fcl, fd)) return list; + } + + return NULL; +} + +GList *fileclusterlist_remove_children_from_list(FileClusterList *fcl, GList *list) +{ + GList *work = list; + + while (work) + { + FileData *fd = work->data; + GList *link = work; + // Advance early in case link needs to be removed/freed. + work = work->next; + + if (fileclusterlist_should_hide(fcl, fd)) + { + list = g_list_remove_link(list, link); + file_data_unref(fd); + g_list_free(link); + } + } + + return list; +} + +/* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */ diff --git a/src/filecluster.h b/src/filecluster.h new file mode 100644 index 00000000..d63ee034 --- /dev/null +++ b/src/filecluster.h @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2008 - 2016 The Geeqie Team + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FILECLUSTER_H +#define FILECLUSTER_H + +#include "main.h" + +// FileCluster methods. +FileCluster *filecluster_new(); // internal? +void filecluster_free(FileCluster *fc); + +gboolean filecluster_toggle_show_children(FileCluster *fc); +gboolean filecluster_has_head(FileCluster *fc, FileData *fd); +gboolean filecluster_has_child(FileCluster *fc, FileData *fd); + + +// FileClusterList methods. + +FileClusterList *fileclusterlist_new(); +void fileclusterlist_free(FileClusterList *fcl); + +// Creates a cluster with items that must already be in the cluster list. Will fail (and make no +// changes) if any of the specified items isn't in the list, or if any of the items is already +// part of a different cluster. +FileCluster *fileclusterlist_create_cluster(FileClusterList *fcl, GList *fd_items); + +gboolean fileclusterlist_has_head(FileClusterList *fcl, FileData *fd); +gboolean fileclusterlist_has_child(FileClusterList *fcl, FileData *fd); + +GList *fileclusterlist_next_non_child(FileClusterList *fcl, GList *list); +GList *fileclusterlist_remove_children_from_list(FileClusterList *fcl, GList *list); + +#endif // FILECLUSTER_H + +/* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */ diff --git a/src/typedefs.h b/src/typedefs.h index 9d7db51f..fcf2d69b 100644 --- a/src/typedefs.h +++ b/src/typedefs.h @@ -256,7 +256,9 @@ typedef enum { SELECTION_NONE = 0, SELECTION_SELECTED = 1 << 0, SELECTION_PRELIGHT = 1 << 1, - SELECTION_FOCUS = 1 << 2 + SELECTION_FOCUS = 1 << 2, + SELECTION_CLUSTER_HEAD = 1 << 3, + SELECTION_CLUSTER_CHILD = 1 << 4 } SelectionType; typedef struct _ImageLoader ImageLoader; @@ -271,6 +273,9 @@ typedef struct _CollectWindow CollectWindow; typedef struct _ImageWindow ImageWindow; +typedef struct _FileCluster FileCluster; +typedef struct _FileClusterList FileClusterList; + typedef struct _FileData FileData; typedef struct _FileDataChangeInfo FileDataChangeInfo; @@ -529,6 +534,20 @@ struct _ImageWindow gboolean mouse_wheel_mode; }; +// A FileCluster is a GList with HashTable access to each node (to perform contains() checks quickly). +struct _FileCluster +{ + GList *head; + GList *items; + gboolean show_children; +}; + +struct _FileClusterList +{ + // A map from any clustered FileData to the FileCluster object that describes the cluster. + GHashTable *clusters; +}; + #define FILEDATA_MARKS_SIZE 6 struct _FileDataChangeInfo { @@ -843,6 +862,7 @@ struct _ViewFile FileData *dir_fd; GList *list; + FileClusterList *cluster_list; SortType sort_method; gboolean sort_ascend; diff --git a/src/ui_tree_edit.c b/src/ui_tree_edit.c index 0fe4b238..464a0ad4 100644 --- a/src/ui_tree_edit.c +++ b/src/ui_tree_edit.c @@ -496,6 +496,7 @@ gint tree_path_to_row(GtkTreePath *tpath) void shift_color(GdkColor *src, gshort val, gint direction) { gshort cs; + static gshort COLOR_MAX = 0xffff; if (val == -1) { @@ -505,11 +506,11 @@ void shift_color(GdkColor *src, gshort val, gint direction) { val = CLAMP(val, 1, 100); } - cs = 0xffff / 100 * val; + cs = COLOR_MAX / 100 * val; /* up or down ? */ if (direction < 0 || - (direction == 0 &&((gint)src->red + (gint)src->green + (gint)src->blue) / 3 > 0xffff / 2)) + (direction == 0 &&((gint)src->red + (gint)src->green + (gint)src->blue) / 3 > COLOR_MAX / 2)) { src->red = MAX(0 , src->red - cs); src->green = MAX(0 , src->green - cs); @@ -517,9 +518,9 @@ void shift_color(GdkColor *src, gshort val, gint direction) } else { - src->red = MIN(0xffff, src->red + cs); - src->green = MIN(0xffff, src->green + cs); - src->blue = MIN(0xffff, src->blue + cs); + src->red = MIN(COLOR_MAX, src->red + cs); + src->green = MIN(COLOR_MAX, src->green + cs); + src->blue = MIN(COLOR_MAX, src->blue + cs); } } diff --git a/src/view_file/view_file.c b/src/view_file/view_file.c index c9dbdef8..97fafae1 100644 --- a/src/view_file/view_file.c +++ b/src/view_file/view_file.c @@ -25,6 +25,7 @@ #include "collect.h" #include "collect-table.h" #include "editors.h" +#include "filecluster.h" #include "layout.h" #include "menu.h" #include "thumb.h" @@ -673,6 +674,7 @@ static void vf_destroy_cb(GtkWidget *widget, gpointer data) gtk_widget_destroy(vf->popup); } + fileclusterlist_free(vf->cluster_list); file_data_unref(vf->dir_fd); g_free(vf->info); g_free(vf); @@ -729,6 +731,7 @@ ViewFile *vf_new(FileViewType type, FileData *dir_fd) gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(vf->scrolled), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); + vf->cluster_list = fileclusterlist_new(); vf->filter = vf_marks_filter_init(vf); vf->widget = gtk_vbox_new(FALSE, 0); diff --git a/src/view_file/view_file_icon.c b/src/view_file/view_file_icon.c index b60074c5..cd5d05ce 100644 --- a/src/view_file/view_file_icon.c +++ b/src/view_file/view_file_icon.c @@ -30,6 +30,7 @@ #include "dnd.h" #include "editors.h" #include "img-view.h" +#include "filecluster.h" #include "filedata.h" #include "layout.h" #include "layout_image.h" @@ -1250,6 +1251,63 @@ gboolean vficon_press_key_cb(GtkWidget *widget, GdkEventKey *event, gpointer dat vf->popup = vf_pop_menu(vf); gtk_menu_popup(GTK_MENU(vf->popup), NULL, NULL, vfi_menu_position_cb, vf, 0, GDK_CURRENT_TIME); break; + case GDK_KEY_Insert: + // DO NOT SUBMIT + // TODO(xsdg): make an actual UX for this. + g_warning("Starting a cluster!"); + fd = vficon_find_data(vf, VFICON(vf)->focus_row, VFICON(vf)->focus_column, NULL); + if (fd) + { + // Make a cluster out of the entire selection + if (VFICON(vf)->selection && VFICON(vf)->selection->next) + { + FileCluster *fc; + // At least two items selected; go for it. + g_warning("Had requisite number of selection items; going for it!"); + fc = fileclusterlist_create_cluster(vf->cluster_list, VFICON(vf)->selection); + if (fc) + { + vficon_selection_add(vf, VFICON(vf)->selection->data, SELECTION_CLUSTER_HEAD, NULL); + vf_refresh(vf); + } + } + else + { + if (VFICON(vf)->selection) + { + g_warning("Only one item selected; need at least two."); + } + else + { + g_warning("No items selected; need at least two."); + } + } + } + break; + case GDK_KEY_F2: + g_warning("Flipping show_children!"); + fd = vficon_find_data(vf, VFICON(vf)->focus_row, VFICON(vf)->focus_column, NULL); + if (fd) + { + FileCluster *fc = g_hash_table_lookup(vf->cluster_list->clusters, fd); + if (fc) + { + if (filecluster_toggle_show_children(fc)) + { + for (GList *work = fc->items; work; work = work->next) + { + // TODO(xsdg): This is broken because the FileData pointer stored in the + // cluster is different from the one just added to vf->list, even though + // they are equivalent. + FileData *fd = work->data; + if (work == fc->head) continue; + vficon_selection_add(vf, fd, SELECTION_CLUSTER_CHILD, NULL); + } + } + vf_refresh(vf); + } + } + break; default: stop_signal = FALSE; break; @@ -1796,9 +1854,11 @@ static gboolean vficon_refresh_real(ViewFile *vf, gboolean keep_position) { ret = filelist_read(vf->dir_fd, &new_filelist, NULL); new_filelist = file_data_filter_marks_list(new_filelist, vf_marks_get_filter(vf)); + new_filelist = fileclusterlist_remove_children_from_list(vf->cluster_list, new_filelist); } - vf->list = filelist_sort(vf->list, vf->sort_method, vf->sort_ascend); /* the list might not be sorted if there were renames */ + /* the list might not be sorted if there were renames */ + vf->list = filelist_sort(vf->list, vf->sort_method, vf->sort_ascend); new_filelist = filelist_sort(new_filelist, vf->sort_method, vf->sort_ascend); if (VFICON(vf)->selection) @@ -1972,6 +2032,21 @@ static void vficon_cell_data_cb(GtkTreeViewColumn *tree_column, GtkCellRenderer memcpy(&color_fg, &style->text[state], sizeof(color_fg)); memcpy(&color_bg, &style->base[state], sizeof(color_bg)); + if (fd->selected & SELECTION_CLUSTER_HEAD) + { + // TODO(xsdg): Cluster coloration should be part of the style. + color_bg.blue = 0x4000; + color_bg.green = 0x4000; + color_bg.red = 0xFFFF; + } + else if (fd->selected & SELECTION_CLUSTER_CHILD) + { + // TODO(xsdg): Cluster coloration should be part of the style. + color_bg.blue = 0x8000; + color_bg.green = 0x8000; + color_bg.red = 0xFFFF; + } + if (fd->selected & SELECTION_PRELIGHT) { shift_color(&color_bg, -1, 0); diff --git a/src/view_file/view_file_list.c b/src/view_file/view_file_list.c index b56a025c..5e02c398 100644 --- a/src/view_file/view_file_list.c +++ b/src/view_file/view_file_list.c @@ -27,6 +27,7 @@ #include "dnd.h" #include "editors.h" #include "img-view.h" +#include "filecluster.h" #include "layout.h" #include "layout_image.h" #include "menu.h" @@ -497,6 +498,77 @@ gboolean vflist_press_key_cb(GtkWidget *widget, GdkEventKey *event, gpointer dat ViewFile *vf = data; GtkTreePath *tpath; + // DO NOT SUBMIT + // TODO(xsdg): these key combos should be handled by the standard, configurable mechanism. + + if (event->keyval == GDK_KEY_Insert || event->keyval == GDK_KEY_F2) + { + // First off, get the selected FDs + GList *selected_fds = NULL; + { + GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(vf->listview)); + if (event->keyval == GDK_KEY_Insert) + { + if (gtk_tree_selection_count_selected_rows(selection) < 2) + { + g_warning("Need at least two items selected to create a cluster."); + return TRUE; + } + } + else + { + if (gtk_tree_selection_count_selected_rows(selection) < 1) + { + g_warning("Must have a node selected to flip show_children."); + return TRUE; + } + } + + // List of GtkTreePath + GList *selected_rows = gtk_tree_selection_get_selected_rows(selection, NULL); + GtkTreeModel *store = gtk_tree_view_get_model(GTK_TREE_VIEW(vf->listview)); + GtkTreeIter iter; + for (GList *work = selected_rows; work; work = work->next) + { + FileData *fd; + GtkTreePath *select_path = work->data; + gtk_tree_model_get_iter(store, &iter, select_path); + gtk_tree_model_get(store, &iter, FILE_COLUMN_POINTER, &fd, -1); + selected_fds = g_list_prepend(selected_fds, file_data_ref(fd)); + } + + selected_fds = g_list_reverse(selected_fds); + g_list_free_full(selected_rows, (GDestroyNotify)gtk_tree_path_free); + } + + if (event->keyval == GDK_KEY_Insert) + { + g_warning("Starting a cluster!"); + FileCluster *fc = fileclusterlist_create_cluster(vf->cluster_list, selected_fds); + if (fc) + { + // TODO(xsdg): mark as in a cluster somehow? + vf_refresh(vf); + } + } + else if (event->keyval == GDK_KEY_F2) + { + FileData *fd = selected_fds->data; + if (fd) + { + g_warning("Flipping show_children!"); + FileCluster *fc = g_hash_table_lookup(vf->cluster_list->clusters, fd); + if (fc) + { + filecluster_toggle_show_children(fc); + vf_refresh(vf); + } + } + } + + return TRUE; // Handled event; stop propagating. + } + if (event->keyval != GDK_KEY_Menu) return FALSE; gtk_tree_view_get_cursor(GTK_TREE_VIEW(vf->listview), &tpath, NULL); @@ -909,9 +981,14 @@ static void vflist_setup_iter_recursive(ViewFile *vf, GtkTreeStore *store, GtkTr else { if (parent_iter) - match = filelist_sort_compare_filedata_full(fd, old_fd, SORT_NAME, TRUE); /* always sort sidecars by name */ + { + /* always sort sidecars by name */ + match = filelist_sort_compare_filedata_full(fd, old_fd, SORT_NAME, TRUE); + } else + { match = filelist_sort_compare_filedata_full(fd, old_fd, vf->sort_method, vf->sort_ascend); + } if (match == 0) g_warning("multiple fd for the same path"); } @@ -1668,6 +1745,7 @@ gboolean vflist_refresh(ViewFile *vf) } vf->list = file_data_filter_marks_list(vf->list, vf_marks_get_filter(vf)); + vf->list = fileclusterlist_remove_children_from_list(vf->cluster_list, vf->list); file_data_register_notify_func(vf_notify_cb, vf, NOTIFY_PRIORITY_MEDIUM); DEBUG_1("%s vflist_refresh: sort", get_exec_time()); @@ -1729,8 +1807,43 @@ static void vflist_listview_color_cb(GtkTreeViewColumn *tree_column, GtkCellRend { ViewFile *vf = data; gboolean set; + FileData *fd; gtk_tree_model_get(tree_model, iter, FILE_COLUMN_COLOR, &set, -1); + gtk_tree_model_get(tree_model, iter, FILE_COLUMN_POINTER, &fd, -1); + // TODO(xsdg): optimize! + if (fd) + { + FileCluster *fc = g_hash_table_lookup(vf->cluster_list->clusters, fd); + if (fc) + { + if (filecluster_has_head(fc, fd)) + { + GdkColor *color_bg = g_new0(GdkColor, 1); + color_bg->blue = 0x4000; + color_bg->green = 0x4000; + color_bg->red = 0xFFFF; + + g_object_set(G_OBJECT(cell), + "cell-background-gdk", color_bg, + "cell-background-set", TRUE, NULL); + return; + } + else if (filecluster_has_child(fc, fd)) + { + GdkColor *color_bg = g_new0(GdkColor, 1); + color_bg->blue = 0x8000; + color_bg->green = 0x8000; + color_bg->red = 0xFFFF; + + g_object_set(G_OBJECT(cell), + "cell-background-gdk", color_bg, + "cell-background-set", TRUE, NULL); + return; + } + } + } + g_object_set(G_OBJECT(cell), "cell-background-gdk", vflist_listview_color_shifted(vf->listview), "cell-background-set", set, NULL);