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 | |
35 | struct _GtkMultiSelection |
36 | { |
37 | GObject parent_instance; |
38 | |
39 | GListModel *model; |
40 | |
41 | GtkBitset *selected; |
42 | GHashTable *items; /* item => position */ |
43 | }; |
44 | |
45 | struct _GtkMultiSelectionClass |
46 | { |
47 | GObjectClass parent_class; |
48 | }; |
49 | |
50 | enum { |
51 | PROP_0, |
52 | PROP_MODEL, |
53 | |
54 | N_PROPS, |
55 | }; |
56 | |
57 | static GParamSpec *properties[N_PROPS] = { NULL, }; |
58 | |
59 | static GType |
60 | gtk_multi_selection_get_item_type (GListModel *list) |
61 | { |
62 | return G_TYPE_OBJECT; |
63 | } |
64 | |
65 | static guint |
66 | gtk_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 | |
76 | static gpointer |
77 | gtk_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 | |
88 | static void |
89 | gtk_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 | |
96 | static gboolean |
97 | gtk_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 | |
105 | static GtkBitset * |
106 | gtk_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 | |
115 | static void |
116 | gtk_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 | |
158 | static gboolean |
159 | gtk_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 | |
196 | static void |
197 | gtk_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 | |
204 | G_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 | |
210 | static void |
211 | gtk_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 | |
271 | static void |
272 | gtk_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 | |
283 | static void |
284 | gtk_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 | |
304 | static void |
305 | gtk_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 | |
324 | static void |
325 | gtk_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 | |
337 | static void |
338 | gtk_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 | |
361 | static void |
362 | gtk_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 | */ |
376 | GtkMultiSelection * |
377 | gtk_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 | */ |
401 | GListModel * |
402 | gtk_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 | */ |
418 | void |
419 | gtk_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 | |