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 | |
21 | /* |
22 | * GtkPropertyLookupListModel: |
23 | * |
24 | * `GtkPropertyLookupListModel` is a `GListModel` implementation that takes an |
25 | * object and a property and then recursively looks up the next element using |
26 | * the property on the previous object. |
27 | * |
28 | * For example, one could use this list model with the GtkWidget:parent property |
29 | * to get a list of a widgets and all its ancestors. |
30 | **/ |
31 | |
32 | #include "config.h" |
33 | |
34 | #include "gtkpropertylookuplistmodelprivate.h" |
35 | |
36 | #include "gtkintl.h" |
37 | #include "gtkprivate.h" |
38 | |
39 | enum { |
40 | PROP_0, |
41 | PROP_ITEM_TYPE, |
42 | PROP_OBJECT, |
43 | PROP_PROPERTY, |
44 | NUM_PROPERTIES |
45 | }; |
46 | |
47 | struct _GtkPropertyLookupListModel |
48 | { |
49 | GObject parent_instance; |
50 | |
51 | GType item_type; |
52 | char *property; |
53 | GPtrArray *items; /* list of items - lazily expanded if there's a NULL sentinel */ |
54 | }; |
55 | |
56 | struct _GtkPropertyLookupListModelClass |
57 | { |
58 | GObjectClass parent_class; |
59 | }; |
60 | |
61 | static GParamSpec *properties[NUM_PROPERTIES] = { NULL, }; |
62 | |
63 | static GType |
64 | gtk_property_lookup_list_model_get_item_type (GListModel *list) |
65 | { |
66 | GtkPropertyLookupListModel *self = GTK_PROPERTY_LOOKUP_LIST_MODEL (list); |
67 | |
68 | return self->item_type; |
69 | } |
70 | |
71 | static gboolean |
72 | gtk_property_lookup_list_model_is_infinite (GtkPropertyLookupListModel *self) |
73 | { |
74 | if (self->items->len == 0) |
75 | return FALSE; |
76 | |
77 | return g_ptr_array_index (self->items, self->items->len - 1) == NULL; |
78 | } |
79 | |
80 | static void |
81 | gtk_property_lookup_list_model_notify_cb (GObject *object, |
82 | GParamSpec *pspec, |
83 | GtkPropertyLookupListModel *self); |
84 | |
85 | static guint |
86 | gtk_property_lookup_list_model_clear (GtkPropertyLookupListModel *self, |
87 | guint remaining) |
88 | { |
89 | guint i; |
90 | |
91 | for (i = remaining; i < self->items->len; i++) |
92 | { |
93 | gpointer object = g_ptr_array_index (self->items, i); |
94 | if (object == NULL) |
95 | break; |
96 | |
97 | g_signal_handlers_disconnect_by_func (object, gtk_property_lookup_list_model_notify_cb, self); |
98 | g_object_unref (object); |
99 | } |
100 | |
101 | /* keeps the sentinel, yay! */ |
102 | g_ptr_array_remove_range (array: self->items, index_: remaining, length: i - remaining); |
103 | |
104 | return i - remaining; |
105 | } |
106 | |
107 | static guint |
108 | gtk_property_lookup_list_model_append (GtkPropertyLookupListModel *self, |
109 | guint n_items) |
110 | { |
111 | gpointer last, next; |
112 | guint i, start; |
113 | |
114 | g_assert (self->items->len > 0); |
115 | g_assert (!gtk_property_lookup_list_model_is_infinite (self)); |
116 | |
117 | last = g_ptr_array_index (self->items, self->items->len - 1); |
118 | start = self->items->len; |
119 | for (i = start; i < n_items; i++) |
120 | { |
121 | g_object_get (object: last, first_property_name: self->property, &next, NULL); |
122 | if (next == NULL) |
123 | return i - start; |
124 | |
125 | g_signal_connect_closure_by_id (instance: next, |
126 | signal_id: g_signal_lookup (name: "notify" , G_OBJECT_TYPE (next)), |
127 | detail: g_quark_from_static_string (string: self->property), |
128 | closure: g_cclosure_new (G_CALLBACK (gtk_property_lookup_list_model_notify_cb), G_OBJECT (self), NULL), |
129 | FALSE); |
130 | |
131 | g_ptr_array_add (array: self->items, data: next); |
132 | last = next; |
133 | } |
134 | |
135 | return i - start; |
136 | } |
137 | |
138 | static void |
139 | gtk_property_lookup_list_model_ensure (GtkPropertyLookupListModel *self, |
140 | guint n_items) |
141 | { |
142 | if (!gtk_property_lookup_list_model_is_infinite (self)) |
143 | return; |
144 | |
145 | if (self->items->len - 1 >= n_items) |
146 | return; |
147 | |
148 | /* remove NULL sentinel */ |
149 | g_ptr_array_remove_index (array: self->items, index_: self->items->len - 1); |
150 | |
151 | if (self->items->len == 0) |
152 | return; |
153 | |
154 | if (gtk_property_lookup_list_model_append (self, n_items) == n_items) |
155 | { |
156 | /* re-add NULL sentinel */ |
157 | g_ptr_array_add (array: self->items, NULL); |
158 | } |
159 | } |
160 | |
161 | static void |
162 | gtk_property_lookup_list_model_notify_cb (GObject *object, |
163 | GParamSpec *pspec, |
164 | GtkPropertyLookupListModel *self) |
165 | { |
166 | guint position, removed, added; |
167 | |
168 | if (!g_ptr_array_find (haystack: self->items, needle: object, index_: &position)) |
169 | { |
170 | /* can only happen if we forgot to disconnect a signal handler */ |
171 | g_assert_not_reached (); |
172 | } |
173 | /* we found the position of the item that notified, but the first change |
174 | * is its child. |
175 | */ |
176 | position++; |
177 | |
178 | removed = gtk_property_lookup_list_model_clear (self, remaining: position); |
179 | |
180 | if (!gtk_property_lookup_list_model_is_infinite (self)) |
181 | added = gtk_property_lookup_list_model_append (self, G_MAXUINT); |
182 | else |
183 | added = 0; |
184 | |
185 | if (removed > 0 || added > 0) |
186 | g_list_model_items_changed (list: G_LIST_MODEL (ptr: self), position, removed, added); |
187 | } |
188 | |
189 | static guint |
190 | gtk_property_lookup_list_model_get_n_items (GListModel *list) |
191 | { |
192 | GtkPropertyLookupListModel *self = GTK_PROPERTY_LOOKUP_LIST_MODEL (list); |
193 | |
194 | gtk_property_lookup_list_model_ensure (self, G_MAXUINT); |
195 | |
196 | return self->items->len; |
197 | } |
198 | |
199 | static gpointer |
200 | gtk_property_lookup_list_model_get_item (GListModel *list, |
201 | guint position) |
202 | { |
203 | GtkPropertyLookupListModel *self = GTK_PROPERTY_LOOKUP_LIST_MODEL (list); |
204 | |
205 | gtk_property_lookup_list_model_ensure (self, n_items: position + 1); |
206 | |
207 | if (position >= self->items->len) |
208 | return NULL; |
209 | |
210 | return g_object_ref (g_ptr_array_index (self->items, position)); |
211 | } |
212 | |
213 | static void |
214 | gtk_property_lookup_list_model_list_model_init (GListModelInterface *iface) |
215 | { |
216 | iface->get_item_type = gtk_property_lookup_list_model_get_item_type; |
217 | iface->get_n_items = gtk_property_lookup_list_model_get_n_items; |
218 | iface->get_item = gtk_property_lookup_list_model_get_item; |
219 | } |
220 | |
221 | G_DEFINE_TYPE_WITH_CODE (GtkPropertyLookupListModel, gtk_property_lookup_list_model, |
222 | G_TYPE_OBJECT, |
223 | G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, gtk_property_lookup_list_model_list_model_init)) |
224 | |
225 | static gboolean |
226 | check_pspec (GType type, |
227 | GParamSpec *pspec) |
228 | { |
229 | if (pspec == NULL) |
230 | return FALSE; |
231 | |
232 | if (!G_IS_PARAM_SPEC_OBJECT (pspec)) |
233 | return FALSE; |
234 | |
235 | if (!g_type_is_a (G_PARAM_SPEC (pspec)->value_type, is_a_type: type)) |
236 | return FALSE; |
237 | |
238 | return TRUE; |
239 | } |
240 | |
241 | static gboolean |
242 | lookup_pspec (GType type, |
243 | const char *name) |
244 | { |
245 | gboolean result; |
246 | |
247 | if (g_type_is_a (type, G_TYPE_INTERFACE)) |
248 | { |
249 | gpointer iface; |
250 | GParamSpec *pspec; |
251 | |
252 | iface = g_type_default_interface_ref (g_type: type); |
253 | pspec = g_object_interface_find_property (g_iface: iface, property_name: name); |
254 | result = check_pspec (type, pspec); |
255 | g_type_default_interface_unref (g_iface: iface); |
256 | } |
257 | else |
258 | { |
259 | GObjectClass *klass; |
260 | GParamSpec *pspec; |
261 | |
262 | klass = g_type_class_ref (type); |
263 | g_return_val_if_fail (klass != NULL, FALSE); |
264 | pspec = g_object_class_find_property (oclass: klass, property_name: name); |
265 | result = check_pspec (type, pspec); |
266 | g_type_class_unref (g_class: klass); |
267 | } |
268 | |
269 | return result; |
270 | } |
271 | |
272 | static void |
273 | gtk_property_lookup_list_model_set_property (GObject *object, |
274 | guint prop_id, |
275 | const GValue *value, |
276 | GParamSpec *pspec) |
277 | { |
278 | GtkPropertyLookupListModel *self = GTK_PROPERTY_LOOKUP_LIST_MODEL (object); |
279 | |
280 | switch (prop_id) |
281 | { |
282 | case PROP_ITEM_TYPE: |
283 | self->item_type = g_value_get_gtype (value); |
284 | g_return_if_fail (self->item_type != 0); |
285 | break; |
286 | |
287 | case PROP_OBJECT: |
288 | gtk_property_lookup_list_model_set_object (self, object: g_value_get_object (value)); |
289 | break; |
290 | |
291 | case PROP_PROPERTY: |
292 | self->property = g_value_dup_string (value); |
293 | break; |
294 | |
295 | default: |
296 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); |
297 | break; |
298 | } |
299 | |
300 | if (self->property && self->item_type && |
301 | !lookup_pspec (type: self->item_type, name: self->property)) |
302 | { |
303 | g_critical ("type %s has no property named \"%s\"" , g_type_name (self->item_type), self->property); |
304 | } |
305 | } |
306 | |
307 | static void |
308 | gtk_property_lookup_list_model_get_property (GObject *object, |
309 | guint prop_id, |
310 | GValue *value, |
311 | GParamSpec *pspec) |
312 | { |
313 | GtkPropertyLookupListModel *self = GTK_PROPERTY_LOOKUP_LIST_MODEL (object); |
314 | |
315 | switch (prop_id) |
316 | { |
317 | case PROP_ITEM_TYPE: |
318 | g_value_set_gtype (value, v_gtype: self->item_type); |
319 | break; |
320 | |
321 | case PROP_OBJECT: |
322 | g_value_set_object (value, v_object: gtk_property_lookup_list_model_get_object (self)); |
323 | break; |
324 | |
325 | case PROP_PROPERTY: |
326 | g_value_set_object (value, v_object: self->property); |
327 | break; |
328 | |
329 | default: |
330 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); |
331 | break; |
332 | } |
333 | } |
334 | |
335 | static void |
336 | gtk_property_lookup_list_model_dispose (GObject *object) |
337 | { |
338 | GtkPropertyLookupListModel *self = GTK_PROPERTY_LOOKUP_LIST_MODEL (object); |
339 | |
340 | gtk_property_lookup_list_model_clear (self, remaining: 0); |
341 | |
342 | G_OBJECT_CLASS (gtk_property_lookup_list_model_parent_class)->dispose (object); |
343 | } |
344 | |
345 | static void |
346 | gtk_property_lookup_list_model_finalize (GObject *object) |
347 | { |
348 | GtkPropertyLookupListModel *self = GTK_PROPERTY_LOOKUP_LIST_MODEL (object); |
349 | |
350 | g_ptr_array_unref (array: self->items); |
351 | g_free (mem: self->property); |
352 | |
353 | G_OBJECT_CLASS (gtk_property_lookup_list_model_parent_class)->finalize (object); |
354 | } |
355 | |
356 | static void |
357 | gtk_property_lookup_list_model_class_init (GtkPropertyLookupListModelClass *klass) |
358 | { |
359 | GObjectClass *object_class = G_OBJECT_CLASS (klass); |
360 | |
361 | object_class->get_property = gtk_property_lookup_list_model_get_property; |
362 | object_class->set_property = gtk_property_lookup_list_model_set_property; |
363 | object_class->dispose = gtk_property_lookup_list_model_dispose; |
364 | object_class->finalize = gtk_property_lookup_list_model_finalize; |
365 | |
366 | /** |
367 | * GtkPropertyLookupListModel:item-type: |
368 | * |
369 | * The `GType` for elements of this object |
370 | */ |
371 | properties[PROP_ITEM_TYPE] = |
372 | g_param_spec_gtype (name: "item-type" , |
373 | P_("Item type" ), |
374 | P_("The type of elements of this object" ), |
375 | G_TYPE_OBJECT, |
376 | GTK_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_EXPLICIT_NOTIFY); |
377 | |
378 | /** |
379 | * GtkPropertyLookupListModel:property: |
380 | * |
381 | * Name of the property used for lookups |
382 | */ |
383 | properties[PROP_PROPERTY] = |
384 | g_param_spec_string (name: "property" , |
385 | P_("type" ), |
386 | P_("Name of the property used for lookups" ), |
387 | NULL, |
388 | GTK_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_EXPLICIT_NOTIFY); |
389 | |
390 | /** |
391 | * GtkPropertyLookupListModel:object: |
392 | * |
393 | * The root object |
394 | */ |
395 | properties[PROP_OBJECT] = |
396 | g_param_spec_object (name: "object" , |
397 | P_("Object" ), |
398 | P_("The root object" ), |
399 | G_TYPE_LIST_MODEL, |
400 | GTK_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); |
401 | |
402 | g_object_class_install_properties (oclass: object_class, n_pspecs: NUM_PROPERTIES, pspecs: properties); |
403 | } |
404 | |
405 | static void |
406 | gtk_property_lookup_list_model_init (GtkPropertyLookupListModel *self) |
407 | { |
408 | self->items = g_ptr_array_new (); |
409 | /* add sentinel */ |
410 | g_ptr_array_add (array: self->items, NULL); |
411 | } |
412 | |
413 | GtkPropertyLookupListModel * |
414 | gtk_property_lookup_list_model_new (GType item_type, |
415 | const char *property_name) |
416 | { |
417 | g_return_val_if_fail (g_type_is_a (item_type, G_TYPE_OBJECT), NULL); |
418 | g_return_val_if_fail (property_name != NULL, NULL); |
419 | |
420 | return g_object_new (GTK_TYPE_PROPERTY_LOOKUP_LIST_MODEL, |
421 | first_property_name: "item-type" , item_type, |
422 | "property" , property_name, |
423 | NULL); |
424 | } |
425 | |
426 | void |
427 | gtk_property_lookup_list_model_set_object (GtkPropertyLookupListModel *self, |
428 | gpointer object) |
429 | { |
430 | guint removed, added; |
431 | |
432 | g_return_if_fail (GTK_IS_PROPERTY_LOOKUP_LIST_MODEL (self)); |
433 | |
434 | if (object) |
435 | { |
436 | if (self->items->len != 0 && |
437 | g_ptr_array_index (self->items, 0) == object) |
438 | return; |
439 | |
440 | removed = gtk_property_lookup_list_model_clear (self, remaining: 0); |
441 | |
442 | g_ptr_array_insert (array: self->items, index_: 0, g_object_ref (object)); |
443 | g_signal_connect_closure_by_id (instance: object, |
444 | signal_id: g_signal_lookup (name: "notify" , G_OBJECT_TYPE (object)), |
445 | detail: g_quark_from_static_string (string: self->property), |
446 | closure: g_cclosure_new (G_CALLBACK (gtk_property_lookup_list_model_notify_cb), G_OBJECT (self), NULL), |
447 | FALSE); |
448 | added = 1; |
449 | if (!gtk_property_lookup_list_model_is_infinite (self)) |
450 | added += gtk_property_lookup_list_model_append (self, G_MAXUINT); |
451 | } |
452 | else |
453 | { |
454 | if (self->items->len == 0 || |
455 | g_ptr_array_index (self->items, 0) == NULL) |
456 | return; |
457 | |
458 | removed = gtk_property_lookup_list_model_clear (self, remaining: 0); |
459 | added = 0; |
460 | } |
461 | |
462 | g_assert (removed != 0 || added != 0); |
463 | |
464 | g_list_model_items_changed (list: G_LIST_MODEL (ptr: self), position: 0, removed, added); |
465 | } |
466 | |
467 | gpointer |
468 | gtk_property_lookup_list_model_get_object (GtkPropertyLookupListModel *self) |
469 | { |
470 | g_return_val_if_fail (GTK_IS_PROPERTY_LOOKUP_LIST_MODEL (self), NULL); |
471 | |
472 | if (self->items->len == 0) |
473 | return NULL; |
474 | |
475 | return g_ptr_array_index (self->items, 0); |
476 | } |
477 | |
478 | |