1 | /* |
2 | * Copyright © 2018 Benjamin Otte |
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: Benjamin Otte <otte@gnome.org> |
18 | */ |
19 | |
20 | #include "config.h" |
21 | |
22 | #include "gtkslicelistmodel.h" |
23 | |
24 | #include "gtkintl.h" |
25 | #include "gtkprivate.h" |
26 | |
27 | /** |
28 | * GtkSliceListModel: |
29 | * |
30 | * `GtkSliceListModel` is a list model that presents a slice of another model. |
31 | * |
32 | * This is useful when implementing paging by setting the size to the number |
33 | * of elements per page and updating the offset whenever a different page is |
34 | * opened. |
35 | */ |
36 | |
37 | #define DEFAULT_SIZE 10 |
38 | |
39 | enum { |
40 | PROP_0, |
41 | PROP_MODEL, |
42 | PROP_OFFSET, |
43 | PROP_SIZE, |
44 | NUM_PROPERTIES |
45 | }; |
46 | |
47 | struct _GtkSliceListModel |
48 | { |
49 | GObject parent_instance; |
50 | |
51 | GListModel *model; |
52 | guint offset; |
53 | guint size; |
54 | |
55 | guint n_items; |
56 | }; |
57 | |
58 | struct _GtkSliceListModelClass |
59 | { |
60 | GObjectClass parent_class; |
61 | }; |
62 | |
63 | static GParamSpec *properties[NUM_PROPERTIES] = { NULL, }; |
64 | |
65 | static GType |
66 | gtk_slice_list_model_get_item_type (GListModel *list) |
67 | { |
68 | return G_TYPE_OBJECT; |
69 | } |
70 | |
71 | static guint |
72 | gtk_slice_list_model_get_n_items (GListModel *list) |
73 | { |
74 | GtkSliceListModel *self = GTK_SLICE_LIST_MODEL (ptr: list); |
75 | guint n_items; |
76 | |
77 | if (self->model == NULL) |
78 | return 0; |
79 | |
80 | /* XXX: This can be done without calling g_list_model_get_n_items() on the parent model |
81 | * by checking if model.get_item(offset + size) != NULL */ |
82 | n_items = g_list_model_get_n_items (list: self->model); |
83 | if (n_items <= self->offset) |
84 | return 0; |
85 | |
86 | n_items -= self->offset; |
87 | return MIN (n_items, self->size); |
88 | } |
89 | |
90 | static gpointer |
91 | gtk_slice_list_model_get_item (GListModel *list, |
92 | guint position) |
93 | { |
94 | GtkSliceListModel *self = GTK_SLICE_LIST_MODEL (ptr: list); |
95 | |
96 | if (self->model == NULL) |
97 | return NULL; |
98 | |
99 | if (position >= self->size) |
100 | return NULL; |
101 | |
102 | return g_list_model_get_item (list: self->model, position: position + self->offset); |
103 | } |
104 | |
105 | static void |
106 | gtk_slice_list_model_model_init (GListModelInterface *iface) |
107 | { |
108 | iface->get_item_type = gtk_slice_list_model_get_item_type; |
109 | iface->get_n_items = gtk_slice_list_model_get_n_items; |
110 | iface->get_item = gtk_slice_list_model_get_item; |
111 | } |
112 | |
113 | G_DEFINE_TYPE_WITH_CODE (GtkSliceListModel, gtk_slice_list_model, G_TYPE_OBJECT, |
114 | G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, gtk_slice_list_model_model_init)) |
115 | |
116 | static void |
117 | gtk_slice_list_model_items_changed_cb (GListModel *model, |
118 | guint position, |
119 | guint removed, |
120 | guint added, |
121 | GtkSliceListModel *self) |
122 | { |
123 | if (position >= self->offset + self->size) |
124 | return; |
125 | |
126 | if (position < self->offset) |
127 | { |
128 | guint skip = MIN (removed, added); |
129 | skip = MIN (skip, self->offset - position); |
130 | |
131 | position += skip; |
132 | removed -= skip; |
133 | added -= skip; |
134 | } |
135 | |
136 | if (removed == added) |
137 | { |
138 | guint changed = removed; |
139 | |
140 | if (changed == 0) |
141 | return; |
142 | |
143 | g_assert (position >= self->offset); |
144 | position -= self->offset; |
145 | changed = MIN (changed, self->size - position); |
146 | |
147 | g_list_model_items_changed (list: G_LIST_MODEL (ptr: self), position, removed: changed, added: changed); |
148 | } |
149 | else |
150 | { |
151 | guint n_after, n_before; |
152 | guint skip; |
153 | |
154 | if (position > self->offset) |
155 | skip = position - self->offset; |
156 | else |
157 | skip = 0; |
158 | |
159 | n_after = g_list_model_get_n_items (list: self->model); |
160 | n_before = n_after - added + removed; |
161 | n_after = CLAMP (n_after, self->offset, self->offset + self->size) - self->offset; |
162 | n_before = CLAMP (n_before, self->offset, self->offset + self->size) - self->offset; |
163 | |
164 | g_list_model_items_changed (list: G_LIST_MODEL (ptr: self), position: skip, removed: n_before - skip, added: n_after - skip); |
165 | } |
166 | } |
167 | |
168 | static void |
169 | gtk_slice_list_model_set_property (GObject *object, |
170 | guint prop_id, |
171 | const GValue *value, |
172 | GParamSpec *pspec) |
173 | { |
174 | GtkSliceListModel *self = GTK_SLICE_LIST_MODEL (ptr: object); |
175 | |
176 | switch (prop_id) |
177 | { |
178 | case PROP_MODEL: |
179 | gtk_slice_list_model_set_model (self, model: g_value_get_object (value)); |
180 | break; |
181 | |
182 | case PROP_OFFSET: |
183 | gtk_slice_list_model_set_offset (self, offset: g_value_get_uint (value)); |
184 | break; |
185 | |
186 | case PROP_SIZE: |
187 | gtk_slice_list_model_set_size (self, size: g_value_get_uint (value)); |
188 | break; |
189 | |
190 | default: |
191 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); |
192 | break; |
193 | } |
194 | } |
195 | |
196 | static void |
197 | gtk_slice_list_model_get_property (GObject *object, |
198 | guint prop_id, |
199 | GValue *value, |
200 | GParamSpec *pspec) |
201 | { |
202 | GtkSliceListModel *self = GTK_SLICE_LIST_MODEL (ptr: object); |
203 | |
204 | switch (prop_id) |
205 | { |
206 | case PROP_MODEL: |
207 | g_value_set_object (value, v_object: self->model); |
208 | break; |
209 | |
210 | case PROP_OFFSET: |
211 | g_value_set_uint (value, v_uint: self->offset); |
212 | break; |
213 | |
214 | case PROP_SIZE: |
215 | g_value_set_uint (value, v_uint: self->size); |
216 | break; |
217 | |
218 | default: |
219 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); |
220 | break; |
221 | } |
222 | } |
223 | |
224 | static void |
225 | gtk_slice_list_model_clear_model (GtkSliceListModel *self) |
226 | { |
227 | if (self->model == NULL) |
228 | return; |
229 | |
230 | g_signal_handlers_disconnect_by_func (self->model, gtk_slice_list_model_items_changed_cb, self); |
231 | g_clear_object (&self->model); |
232 | } |
233 | |
234 | static void |
235 | gtk_slice_list_model_dispose (GObject *object) |
236 | { |
237 | GtkSliceListModel *self = GTK_SLICE_LIST_MODEL (ptr: object); |
238 | |
239 | gtk_slice_list_model_clear_model (self); |
240 | |
241 | G_OBJECT_CLASS (gtk_slice_list_model_parent_class)->dispose (object); |
242 | }; |
243 | |
244 | static void |
245 | gtk_slice_list_model_class_init (GtkSliceListModelClass *class) |
246 | { |
247 | GObjectClass *gobject_class = G_OBJECT_CLASS (class); |
248 | |
249 | gobject_class->set_property = gtk_slice_list_model_set_property; |
250 | gobject_class->get_property = gtk_slice_list_model_get_property; |
251 | gobject_class->dispose = gtk_slice_list_model_dispose; |
252 | |
253 | /** |
254 | * GtkSliceListModel:model: (attributes org.gtk.Property.get=gtk_slice_list_model_get_model org.gtk.Property.set=gtk_slice_list_model_set_model) |
255 | * |
256 | * Child model to take slice from. |
257 | */ |
258 | properties[PROP_MODEL] = |
259 | g_param_spec_object (name: "model" , |
260 | P_("Model" ), |
261 | P_("Child model to take slice from" ), |
262 | G_TYPE_LIST_MODEL, |
263 | GTK_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); |
264 | |
265 | /** |
266 | * GtkSliceListModel:offset: (attributes org.gtk.Property.get=gtk_slice_list_model_get_offset org.gtk.Property.set=gtk_slice_list_model_set_offset) |
267 | * |
268 | * Offset of slice. |
269 | */ |
270 | properties[PROP_OFFSET] = |
271 | g_param_spec_uint (name: "offset" , |
272 | P_("Offset" ), |
273 | P_("Offset of slice" ), |
274 | minimum: 0, G_MAXUINT, default_value: 0, |
275 | GTK_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); |
276 | |
277 | /** |
278 | * GtkSliceListModel:size: (attributes org.gtk.Property.get=gtk_slice_list_model_get_size org.gtk.Property.set=gtk_slice_list_model_set_size) |
279 | * |
280 | * Maximum size of slice. |
281 | */ |
282 | properties[PROP_SIZE] = |
283 | g_param_spec_uint (name: "size" , |
284 | P_("Size" ), |
285 | P_("Maximum size of slice" ), |
286 | minimum: 0, G_MAXUINT, DEFAULT_SIZE, |
287 | GTK_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); |
288 | |
289 | g_object_class_install_properties (oclass: gobject_class, n_pspecs: NUM_PROPERTIES, pspecs: properties); |
290 | } |
291 | |
292 | static void |
293 | gtk_slice_list_model_init (GtkSliceListModel *self) |
294 | { |
295 | self->size = DEFAULT_SIZE; |
296 | } |
297 | |
298 | /** |
299 | * gtk_slice_list_model_new: |
300 | * @model: (transfer full) (nullable): The model to use |
301 | * @offset: the offset of the slice |
302 | * @size: maximum size of the slice |
303 | * |
304 | * Creates a new slice model. |
305 | * |
306 | * It presents the slice from @offset to offset + @size |
307 | * of the given @model. |
308 | * |
309 | * Returns: A new `GtkSliceListModel` |
310 | */ |
311 | GtkSliceListModel * |
312 | gtk_slice_list_model_new (GListModel *model, |
313 | guint offset, |
314 | guint size) |
315 | { |
316 | GtkSliceListModel *self; |
317 | |
318 | g_return_val_if_fail (model == NULL || G_IS_LIST_MODEL (model), NULL); |
319 | |
320 | self = g_object_new (GTK_TYPE_SLICE_LIST_MODEL, |
321 | first_property_name: "model" , model, |
322 | "offset" , offset, |
323 | "size" , size, |
324 | NULL); |
325 | |
326 | /* consume the reference */ |
327 | g_clear_object (&model); |
328 | |
329 | return self; |
330 | } |
331 | |
332 | /** |
333 | * gtk_slice_list_model_set_model: (attributes org.gtk.Method.set_property=model) |
334 | * @self: a `GtkSliceListModel` |
335 | * @model: (nullable): The model to be sliced |
336 | * |
337 | * Sets the model to show a slice of. |
338 | * |
339 | * The model's item type must conform to @self's item type. |
340 | */ |
341 | void |
342 | gtk_slice_list_model_set_model (GtkSliceListModel *self, |
343 | GListModel *model) |
344 | { |
345 | guint removed, added; |
346 | |
347 | g_return_if_fail (GTK_IS_SLICE_LIST_MODEL (self)); |
348 | g_return_if_fail (model == NULL || G_IS_LIST_MODEL (model)); |
349 | |
350 | if (self->model == model) |
351 | return; |
352 | |
353 | removed = g_list_model_get_n_items (list: G_LIST_MODEL (ptr: self)); |
354 | gtk_slice_list_model_clear_model (self); |
355 | |
356 | if (model) |
357 | { |
358 | self->model = g_object_ref (model); |
359 | g_signal_connect (model, "items-changed" , G_CALLBACK (gtk_slice_list_model_items_changed_cb), self); |
360 | added = g_list_model_get_n_items (list: G_LIST_MODEL (ptr: self)); |
361 | } |
362 | else |
363 | { |
364 | added = 0; |
365 | } |
366 | |
367 | if (removed > 0 || added > 0) |
368 | g_list_model_items_changed (list: G_LIST_MODEL (ptr: self), position: 0, removed, added); |
369 | |
370 | g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_MODEL]); |
371 | } |
372 | |
373 | /** |
374 | * gtk_slice_list_model_get_model: (attributes org.gtk.Method.get_property=model) |
375 | * @self: a `GtkSliceListModel` |
376 | * |
377 | * Gets the model that is currently being used or %NULL if none. |
378 | * |
379 | * Returns: (nullable) (transfer none): The model in use |
380 | */ |
381 | GListModel * |
382 | gtk_slice_list_model_get_model (GtkSliceListModel *self) |
383 | { |
384 | g_return_val_if_fail (GTK_IS_SLICE_LIST_MODEL (self), NULL); |
385 | |
386 | return self->model; |
387 | } |
388 | |
389 | /** |
390 | * gtk_slice_list_model_set_offset: (attributes org.gtk.Method.set_property=offset) |
391 | * @self: a `GtkSliceListModel` |
392 | * @offset: the new offset to use |
393 | * |
394 | * Sets the offset into the original model for this slice. |
395 | * |
396 | * If the offset is too large for the sliced model, |
397 | * @self will end up empty. |
398 | */ |
399 | void |
400 | gtk_slice_list_model_set_offset (GtkSliceListModel *self, |
401 | guint offset) |
402 | { |
403 | guint before, after; |
404 | |
405 | g_return_if_fail (GTK_IS_SLICE_LIST_MODEL (self)); |
406 | |
407 | if (self->offset == offset) |
408 | return; |
409 | |
410 | before = g_list_model_get_n_items (list: G_LIST_MODEL (ptr: self)); |
411 | |
412 | self->offset = offset; |
413 | |
414 | after = g_list_model_get_n_items (list: G_LIST_MODEL (ptr: self)); |
415 | |
416 | if (before > 0 || after > 0) |
417 | g_list_model_items_changed (list: G_LIST_MODEL (ptr: self), position: 0, removed: before, added: after); |
418 | |
419 | g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_OFFSET]); |
420 | } |
421 | |
422 | /** |
423 | * gtk_slice_list_model_get_offset: (attributes org.gtk.Method.get_property=offset) |
424 | * @self: a `GtkSliceListModel` |
425 | * |
426 | * Gets the offset set via gtk_slice_list_model_set_offset(). |
427 | * |
428 | * Returns: The offset |
429 | */ |
430 | guint |
431 | gtk_slice_list_model_get_offset (GtkSliceListModel *self) |
432 | { |
433 | g_return_val_if_fail (GTK_IS_SLICE_LIST_MODEL (self), 0); |
434 | |
435 | return self->offset; |
436 | } |
437 | |
438 | /** |
439 | * gtk_slice_list_model_set_size: (attributes org.gtk.Method.set_property=size) |
440 | * @self: a `GtkSliceListModel` |
441 | * @size: the maximum size |
442 | * |
443 | * Sets the maximum size. @self will never have more items |
444 | * than @size. |
445 | * |
446 | * It can however have fewer items if the offset is too large |
447 | * or the model sliced from doesn't have enough items. |
448 | */ |
449 | void |
450 | gtk_slice_list_model_set_size (GtkSliceListModel *self, |
451 | guint size) |
452 | { |
453 | guint before, after; |
454 | |
455 | g_return_if_fail (GTK_IS_SLICE_LIST_MODEL (self)); |
456 | |
457 | if (self->size == size) |
458 | return; |
459 | |
460 | before = g_list_model_get_n_items (list: G_LIST_MODEL (ptr: self)); |
461 | |
462 | self->size = size; |
463 | |
464 | after = g_list_model_get_n_items (list: G_LIST_MODEL (ptr: self)); |
465 | |
466 | if (before > after) |
467 | g_list_model_items_changed (list: G_LIST_MODEL (ptr: self), position: after, removed: before - after, added: 0); |
468 | else if (before < after) |
469 | g_list_model_items_changed (list: G_LIST_MODEL (ptr: self), position: before, removed: 0, added: after - before); |
470 | /* else nothing */ |
471 | |
472 | g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_SIZE]); |
473 | } |
474 | |
475 | /** |
476 | * gtk_slice_list_model_get_size: (attributes org.gtk.Method.get_property=size) |
477 | * @self: a `GtkSliceListModel` |
478 | * |
479 | * Gets the size set via gtk_slice_list_model_set_size(). |
480 | * |
481 | * Returns: The size |
482 | */ |
483 | guint |
484 | gtk_slice_list_model_get_size (GtkSliceListModel *self) |
485 | { |
486 | g_return_val_if_fail (GTK_IS_SLICE_LIST_MODEL (self), DEFAULT_SIZE); |
487 | |
488 | return self->size; |
489 | } |
490 | |