summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMatthias Clasen <mclasen@redhat.com>2019-12-09 01:19:38 -0500
committerMatthias Clasen <mclasen@redhat.com>2019-12-09 12:10:05 -0500
commitc1995384f29ab8145a35f30bc06624186847144f (patch)
tree3565e2bfd033a820ff22cb3aff2714a1673cb44d
parent9b3f42c6952bf9926a9385c3130b89c3b5b6fa8f (diff)
downloadgtk+-c1995384f29ab8145a35f30bc06624186847144f.tar.gz
wip: Add GtkMultiSelectionwip/multiselection
This is implemented using a private GtkSet helper.
-rw-r--r--gtk/gtk.h1
-rw-r--r--gtk/gtkmultiselection.c333
-rw-r--r--gtk/gtkmultiselection.h37
-rw-r--r--gtk/gtkset.c340
-rw-r--r--gtk/gtkset.h69
-rw-r--r--gtk/meson.build3
-rw-r--r--testsuite/gtk/meson.build1
-rw-r--r--testsuite/gtk/multiselection.c427
8 files changed, 1211 insertions, 0 deletions
diff --git a/gtk/gtk.h b/gtk/gtk.h
index 325234cbfb..df83fc9da0 100644
--- a/gtk/gtk.h
+++ b/gtk/gtk.h
@@ -180,6 +180,7 @@
#include <gtk/gtkmenutoolbutton.h>
#include <gtk/gtkmessagedialog.h>
#include <gtk/gtkmountoperation.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 1f73829c0c..b7740706db 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',
@@ -318,6 +319,7 @@ gtk_public_sources = files([
'gtkmodelmenuitem.c',
'gtkmodules.c',
'gtkmountoperation.c',
+ 'gtkmultiselection.c',
'gtkmultisorter.c',
'gtknativedialog.c',
'gtknomediafile.c',
@@ -602,6 +604,7 @@ gtk_public_headers = files([
'gtkmenutoolbutton.h',
'gtkmessagedialog.h',
'gtkmountoperation.h',
+ 'gtkmultiselection.h',
'gtkmultisorter.h',
'gtknative.h',
'gtknativedialog.h',
diff --git a/testsuite/gtk/meson.build b/testsuite/gtk/meson.build
index dc64e0f215..1660bd05b1 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..b7d4c1f216
--- /dev/null
+++ b/testsuite/gtk/multiselection.c
@@ -0,0 +1,427 @@
+/*
+ * 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);
+}
+
+static void
+test_monitor (void)
+{
+ GtkSelectionModel *selection;
+ GtkSelectionMonitor *monitor;
+ GListStore *store;
+
+ store = new_store (1, 20, 1);
+ selection = new_model (store);
+ monitor = gtk_selection_monitor_new (selection);
+
+ assert_selection (selection, "");
+ assert_model (monitor, "");
+
+ gtk_selection_model_select_range (selection, 2, 2, FALSE);
+
+ assert_selection (selection, "3 4");
+ assert_model (monitor, "3 4");
+
+ gtk_selection_model_select_range (selection, 7, 3, FALSE);
+
+ assert_selection (selection, "3 4 8 9 10");
+ assert_model (monitor, "3 4 8 9 10");
+
+ gtk_selection_model_select_item (selection, 18, FALSE);
+
+ assert_selection (selection, "3 4 8 9 10 19");
+ assert_model (monitor, "3 4 8 9 10 19");
+
+ assert_selection_changes (selection, "2:2, 7:3, 18:1");
+ ignore_changes (selection);
+
+ g_object_unref (store);
+ g_object_unref (selection);
+ g_object_unref (monitor);
+}
+
+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);
+ g_test_add_func ("/multiselection/monitor", test_monitor);
+
+ return g_test_run ();
+}