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
39enum {
40 PROP_0,
41 PROP_MODEL,
42 PROP_OFFSET,
43 PROP_SIZE,
44 NUM_PROPERTIES
45};
46
47struct _GtkSliceListModel
48{
49 GObject parent_instance;
50
51 GListModel *model;
52 guint offset;
53 guint size;
54
55 guint n_items;
56};
57
58struct _GtkSliceListModelClass
59{
60 GObjectClass parent_class;
61};
62
63static GParamSpec *properties[NUM_PROPERTIES] = { NULL, };
64
65static GType
66gtk_slice_list_model_get_item_type (GListModel *list)
67{
68 return G_TYPE_OBJECT;
69}
70
71static guint
72gtk_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
90static gpointer
91gtk_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
105static void
106gtk_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
113G_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
116static void
117gtk_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
168static void
169gtk_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
196static void
197gtk_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
224static void
225gtk_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
234static void
235gtk_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
244static void
245gtk_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
292static void
293gtk_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 */
311GtkSliceListModel *
312gtk_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 */
341void
342gtk_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 */
381GListModel *
382gtk_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 */
399void
400gtk_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 */
430guint
431gtk_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 */
449void
450gtk_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 */
483guint
484gtk_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

source code of gtk/gtk/gtkslicelistmodel.c