Revamp pan view filtering to support different modes and grouping.
authorOmari Stephens <xsdg@google.com>
Tue, 27 Dec 2016 20:35:59 +0000 (20:35 +0000)
committerOmari Stephens <xsdg@google.com>
Mon, 3 Jul 2017 21:20:24 +0000 (21:20 +0000)
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
src/pan-view/pan-types.h
src/pan-view/pan-view-filter.c
src/pan-view/pan-view-filter.h

index d87dbed..15ffd87 100644 (file)
@@ -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)
                {
index c309159..9fd8916 100644 (file)
@@ -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
index 5c0daa2..1c66e61 100644 (file)
@@ -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;
 }
index f284328..102d009 100644 (file)
 #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: */