1/*
2 * Copyright © 2019 Red Hat, Inc.
3 *
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Lesser General Public
6 * License as published by the Free Software Foundation; either
7 * version 2.1 of the License, or (at your option) any later version.
8 *
9 * This library is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 * Lesser General Public License for more details.
13 *
14 * You should have received a copy of the GNU Lesser General Public
15 * License along with this library. If not, see <http://www.gnu.org/licenses/>.
16 *
17 * Authors: Matthias Clasen <mclasen@redhat.com>
18 */
19
20#include "config.h"
21
22#include "gtkmultiselection.h"
23
24#include "gtkbitset.h"
25#include "gtkintl.h"
26#include "gtkselectionmodel.h"
27
28/**
29 * GtkMultiSelection:
30 *
31 * `GtkMultiSelection` is a `GtkSelectionModel` that allows selecting multiple
32 * elements.
33 */
34
35struct _GtkMultiSelection
36{
37 GObject parent_instance;
38
39 GListModel *model;
40
41 GtkBitset *selected;
42 GHashTable *items; /* item => position */
43};
44
45struct _GtkMultiSelectionClass
46{
47 GObjectClass parent_class;
48};
49
50enum {
51 PROP_0,
52 PROP_MODEL,
53
54 N_PROPS,
55};
56
57static GParamSpec *properties[N_PROPS] = { NULL, };
58
59static GType
60gtk_multi_selection_get_item_type (GListModel *list)
61{
62 return G_TYPE_OBJECT;
63}
64
65static guint
66gtk_multi_selection_get_n_items (GListModel *list)
67{
68 GtkMultiSelection *self = GTK_MULTI_SELECTION (ptr: list);
69
70 if (self->model == NULL)
71 return 0;
72
73 return g_list_model_get_n_items (list: self->model);
74}
75
76static gpointer
77gtk_multi_selection_get_item (GListModel *list,
78 guint position)
79{
80 GtkMultiSelection *self = GTK_MULTI_SELECTION (ptr: list);
81
82 if (self->model == NULL)
83 return NULL;
84
85 return g_list_model_get_item (list: self->model, position);
86}
87
88static void
89gtk_multi_selection_list_model_init (GListModelInterface *iface)
90{
91 iface->get_item_type = gtk_multi_selection_get_item_type;
92 iface->get_n_items = gtk_multi_selection_get_n_items;
93 iface->get_item = gtk_multi_selection_get_item;
94}
95
96static gboolean
97gtk_multi_selection_is_selected (GtkSelectionModel *model,
98 guint position)
99{
100 GtkMultiSelection *self = GTK_MULTI_SELECTION (ptr: model);
101
102 return gtk_bitset_contains (self: self->selected, value: position);
103}
104
105static GtkBitset *
106gtk_multi_selection_get_selection_in_range (GtkSelectionModel *model,
107 guint pos,
108 guint n_items)
109{
110 GtkMultiSelection *self = GTK_MULTI_SELECTION (ptr: model);
111
112 return gtk_bitset_ref (self: self->selected);
113}
114
115static void
116gtk_multi_selection_toggle_selection (GtkMultiSelection *self,
117 GtkBitset *changes)
118{
119 GListModel *model = G_LIST_MODEL (ptr: self);
120 GtkBitsetIter change_iter, selected_iter;
121 GtkBitset *selected;
122 guint change_pos, selected_pos;
123 gboolean more;
124
125 gtk_bitset_difference (self: self->selected, other: changes);
126
127 selected = gtk_bitset_copy (self: changes);
128 gtk_bitset_intersect (self: selected, other: self->selected);
129
130 if (!gtk_bitset_iter_init_first (iter: &selected_iter, set: selected, value: &selected_pos))
131 selected_pos = G_MAXUINT;
132
133 for (more = gtk_bitset_iter_init_first (iter: &change_iter, set: changes, value: &change_pos);
134 more;
135 more = gtk_bitset_iter_next (iter: &change_iter, value: &change_pos))
136 {
137 gpointer item = g_list_model_get_item (list: model, position: change_pos);
138
139 if (change_pos < selected_pos)
140 {
141 g_hash_table_remove (hash_table: self->items, key: item);
142 g_object_unref (object: item);
143 }
144 else
145 {
146 g_assert (change_pos == selected_pos);
147
148 g_hash_table_insert (hash_table: self->items, key: item, GUINT_TO_POINTER (change_pos));
149
150 if (!gtk_bitset_iter_next (iter: &selected_iter, value: &selected_pos))
151 selected_pos = G_MAXUINT;
152 }
153 }
154
155 gtk_bitset_unref (self: selected);
156}
157
158static gboolean
159gtk_multi_selection_set_selection (GtkSelectionModel *model,
160 GtkBitset *selected,
161 GtkBitset *mask)
162{
163 GtkMultiSelection *self = GTK_MULTI_SELECTION (ptr: model);
164 GtkBitset *changes;
165 guint min, max, n_items;
166
167 /* changes = (self->selected XOR selected) AND mask
168 * But doing it this way avoids looking at all values outside the mask
169 */
170 changes = gtk_bitset_copy (self: selected);
171 gtk_bitset_difference (self: changes, other: self->selected);
172 gtk_bitset_intersect (self: changes, other: mask);
173
174 min = gtk_bitset_get_minimum (self: changes);
175 max = gtk_bitset_get_maximum (self: changes);
176
177 /* sanity check */
178 n_items = self->model ? g_list_model_get_n_items (list: self->model) : 0;
179 if (max >= n_items)
180 {
181 gtk_bitset_remove_range_closed (self: changes, first: n_items, last: max);
182 max = gtk_bitset_get_maximum (self: changes);
183 }
184
185 /* actually do the change */
186 gtk_multi_selection_toggle_selection (self, changes);
187
188 gtk_bitset_unref (self: changes);
189
190 if (min <= max)
191 gtk_selection_model_selection_changed (model, position: min, n_items: max - min + 1);
192
193 return TRUE;
194}
195
196static void
197gtk_multi_selection_selection_model_init (GtkSelectionModelInterface *iface)
198{
199 iface->is_selected = gtk_multi_selection_is_selected;
200 iface->get_selection_in_range = gtk_multi_selection_get_selection_in_range;
201 iface->set_selection = gtk_multi_selection_set_selection;
202}
203
204G_DEFINE_TYPE_EXTENDED (GtkMultiSelection, gtk_multi_selection, G_TYPE_OBJECT, 0,
205 G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL,
206 gtk_multi_selection_list_model_init)
207 G_IMPLEMENT_INTERFACE (GTK_TYPE_SELECTION_MODEL,
208 gtk_multi_selection_selection_model_init))
209
210static void
211gtk_multi_selection_items_changed_cb (GListModel *model,
212 guint position,
213 guint removed,
214 guint added,
215 GtkMultiSelection *self)
216{
217 GHashTableIter iter;
218 gpointer item, pos_pointer;
219 GHashTable *pending = NULL;
220 guint i;
221
222 gtk_bitset_splice (self: self->selected, position, removed, added);
223
224 g_hash_table_iter_init (iter: &iter, hash_table: self->items);
225 while (g_hash_table_iter_next (iter: &iter, key: &item, value: &pos_pointer))
226 {
227 guint pos = GPOINTER_TO_UINT (pos_pointer);
228
229 if (pos < position)
230 continue;
231 else if (pos >= position + removed)
232 g_hash_table_iter_replace (iter: &iter, GUINT_TO_POINTER (pos - removed + added));
233 else /* if pos is in the removed range */
234 {
235 if (added == 0)
236 {
237 g_hash_table_iter_remove (iter: &iter);
238 }
239 else
240 {
241 g_hash_table_iter_steal (iter: &iter);
242 if (pending == NULL)
243 pending = g_hash_table_new_full (NULL, NULL, key_destroy_func: g_object_unref, NULL);
244 g_hash_table_add (hash_table: pending, key: item);
245 }
246 }
247 }
248
249 for (i = position; pending != NULL && i < position + added; i++)
250 {
251 item = g_list_model_get_item (list: model, position: i);
252 if (g_hash_table_contains (hash_table: pending, key: item))
253 {
254 gtk_bitset_add (self: self->selected, value: i);
255 g_hash_table_insert (hash_table: self->items, key: item, GUINT_TO_POINTER (i));
256 g_hash_table_remove (hash_table: pending, key: item);
257 if (g_hash_table_size (hash_table: pending) == 0)
258 g_clear_pointer (&pending, g_hash_table_unref);
259 }
260 else
261 {
262 g_object_unref (object: item);
263 }
264 }
265
266 g_clear_pointer (&pending, g_hash_table_unref);
267
268 g_list_model_items_changed (list: G_LIST_MODEL (ptr: self), position, removed, added);
269}
270
271static void
272gtk_multi_selection_clear_model (GtkMultiSelection *self)
273{
274 if (self->model == NULL)
275 return;
276
277 g_signal_handlers_disconnect_by_func (self->model,
278 gtk_multi_selection_items_changed_cb,
279 self);
280 g_clear_object (&self->model);
281}
282
283static void
284gtk_multi_selection_set_property (GObject *object,
285 guint prop_id,
286 const GValue *value,
287 GParamSpec *pspec)
288
289{
290 GtkMultiSelection *self = GTK_MULTI_SELECTION (ptr: object);
291
292 switch (prop_id)
293 {
294 case PROP_MODEL:
295 gtk_multi_selection_set_model (self, model: g_value_get_object (value));
296 break;
297
298 default:
299 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
300 break;
301 }
302}
303
304static void
305gtk_multi_selection_get_property (GObject *object,
306 guint prop_id,
307 GValue *value,
308 GParamSpec *pspec)
309{
310 GtkMultiSelection *self = GTK_MULTI_SELECTION (ptr: object);
311
312 switch (prop_id)
313 {
314 case PROP_MODEL:
315 g_value_set_object (value, v_object: self->model);
316 break;
317
318 default:
319 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
320 break;
321 }
322}
323
324static void
325gtk_multi_selection_dispose (GObject *object)
326{
327 GtkMultiSelection *self = GTK_MULTI_SELECTION (ptr: object);
328
329 gtk_multi_selection_clear_model (self);
330
331 g_clear_pointer (&self->selected, gtk_bitset_unref);
332 g_clear_pointer (&self->items, g_hash_table_unref);
333
334 G_OBJECT_CLASS (gtk_multi_selection_parent_class)->dispose (object);
335}
336
337static void
338gtk_multi_selection_class_init (GtkMultiSelectionClass *klass)
339{
340 GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
341
342 gobject_class->get_property = gtk_multi_selection_get_property;
343 gobject_class->set_property = gtk_multi_selection_set_property;
344 gobject_class->dispose = gtk_multi_selection_dispose;
345
346 /**
347 * GtkMultiSelection:model: (attributes org.gtk.Property.get=gtk_multi_selection_get_model org.gtk.Property.set=gtk_multi_selection_set_model)
348 *
349 * The list managed by this selection.
350 */
351 properties[PROP_MODEL] =
352 g_param_spec_object (name: "model",
353 P_("Model"),
354 P_("List managed by this selection"),
355 G_TYPE_LIST_MODEL,
356 flags: G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
357
358 g_object_class_install_properties (oclass: gobject_class, n_pspecs: N_PROPS, pspecs: properties);
359}
360
361static void
362gtk_multi_selection_init (GtkMultiSelection *self)
363{
364 self->selected = gtk_bitset_new_empty ();
365 self->items = g_hash_table_new_full (NULL, NULL, key_destroy_func: g_object_unref, NULL);
366}
367
368/**
369 * gtk_multi_selection_new:
370 * @model: (nullable) (transfer full): the `GListModel` to manage
371 *
372 * Creates a new selection to handle @model.
373 *
374 * Returns: (transfer full): a new `GtkMultiSelection`
375 */
376GtkMultiSelection *
377gtk_multi_selection_new (GListModel *model)
378{
379 GtkMultiSelection *self;
380
381 g_return_val_if_fail (model == NULL || G_IS_LIST_MODEL (model), NULL);
382
383 self = g_object_new (GTK_TYPE_MULTI_SELECTION,
384 first_property_name: "model", model,
385 NULL);
386
387 /* consume the reference */
388 g_clear_object (&model);
389
390 return self;
391}
392
393/**
394 * gtk_multi_selection_get_model: (attributes org.gtk.Method.get_property=model)
395 * @self: a `GtkMultiSelection`
396 *
397 * Returns the underlying model of @self.
398 *
399 * Returns: (transfer none) (nullable): the underlying model
400 */
401GListModel *
402gtk_multi_selection_get_model (GtkMultiSelection *self)
403{
404 g_return_val_if_fail (GTK_IS_MULTI_SELECTION (self), NULL);
405
406 return self->model;
407}
408
409/**
410 * gtk_multi_selection_set_model: (attributes org.gtk.Method.set_property=model)
411 * @self: a `GtkMultiSelection`
412 * @model: (nullable): A `GListModel` to wrap
413 *
414 * Sets the model that @self should wrap.
415 *
416 * If @model is %NULL, @self will be empty.
417 */
418void
419gtk_multi_selection_set_model (GtkMultiSelection *self,
420 GListModel *model)
421{
422 guint n_items_before;
423
424 g_return_if_fail (GTK_IS_MULTI_SELECTION (self));
425 g_return_if_fail (model == NULL || G_IS_LIST_MODEL (model));
426
427 if (self->model == model)
428 return;
429
430 n_items_before = self->model ? g_list_model_get_n_items (list: self->model) : 0;
431 gtk_multi_selection_clear_model (self);
432
433 if (model)
434 {
435 self->model = g_object_ref (model);
436 g_signal_connect (self->model,
437 "items-changed",
438 G_CALLBACK (gtk_multi_selection_items_changed_cb),
439 self);
440 gtk_multi_selection_items_changed_cb (model: self->model,
441 position: 0,
442 removed: n_items_before,
443 added: g_list_model_get_n_items (list: model),
444 self);
445 }
446 else
447 {
448 gtk_bitset_remove_all (self: self->selected);
449 g_hash_table_remove_all (hash_table: self->items);
450 g_list_model_items_changed (list: G_LIST_MODEL (ptr: self), position: 0, removed: n_items_before, added: 0);
451 }
452
453 g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_MODEL]);
454}
455

source code of gtk/gtk/gtkmultiselection.c