Adds a keyword filtering feature to Timeline PanView.
authorOmari Stephens <xsdg@google.com>
Sun, 25 Dec 2016 08:25:13 +0000 (08:25 +0000)
committerOmari Stephens <xsdg@google.com>
Mon, 3 Jul 2017 21:19:59 +0000 (21:19 +0000)
UI needs some work, and currently only supports intersection between multiple
keywords.  Would be easy to support keyword unions, if the UI supported it.

Should probably hide the button in modes that don't yet support it.

src/pan-view/Makefile.am
src/pan-view/pan-timeline.c
src/pan-view/pan-types.h
src/pan-view/pan-view-filter.c [new file with mode: 0644]
src/pan-view/pan-view-filter.h [new file with mode: 0644]
src/pan-view/pan-view-search.c
src/pan-view/pan-view.c
src/pan-view/pan-view.h

index 154149b..2401b5e 100644 (file)
@@ -14,6 +14,8 @@ module_pan_view = \
        %D%/pan-util.h  \
        %D%/pan-view.c  \
        %D%/pan-view.h  \
+       %D%/pan-view-filter.c   \
+       %D%/pan-view-filter.h   \
        %D%/pan-view-search.c   \
        %D%/pan-view-search.h
 
index 9a96420..e60930e 100644 (file)
 
 #include "pan-timeline.h"
 
+#include "metadata.h"
 #include "pan-item.h"
 #include "pan-util.h"
 #include "pan-view.h"
+#include "ui_fileops.h"
 
 void pan_timeline_compute(PanWindow *pw, FileData *dir_fd, gint *width, gint *height)
 {
        GList *list;
        GList *work;
+       GHashTable *filter_kw_table;
+       GHashTableIter filter_kw_iter;
+       gchar *filter_kw;
        gint x, y;
-       time_t tc;
+       time_t group_start_date;
        gint total;
        gint count;
        PanItem *pi_month = NULL;
@@ -41,6 +46,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);
+       filter_kw_table = pw->filter_ui->filter_kw_table;  // Shorthand.
 
        if (pw->cache_list && pw->exif_date_enable)
                {
@@ -61,7 +67,8 @@ void pan_timeline_compute(PanWindow *pw, FileData *dir_fd, gint *width, gint *he
        day_start = month_start;
        x_width = 0;
        y_height = 0;
-       tc = 0;
+       group_start_date = 0;
+       // total and count are used to enforce a stride of PAN_GROUP_MAX thumbs.
        total = 0;
        count = 0;
        work = list;
@@ -73,13 +80,44 @@ void pan_timeline_compute(PanWindow *pw, FileData *dir_fd, gint *width, gint *he
                fd = work->data;
                work = work->next;
 
-               if (!pan_date_compare(fd->date, tc, PAN_DATE_LENGTH_DAY))
+               // Don't show images that fail the keyword test.
+               if (g_hash_table_size(filter_kw_table) > 0)
                        {
+                       gint match_count = 0;
+                       gint miss_count = 0;
+                       // TODO(xsdg): OPTIMIZATION Do the search inside of metadata.c to avoid a
+                       // bunch of string list copies.
+                       // TODO(xsdg): Allow user to switch between union and intersection.
+                       GList *img_keywords = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN);
+                       if (!img_keywords) continue;
+
+                       g_hash_table_iter_init(&filter_kw_iter, filter_kw_table);
+                       while (g_hash_table_iter_next(&filter_kw_iter, (void**)&filter_kw, NULL))
+                               {
+                               if (g_list_find_custom(img_keywords, filter_kw, (GCompareFunc)g_strcmp0))
+                                       {
+                                       ++match_count;
+                                       }
+                               else
+                                       {
+                                       ++miss_count;
+                                       }
+                               if (miss_count > 0) break;
+                               }
+
+                       string_list_free(img_keywords);
+                       if (miss_count > 0 || match_count == 0) continue;
+                       }
+
+               if (!pan_date_compare(fd->date, group_start_date, PAN_DATE_LENGTH_DAY))
+                       {
+                       // FD starts a new day group.
                        GList *needle;
                        gchar *buf;
 
-                       if (!pan_date_compare(fd->date, tc, PAN_DATE_LENGTH_MONTH))
+                       if (!pan_date_compare(fd->date, group_start_date, PAN_DATE_LENGTH_MONTH))
                                {
+                               // FD starts a new month group.
                                pi_day = NULL;
 
                                if (pi_month)
@@ -114,7 +152,7 @@ void pan_timeline_compute(PanWindow *pw, FileData *dir_fd, gint *width, gint *he
 
                        if (pi_day) x = pi_day->x + pi_day->width + PAN_BOX_BORDER;
 
-                       tc = fd->date;
+                       group_start_date = fd->date;
                        total = 1;
                        count = 0;
 
@@ -124,7 +162,7 @@ void pan_timeline_compute(PanWindow *pw, FileData *dir_fd, gint *width, gint *he
                                FileData *nfd;
 
                                nfd = needle->data;
-                               if (pan_date_compare(nfd->date, tc, PAN_DATE_LENGTH_DAY))
+                               if (pan_date_compare(nfd->date, group_start_date, PAN_DATE_LENGTH_DAY))
                                        {
                                        needle = needle->next;
                                        total++;
index 901b248..c309159 100644 (file)
@@ -179,6 +179,18 @@ struct _PanViewSearchUi
        GtkWidget *search_button_arrow;
 };
 
+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
 {
@@ -193,6 +205,7 @@ struct _PanWindow
        GtkWidget *label_zoom;
 
        PanViewSearchUi *search_ui;
+       PanViewFilterUi *filter_ui;
 
        GtkWidget *date_button;
 
diff --git a/src/pan-view/pan-view-filter.c b/src/pan-view/pan-view-filter.c
new file mode 100644 (file)
index 0000000..38eff62
--- /dev/null
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2006 John Ellis
+ * Copyright (C) 2008 - 2016 The Geeqie Team
+ *
+ * Author: John Ellis
+ *
+ * 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 "pan-view-filter.h"
+
+#include "image.h"
+#include "pan-item.h"
+#include "pan-util.h"
+#include "pan-view.h"
+#include "ui_tabcomp.h"
+#include "ui_misc.h"
+
+PanViewFilterUi *pan_filter_ui_new(PanWindow *pw)
+{
+       PanViewFilterUi *ui = g_new0(PanViewFilterUi, 1);
+       GtkWidget *combo;
+       GtkWidget *hbox;
+
+       // 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:"));
+
+       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);
+
+       combo = tab_completion_new_with_history(&ui->filter_entry, "", "pan_view_filter", -1,
+                                               pan_filter_activate_cb, pw);
+       gtk_box_pack_start(GTK_BOX(hbox), combo, TRUE, TRUE, 0);
+       gtk_widget_show(combo);
+
+       // TODO(xsdg): Figure out whether it's useful to keep this label around.
+       ui->filter_label = gtk_label_new("");
+       //gtk_box_pack_start(GTK_BOX(hbox), ui->filter_label, FALSE, FALSE, 0);
+       //gtk_widget_show(ui->filter_label);
+
+       ui->filter_kw_hbox = gtk_hbox_new(FALSE, PREF_PAD_SPACE);
+       gtk_box_pack_start(GTK_BOX(hbox), ui->filter_kw_hbox, TRUE, TRUE, 0);
+       gtk_widget_show(ui->filter_kw_hbox);
+
+       // Build the spin-button to show/hide the filter UI.
+       ui->filter_button = gtk_toggle_button_new();
+       gtk_button_set_relief(GTK_BUTTON(ui->filter_button), GTK_RELIEF_NONE);
+       gtk_button_set_focus_on_click(GTK_BUTTON(ui->filter_button), FALSE);
+       hbox = gtk_hbox_new(FALSE, PREF_PAD_GAP);
+       gtk_container_add(GTK_CONTAINER(ui->filter_button), hbox);
+       gtk_widget_show(hbox);
+       ui->filter_button_arrow = gtk_arrow_new(GTK_ARROW_UP, GTK_SHADOW_NONE);
+       gtk_box_pack_start(GTK_BOX(hbox), ui->filter_button_arrow, FALSE, FALSE, 0);
+       gtk_widget_show(ui->filter_button_arrow);
+       pref_label_new(hbox, _("Filter"));
+
+       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;
+}
+
+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_free(*ui_ptr);
+       *ui_ptr = NULL;
+}
+
+static void pan_filter_status(PanWindow *pw, const gchar *text)
+{
+       gtk_label_set_text(GTK_LABEL(pw->filter_ui->filter_label), (text) ? text : "");
+}
+
+static void pan_filter_kw_button_cb(GtkButton *widget, gpointer data)
+{
+       PanWindow *pw = data;
+       PanViewFilterUi *ui = pw->filter_ui;
+
+       g_hash_table_remove(ui->filter_kw_table, gtk_button_get_label(GTK_BUTTON(widget)));
+       gtk_widget_destroy(GTK_WIDGET(widget));
+
+       pan_filter_status(pw, _("Removed keyword…"));
+       pan_layout_update(pw);
+}
+
+void pan_filter_activate_cb(const gchar *text, gpointer data)
+{
+       GtkWidget *kw_button;
+       PanWindow *pw = data;
+       PanViewFilterUi *ui = pw->filter_ui;
+
+       if (!text) return;
+
+       gtk_entry_set_text(GTK_ENTRY(ui->filter_entry), "");
+
+       if (g_hash_table_contains(ui->filter_kw_table, text))
+               {
+               pan_filter_status(pw, _("Already added…"));
+               return;
+               }
+
+       tab_completion_append_to_history(ui->filter_entry, text);
+
+       g_hash_table_add(ui->filter_kw_table, g_strdup(text));
+
+       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);
+
+       g_signal_connect(G_OBJECT(kw_button), "clicked",
+                        G_CALLBACK(pan_filter_kw_button_cb), pw);
+
+       pan_filter_status(pw, _("Added keyword…"));
+       pan_layout_update(pw);
+}
+
+void pan_filter_activate(PanWindow *pw)
+{
+       gchar *text;
+
+       text = g_strdup(gtk_entry_get_text(GTK_ENTRY(pw->filter_ui->filter_entry)));
+       pan_filter_activate_cb(text, pw);
+       g_free(text);
+}
+
+void pan_filter_toggle_cb(GtkWidget *button, gpointer data)
+{
+       PanWindow *pw = data;
+       PanViewFilterUi *ui = pw->filter_ui;
+       gboolean visible;
+
+       visible = gtk_widget_get_visible(ui->filter_box);
+       if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button)) == visible) return;
+
+       if (visible)
+               {
+               gtk_widget_hide(ui->filter_box);
+               gtk_arrow_set(GTK_ARROW(ui->filter_button_arrow), GTK_ARROW_UP, GTK_SHADOW_NONE);
+               }
+       else
+               {
+               gtk_widget_show(ui->filter_box);
+               gtk_arrow_set(GTK_ARROW(ui->filter_button_arrow), GTK_ARROW_DOWN, GTK_SHADOW_NONE);
+               gtk_widget_grab_focus(ui->filter_entry);
+               }
+}
+
+void pan_filter_toggle_visible(PanWindow *pw, gboolean enable)
+{
+       PanViewFilterUi *ui = pw->filter_ui;
+       if (pw->fs) return;
+
+       if (enable)
+               {
+               if (gtk_widget_get_visible(ui->filter_box))
+                       {
+                       gtk_widget_grab_focus(ui->filter_entry);
+                       }
+               else
+                       {
+                       gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(ui->filter_button), TRUE);
+                       }
+               }
+       else
+               {
+               if (gtk_widget_get_visible(ui->filter_entry))
+                       {
+                       if (gtk_widget_has_focus(ui->filter_entry))
+                               {
+                               gtk_widget_grab_focus(GTK_WIDGET(pw->imd->widget));
+                               }
+                       gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(ui->filter_button), FALSE);
+                       }
+               }
+}
diff --git a/src/pan-view/pan-view-filter.h b/src/pan-view/pan-view-filter.h
new file mode 100644 (file)
index 0000000..ca6f198
--- /dev/null
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2006 John Ellis
+ * Copyright (C) 2008 - 2016 The Geeqie Team
+ *
+ * Author: John Ellis
+ *
+ * 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 PAN_VIEW_PAN_VIEW_FILTER_H
+#define PAN_VIEW_PAN_VIEW_FILTER_H
+
+#include "main.h"
+#include "pan-types.h"
+
+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);
+void pan_filter_toggle_cb(GtkWidget *button, gpointer data);
+
+// Creates a new PanViewFilterUi instance and returns it.
+PanViewFilterUi *pan_filter_ui_new(PanWindow *pw);
+
+// Destroys the specified PanViewFilterUi and sets the pointer to NULL.
+void pan_filter_ui_destroy(PanViewFilterUi **ui);
+
+#endif
+/* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */
index 2577d38..5ddef13 100644 (file)
@@ -75,16 +75,7 @@ void pan_search_ui_destroy(PanViewSearchUi **ui_ptr)
 {
        if (ui_ptr == NULL || *ui_ptr == NULL) return;
 
-       PanViewSearchUi *ui = *ui_ptr;  // For convenience.
-
-       // Note that g_clear_object handles already-NULL pointers.
-       g_clear_object(&ui->search_label);
-       g_clear_object(&ui->search_button);
-       g_clear_object(&ui->search_box);
-       g_clear_object(&ui->search_button_arrow);
-       g_clear_object(&ui->search_button);
-
-       g_free(ui);
+       g_free(*ui_ptr);
        *ui_ptr = NULL;
 }
 
index c3d7f74..59c4cc0 100644 (file)
@@ -38,6 +38,7 @@
 #include "pan-item.h"
 #include "pan-timeline.h"
 #include "pan-util.h"
+#include "pan-view-filter.h"
 #include "pan-view-search.h"
 #include "pixbuf-renderer.h"
 #include "pixbuf_util.h"
@@ -1070,7 +1071,7 @@ static void pan_layout_update_idle(PanWindow *pw)
                }
 }
 
-static void pan_layout_update(PanWindow *pw)
+void pan_layout_update(PanWindow *pw)
 {
        pan_window_message(pw, _("Sorting images..."));
        pan_layout_update_idle(pw);
@@ -1132,7 +1133,8 @@ static gboolean pan_window_key_press_cb(GtkWidget *widget, GdkEventKey *event, g
        imd_widget = gtk_container_get_focus_child(GTK_CONTAINER(pw->imd->widget));
        focused = (pw->fs || (imd_widget && gtk_widget_has_focus(imd_widget)));
        on_entry = (gtk_widget_has_focus(pw->path_entry) ||
-                   gtk_widget_has_focus(pw->search_ui->search_entry));
+                   gtk_widget_has_focus(pw->search_ui->search_entry) ||
+                   gtk_widget_has_focus(pw->filter_ui->filter_entry));
 
        if (focused)
                {
@@ -1735,6 +1737,8 @@ static void pan_window_close(PanWindow *pw)
                }
 
        pan_fullscreen_toggle(pw, TRUE);
+       pan_search_ui_destroy(&pw->search_ui);
+       pan_filter_ui_destroy(&pw->filter_ui);
        gtk_widget_destroy(pw->window);
 
        pan_window_items_free(pw);
@@ -1883,6 +1887,10 @@ static void pan_window_new_real(FileData *dir_fd)
        pw->search_ui = pan_search_ui_new(pw);
        gtk_box_pack_start(GTK_BOX(vbox), pw->search_ui->search_box, FALSE, FALSE, 2);
 
+    /* filter bar */
+    pw->filter_ui = pan_filter_ui_new(pw);
+    gtk_box_pack_start(GTK_BOX(vbox), pw->filter_ui->filter_box, FALSE, FALSE, 2);
+
        /* status bar */
 
        box = pref_box_new(vbox, FALSE, GTK_ORIENTATION_HORIZONTAL, 0);
@@ -1914,6 +1922,10 @@ static void pan_window_new_real(FileData *dir_fd)
        gtk_box_pack_end(GTK_BOX(box), pw->search_ui->search_button, FALSE, FALSE, 0);
        gtk_widget_show(pw->search_ui->search_button);
 
+       // Add the "Filter" button to the status bar area.
+       gtk_box_pack_end(GTK_BOX(box), pw->filter_ui->filter_button, FALSE, FALSE, 0);
+       gtk_widget_show(pw->filter_ui->filter_button);
+
        g_signal_connect(G_OBJECT(pw->window), "delete_event",
                         G_CALLBACK(pan_window_delete_cb), pw);
        g_signal_connect(G_OBJECT(pw->window), "key_press_event",
index 586f173..a17c6ab 100644 (file)
@@ -25,6 +25,7 @@
 #include "main.h"
 #include "pan-types.h"
 
+void pan_layout_update(PanWindow *pw);
 GList *pan_layout_intersect(PanWindow *pw, gint x, gint y, gint width, gint height);
 void pan_layout_resize(PanWindow *pw);