diff --git a/gtk/gtk.h b/gtk/gtk.h index bcd144a780..35426397c0 100644 --- a/gtk/gtk.h +++ b/gtk/gtk.h @@ -157,6 +157,7 @@ #include #include #include +#include #include #include #include diff --git a/gtk/gtklistitemmanager.c b/gtk/gtklistitemmanager.c index 9d8fbbc85a..ef5298a1ca 100644 --- a/gtk/gtklistitemmanager.c +++ b/gtk/gtklistitemmanager.c @@ -143,11 +143,11 @@ gtk_list_item_manager_select (GtkListItemManager *self, if (gtk_list_item_get_selected (item)) gtk_selection_model_unselect_item (self->model, pos); else - gtk_selection_model_select_item (self->model, pos, FALSE); + gtk_selection_model_select_item (self->model, pos, FALSE, extend); } else { - gtk_selection_model_select_item (self->model, pos, TRUE); + gtk_selection_model_select_item (self->model, pos, TRUE, extend); } } diff --git a/gtk/gtklistview.c b/gtk/gtklistview.c index f45068f799..f21ce89249 100644 --- a/gtk/gtklistview.c +++ b/gtk/gtklistview.c @@ -29,6 +29,7 @@ #include "gtkscrollable.h" #include "gtkselectionmodel.h" #include "gtksingleselection.h" +#include "gtkmultiselection.h" #include "gtksnapshot.h" #include "gtkwidgetprivate.h" @@ -1361,7 +1362,7 @@ gtk_list_view_set_model (GtkListView *self, if (GTK_IS_SELECTION_MODEL (model)) selection_model = GTK_SELECTION_MODEL (g_object_ref (model)); else - selection_model = GTK_SELECTION_MODEL (gtk_single_selection_new (model)); + selection_model = GTK_SELECTION_MODEL (gtk_multi_selection_new (model)); gtk_list_item_manager_set_model (self->item_manager, selection_model); diff --git a/gtk/gtkmultiselection.c b/gtk/gtkmultiselection.c new file mode 100644 index 0000000000..5b085a248a --- /dev/null +++ b/gtk/gtkmultiselection.c @@ -0,0 +1,329 @@ +/* + * 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 . + * + * Authors: Matthias Clasen + */ + +#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, + gboolean extend) +{ + GtkMultiSelection *self = GTK_MULTI_SELECTION (model); + guint pos, n_items; + + if (extend && self->last_selected != GTK_INVALID_LIST_POSITION) + { + pos = MIN (position, self->last_selected); + n_items = MAX (position, self->last_selected) - pos + 1; + } + else + { + 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_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; +} + +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 . + * + * Authors: Matthias Clasen + */ + +#ifndef __GTK_MULTI_SELECTION_H__ +#define __GTK_MULTI_SELECTION_H__ + +#include + +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/gtkselectionmodel.c b/gtk/gtkselectionmodel.c index 7075402e11..2e026984f1 100644 --- a/gtk/gtkselectionmodel.c +++ b/gtk/gtkselectionmodel.c @@ -79,7 +79,8 @@ gtk_selection_model_default_is_selected (GtkSelectionModel *model, static gboolean gtk_selection_model_default_select_item (GtkSelectionModel *model, guint position, - gboolean exclusive) + gboolean exclusive, + gboolean extend) { return FALSE; } @@ -181,14 +182,15 @@ gtk_selection_model_is_selected (GtkSelectionModel *model, gboolean gtk_selection_model_select_item (GtkSelectionModel *model, guint position, - gboolean exclusive) + gboolean exclusive, + gboolean extend) { GtkSelectionModelInterface *iface; g_return_val_if_fail (GTK_IS_SELECTION_MODEL (model), 0); iface = GTK_SELECTION_MODEL_GET_IFACE (model); - return iface->select_item (model, position, exclusive); + return iface->select_item (model, position, exclusive, extend); } gboolean diff --git a/gtk/gtkselectionmodel.h b/gtk/gtkselectionmodel.h index 9904888d82..f4e5a1eca5 100644 --- a/gtk/gtkselectionmodel.h +++ b/gtk/gtkselectionmodel.h @@ -67,7 +67,8 @@ struct _GtkSelectionModelInterface gboolean (* select_item) (GtkSelectionModel *model, guint position, - gboolean exclusive); + gboolean exclusive, + gboolean extend); gboolean (* unselect_item) (GtkSelectionModel *model, guint position); gboolean (* select_range) (GtkSelectionModel *model, @@ -88,7 +89,8 @@ gboolean gtk_selection_model_is_selected (GtkSelectionMod GDK_AVAILABLE_IN_ALL gboolean gtk_selection_model_select_item (GtkSelectionModel *model, guint position, - gboolean exclusive); + gboolean exclusive, + gboolean extend); GDK_AVAILABLE_IN_ALL gboolean gtk_selection_model_unselect_item (GtkSelectionModel *model, guint position); diff --git a/gtk/gtkset.c b/gtk/gtkset.c new file mode 100644 index 0000000000..0c2dfffb11 --- /dev/null +++ b/gtk/gtkset.c @@ -0,0 +1,229 @@ +/* + * 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 . + * + * Authors: Matthias Clasen + */ + +#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; +}; + +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) + { + if (first > -1) + { + 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); + } + } + + if (cmp == 0) + { + if (first < 0) + first = i; + last = i; + } + } +} + +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; + } +} diff --git a/gtk/gtkset.h b/gtk/gtkset.h new file mode 100644 index 0000000000..c8573d1d43 --- /dev/null +++ b/gtk/gtkset.h @@ -0,0 +1,49 @@ +/* + * 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 . + * + * Authors: Matthias Clasen + */ + +#ifndef __GTK_SET_H__ +#define __GTK_SET_H__ + +#include + +typedef struct _GtkSet GtkSet; + +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_shift (GtkSet *set, + guint first, + int shift); + +#endif /* __GTK_SET_H__ */ diff --git a/gtk/gtksingleselection.c b/gtk/gtksingleselection.c index b6ec5d7266..99ef1d9576 100644 --- a/gtk/gtksingleselection.c +++ b/gtk/gtksingleselection.c @@ -108,7 +108,8 @@ gtk_single_selection_is_selected (GtkSelectionModel *model, static gboolean gtk_single_selection_select_item (GtkSelectionModel *model, guint position, - gboolean exclusive) + gboolean exclusive, + gboolean extend) { GtkSingleSelection *self = GTK_SINGLE_SELECTION (model); diff --git a/gtk/meson.build b/gtk/meson.build index f10346e426..9d0dbbea38 100644 --- a/gtk/meson.build +++ b/gtk/meson.build @@ -137,6 +137,7 @@ gtk_private_sources = files([ 'gtksearchengine.c', 'gtksearchenginemodel.c', 'gtksearchenginesimple.c', + 'gtkset.c', 'gtksizerequestcache.c', 'gtkstyleanimation.c', 'gtkstylecascade.c', @@ -289,6 +290,7 @@ gtk_public_sources = files([ 'gtkmodelmenuitem.c', 'gtkmodules.c', 'gtkmountoperation.c', + 'gtkmultiselection.c', 'gtknativedialog.c', 'gtknomediafile.c', 'gtknotebook.c',