[gtk/prop-list: 73/83] Add GtkMultiSelection



commit 6191b06a3ebf1001b9f9fa9e032bece0c2c5777e
Author: Matthias Clasen <mclasen redhat com>
Date:   Mon Dec 9 01:19:38 2019 -0500

    Add GtkMultiSelection
    
    This is implemented using a private GtkSet helper.

 gtk/gtk.h                      |   1 +
 gtk/gtkmultiselection.c        | 333 +++++++++++++++++++++++++++++++++++
 gtk/gtkmultiselection.h        |  37 ++++
 gtk/gtkset.c                   | 340 +++++++++++++++++++++++++++++++++++
 gtk/gtkset.h                   |  69 ++++++++
 gtk/meson.build                |   3 +
 testsuite/gtk/meson.build      |   1 +
 testsuite/gtk/multiselection.c | 389 +++++++++++++++++++++++++++++++++++++++++
 8 files changed, 1173 insertions(+)
---
diff --git a/gtk/gtk.h b/gtk/gtk.h
index 6a1e8f40fd..1810cfe0bc 100644
--- a/gtk/gtk.h
+++ b/gtk/gtk.h
@@ -181,6 +181,7 @@
 #include <gtk/gtkmessagedialog.h>
 #include <gtk/gtkmountoperation.h>
 #include <gtk/gtkmultifilter.h>
+#include <gtk/gtkmultiselection.h>
 #include <gtk/gtkmultisorter.h>
 #include <gtk/gtknative.h>
 #include <gtk/gtknativedialog.h>
diff --git a/gtk/gtkmultiselection.c b/gtk/gtkmultiselection.c
new file mode 100644
index 0000000000..522c524788
--- /dev/null
+++ b/gtk/gtkmultiselection.c
@@ -0,0 +1,333 @@
+/*
+ * Copyright © 2019 Red Hat, Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Matthias Clasen <mclasen redhat com>
+ */
+
+#include "config.h"
+
+#include "gtkmultiselection.h"
+
+#include "gtkintl.h"
+#include "gtkselectionmodel.h"
+#include "gtksingleselection.h"
+#include "gtkset.h"
+
+/**
+ * SECTION:gtkmultiselection
+ * @Short_description: A selection model that allows selecting a multiple items
+ * @Title: GtkMultiSelection
+ * @see_also: #GtkSelectionModel
+ *
+ * GtkMultiSelection is an implementation of the #GtkSelectionModel interface 
+ * that allows selecting multiple elements.
+ */
+
+struct _GtkMultiSelection
+{
+  GObject parent_instance;
+
+  GListModel *model;
+
+  GtkSet *selected;
+  guint last_selected;
+};
+
+struct _GtkMultiSelectionClass
+{
+  GObjectClass parent_class;
+};
+
+enum {
+  PROP_0,
+  PROP_MODEL,
+
+  N_PROPS,
+};
+
+static GParamSpec *properties[N_PROPS] = { NULL, };
+
+static GType
+gtk_multi_selection_get_item_type (GListModel *list)
+{
+  GtkMultiSelection *self = GTK_MULTI_SELECTION (list);
+
+  return g_list_model_get_item_type (self->model);
+}
+
+static guint
+gtk_multi_selection_get_n_items (GListModel *list)
+{
+  GtkMultiSelection *self = GTK_MULTI_SELECTION (list);
+
+  return g_list_model_get_n_items (self->model);
+}
+
+static gpointer
+gtk_multi_selection_get_item (GListModel *list,
+                              guint       position)
+{
+  GtkMultiSelection *self = GTK_MULTI_SELECTION (list);
+
+  return g_list_model_get_item (self->model, position);
+}
+
+static void
+gtk_multi_selection_list_model_init (GListModelInterface *iface)
+{
+  iface->get_item_type = gtk_multi_selection_get_item_type;
+  iface->get_n_items = gtk_multi_selection_get_n_items;
+  iface->get_item = gtk_multi_selection_get_item;
+}
+
+static gboolean
+gtk_multi_selection_is_selected (GtkSelectionModel *model,
+                                 guint              position)
+{
+  GtkMultiSelection *self = GTK_MULTI_SELECTION (model);
+
+  return gtk_set_contains (self->selected, position);
+}
+
+static gboolean
+gtk_multi_selection_select_range (GtkSelectionModel *model,
+                                  guint              position,
+                                  guint              n_items,
+                                  gboolean           exclusive)
+{
+  GtkMultiSelection *self = GTK_MULTI_SELECTION (model);
+
+  if (exclusive)
+    gtk_set_remove_all (self->selected);
+  gtk_set_add_range (self->selected, position, n_items);
+  gtk_selection_model_selection_changed (model, position, n_items);
+
+  return TRUE;
+}
+
+static gboolean
+gtk_multi_selection_unselect_range (GtkSelectionModel *model,
+                                    guint              position,
+                                    guint              n_items)
+{
+  GtkMultiSelection *self = GTK_MULTI_SELECTION (model);
+
+  gtk_set_remove_range (self->selected, position, n_items);
+  gtk_selection_model_selection_changed (model, position, n_items);
+
+  return TRUE;
+}
+
+static gboolean
+gtk_multi_selection_select_item (GtkSelectionModel *model,
+                                 guint              position,
+                                 gboolean           exclusive)
+{
+  GtkMultiSelection *self = GTK_MULTI_SELECTION (model);
+  guint pos, n_items;
+
+  pos = position;
+  n_items = 1;
+
+  self->last_selected = position;
+  return gtk_multi_selection_select_range (model, pos, n_items, exclusive);
+}
+
+static gboolean
+gtk_multi_selection_unselect_item (GtkSelectionModel *model,
+                                   guint              position)
+{
+  return gtk_multi_selection_unselect_range (model, position, 1);
+}
+
+static gboolean
+gtk_multi_selection_select_all (GtkSelectionModel *model)
+{
+  return gtk_multi_selection_select_range (model, 0, g_list_model_get_n_items (G_LIST_MODEL (model)), FALSE);
+}
+
+static gboolean
+gtk_multi_selection_unselect_all (GtkSelectionModel *model)
+{
+  GtkMultiSelection *self = GTK_MULTI_SELECTION (model);
+  self->last_selected = GTK_INVALID_LIST_POSITION;
+  return gtk_multi_selection_unselect_range (model, 0, g_list_model_get_n_items (G_LIST_MODEL (model)));
+}
+
+static void
+gtk_multi_selection_query_range (GtkSelectionModel *model,
+                                 guint              position,
+                                 guint             *start_range,
+                                 guint             *n_items,
+                                 gboolean          *selected)
+{
+  GtkMultiSelection *self = GTK_MULTI_SELECTION (model);
+  guint upper_bound = g_list_model_get_n_items (self->model);
+
+  gtk_set_find_range (self->selected, position, upper_bound, start_range, n_items, selected);
+}  
+
+static void
+gtk_multi_selection_selection_model_init (GtkSelectionModelInterface *iface)
+{
+  iface->is_selected = gtk_multi_selection_is_selected; 
+  iface->select_item = gtk_multi_selection_select_item; 
+  iface->unselect_item = gtk_multi_selection_unselect_item; 
+  iface->select_range = gtk_multi_selection_select_range; 
+  iface->unselect_range = gtk_multi_selection_unselect_range; 
+  iface->select_all = gtk_multi_selection_select_all; 
+  iface->unselect_all = gtk_multi_selection_unselect_all; 
+  iface->query_range = gtk_multi_selection_query_range;
+}
+
+G_DEFINE_TYPE_EXTENDED (GtkMultiSelection, gtk_multi_selection, G_TYPE_OBJECT, 0,
+                        G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL,
+                                               gtk_multi_selection_list_model_init)
+                        G_IMPLEMENT_INTERFACE (GTK_TYPE_SELECTION_MODEL,
+                                               gtk_multi_selection_selection_model_init))
+
+static void
+gtk_multi_selection_items_changed_cb (GListModel        *model,
+                                      guint              position,
+                                      guint              removed,
+                                      guint              added,
+                                      GtkMultiSelection *self)
+{
+  gtk_set_remove_range (self->selected, position, removed);
+  gtk_set_shift (self->selected, position, (int)added - (int)removed);
+  g_list_model_items_changed (G_LIST_MODEL (self), position, removed, added);
+}
+
+static void
+gtk_multi_selection_clear_model (GtkMultiSelection *self)
+{
+  if (self->model == NULL)
+    return;
+
+  g_signal_handlers_disconnect_by_func (self->model, 
+                                        gtk_multi_selection_items_changed_cb,
+                                        self);
+  g_clear_object (&self->model);
+}
+
+static void
+gtk_multi_selection_set_property (GObject      *object,
+                                   guint         prop_id,
+                                   const GValue *value,
+                                   GParamSpec   *pspec)
+
+{
+  GtkMultiSelection *self = GTK_MULTI_SELECTION (object);
+
+  switch (prop_id)
+    {
+    case PROP_MODEL:
+      self->model = g_value_dup_object (value);
+      g_warn_if_fail (self->model != NULL);
+      g_signal_connect (self->model,
+                        "items-changed",
+                        G_CALLBACK (gtk_multi_selection_items_changed_cb),
+                        self);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+      break;
+    }
+}
+
+static void
+gtk_multi_selection_get_property (GObject    *object,
+                                   guint       prop_id,
+                                   GValue     *value,
+                                   GParamSpec *pspec)
+{
+  GtkMultiSelection *self = GTK_MULTI_SELECTION (object);
+
+  switch (prop_id)
+    {
+    case PROP_MODEL:
+      g_value_set_object (value, self->model);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+      break;
+    }
+}
+
+static void
+gtk_multi_selection_dispose (GObject *object)
+{
+  GtkMultiSelection *self = GTK_MULTI_SELECTION (object);
+
+  gtk_multi_selection_clear_model (self);
+
+  g_clear_pointer (&self->selected, gtk_set_free);
+  self->last_selected = GTK_INVALID_LIST_POSITION;
+
+  G_OBJECT_CLASS (gtk_multi_selection_parent_class)->dispose (object);
+}
+
+static void
+gtk_multi_selection_class_init (GtkMultiSelectionClass *klass)
+{
+  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+
+  gobject_class->get_property = gtk_multi_selection_get_property;
+  gobject_class->set_property = gtk_multi_selection_set_property;
+  gobject_class->dispose = gtk_multi_selection_dispose;
+
+  /**
+   * GtkMultiSelection:model
+   *
+   * The list managed by this selection
+   */
+  properties[PROP_MODEL] =
+    g_param_spec_object ("model",
+                         P_("Model"),
+                         P_("List managed by this selection"),
+                         G_TYPE_LIST_MODEL,
+                         G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_EXPLICIT_NOTIFY | 
G_PARAM_STATIC_STRINGS);
+
+  g_object_class_install_properties (gobject_class, N_PROPS, properties);
+}
+
+static void
+gtk_multi_selection_init (GtkMultiSelection *self)
+{
+  self->selected = gtk_set_new ();
+  self->last_selected = GTK_INVALID_LIST_POSITION;
+}
+
+/**
+ * gtk_multi_selection_new:
+ * @model: (transfer none): the #GListModel to manage
+ *
+ * Creates a new selection to handle @model.
+ *
+ * Returns: (transfer full) (type GtkMultiSelection): a new #GtkMultiSelection
+ **/
+GListModel *
+gtk_multi_selection_new (GListModel *model)
+{
+  g_return_val_if_fail (G_IS_LIST_MODEL (model), NULL);
+
+  return g_object_new (GTK_TYPE_MULTI_SELECTION,
+                       "model", model,
+                       NULL);
+}
+
diff --git a/gtk/gtkmultiselection.h b/gtk/gtkmultiselection.h
new file mode 100644
index 0000000000..cca0dc62d2
--- /dev/null
+++ b/gtk/gtkmultiselection.h
@@ -0,0 +1,37 @@
+/*
+ * Copyright © 2019 Red Hat, Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Matthias Clasen <mclasen redhat com>
+ */
+
+#ifndef __GTK_MULTI_SELECTION_H__
+#define __GTK_MULTI_SELECTION_H__
+
+#include <gtk/gtktypes.h>
+
+G_BEGIN_DECLS
+
+#define GTK_TYPE_MULTI_SELECTION (gtk_multi_selection_get_type ())
+
+GDK_AVAILABLE_IN_ALL
+G_DECLARE_FINAL_TYPE (GtkMultiSelection, gtk_multi_selection, GTK, MULTI_SELECTION, GObject)
+
+GDK_AVAILABLE_IN_ALL
+GListModel *    gtk_multi_selection_new                (GListModel           *model);
+
+G_END_DECLS
+
+#endif /* __GTK_MULTI_SELECTION_H__ */
diff --git a/gtk/gtkset.c b/gtk/gtkset.c
new file mode 100644
index 0000000000..807743a7dc
--- /dev/null
+++ b/gtk/gtkset.c
@@ -0,0 +1,340 @@
+/*
+ * Copyright © 2019 Red Hat, Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Matthias Clasen <mclasen redhat com>
+ */
+
+#include "gtkset.h"
+
+/* Store a set of unsigned integers as a sorted array of ranges.
+ */
+
+typedef struct
+{
+  guint first;
+  guint n_items;
+} Range;
+
+struct _GtkSet
+{
+  GArray *ranges;
+};
+
+typedef struct
+{
+  GtkSet *set;
+  Range *current;
+  int idx;
+  guint pos;
+} GtkRealSetIter;
+
+GtkSet *
+gtk_set_new (void)
+{
+  GtkSet *set;
+
+  set = g_new (GtkSet, 1);
+  set->ranges = g_array_new (FALSE, FALSE, sizeof (Range));
+ 
+  return set;
+}
+
+void
+gtk_set_free (GtkSet *set)
+{
+  g_array_free (set->ranges, TRUE);
+  g_free (set);
+}
+
+gboolean
+gtk_set_contains (GtkSet   *set,
+                  guint     item)
+{
+  int i;
+
+  for (i = 0; i < set->ranges->len; i++)
+    {
+      Range *r = &g_array_index (set->ranges, Range, i);
+
+      if (item < r->first)
+        return FALSE;
+
+      if (item < r->first + r->n_items)
+        return TRUE;
+    }
+
+  return FALSE;
+}
+
+void
+gtk_set_remove_all (GtkSet *set)
+{
+  g_array_set_size (set->ranges, 0);
+}
+
+static int
+range_compare (Range *r, Range *s)
+{
+  int ret = 0;
+
+  if (r->first + r->n_items < s->first)
+    ret = -1;
+  else if (s->first + s->n_items < r->first)
+    ret = 1;
+
+  return ret;
+}
+
+void
+gtk_set_add_range (GtkSet   *set,
+                   guint     first_item,
+                   guint     n_items)
+{
+  int i;
+  Range s;
+  int first = -1;
+  int last = -1;
+
+  s.first = first_item;
+  s.n_items = n_items;
+
+  for (i = 0; i < set->ranges->len; i++)
+    {
+      Range *r = &g_array_index (set->ranges, Range, i);
+      int cmp = range_compare (&s, r);
+
+      if (cmp < 0)
+        break;
+
+      if (cmp == 0)
+        {
+          if (first < 0)
+            first = i;
+          last = i;
+        }
+    }
+
+  if (first > -1)
+    {
+      Range *r;
+      guint start;
+      guint end;
+
+      r = &g_array_index (set->ranges, Range, first);
+      start = MIN (s.first, r->first);
+
+      r = &g_array_index (set->ranges, Range, last);
+      end = MAX (s.first + s.n_items - 1, r->first + r->n_items - 1);
+
+      s.first = start;
+      s.n_items = end - start + 1;
+
+      g_array_remove_range (set->ranges, first, last - first + 1);
+      g_array_insert_val (set->ranges, first, s);
+    }
+  else
+    g_array_insert_val (set->ranges, i, s);
+}
+
+void
+gtk_set_remove_range (GtkSet *set,
+                      guint   first_item,
+                      guint   n_items)
+{
+  Range s;
+  int i;
+  int first = -1;
+  int last = -1;
+
+  s.first = first_item;
+  s.n_items = n_items;
+
+  for (i = 0; i < set->ranges->len; i++)
+    {
+      Range *r = &g_array_index (set->ranges, Range, i);
+      int cmp = range_compare (&s, r);
+
+      if (cmp < 0)
+        break;
+
+      if (cmp == 0)
+        {
+          if (first < 0)
+            first = i;
+          last = i;
+        }
+    }
+
+  if (first > -1)
+    {
+      Range *r;
+      Range a[2];
+      int k = 0;
+
+      r = &g_array_index (set->ranges, Range, first);
+      if (r->first < s.first)
+        {
+          a[k].first = r->first;
+          a[k].n_items = s.first - r->first;
+          k++;
+        }
+      r = &g_array_index (set->ranges, Range, last);
+      if (r->first + r->n_items > s.first + s.n_items)
+        {
+          a[k].first = s.first + s.n_items;
+          a[k].n_items = r->first + r->n_items - a[k].first;
+          k++;
+        }
+      g_array_remove_range (set->ranges, first, last - first + 1);
+      if (k > 0)
+        g_array_insert_vals (set->ranges, first, a, k);
+   }
+}
+
+void
+gtk_set_find_range (GtkSet   *set,
+                    guint     position,
+                    guint     upper_bound,
+                    guint    *start,
+                    guint    *n_items,
+                    gboolean *contained)
+{
+  int i;
+  int last = 0;
+
+  if (position >= upper_bound)
+    {
+      *start = 0;
+      *n_items = 0;
+      *contained = FALSE;
+      return;
+    }
+
+  for (i = 0; i < set->ranges->len; i++)
+    {
+      Range *r = &g_array_index (set->ranges, Range, i);
+
+      if (position < r->first)
+        {
+           *start = last;
+           *n_items = r->first - last;
+           *contained = FALSE;
+
+           return;
+        }
+      else if (r->first <= position && position < r->first + r->n_items)
+        {
+          *start = r->first;
+          *n_items = r->n_items;
+          *contained = TRUE;
+
+          return;
+        }
+      else
+        last = r->first + r->n_items;
+    }
+
+  *start = last;
+  *n_items = upper_bound - last;
+  *contained = FALSE;
+}
+
+void
+gtk_set_add_item (GtkSet *set,
+                  guint   item)
+{
+  gtk_set_add_range (set, item, 1);
+}
+
+void
+gtk_set_remove_item (GtkSet   *set,
+                     guint     item)
+{
+  gtk_set_remove_range (set, item, 1);
+}
+
+/* This is peculiar operation: Replace every number n >= first by n + shift
+ * This is only supported for negatie if the shifting does not cause any
+ * ranges to overlap.
+ */
+void
+gtk_set_shift (GtkSet *set,
+               guint first,
+               int shift)
+{
+  int i;
+
+  for (i = 0; i < set->ranges->len; i++)
+    {
+      Range *r = &g_array_index (set->ranges, Range, i);
+      if (r->first >= first)
+        r->first += shift;
+    }
+}
+
+void
+gtk_set_iter_init (GtkSetIter *iter,
+                   GtkSet     *set)
+{
+  GtkRealSetIter *ri = (GtkRealSetIter *)iter;
+
+  ri->set = set;
+  ri->idx = -1;
+  ri->current = 0;
+}
+
+gboolean
+gtk_set_iter_next (GtkSetIter *iter,
+                   guint      *item)
+{
+  GtkRealSetIter *ri = (GtkRealSetIter *)iter;
+
+  if (ri->idx == -1)
+    { 
+next_range:
+      ri->idx++;
+
+      if (ri->idx == ri->set->ranges->len)
+        return FALSE;
+
+      ri->current = &g_array_index (ri->set->ranges, Range, ri->idx);
+      ri->pos = ri->current->first;
+    }
+  else
+    {
+      ri->pos++;
+      if (ri->pos == ri->current->first + ri->current->n_items)
+        goto next_range;
+    }
+
+  *item = ri->pos;
+  return TRUE;
+}
+
+#if 0
+void
+gtk_set_dump (GtkSet *set)
+{
+  int i;
+
+  for (i = 0; i < set->ranges->len; i++)
+    {
+      Range *r = &g_array_index (set->ranges, Range, i);
+      g_print (" %u:%u", r->first, r->n_items);
+    }
+  g_print ("\n");
+}
+#endif
diff --git a/gtk/gtkset.h b/gtk/gtkset.h
new file mode 100644
index 0000000000..25351e2090
--- /dev/null
+++ b/gtk/gtkset.h
@@ -0,0 +1,69 @@
+/*
+ * Copyright © 2019 Red Hat, Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Matthias Clasen <mclasen redhat com>
+ */
+
+#ifndef __GTK_SET_H__
+#define __GTK_SET_H__
+
+#include <glib.h>
+
+typedef struct _GtkSet GtkSet;
+typedef struct _GtkSetIter GtkSetIter;
+
+struct _GtkSetIter
+{
+  gpointer dummy1;
+  gpointer dummy2;
+  int      dummy3;
+  int      dummy4;
+};
+
+GtkSet   *gtk_set_new          (void);
+void      gtk_set_free         (GtkSet   *set);
+
+gboolean  gtk_set_contains     (GtkSet   *set,
+                                guint     item);
+
+void      gtk_set_remove_all   (GtkSet   *set);
+void      gtk_set_add_item     (GtkSet   *set,
+                                guint     item);
+void      gtk_set_remove_item  (GtkSet   *set,
+                                guint     item);
+void      gtk_set_add_range    (GtkSet   *set,
+                                guint     first,
+                                guint     n);
+void      gtk_set_remove_range (GtkSet   *set,
+                                guint     first,
+                                guint     n);
+void      gtk_set_find_range   (GtkSet   *set,
+                                guint     position,
+                                guint     upper_bound,
+                                guint    *start,
+                                guint    *n_items,
+                                gboolean *contained);
+
+void      gtk_set_shift        (GtkSet   *set,
+                                guint     first,
+                                int       shift);
+
+void      gtk_set_iter_init    (GtkSetIter *iter,
+                                GtkSet     *set);
+gboolean  gtk_set_iter_next    (GtkSetIter *iter,
+                                guint      *item);
+
+#endif  /* __GTK_SET_H__ */
diff --git a/gtk/meson.build b/gtk/meson.build
index 034392be20..4042cb0270 100644
--- a/gtk/meson.build
+++ b/gtk/meson.build
@@ -144,6 +144,7 @@ gtk_private_sources = files([
   'gtksearchengine.c',
   'gtksearchenginemodel.c',
   'gtksearchenginesimple.c',
+  'gtkset.c',
   'gtksizerequestcache.c',
   'gtkstyleanimation.c',
   'gtkstylecascade.c',
@@ -319,6 +320,7 @@ gtk_public_sources = files([
   'gtkmodules.c',
   'gtkmountoperation.c',
   'gtkmultifilter.c',
+  'gtkmultiselection.c',
   'gtkmultisorter.c',
   'gtknativedialog.c',
   'gtknomediafile.c',
@@ -604,6 +606,7 @@ gtk_public_headers = files([
   'gtkmessagedialog.h',
   'gtkmountoperation.h',
   'gtkmultifilter.h',
+  'gtkmultiselection.h',
   'gtkmultisorter.h',
   'gtknative.h',
   'gtknativedialog.h',
diff --git a/testsuite/gtk/meson.build b/testsuite/gtk/meson.build
index f7ab2daaea..313f25d653 100644
--- a/testsuite/gtk/meson.build
+++ b/testsuite/gtk/meson.build
@@ -42,6 +42,7 @@ tests = [
   ['listbox'],
   ['main'],
   ['maplistmodel'],
+  ['multiselection'],
   ['notify'],
   ['no-gtk-init'],
   ['object'],
diff --git a/testsuite/gtk/multiselection.c b/testsuite/gtk/multiselection.c
new file mode 100644
index 0000000000..162363e543
--- /dev/null
+++ b/testsuite/gtk/multiselection.c
@@ -0,0 +1,389 @@
+/* 
+ * Copyright (C) 2019, Red Hat, Inc.
+ * Authors: Matthias Clasen <mclasen redhat com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <locale.h>
+
+#include <gtk/gtk.h>
+
+static GQuark number_quark;
+static GQuark changes_quark;
+static GQuark selection_quark;
+
+static guint
+get (GListModel *model,
+     guint       position)
+{
+  GObject *object = g_list_model_get_item (model, position);
+  g_assert (object != NULL);
+  return GPOINTER_TO_UINT (g_object_get_qdata (object, number_quark));
+}
+
+static char *
+model_to_string (GListModel *model)
+{
+  GString *string = g_string_new (NULL);
+  guint i;
+
+  for (i = 0; i < g_list_model_get_n_items (model); i++)
+    {
+      if (i > 0)
+        g_string_append (string, " ");
+      g_string_append_printf (string, "%u", get (model, i));
+    }
+
+  return g_string_free (string, FALSE);
+}
+
+static char *
+selection_to_string (GListModel *model)
+{
+  GString *string = g_string_new (NULL);
+  guint i;
+
+  for (i = 0; i < g_list_model_get_n_items (model); i++)
+    {
+      if (!gtk_selection_model_is_selected (GTK_SELECTION_MODEL (model), i))
+        continue;
+
+      if (string->len > 0)
+        g_string_append (string, " ");
+      g_string_append_printf (string, "%u", get (model, i));
+    }
+
+  return g_string_free (string, FALSE);
+}
+
+static GListStore *
+new_store (guint start,
+           guint end,
+           guint step);
+
+static GObject *
+make_object (guint number)
+{
+  GObject *object;
+
+  /* 0 cannot be differentiated from NULL, so don't use it */
+  g_assert (number != 0);
+
+  object = g_object_new (G_TYPE_OBJECT, NULL);
+  g_object_set_qdata (object, number_quark, GUINT_TO_POINTER (number));
+
+  return object;
+}
+
+static void
+splice (GListStore *store,
+        guint       pos,
+        guint       removed,
+        guint      *numbers,
+        guint       added)
+{
+  GObject *objects[added];
+  guint i;
+
+  for (i = 0; i < added; i++)
+    objects[i] = make_object (numbers[i]);
+
+  g_list_store_splice (store, pos, removed, (gpointer *) objects, added);
+
+  for (i = 0; i < added; i++)
+    g_object_unref (objects[i]);
+}
+
+static void
+add (GListStore *store,
+     guint       number)
+{
+  GObject *object = make_object (number);
+  g_list_store_append (store, object);
+  g_object_unref (object);
+}
+
+static void
+insert (GListStore *store,
+        guint position,
+        guint number)
+{
+  GObject *object = make_object (number);
+  g_list_store_insert (store, position, object);
+  g_object_unref (object);
+}
+
+#define assert_model(model, expected) G_STMT_START{ \
+  char *s = model_to_string (G_LIST_MODEL (model)); \
+  if (!g_str_equal (s, expected)) \
+     g_assertion_message_cmpstr (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, \
+         #model " == " #expected, s, "==", expected); \
+  g_free (s); \
+}G_STMT_END
+
+#define ignore_changes(model) G_STMT_START{ \
+  GString *changes = g_object_get_qdata (G_OBJECT (model), changes_quark); \
+  g_string_set_size (changes, 0); \
+}G_STMT_END
+
+#define assert_changes(model, expected) G_STMT_START{ \
+  GString *changes = g_object_get_qdata (G_OBJECT (model), changes_quark); \
+  if (!g_str_equal (changes->str, expected)) \
+     g_assertion_message_cmpstr (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, \
+         #model " == " #expected, changes->str, "==", expected); \
+  g_string_set_size (changes, 0); \
+}G_STMT_END
+
+#define assert_selection(model, expected) G_STMT_START{ \
+  char *s = selection_to_string (G_LIST_MODEL (model)); \
+  if (!g_str_equal (s, expected)) \
+     g_assertion_message_cmpstr (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, \
+         #model " == " #expected, s, "==", expected); \
+  g_free (s); \
+}G_STMT_END
+
+#define ignore_selection_changes(model) G_STMT_START{ \
+  GString *changes = g_object_get_qdata (G_OBJECT (model), selection_quark); \
+  g_string_set_size (changes, 0); \
+}G_STMT_END
+
+#define assert_selection_changes(model, expected) G_STMT_START{ \
+  GString *changes = g_object_get_qdata (G_OBJECT (model), selection_quark); \
+  if (!g_str_equal (changes->str, expected)) \
+     g_assertion_message_cmpstr (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, \
+         #model " == " #expected, changes->str, "==", expected); \
+  g_string_set_size (changes, 0); \
+}G_STMT_END
+
+static GListStore *
+new_empty_store (void)
+{
+  return g_list_store_new (G_TYPE_OBJECT);
+}
+
+static GListStore *
+new_store (guint start,
+           guint end,
+           guint step)
+{
+  GListStore *store = new_empty_store ();
+  guint i;
+
+  for (i = start; i <= end; i += step)
+    add (store, i);
+
+  return store;
+}
+
+static void
+items_changed (GListModel *model,
+               guint       position,
+               guint       removed,
+               guint       added,
+               GString    *changes)
+{
+  g_assert (removed != 0 || added != 0);
+
+  if (changes->len)
+    g_string_append (changes, ", ");
+
+  if (removed == 1 && added == 0)
+    {
+      g_string_append_printf (changes, "-%u", position);
+    }
+  else if (removed == 0 && added == 1)
+    {
+      g_string_append_printf (changes, "+%u", position);
+    }
+  else
+    {
+      g_string_append_printf (changes, "%u", position);
+      if (removed > 0)
+        g_string_append_printf (changes, "-%u", removed);
+      if (added > 0)
+        g_string_append_printf (changes, "+%u", added);
+    }
+}
+
+static void
+selection_changed (GListModel *model,
+                   guint       position,
+                   guint       n_items,
+                   GString    *changes)
+{
+  if (changes->len)
+    g_string_append (changes, ", ");
+
+  g_string_append_printf (changes, "%u:%u", position, n_items);
+}
+
+static void
+free_changes (gpointer data)
+{
+  GString *changes = data;
+
+  /* all changes must have been checked via assert_changes() before */
+  g_assert_cmpstr (changes->str, ==, "");
+
+  g_string_free (changes, TRUE);
+}
+
+static GtkSelectionModel *
+new_model (GListStore *store)
+{
+  GtkSelectionModel *result;
+  GString *changes;
+
+  result = GTK_SELECTION_MODEL (gtk_multi_selection_new (G_LIST_MODEL (store)));
+
+  changes = g_string_new ("");
+  g_object_set_qdata_full (G_OBJECT(result), changes_quark, changes, free_changes);
+  g_signal_connect (result, "items-changed", G_CALLBACK (items_changed), changes);
+
+  changes = g_string_new ("");
+  g_object_set_qdata_full (G_OBJECT(result), selection_quark, changes, free_changes);
+  g_signal_connect (result, "selection-changed", G_CALLBACK (selection_changed), changes);
+
+  return result;
+}
+
+static void
+test_create (void)
+{
+  GtkSelectionModel *selection;
+  GListStore *store;
+  
+  store = new_store (1, 5, 2);
+  selection = new_model (store);
+
+  assert_model (selection, "1 3 5");
+  assert_changes (selection, "");
+  assert_selection (selection, "");
+  assert_selection_changes (selection, "");
+
+  g_object_unref (store);
+  assert_model (selection, "1 3 5");
+  assert_changes (selection, "");
+  assert_selection (selection, "");
+  assert_selection_changes (selection, "");
+
+  g_object_unref (selection);
+}
+
+static void
+test_changes (void)
+{
+  GtkSelectionModel *selection;
+  GListStore *store;
+  
+  store = new_store (1, 5, 1);
+  selection = new_model (store);
+  assert_model (selection, "1 2 3 4 5");
+  assert_changes (selection, "");
+  assert_selection (selection, "");
+  assert_selection_changes (selection, "");
+
+  g_list_store_remove (store, 3);
+  assert_model (selection, "1 2 3 5");
+  assert_changes (selection, "-3");
+  assert_selection (selection, "");
+  assert_selection_changes (selection, "");
+
+  insert (store, 3, 99);
+  assert_model (selection, "1 2 3 99 5");
+  assert_changes (selection, "+3");
+  assert_selection (selection, "");
+  assert_selection_changes (selection, "");
+
+  splice (store, 3, 2, (guint[]) { 97 }, 1);
+  assert_model (selection, "1 2 3 97");
+  assert_changes (selection, "3-2+1");
+  assert_selection (selection, "");
+  assert_selection_changes (selection, "");
+
+  g_object_unref (store);
+  g_object_unref (selection);
+}
+
+static void
+test_selection (void)
+{
+  GtkSelectionModel *selection;
+  GListStore *store;
+  gboolean ret;
+  
+  store = new_store (1, 5, 1);
+  selection = new_model (store);
+  assert_selection (selection, "");
+  assert_selection_changes (selection, "");
+
+  ret = gtk_selection_model_select_item (selection, 3, FALSE);
+  g_assert_true (ret);
+  assert_selection (selection, "4");
+  assert_selection_changes (selection, "3:1");
+
+  ret = gtk_selection_model_unselect_item (selection, 3);
+  g_assert_true (ret);
+  assert_selection (selection, "");
+  assert_selection_changes (selection, "3:1");
+
+  ret = gtk_selection_model_select_item (selection, 1, FALSE);
+  g_assert_true (ret);
+  assert_selection (selection, "2");
+  assert_selection_changes (selection, "1:1");
+
+  ret = gtk_selection_model_select_range (selection, 3, 2, FALSE);
+  g_assert_true (ret);
+  assert_selection (selection, "2 4 5");
+  assert_selection_changes (selection, "3:2");
+
+  ret = gtk_selection_model_unselect_range (selection, 3, 2);
+  g_assert_true (ret);
+  assert_selection (selection, "2");
+  assert_selection_changes (selection, "3:2");
+
+  ret = gtk_selection_model_select_all (selection);
+  g_assert_true (ret);
+  assert_selection (selection, "1 2 3 4 5");
+  assert_selection_changes (selection, "0:5");
+
+  ret = gtk_selection_model_unselect_all (selection);
+  g_assert_true (ret);
+  assert_selection (selection, "");
+  assert_selection_changes (selection, "0:5");
+
+  g_object_unref (store);
+  g_object_unref (selection);
+}
+
+int
+main (int argc, char *argv[])
+{
+  g_test_init (&argc, &argv, NULL);
+  setlocale (LC_ALL, "C");
+  g_test_bug_base ("http://bugzilla.gnome.org/show_bug.cgi?id=%s";);
+
+  number_quark = g_quark_from_static_string ("Hell and fire was spawned to be released.");
+  changes_quark = g_quark_from_static_string ("What did I see? Can I believe what I saw?");
+  selection_quark = g_quark_from_static_string ("Mana mana, badibidibi");
+
+  g_test_add_func ("/multiselection/create", test_create);
+#if GLIB_CHECK_VERSION (2, 58, 0) /* g_list_store_splice() is broken before 2.58 */
+  g_test_add_func ("/multiselection/changes", test_changes);
+#endif
+  g_test_add_func ("/multiselection/selection", test_selection);
+
+  return g_test_run ();
+}


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