diff options
author | Matthias Clasen <mclasen@redhat.com> | 2019-12-09 01:19:38 -0500 |
---|---|---|
committer | Matthias Clasen <mclasen@redhat.com> | 2020-06-03 13:32:57 -0400 |
commit | 28f6e2727635497b21ff098aeb7f88ab0404965e (patch) | |
tree | afe816a6495299f21ea9910a523d2794665323de | |
parent | 5ef427bd65552bfeddd0f5708d7fcf0f5c852f37 (diff) | |
download | gtk+-28f6e2727635497b21ff098aeb7f88ab0404965e.tar.gz |
Add GtkMultiSelection
This is implemented using a private GtkSet helper.
Includes tests.
-rw-r--r-- | docs/reference/gtk/gtk4-docs.xml | 1 | ||||
-rw-r--r-- | docs/reference/gtk/gtk4-sections.txt | 10 | ||||
-rw-r--r-- | docs/reference/gtk/gtk4.types.in | 1 | ||||
-rw-r--r-- | gtk/gtk.h | 1 | ||||
-rw-r--r-- | gtk/gtkmultiselection.c | 380 | ||||
-rw-r--r-- | gtk/gtkmultiselection.h | 41 | ||||
-rw-r--r-- | gtk/gtkset.c | 351 | ||||
-rw-r--r-- | gtk/gtkset.h | 70 | ||||
-rw-r--r-- | gtk/meson.build | 3 | ||||
-rw-r--r-- | testsuite/gtk/defaultvalue.c | 6 | ||||
-rw-r--r-- | testsuite/gtk/meson.build | 1 | ||||
-rw-r--r-- | testsuite/gtk/multiselection.c | 393 | ||||
-rw-r--r-- | testsuite/gtk/notify.c | 3 | ||||
-rw-r--r-- | testsuite/gtk/objects-finalize.c | 3 |
14 files changed, 1260 insertions, 4 deletions
diff --git a/docs/reference/gtk/gtk4-docs.xml b/docs/reference/gtk/gtk4-docs.xml index 4cdda20611..a54bd1a58d 100644 --- a/docs/reference/gtk/gtk4-docs.xml +++ b/docs/reference/gtk/gtk4-docs.xml @@ -70,6 +70,7 @@ <xi:include href="xml/gtkselectionmodel.xml" /> <xi:include href="xml/gtknoselection.xml" /> <xi:include href="xml/gtksingleselection.xml" /> + <xi:include href="xml/gtkmultiselection.xml" /> <xi:include href="xml/gtkdirectorylist.xml" /> </chapter> diff --git a/docs/reference/gtk/gtk4-sections.txt b/docs/reference/gtk/gtk4-sections.txt index dc19164f0c..616d852bff 100644 --- a/docs/reference/gtk/gtk4-sections.txt +++ b/docs/reference/gtk/gtk4-sections.txt @@ -393,6 +393,16 @@ gtk_single_selection_get_type </SECTION> <SECTION> +<FILE>gtkmultiselection</FILE> +<TITLE>GtkMultiSeledction</TITLE> +GtkMultiSelection +gtk_multi_selection_new +gtk_multi_selection_copy +<SUBSECTION Private> +gtk_multi_selection_get_type +</SECTION> + +<SECTION> <FILE>gtklistitem</FILE> <TITLE>GtkListItem</TITLE> GtkListItem diff --git a/docs/reference/gtk/gtk4.types.in b/docs/reference/gtk/gtk4.types.in index 3accd6fa6d..446aa62004 100644 --- a/docs/reference/gtk/gtk4.types.in +++ b/docs/reference/gtk/gtk4.types.in @@ -139,6 +139,7 @@ gtk_menu_button_get_type gtk_message_dialog_get_type gtk_mount_operation_get_type gtk_multi_filter_get_type +gtk_multi_selection_get_type gtk_multi_sorter_get_type gtk_native_get_type gtk_native_dialog_get_type @@ -174,6 +174,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..f8ed44c2d4 --- /dev/null +++ b/gtk/gtkmultiselection.c @@ -0,0 +1,380 @@ +/* + * 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); +} + +/** + * gtk_multi_selection_copy: + * @selection: the #GtkSelectionModel to copy + * + * Creates a #GtkMultiSelection that has the same underlying + * model and the same selected items as @selection. + * + * Returns: (transfer full): a new #GtkMultiSelection + */ +GtkMultiSelection * +gtk_multi_selection_copy (GtkSelectionModel *selection) +{ + GtkMultiSelection *copy; + GListModel *model; + + g_object_get (selection, "model", &model, NULL); + + copy = GTK_MULTI_SELECTION (gtk_multi_selection_new (model)); + + if (GTK_IS_MULTI_SELECTION (selection)) + { + GtkMultiSelection *multi = GTK_MULTI_SELECTION (selection); + + gtk_set_free (copy->selected); + copy->selected = gtk_set_copy (multi->selected); + copy->last_selected = multi->last_selected; + } + else + { + guint pos, n; + guint start, n_items; + gboolean selected; + + n = g_list_model_get_n_items (model); + n_items = 0; + for (pos = 0; pos < n; pos += n_items) + { + gtk_selection_model_query_range (selection, pos, &start, &n_items, &selected); + if (selected) + gtk_selection_model_select_range (GTK_SELECTION_MODEL (copy), start, n_items, FALSE); + } + } + + g_object_unref (model); + + return copy; +} diff --git a/gtk/gtkmultiselection.h b/gtk/gtkmultiselection.h new file mode 100644 index 0000000000..e4147c2a66 --- /dev/null +++ b/gtk/gtkmultiselection.h @@ -0,0 +1,41 @@ +/* + * 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> +#include <gtk/gtkselectionmodel.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); + +GDK_AVAILABLE_IN_ALL +GtkMultiSelection * gtk_multi_selection_copy (GtkSelectionModel *selection); + +G_END_DECLS + +#endif /* __GTK_MULTI_SELECTION_H__ */ diff --git a/gtk/gtkset.c b/gtk/gtkset.c new file mode 100644 index 0000000000..69f1dea72e --- /dev/null +++ b/gtk/gtkset.c @@ -0,0 +1,351 @@ +/* + * 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; +} + +GtkSet * +gtk_set_copy (GtkSet *set) +{ + GtkSet *copy; + + copy = g_new (GtkSet, 1); + copy->ranges = g_array_copy (set->ranges); + + return copy; +} + +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 negative shifts 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..d0ba4e1a96 --- /dev/null +++ b/gtk/gtkset.h @@ -0,0 +1,70 @@ +/* + * 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); +GtkSet *gtk_set_copy (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 f9202b8759..2ca67edcbd 100644 --- a/gtk/meson.build +++ b/gtk/meson.build @@ -134,6 +134,7 @@ gtk_private_sources = files([ 'gtkscaler.c', 'gtksearchengine.c', 'gtksearchenginemodel.c', + 'gtkset.c', 'gtksizerequestcache.c', 'gtkstyleanimation.c', 'gtkstylecascade.c', @@ -301,6 +302,7 @@ gtk_public_sources = files([ 'gtkmodules.c', 'gtkmountoperation.c', 'gtkmultifilter.c', + 'gtkmultiselection.c', 'gtkmultisorter.c', 'gtknativedialog.c', 'gtknomediafile.c', @@ -574,6 +576,7 @@ gtk_public_headers = files([ 'gtkmessagedialog.h', 'gtkmountoperation.h', 'gtkmultifilter.h', + 'gtkmultiselection.h', 'gtkmultisorter.h', 'gtknative.h', 'gtknativedialog.h', diff --git a/testsuite/gtk/defaultvalue.c b/testsuite/gtk/defaultvalue.c index 98f6dc7b43..1d555e1304 100644 --- a/testsuite/gtk/defaultvalue.c +++ b/testsuite/gtk/defaultvalue.c @@ -114,7 +114,8 @@ test_type (gconstpointer data) } else if (g_type_is_a (type, GTK_TYPE_FILTER_LIST_MODEL) || g_type_is_a (type, GTK_TYPE_NO_SELECTION) || - g_type_is_a (type, GTK_TYPE_SINGLE_SELECTION)) + g_type_is_a (type, GTK_TYPE_SINGLE_SELECTION) || + g_type_is_a (type, GTK_TYPE_MULTI_SELECTION)) { GListStore *list_store = g_list_store_new (G_TYPE_OBJECT); instance = g_object_new (type, @@ -277,7 +278,8 @@ G_GNUC_END_IGNORE_DEPRECATIONS if ((g_type_is_a (type, GTK_TYPE_FILTER_LIST_MODEL) || g_type_is_a (type, GTK_TYPE_NO_SELECTION) || - g_type_is_a (type, GTK_TYPE_SINGLE_SELECTION)) && + g_type_is_a (type, GTK_TYPE_SINGLE_SELECTION) || + g_type_is_a (type, GTK_TYPE_MULTI_SELECTION)) && strcmp (pspec->name, "model") == 0) continue; diff --git a/testsuite/gtk/meson.build b/testsuite/gtk/meson.build index 156b36d574..30e7c50837 100644 --- a/testsuite/gtk/meson.build +++ b/testsuite/gtk/meson.build @@ -39,6 +39,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..b00c68c7c8 --- /dev/null +++ b/testsuite/gtk/multiselection.c @@ -0,0 +1,393 @@ +/* + * 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; + guint i; + + objects = g_new0 (GObject *, added); + + 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]); + + g_free (objects); +} + +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 (); +} diff --git a/testsuite/gtk/notify.c b/testsuite/gtk/notify.c index a5944a2b5c..ddd806948d 100644 --- a/testsuite/gtk/notify.c +++ b/testsuite/gtk/notify.c @@ -457,7 +457,8 @@ test_type (gconstpointer data) } else if (g_type_is_a (type, GTK_TYPE_FILTER_LIST_MODEL) || g_type_is_a (type, GTK_TYPE_NO_SELECTION) || - g_type_is_a (type, GTK_TYPE_SINGLE_SELECTION)) + g_type_is_a (type, GTK_TYPE_SINGLE_SELECTION) || + g_type_is_a (type, GTK_TYPE_MULTI_SELECTION)) { GListStore *list_store = g_list_store_new (G_TYPE_OBJECT); instance = g_object_new (type, diff --git a/testsuite/gtk/objects-finalize.c b/testsuite/gtk/objects-finalize.c index d3ff56c3e9..ef5223b923 100644 --- a/testsuite/gtk/objects-finalize.c +++ b/testsuite/gtk/objects-finalize.c @@ -71,7 +71,8 @@ test_finalize_object (gconstpointer data) } else if (g_type_is_a (test_type, GTK_TYPE_FILTER_LIST_MODEL) || g_type_is_a (test_type, GTK_TYPE_NO_SELECTION) || - g_type_is_a (test_type, GTK_TYPE_SINGLE_SELECTION)) + g_type_is_a (test_type, GTK_TYPE_SINGLE_SELECTION) || + g_type_is_a (test_type, GTK_TYPE_MULTI_SELECTION)) { GListStore *list_store = g_list_store_new (G_TYPE_OBJECT); object = g_object_new (test_type, |