From e67dcc4f4a53d0f583ea07066aa0b65f4a339007 Mon Sep 17 00:00:00 2001 From: Omari Stephens Date: Tue, 27 Dec 2016 20:35:59 +0000 Subject: [PATCH] Revamp pan view filtering to support different modes and grouping. Conceptually, the filter is a sequence of filter elements, which are considered in order for each image in the pan view. The end result is that each image is either rejected or displayed as normal. --- src/pan-view/pan-timeline.c | 2 +- src/pan-view/pan-types.h | 11 +-- src/pan-view/pan-view-filter.c | 145 +++++++++++++++++++++++---------- src/pan-view/pan-view-filter.h | 36 +++++++- 4 files changed, 137 insertions(+), 57 deletions(-) diff --git a/src/pan-view/pan-timeline.c b/src/pan-view/pan-timeline.c index d87dbed4..15ffd87a 100644 --- a/src/pan-view/pan-timeline.c +++ b/src/pan-view/pan-timeline.c @@ -42,7 +42,7 @@ void pan_timeline_compute(PanWindow *pw, FileData *dir_fd, gint *width, gint *he gint y_height; list = pan_list_tree(dir_fd, SORT_NONE, TRUE, pw->ignore_symlinks); - gboolean changed = pan_filter_fd_list(&list, pw->filter_ui->filter_kw_table, PAN_VIEW_INTERSECTION); + gboolean changed = pan_filter_fd_list(&list, pw->filter_ui->filter_elements); if (pw->cache_list && pw->exif_date_enable) { diff --git a/src/pan-view/pan-types.h b/src/pan-view/pan-types.h index c3091592..9fd8916c 100644 --- a/src/pan-view/pan-types.h +++ b/src/pan-view/pan-types.h @@ -179,17 +179,8 @@ struct _PanViewSearchUi GtkWidget *search_button_arrow; }; +// Defined in pan-view-filter.h typedef struct _PanViewFilterUi PanViewFilterUi; -struct _PanViewFilterUi -{ - GtkWidget *filter_box; - GtkWidget *filter_entry; - GtkWidget *filter_label; - GtkWidget *filter_button; - GtkWidget *filter_button_arrow; - GHashTable *filter_kw_table; - GtkWidget *filter_kw_hbox; -}; typedef struct _PanWindow PanWindow; struct _PanWindow diff --git a/src/pan-view/pan-view-filter.c b/src/pan-view/pan-view-filter.c index 5c0daa2b..1c66e615 100644 --- a/src/pan-view/pan-view-filter.c +++ b/src/pan-view/pan-view-filter.c @@ -36,11 +36,43 @@ PanViewFilterUi *pan_filter_ui_new(PanWindow *pw) GtkWidget *combo; GtkWidget *hbox; + /* Since we're using the GHashTable as a HashSet (in which key and value pointers + * are always identical), specifying key _and_ value destructor callbacks will + * cause a double-free. + */ + { + GtkTreeIter iter; + ui->filter_mode_model = gtk_list_store_new(3, G_TYPE_INT, G_TYPE_STRING, G_TYPE_STRING); + gtk_list_store_append(ui->filter_mode_model, &iter); + gtk_list_store_set(ui->filter_mode_model, &iter, + 0, PAN_VIEW_FILTER_REQUIRE, 1, _("Require"), 2, _("R"), -1); + gtk_list_store_append(ui->filter_mode_model, &iter); + gtk_list_store_set(ui->filter_mode_model, &iter, + 0, PAN_VIEW_FILTER_EXCLUDE, 1, _("Exclude"), 2, _("E"), -1); + gtk_list_store_append(ui->filter_mode_model, &iter); + gtk_list_store_set(ui->filter_mode_model, &iter, + 0, PAN_VIEW_FILTER_INCLUDE, 1, _("Include"), 2, _("I"), -1); + gtk_list_store_append(ui->filter_mode_model, &iter); + gtk_list_store_set(ui->filter_mode_model, &iter, + 0, PAN_VIEW_FILTER_GROUP, 1, _("Group"), 2, _("G"), -1); + + ui->filter_mode_combo = gtk_combo_box_new_with_model(GTK_TREE_MODEL(ui->filter_mode_model)); + gtk_combo_box_set_focus_on_click(GTK_COMBO_BOX(ui->filter_mode_combo), FALSE); + gtk_combo_box_set_active(GTK_COMBO_BOX(ui->filter_mode_combo), 0); + + GtkCellRenderer *render = gtk_cell_renderer_text_new(); + gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(ui->filter_mode_combo), render, TRUE); + gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(ui->filter_mode_combo), render, "text", 1, NULL); + } + // Build the actual filter UI. ui->filter_box = gtk_hbox_new(FALSE, PREF_PAD_SPACE); pref_spacer(ui->filter_box, 0); pref_label_new(ui->filter_box, _("Keyword Filter:")); + gtk_box_pack_start(GTK_BOX(ui->filter_box), ui->filter_mode_combo, TRUE, TRUE, 0); + gtk_widget_show(ui->filter_mode_combo); + hbox = gtk_hbox_new(TRUE, PREF_PAD_SPACE); gtk_box_pack_start(GTK_BOX(ui->filter_box), hbox, TRUE, TRUE, 0); gtk_widget_show(hbox); @@ -74,12 +106,6 @@ PanViewFilterUi *pan_filter_ui_new(PanWindow *pw) g_signal_connect(G_OBJECT(ui->filter_button), "clicked", G_CALLBACK(pan_filter_toggle_cb), pw); - /* Since we're using the GHashTable as a HashSet (in which key and value pointers - * are always identical), specifying key _and_ value destructor callbacks will - * cause a double-free. - */ - ui->filter_kw_table = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); - return ui; } @@ -88,7 +114,7 @@ void pan_filter_ui_destroy(PanViewFilterUi **ui_ptr) if (ui_ptr == NULL || *ui_ptr == NULL) return; // Note that g_clear_pointer handles already-NULL pointers. - g_clear_pointer(&(*ui_ptr)->filter_kw_table, g_hash_table_destroy); + //g_clear_pointer(&(*ui_ptr)->filter_kw_table, g_hash_table_destroy); g_free(*ui_ptr); *ui_ptr = NULL; @@ -101,11 +127,14 @@ static void pan_filter_status(PanWindow *pw, const gchar *text) static void pan_filter_kw_button_cb(GtkButton *widget, gpointer data) { - PanWindow *pw = data; + PanFilterCallbackState *cb_state = data; + PanWindow *pw = cb_state->pw; PanViewFilterUi *ui = pw->filter_ui; - g_hash_table_remove(ui->filter_kw_table, gtk_button_get_label(GTK_BUTTON(widget))); + // TODO(xsdg): Fix filter element pointed object memory leak. + ui->filter_elements = g_list_delete_link(ui->filter_elements, cb_state->filter_element); gtk_widget_destroy(GTK_WIDGET(widget)); + g_free(cb_state); pan_filter_status(pw, _("Removed keyword…")); pan_layout_update(pw); @@ -116,29 +145,41 @@ void pan_filter_activate_cb(const gchar *text, gpointer data) GtkWidget *kw_button; PanWindow *pw = data; PanViewFilterUi *ui = pw->filter_ui; + GtkTreeIter iter; if (!text) return; + // Get all relevant state and reset UI. + gtk_combo_box_get_active_iter(GTK_COMBO_BOX(ui->filter_mode_combo), &iter); gtk_entry_set_text(GTK_ENTRY(ui->filter_entry), ""); + tab_completion_append_to_history(ui->filter_entry, text); - if (g_hash_table_contains(ui->filter_kw_table, text)) - { - pan_filter_status(pw, _("Already added…")); - return; - } + // Add new filter element. + PanViewFilterElement *element = g_new0(PanViewFilterElement, 1); + gtk_tree_model_get(GTK_TREE_MODEL(ui->filter_mode_model), &iter, 0, &element->mode, -1); + element->keyword = g_strdup(text); + ui->filter_elements = g_list_append(ui->filter_elements, element); - tab_completion_append_to_history(ui->filter_entry, text); + // Get the short version of the mode value. + gchar *short_mode; + gtk_tree_model_get(GTK_TREE_MODEL(ui->filter_mode_model), &iter, 2, &short_mode, -1); - g_hash_table_add(ui->filter_kw_table, g_strdup(text)); + // Create the button. + // TODO(xsdg): Use MVC so that the button list is an actual representation of the GList + gchar *label = g_strdup_printf("(%s) %s", short_mode, text); + kw_button = gtk_button_new_with_label(label); + g_clear_pointer(&label, g_free); - kw_button = gtk_button_new_with_label(text); gtk_box_pack_start(GTK_BOX(ui->filter_kw_hbox), kw_button, FALSE, FALSE, 0); gtk_widget_show(kw_button); + PanFilterCallbackState *cb_state = g_new0(PanFilterCallbackState, 1); + cb_state->pw = pw; + cb_state->filter_element = g_list_last(ui->filter_elements); + g_signal_connect(G_OBJECT(kw_button), "clicked", - G_CALLBACK(pan_filter_kw_button_cb), pw); + G_CALLBACK(pan_filter_kw_button_cb), cb_state); - pan_filter_status(pw, _("Added keyword…")); pan_layout_update(pw); } @@ -202,17 +243,17 @@ void pan_filter_toggle_visible(PanWindow *pw, gboolean enable) } } -gboolean pan_filter_fd_list(GList **fd_list, GHashTable *kw_table, PanViewFilterMode mode) +gboolean pan_filter_fd_list(GList **fd_list, GList *filter_elements) { GList *work; gboolean modified = FALSE; - GHashTableIter kw_iter; - gchar *filter_kw; + GHashTable *seen_kw_table = NULL; + if (!fd_list || !*fd_list || !filter_elements) return modified; - if (!fd_list || !*fd_list || g_hash_table_size(kw_table) == 0) return modified; + // seen_kw_table is only valid in this scope, so don't take ownership of any strings. + seen_kw_table = g_hash_table_new_full(g_str_hash, g_str_equal, NULL, NULL); - // TODO(xsdg): Pay attention to filter mode. work = *fd_list; while (work) { @@ -223,36 +264,56 @@ gboolean pan_filter_fd_list(GList **fd_list, GHashTable *kw_table, PanViewFilter // TODO(xsdg): OPTIMIZATION Do the search inside of metadata.c to avoid a // bunch of string list copies. GList *img_keywords = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN); - if (!img_keywords) - { - *fd_list = g_list_delete_link(*fd_list, last_work); - modified = TRUE; - continue; - } - gint match_count = 0; - gint miss_count = 0; - g_hash_table_iter_init(&kw_iter, kw_table); - while (g_hash_table_iter_next(&kw_iter, (void**)&filter_kw, NULL)) + // TODO(xsdg): OPTIMIZATION Determine a heuristic for when to linear-search the + // keywords list, and when to build a hash table for the image's keywords. + gboolean should_reject = FALSE; + gchar *group_kw = NULL; + GList *filter_element = filter_elements; + while (filter_element) { - if (g_list_find_custom(img_keywords, filter_kw, (GCompareFunc)g_strcmp0)) - { - ++match_count; - } - else + PanViewFilterElement *filter = filter_element->data; + filter_element = filter_element->next; + gboolean has_kw = !!g_list_find_custom(img_keywords, filter->keyword, (GCompareFunc)g_strcmp0); + + switch (filter->mode) { - ++miss_count; + case PAN_VIEW_FILTER_REQUIRE: + should_reject |= !has_kw; + break; + case PAN_VIEW_FILTER_EXCLUDE: + should_reject |= has_kw; + break; + case PAN_VIEW_FILTER_INCLUDE: + if (has_kw) should_reject = FALSE; + break; + case PAN_VIEW_FILTER_GROUP: + if (has_kw) + { + if (g_hash_table_contains(seen_kw_table, filter->keyword)) + { + should_reject = TRUE; + } + else if (group_kw == NULL) + { + group_kw = filter->keyword; + } + } + break; } - if (miss_count > 0) break; } + if (!should_reject && group_kw != NULL) g_hash_table_add(seen_kw_table, group_kw); + string_list_free(img_keywords); - if (miss_count > 0 || match_count == 0) + + if (should_reject) { *fd_list = g_list_delete_link(*fd_list, last_work); modified = TRUE; } } + g_hash_table_destroy(seen_kw_table); return modified; } diff --git a/src/pan-view/pan-view-filter.h b/src/pan-view/pan-view-filter.h index f284328f..102d0092 100644 --- a/src/pan-view/pan-view-filter.h +++ b/src/pan-view/pan-view-filter.h @@ -26,11 +26,39 @@ #include "pan-types.h" typedef enum { - PAN_VIEW_UNION, - PAN_VIEW_INTERSECTION, - PAN_VIEW_GROUP + PAN_VIEW_FILTER_REQUIRE, + PAN_VIEW_FILTER_EXCLUDE, + PAN_VIEW_FILTER_INCLUDE, + PAN_VIEW_FILTER_GROUP } PanViewFilterMode; +typedef struct _PanViewFilterElement PanViewFilterElement; +struct _PanViewFilterElement +{ + PanViewFilterMode mode; + gchar *keyword; +}; + +typedef struct _PanFilterCallbackState PanFilterCallbackState; +struct _PanFilterCallbackState +{ + PanWindow *pw; + GList *filter_element; +}; + +struct _PanViewFilterUi +{ + GtkWidget *filter_box; + GtkWidget *filter_entry; + GtkWidget *filter_label; + GtkWidget *filter_button; + GtkWidget *filter_button_arrow; + GtkWidget *filter_kw_hbox; + GtkListStore *filter_mode_model; + GtkWidget *filter_mode_combo; + GList *filter_elements; // List of PanViewFilterElement. +}; + void pan_filter_toggle_visible(PanWindow *pw, gboolean enable); void pan_filter_activate(PanWindow *pw); void pan_filter_activate_cb(const gchar *text, gpointer data); @@ -42,7 +70,7 @@ PanViewFilterUi *pan_filter_ui_new(PanWindow *pw); // Destroys the specified PanViewFilterUi and sets the pointer to NULL. void pan_filter_ui_destroy(PanViewFilterUi **ui); -gboolean pan_filter_fd_list(GList **fd_list, GHashTable *kw_table, PanViewFilterMode mode); +gboolean pan_filter_fd_list(GList **fd_list, GList *filter_elements); #endif /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */ -- 2.20.1