[gtk/columnview-search: 2/2] Add search support



commit d37382e556c68f2368f5e38c342ab849378f3c45
Author: Matthias Clasen <mclasen redhat com>
Date:   Tue Dec 24 15:32:59 2019 -0500

    Add search support
    
    Add a filter to GtkListBase, and move the selection
    to the first matching item whenever the filter changes.
    This is meant to be used with single selection and
    a string filter that is hooked up to a search entry.

 docs/reference/gtk/gtk4-sections.txt |   9 ++
 gtk/gtkcolumnview.c                  |  84 ++++++++++++
 gtk/gtkcolumnview.h                  |  10 ++
 gtk/gtkgridview.c                    |  83 +++++++++++
 gtk/gtkgridview.h                    |  11 ++
 gtk/gtklistbase.c                    | 257 ++++++++++++++++++++++++++++++++---
 gtk/gtklistbaseprivate.h             |   6 +
 gtk/gtklistview.c                    |  84 ++++++++++++
 gtk/gtklistview.h                    |  11 ++
 9 files changed, 535 insertions(+), 20 deletions(-)
---
diff --git a/docs/reference/gtk/gtk4-sections.txt b/docs/reference/gtk/gtk4-sections.txt
index 87f55a2193..35f82d7e78 100644
--- a/docs/reference/gtk/gtk4-sections.txt
+++ b/docs/reference/gtk/gtk4-sections.txt
@@ -491,6 +491,9 @@ gtk_list_view_set_single_click_activate
 gtk_list_view_get_single_click_activate
 gtk_list_view_set_enable_rubberband
 gtk_list_view_get_enable_rubberband
+gtk_list_view_set_search_filter
+gtk_list_view_get_search_filter
+gtk_list_view_next_match
 <SUBSECTION Standard>
 GTK_LIST_VIEW
 GTK_LIST_VIEW_CLASS
@@ -521,6 +524,9 @@ gtk_column_view_set_single_click_activate
 gtk_column_view_get_single_click_activate
 gtk_column_view_set_enable_rubberband
 gtk_column_view_get_enable_rubberband
+gtk_column_view_set_search_filter
+gtk_column_view_get_search_filter
+gtk_column_view_next_match
 <SUBSECTION Standard>
 GTK_COLUMN_VIEW
 GTK_COLUMN_VIEW_CLASS
@@ -579,6 +585,9 @@ gtk_grid_view_set_single_click_activate
 gtk_grid_view_get_single_click_activate
 gtk_grid_view_set_enable_rubberband
 gtk_grid_view_get_enable_rubberband
+gtk_grid_view_set_search_filter
+gtk_grid_view_get_search_filter
+gtk_grid_view_next_match
 <SUBSECTION Standard>
 GTK_GRID_VIEW
 GTK_GRID_VIEW_CLASS
diff --git a/gtk/gtkcolumnview.c b/gtk/gtkcolumnview.c
index ce5cf665ba..04def52b5d 100644
--- a/gtk/gtkcolumnview.c
+++ b/gtk/gtkcolumnview.c
@@ -40,6 +40,7 @@
 #include "gtkeventcontrollermotion.h"
 #include "gtkdragsource.h"
 #include "gtkeventcontrollerkey.h"
+#include "gtklistbaseprivate.h"
 
 /**
  * SECTION:gtkcolumnview
@@ -104,6 +105,7 @@ enum
   PROP_VSCROLL_POLICY,
   PROP_SINGLE_CLICK_ACTIVATE,
   PROP_ENABLE_RUBBERBAND,
+  PROP_SEARCH_FILTER,
 
   N_PROPS
 };
@@ -399,6 +401,10 @@ gtk_column_view_get_property (GObject    *object,
       g_value_set_boolean (value, gtk_column_view_get_enable_rubberband (self));
       break;
 
+    case PROP_SEARCH_FILTER:
+      g_value_set_object (value, gtk_column_view_get_search_filter (self));
+      break;
+
     default:
       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
       break;
@@ -474,6 +480,10 @@ gtk_column_view_set_property (GObject      *object,
       gtk_column_view_set_enable_rubberband (self, g_value_get_boolean (value));
       break;
 
+    case PROP_SEARCH_FILTER:
+      gtk_column_view_set_search_filter (self, g_value_get_object (value));
+      break;
+
     default:
       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
       break;
@@ -582,6 +592,18 @@ gtk_column_view_class_init (GtkColumnViewClass *klass)
                           FALSE,
                           G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
 
+  /**
+   * GtkColumnView:search-filter:
+   *
+   * Filter used for search
+   */
+  properties[PROP_SEARCH_FILTER] =
+    g_param_spec_object ("search-filter",
+                         P_("Search filter"),
+                         P_("Filter used for searching"),
+                         GTK_TYPE_FILTER,
+                         G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
   g_object_class_install_properties (gobject_class, N_PROPS, properties);
 
   /**
@@ -1390,3 +1412,65 @@ gtk_column_view_get_enable_rubberband (GtkColumnView *self)
 
   return gtk_list_view_get_enable_rubberband (self->listview);
 }
+
+/**
+ * gtk_column_view_set_search_filter:
+ * @self: a #GtkColumnView
+ * @filter: (nullable): the filter to use for search, or %NULL
+ *
+ * Sets a search filter.
+ *
+ * The selection will be moved to first item matching the
+ * filter whenever the filter changes.
+ *
+ * This can be used with single selection and a string
+ * filter that is connected to a search entry.
+ */
+void
+gtk_column_view_set_search_filter (GtkColumnView *self,
+                                   GtkFilter     *filter)
+{
+  g_return_if_fail (GTK_IS_COLUMN_VIEW (self));
+  g_return_if_fail (filter == NULL || GTK_IS_FILTER (filter));
+
+  if (filter == gtk_list_view_get_search_filter (GTK_LIST_VIEW (self->listview)))
+    return;
+
+  gtk_list_view_set_search_filter (GTK_LIST_VIEW (self->listview), filter);
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SEARCH_FILTER]);
+}
+
+/**
+ * gtk_column_view_get_search_filter:
+ * @self: a #GtkColumnView
+ *
+ * Gets the search filter that was set with
+ * gtk_column_view_set_search_filter().
+ *
+ * Returns: (transfer none): The search filter of @self
+ */
+GtkFilter *
+gtk_column_view_get_search_filter (GtkColumnView *self)
+{
+  g_return_val_if_fail (GTK_IS_COLUMN_VIEW (self), NULL);
+
+  return gtk_list_view_get_search_filter (GTK_LIST_VIEW (self->listview));
+}
+
+/**
+ * gtk_column_view_select_next_match:
+ * @self: a #GtkColumnView
+ * @forward: whether to move forward or back
+ *
+ * Moves the selection to the next item matching the
+ * search filter set with gtk_column_view_set_search_filter().
+ */
+void
+gtk_column_view_select_next_match (GtkColumnView *self,
+                                   gboolean       forward)
+{
+  g_return_if_fail (GTK_IS_COLUMN_VIEW (self));
+
+  gtk_list_view_select_next_match (GTK_LIST_VIEW (self->listview), forward);
+}
diff --git a/gtk/gtkcolumnview.h b/gtk/gtkcolumnview.h
index 8b38986417..b871b6e6da 100644
--- a/gtk/gtkcolumnview.h
+++ b/gtk/gtkcolumnview.h
@@ -27,6 +27,7 @@
 #include <gtk/gtktypes.h>
 #include <gtk/gtksortlistmodel.h>
 #include <gtk/gtksorter.h>
+#include <gtk/gtkfilter.h>
 
 G_BEGIN_DECLS
 
@@ -98,6 +99,15 @@ void            gtk_column_view_set_enable_rubberband           (GtkColumnView
 GDK_AVAILABLE_IN_ALL
 gboolean        gtk_column_view_get_enable_rubberband           (GtkColumnView          *self);
 
+GDK_AVAILABLE_IN_ALL
+void            gtk_column_view_set_search_filter               (GtkColumnView          *self,
+                                                                 GtkFilter              *filter);
+GDK_AVAILABLE_IN_ALL
+GtkFilter *     gtk_column_view_get_search_filter               (GtkColumnView          *self);
+
+GDK_AVAILABLE_IN_ALL
+void            gtk_column_view_select_next_match               (GtkColumnView          *self,
+                                                                 gboolean                forward);
 
 G_END_DECLS
 
diff --git a/gtk/gtkgridview.c b/gtk/gtkgridview.c
index 01feba2c16..ad862d2abe 100644
--- a/gtk/gtkgridview.c
+++ b/gtk/gtkgridview.c
@@ -92,6 +92,7 @@ enum
   PROP_MODEL,
   PROP_SINGLE_CLICK_ACTIVATE,
   PROP_ENABLE_RUBBERBAND,
+  PROP_SEARCH_FILTER,
 
   N_PROPS
 };
@@ -919,6 +920,10 @@ gtk_grid_view_get_property (GObject    *object,
       g_value_set_boolean (value, gtk_list_base_get_enable_rubberband (GTK_LIST_BASE (self)));
       break;
 
+    case PROP_SEARCH_FILTER:
+      g_value_set_object (value, gtk_grid_view_get_search_filter (self));
+      break;
+
     default:
       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
       break;
@@ -959,6 +964,10 @@ gtk_grid_view_set_property (GObject      *object,
       gtk_grid_view_set_enable_rubberband (self, g_value_get_boolean (value));
       break;
 
+    case PROP_SEARCH_FILTER:
+      gtk_grid_view_set_search_filter (self, g_value_get_object (value));
+      break;
+
     default:
       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
       break;
@@ -1083,6 +1092,18 @@ gtk_grid_view_class_init (GtkGridViewClass *klass)
                           FALSE,
                           G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
 
+  /**
+   * GtkGridView:search-filter:
+   *
+   * Filter used for search
+   */
+  properties[PROP_SEARCH_FILTER] =
+    g_param_spec_object ("search-filter",
+                         P_("Search filter"),
+                         P_("Filter used for searching"),
+                         GTK_TYPE_FILTER,
+                         G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
   g_object_class_install_properties (gobject_class, N_PROPS, properties);
 
   /**
@@ -1428,3 +1449,65 @@ gtk_grid_view_get_enable_rubberband (GtkGridView *self)
 
   return gtk_list_base_get_enable_rubberband (GTK_LIST_BASE (self));
 }
+
+/**
+ * gtk_grid_view_set_search_filter:
+ * @self: a #GtkGridView
+ * @filter: (nullable): the filter to use for search, or %NULL
+ *
+ * Sets a search filter.
+ *
+ * The selection will be moved to first item matching the
+ * filter whenever the filter changes.
+ *
+ * This can be used with single selection and a string
+ * filter that is connected to a search entry.
+ */
+void
+gtk_grid_view_set_search_filter (GtkGridView *self,
+                                 GtkFilter   *filter)
+{
+  g_return_if_fail (GTK_IS_GRID_VIEW (self));
+  g_return_if_fail (filter == NULL || GTK_IS_FILTER (filter));
+
+  if (filter == gtk_list_base_get_search_filter (GTK_LIST_BASE (self)))
+    return;
+
+  gtk_list_base_set_search_filter (GTK_LIST_BASE (self), filter);
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SEARCH_FILTER]);
+}
+
+/**
+ * gtk_grid_view_get_search_filter:
+ * @self: a #GtkGridView
+ *
+ * Gets the search filter that was set with
+ * gtk_grid_view_set_search_filter().
+ *
+ * Returns: (transfer none): The search filter of @self
+ */
+GtkFilter *
+gtk_grid_view_get_search_filter (GtkGridView *self)
+{
+  g_return_val_if_fail (GTK_IS_GRID_VIEW (self), NULL);
+
+  return gtk_list_base_get_search_filter (GTK_LIST_BASE (self));
+}
+
+/**
+ * gtk_grid_view_select_next_match:
+ * @self: a #GtkGridView
+ * @forward: whether to move forward or back
+ *
+ * Moves the selection to the next item matching the
+ * search filter set with gtk_grid_view_set_search_filter().
+ */
+void
+gtk_grid_view_select_next_match (GtkGridView *self,
+                                 gboolean     forward)
+{
+  g_return_if_fail (GTK_IS_GRID_VIEW (self));
+
+  gtk_list_base_select_next_match (GTK_LIST_BASE (self), forward);
+}
diff --git a/gtk/gtkgridview.h b/gtk/gtkgridview.h
index 6fb035ce12..05cf4d4e2c 100644
--- a/gtk/gtkgridview.h
+++ b/gtk/gtkgridview.h
@@ -25,6 +25,7 @@
 #endif
 
 #include <gtk/gtklistbase.h>
+#include <gtk/gtkfilter.h>
 
 G_BEGIN_DECLS
 
@@ -85,6 +86,16 @@ void            gtk_grid_view_set_single_click_activate         (GtkGridView
 GDK_AVAILABLE_IN_ALL
 gboolean        gtk_grid_view_get_single_click_activate         (GtkGridView            *self);
 
+GDK_AVAILABLE_IN_ALL
+void            gtk_grid_view_set_search_filter                 (GtkGridView            *self,
+                                                                 GtkFilter              *filter);
+GDK_AVAILABLE_IN_ALL
+GtkFilter *     gtk_grid_view_get_search_filter                 (GtkGridView            *self);
+
+GDK_AVAILABLE_IN_ALL
+void            gtk_grid_view_select_next_match                 (GtkGridView            *self,
+                                                                 gboolean                forward);
+
 
 G_END_DECLS
 
diff --git a/gtk/gtklistbase.c b/gtk/gtklistbase.c
index cf0e4e2912..5daf6e83f8 100644
--- a/gtk/gtklistbase.c
+++ b/gtk/gtklistbase.c
@@ -72,6 +72,8 @@ struct _GtkListBasePrivate
   guint autoscroll_id;
   double autoscroll_delta_x;
   double autoscroll_delta_y;
+
+  GtkFilter *search_filter;
 };
 
 enum
@@ -545,6 +547,8 @@ gtk_list_base_focus (GtkWidget        *widget,
     }
 }
 
+static void gtk_list_base_clear_search_filter (GtkListBase *self);
+
 static void
 gtk_list_base_dispose (GObject *object)
 {
@@ -573,6 +577,8 @@ gtk_list_base_dispose (GObject *object)
 
   g_clear_object (&priv->model);
 
+  gtk_list_base_clear_search_filter (self);
+
   G_OBJECT_CLASS (gtk_list_base_parent_class)->dispose (object);
 }
 
@@ -786,26 +792,18 @@ gtk_list_base_update_focus_tracker (GtkListBase *self)
     }
 }
 
-static void
-gtk_list_base_scroll_to_item (GtkWidget  *widget,
-                              const char *action_name,
-                              GVariant   *parameter)
+static gboolean
+gtk_list_base_scroll_to_position (GtkListBase *self,
+                                  guint        pos)
 {
-  GtkListBase *self = GTK_LIST_BASE (widget);
   GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
   int start, end;
   double align_along, align_across;
   GtkPackType side_along, side_across;
-  guint pos;
-
-  if (!g_variant_check_format_string (parameter, "u", FALSE))
-    return;
-
-  g_variant_get (parameter, "u", &pos);
 
   /* figure out primary orientation and if position is valid */
   if (!gtk_list_base_get_allocation_along (GTK_LIST_BASE (self), pos, &start, &end))
-    return;
+    return FALSE;
 
   end += start;
   gtk_list_base_compute_scroll_align (self,
@@ -816,7 +814,7 @@ gtk_list_base_scroll_to_item (GtkWidget  *widget,
 
   /* now do the same thing with the other orientation */
   if (!gtk_list_base_get_allocation_across (GTK_LIST_BASE (self), pos, &start, &end))
-    return;
+    return FALSE;
 
   end += start;
   gtk_list_base_compute_scroll_align (self,
@@ -830,13 +828,32 @@ gtk_list_base_scroll_to_item (GtkWidget  *widget,
                             align_across, side_across,
                             align_along, side_along);
 
-  /* HACK HACK HACK
-   *
-   * GTK has no way to track the focused child. But we now that when a listitem
-   * gets focus, it calls this action. So we update our focus tracker from here
-   * because it's the closest we can get to accurate tracking.
-   */
-  gtk_list_base_update_focus_tracker (self);
+  return TRUE;
+}
+
+static void
+gtk_list_base_scroll_to_item (GtkWidget  *widget,
+                              const char *action_name,
+                              GVariant   *parameter)
+{
+  GtkListBase *self = GTK_LIST_BASE (widget);
+  guint pos;
+
+  if (!g_variant_check_format_string (parameter, "u", FALSE))
+    return;
+
+  g_variant_get (parameter, "u", &pos);
+
+  if (gtk_list_base_scroll_to_position (self, pos))
+    {
+      /* HACK HACK HACK
+       *
+       * GTK has no way to track the focused child. But we know that when a listitem
+       * gets focus, it calls this action. So we update our focus tracker from here
+       * because it's the closest we can get to accurate tracking.
+       */
+      gtk_list_base_update_focus_tracker (self);
+    }
 }
 
 static void
@@ -885,6 +902,17 @@ gtk_list_base_unselect_all (GtkWidget  *widget,
   gtk_selection_model_unselect_all (selection_model);
 }
 
+static void
+gtk_list_base_next_match_action (GtkWidget  *widget,
+                                 const char *action_name,
+                                 GVariant   *parameter)
+{
+  gboolean forward;
+
+  g_variant_get (parameter, "(b)", &forward);
+  gtk_list_base_select_next_match (GTK_LIST_BASE (widget), forward);
+}
+
 static gboolean
 gtk_list_base_move_cursor_to_start (GtkWidget *widget,
                                     GVariant  *args,
@@ -1199,6 +1227,11 @@ gtk_list_base_class_init (GtkListBaseClass *klass)
                                    NULL,
                                    gtk_list_base_unselect_all);
 
+  gtk_widget_class_install_action (widget_class,
+                                   "list.next-match",
+                                   "(b)",
+                                   gtk_list_base_next_match_action);
+
   gtk_list_base_add_move_binding (widget_class, GDK_KEY_Up, GTK_ORIENTATION_VERTICAL, -1);
   gtk_list_base_add_move_binding (widget_class, GDK_KEY_KP_Up, GTK_ORIENTATION_VERTICAL, -1);
   gtk_list_base_add_move_binding (widget_class, GDK_KEY_Down, GTK_ORIENTATION_VERTICAL, 1);
@@ -1221,6 +1254,9 @@ gtk_list_base_class_init (GtkListBaseClass *klass)
   gtk_widget_class_add_binding_action (widget_class, GDK_KEY_slash, GDK_CONTROL_MASK, "list.select-all", 
NULL);
   gtk_widget_class_add_binding_action (widget_class, GDK_KEY_A, GDK_CONTROL_MASK | GDK_SHIFT_MASK, 
"list.unselect-all", NULL);
   gtk_widget_class_add_binding_action (widget_class, GDK_KEY_backslash, GDK_CONTROL_MASK, 
"list.unselect-all", NULL);
+
+  gtk_widget_class_add_binding_action (widget_class, GDK_KEY_g, GDK_CONTROL_MASK, "list.next-match", "(b)", 
TRUE);
+  gtk_widget_class_add_binding_action (widget_class, GDK_KEY_g, GDK_CONTROL_MASK | GDK_SHIFT_MASK, 
"list.next-match", "(b)", FALSE);
 }
 
 static void gtk_list_base_update_rubberband_selection (GtkListBase *self);
@@ -1906,3 +1942,184 @@ gtk_list_base_set_model (GtkListBase *self,
 
   return TRUE;
 }
+
+static guint
+find_first_selected (GtkSelectionModel *model)
+{
+  guint i, start, n_items;
+  gboolean selected;
+
+  n_items = 0;
+  for (i = 0; i < g_list_model_get_n_items (G_LIST_MODEL (model)); i += n_items)
+    {
+      gtk_selection_model_query_range (model, i, &start, &n_items, &selected);
+      if (selected)
+        return i;
+    }
+
+  return GTK_INVALID_LIST_POSITION;
+}
+
+static gboolean
+match_item (GListModel *model,
+            GtkFilter  *filter,
+            guint       position)
+{
+  gpointer item;
+  gboolean result;
+
+  item = g_list_model_get_item (model, position);
+  result = gtk_filter_match (filter, item);
+  g_object_unref (item);
+
+  return result;
+}
+
+static guint
+find_next_match (GListModel *model,
+                 GtkFilter   *filter,
+                 guint        start,
+                 gboolean     forward)
+{
+  guint i;
+
+  if (start == GTK_INVALID_LIST_POSITION)
+    start = 0;
+
+  if (forward)
+    for (i = start; i < g_list_model_get_n_items (model); i++)
+      {
+        if (match_item (model, filter, i))
+          return i;
+      }
+  else
+    for (i = start; ; i--)
+      {
+        if (match_item (model, filter, i))
+          return i;
+        if (i == 0)
+          break;
+      }
+
+  return GTK_INVALID_LIST_POSITION;
+}
+
+static void
+gtk_list_base_search_filter_changed_cb (GtkFilter       *filter,
+                                           GtkFilterChange  change,
+                                           GtkListBase     *self)
+{
+  GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
+  GtkSelectionModel *model = gtk_list_item_manager_get_model (priv->item_manager);
+
+  if (model == NULL)
+    return;
+
+  if (gtk_filter_get_strictness (priv->search_filter) == GTK_FILTER_MATCH_NONE)
+    gtk_selection_model_unselect_all (model);
+  else
+    {
+      guint position;
+
+      switch (change)
+        {
+        case GTK_FILTER_CHANGE_DIFFERENT:
+        case GTK_FILTER_CHANGE_LESS_STRICT:
+          position = find_next_match (G_LIST_MODEL (model), priv->search_filter, 0, TRUE);
+          break;
+
+        case GTK_FILTER_CHANGE_MORE_STRICT:
+          position = find_first_selected (model);
+          if (position == GTK_INVALID_LIST_POSITION)
+            position = 0;
+          position = find_next_match (G_LIST_MODEL (model), priv->search_filter, position, TRUE);
+          break;
+
+        default:
+          g_assert_not_reached ();
+        }
+
+      if (position == GTK_INVALID_LIST_POSITION)
+        gtk_selection_model_unselect_all (model);
+      else
+        gtk_list_base_grab_focus_on_item (self, position, TRUE, FALSE, FALSE);
+    }
+}
+
+static void
+gtk_list_base_clear_search_filter (GtkListBase *self)
+{
+  GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
+
+  if (priv->search_filter == NULL)
+    return;
+
+  g_signal_handlers_disconnect_by_func (priv->search_filter,
+                                        gtk_list_base_search_filter_changed_cb,
+                                        self);
+
+  g_clear_object (&priv->search_filter);
+}
+
+void
+gtk_list_base_set_search_filter (GtkListBase *self,
+                                    GtkFilter   *filter)
+{
+  GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
+
+  if (priv->search_filter == filter)
+    return;
+
+  gtk_list_base_clear_search_filter (self);
+
+  if (filter)
+    {
+      priv->search_filter = g_object_ref (filter);
+      g_signal_connect (priv->search_filter, "changed",
+                        G_CALLBACK (gtk_list_base_search_filter_changed_cb), self);
+      gtk_list_base_search_filter_changed_cb (priv->search_filter, GTK_FILTER_CHANGE_DIFFERENT, self);
+    }
+
+  gtk_widget_action_set_enabled (GTK_WIDGET (self), "list.next-match", filter != NULL);
+}
+
+GtkFilter *
+gtk_list_base_get_search_filter (GtkListBase *self)
+{
+  GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
+
+  return priv->search_filter;
+}
+
+gboolean
+gtk_list_base_select_next_match (GtkListBase *self,
+                                 gboolean     forward)
+{
+  GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
+  GtkSelectionModel *model = gtk_list_item_manager_get_model (priv->item_manager);
+  guint position;
+
+  if (!priv->search_filter)
+    return FALSE;
+
+  position = find_first_selected (model);
+  if (position == GTK_INVALID_LIST_POSITION)
+    return FALSE;
+
+  if (forward)
+    position = position + 1;
+  else if (position > 0)
+    position = position - 1;
+  else
+    return FALSE;
+
+  position = find_next_match (G_LIST_MODEL (model), priv->search_filter, position, forward);
+  if (position == GTK_INVALID_LIST_POSITION)
+    {
+      gtk_widget_error_bell (GTK_WIDGET (self));
+      return FALSE;
+    }
+
+  gtk_list_base_grab_focus_on_item (self, position, TRUE, FALSE, FALSE);
+  return TRUE;
+}
diff --git a/gtk/gtklistbaseprivate.h b/gtk/gtklistbaseprivate.h
index 73d20151c8..3485ea9da6 100644
--- a/gtk/gtklistbaseprivate.h
+++ b/gtk/gtklistbaseprivate.h
@@ -23,6 +23,7 @@
 #include "gtklistbase.h"
 
 #include "gtklistitemmanagerprivate.h"
+#include "gtkfilter.h"
 #include "gtkprivate.h"
 
 struct _GtkListBase
@@ -102,5 +103,10 @@ gboolean               gtk_list_base_grab_focus_on_item         (GtkListBase
 void                   gtk_list_base_set_enable_rubberband      (GtkListBase            *self,
                                                                  gboolean                enable);
 gboolean               gtk_list_base_get_enable_rubberband      (GtkListBase            *self);
+void                   gtk_list_base_set_search_filter          (GtkListBase            *self,
+                                                                 GtkFilter              *filter);
+GtkFilter *            gtk_list_base_get_search_filter          (GtkListBase            *self);
+gboolean               gtk_list_base_select_next_match          (GtkListBase            *self,
+                                                                 gboolean                forward);
 
 #endif /* __GTK_LIST_BASE_PRIVATE_H__ */
diff --git a/gtk/gtklistview.c b/gtk/gtklistview.c
index ea757c1f41..18288c9c06 100644
--- a/gtk/gtklistview.c
+++ b/gtk/gtklistview.c
@@ -86,6 +86,7 @@ enum
   PROP_SHOW_SEPARATORS,
   PROP_SINGLE_CLICK_ACTIVATE,
   PROP_ENABLE_RUBBERBAND,
+  PROP_SEARCH_FILTER,
 
   N_PROPS
 };
@@ -648,6 +649,10 @@ gtk_list_view_get_property (GObject    *object,
       g_value_set_boolean (value, gtk_list_base_get_enable_rubberband (GTK_LIST_BASE (self)));
       break;
 
+    case PROP_SEARCH_FILTER:
+      g_value_set_object (value, gtk_list_view_get_search_filter (self));
+      break;
+
     default:
       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
       break;
@@ -684,6 +689,10 @@ gtk_list_view_set_property (GObject      *object,
       gtk_list_view_set_enable_rubberband (self, g_value_get_boolean (value));
       break;
 
+    case PROP_SEARCH_FILTER:
+      gtk_list_view_set_search_filter (self, g_value_get_object (value));
+      break;
+
     default:
       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
       break;
@@ -792,6 +801,18 @@ gtk_list_view_class_init (GtkListViewClass *klass)
                           FALSE,
                           G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
 
+  /**
+   * GtkListView:search-filter:
+   *
+   * Filter used for search
+   */
+  properties[PROP_SEARCH_FILTER] =
+    g_param_spec_object ("search-filter",
+                         P_("Search filter"),
+                         P_("Filter used for searching"),
+                         GTK_TYPE_FILTER,
+                         G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
   g_object_class_install_properties (gobject_class, N_PROPS, properties);
 
   /**
@@ -1091,3 +1112,66 @@ gtk_list_view_get_enable_rubberband (GtkListView *self)
 
   return gtk_list_base_get_enable_rubberband (GTK_LIST_BASE (self));
 }
+
+/**
+ * gtk_list_view_set_search_filter:
+ * @self: a #GtkListView
+ * @filter: (nullable): the filter ot use for search, or %NULL
+ *
+ * Sets a search filter.
+ *
+ * The selection will be moved to first item matching the
+ * filter whenever the filter changes.
+ *
+ * This can be used with single selection and a string
+ * filter that is connected to a search entry.
+ */
+void
+gtk_list_view_set_search_filter (GtkListView *self,
+                                 GtkFilter   *filter)
+{
+  g_return_if_fail (GTK_IS_LIST_VIEW (self));
+  g_return_if_fail (filter == NULL || GTK_IS_FILTER (filter));
+
+  if (filter == gtk_list_base_get_search_filter (GTK_LIST_BASE (self)))
+    return;
+
+  gtk_list_base_set_search_filter (GTK_LIST_BASE (self), filter);
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SEARCH_FILTER]);
+}
+
+/**
+ * gtk_list_view_get_search_filter:
+ * @self: a #GtkListView
+ *
+ * Gets the search filter that was set with
+ * gtk_list_view_set_search_filter().
+ *
+ * Returns: (transfer none): The search filter of @self
+ */
+GtkFilter *
+gtk_list_view_get_search_filter (GtkListView *self)
+{
+  g_return_val_if_fail (GTK_IS_LIST_VIEW (self), NULL);
+
+  return gtk_list_base_get_search_filter (GTK_LIST_BASE (self));
+}
+
+/**
+ * gtk_list_view_select_next_match:
+ * @self: a #GtkListView
+ * @forward: whether to move forward or back
+ *
+ * Moves the selection to the next item matching the
+ * search filter set with gtk_list_view_set_search_filter().
+ */
+void
+gtk_list_view_select_next_match (GtkListView *self,
+                                 gboolean     forward)
+{
+  g_return_if_fail (GTK_IS_LIST_VIEW (self));
+
+  gtk_list_base_select_next_match (GTK_LIST_BASE (self), forward);
+}
+
diff --git a/gtk/gtklistview.h b/gtk/gtklistview.h
index afcfb6225f..e2713b894c 100644
--- a/gtk/gtklistview.h
+++ b/gtk/gtklistview.h
@@ -25,6 +25,7 @@
 #endif
 
 #include <gtk/gtklistbase.h>
+#include <gtk/gtkfilter.h>
 
 G_BEGIN_DECLS
 
@@ -81,6 +82,16 @@ void            gtk_list_view_set_enable_rubberband             (GtkListView
 GDK_AVAILABLE_IN_ALL
 gboolean        gtk_list_view_get_enable_rubberband             (GtkListView            *self);
 
+GDK_AVAILABLE_IN_ALL
+void            gtk_list_view_set_search_filter                 (GtkListView            *self,
+                                                                 GtkFilter              *filter);
+GDK_AVAILABLE_IN_ALL
+GtkFilter *     gtk_list_view_get_search_filter                 (GtkListView            *self);
+
+GDK_AVAILABLE_IN_ALL
+void            gtk_list_view_select_next_match                 (GtkListView            *self,
+                                                                 gboolean                forward);
+
 G_END_DECLS
 
 #endif  /* __GTK_LIST_VIEW_H__ */


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]