1 | /* |
2 | * Copyright © 2020 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 "gtkstringlist.h" |
23 | |
24 | #include "gtkbuildable.h" |
25 | #include "gtkbuilderprivate.h" |
26 | #include "gtkintl.h" |
27 | #include "gtkprivate.h" |
28 | |
29 | /** |
30 | * GtkStringList: |
31 | * |
32 | * `GtkStringList` is a list model that wraps an array of strings. |
33 | * |
34 | * The objects in the model have a "string" property. |
35 | * |
36 | * `GtkStringList` is well-suited for any place where you would |
37 | * typically use a `char*[]`, but need a list model. |
38 | * |
39 | * # GtkStringList as GtkBuildable |
40 | * |
41 | * The `GtkStringList` implementation of the `GtkBuildable` interface |
42 | * supports adding items directly using the <items> element and |
43 | * specifying <item> elements for each item. Each <item> element |
44 | * supports the regular translation attributes “translatable”, |
45 | * “context” and “comments”. |
46 | * |
47 | * Here is a UI definition fragment specifying a `GtkStringList` |
48 | * |
49 | * ```xml |
50 | * <object class="GtkStringList"> |
51 | * <items> |
52 | * <item translatable="yes">Factory</item> |
53 | * <item translatable="yes">Home</item> |
54 | * <item translatable="yes">Subway</item> |
55 | * </items> |
56 | * </object> |
57 | * ``` |
58 | */ |
59 | |
60 | /** |
61 | * GtkStringObject: |
62 | * |
63 | * `GtkStringObject` is the type of items in a `GtkStringList`. |
64 | * |
65 | * A `GtkStringObject` is a wrapper around a `const char*`; it has |
66 | * a [property@Gtk.StringObject:string] property. |
67 | */ |
68 | |
69 | #define GDK_ARRAY_ELEMENT_TYPE GtkStringObject * |
70 | #define GDK_ARRAY_NAME objects |
71 | #define GDK_ARRAY_TYPE_NAME Objects |
72 | #define GDK_ARRAY_FREE_FUNC g_object_unref |
73 | #include "gdk/gdkarrayimpl.c" |
74 | |
75 | struct _GtkStringObject |
76 | { |
77 | GObject parent_instance; |
78 | char *string; |
79 | }; |
80 | |
81 | enum { |
82 | PROP_STRING = 1, |
83 | PROP_NUM_PROPERTIES |
84 | }; |
85 | |
86 | G_DEFINE_TYPE (GtkStringObject, gtk_string_object, G_TYPE_OBJECT); |
87 | |
88 | static void |
89 | gtk_string_object_init (GtkStringObject *object) |
90 | { |
91 | } |
92 | |
93 | static void |
94 | gtk_string_object_finalize (GObject *object) |
95 | { |
96 | GtkStringObject *self = GTK_STRING_OBJECT (ptr: object); |
97 | |
98 | g_free (mem: self->string); |
99 | |
100 | G_OBJECT_CLASS (gtk_string_object_parent_class)->finalize (object); |
101 | } |
102 | |
103 | static void |
104 | gtk_string_object_get_property (GObject *object, |
105 | guint property_id, |
106 | GValue *value, |
107 | GParamSpec *pspec) |
108 | { |
109 | GtkStringObject *self = GTK_STRING_OBJECT (ptr: object); |
110 | |
111 | switch (property_id) |
112 | { |
113 | case PROP_STRING: |
114 | g_value_set_string (value, v_string: self->string); |
115 | break; |
116 | |
117 | default: |
118 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); |
119 | break; |
120 | } |
121 | } |
122 | |
123 | static void |
124 | gtk_string_object_class_init (GtkStringObjectClass *class) |
125 | { |
126 | GObjectClass *object_class = G_OBJECT_CLASS (class); |
127 | GParamSpec *pspec; |
128 | |
129 | object_class->finalize = gtk_string_object_finalize; |
130 | object_class->get_property = gtk_string_object_get_property; |
131 | |
132 | /** |
133 | * GtkStringObject:string: (attributes org.gtk.Property.get=gtk_string_object_get_string) |
134 | * |
135 | * The string. |
136 | */ |
137 | pspec = g_param_spec_string (name: "string" , nick: "String" , blurb: "String" , |
138 | NULL, |
139 | flags: G_PARAM_READABLE | |
140 | G_PARAM_STATIC_STRINGS); |
141 | |
142 | g_object_class_install_property (oclass: object_class, property_id: PROP_STRING, pspec); |
143 | |
144 | } |
145 | |
146 | static GtkStringObject * |
147 | gtk_string_object_new_take (char *string) |
148 | { |
149 | GtkStringObject *obj; |
150 | |
151 | obj = g_object_new (GTK_TYPE_STRING_OBJECT, NULL); |
152 | obj->string = string; |
153 | |
154 | return obj; |
155 | } |
156 | |
157 | /** |
158 | * gtk_string_object_new: |
159 | * @string: (not nullable): The string to wrap |
160 | * |
161 | * Wraps a string in an object for use with `GListModel`. |
162 | * |
163 | * Returns: a new `GtkStringObject` |
164 | */ |
165 | GtkStringObject * |
166 | gtk_string_object_new (const char *string) |
167 | { |
168 | return gtk_string_object_new_take (string: g_strdup (str: string)); |
169 | } |
170 | |
171 | /** |
172 | * gtk_string_object_get_string: (attributes org.gtk.Method.get_property=string) |
173 | * @self: a `GtkStringObject` |
174 | * |
175 | * Returns the string contained in a `GtkStringObject`. |
176 | * |
177 | * Returns: the string of @self |
178 | */ |
179 | const char * |
180 | gtk_string_object_get_string (GtkStringObject *self) |
181 | { |
182 | g_return_val_if_fail (GTK_IS_STRING_OBJECT (self), NULL); |
183 | |
184 | return self->string; |
185 | } |
186 | |
187 | struct _GtkStringList |
188 | { |
189 | GObject parent_instance; |
190 | |
191 | Objects items; |
192 | }; |
193 | |
194 | struct _GtkStringListClass |
195 | { |
196 | GObjectClass parent_class; |
197 | }; |
198 | |
199 | static GType |
200 | gtk_string_list_get_item_type (GListModel *list) |
201 | { |
202 | return G_TYPE_OBJECT; |
203 | } |
204 | |
205 | static guint |
206 | gtk_string_list_get_n_items (GListModel *list) |
207 | { |
208 | GtkStringList *self = GTK_STRING_LIST (ptr: list); |
209 | |
210 | return objects_get_size (self: &self->items); |
211 | } |
212 | |
213 | static gpointer |
214 | gtk_string_list_get_item (GListModel *list, |
215 | guint position) |
216 | { |
217 | GtkStringList *self = GTK_STRING_LIST (ptr: list); |
218 | |
219 | if (position >= objects_get_size (self: &self->items)) |
220 | return NULL; |
221 | |
222 | return g_object_ref (objects_get (&self->items, position)); |
223 | } |
224 | |
225 | static void |
226 | gtk_string_list_model_init (GListModelInterface *iface) |
227 | { |
228 | iface->get_item_type = gtk_string_list_get_item_type; |
229 | iface->get_n_items = gtk_string_list_get_n_items; |
230 | iface->get_item = gtk_string_list_get_item; |
231 | } |
232 | |
233 | typedef struct |
234 | { |
235 | GtkBuilder *builder; |
236 | GtkStringList *list; |
237 | GString *string; |
238 | const char *domain; |
239 | char *context; |
240 | |
241 | guint translatable : 1; |
242 | guint is_text : 1; |
243 | } ItemParserData; |
244 | |
245 | static void |
246 | item_start_element (GtkBuildableParseContext *context, |
247 | const char *element_name, |
248 | const char **names, |
249 | const char **values, |
250 | gpointer user_data, |
251 | GError **error) |
252 | { |
253 | ItemParserData *data = (ItemParserData*)user_data; |
254 | |
255 | if (strcmp (s1: element_name, s2: "items" ) == 0) |
256 | { |
257 | if (!_gtk_builder_check_parent (builder: data->builder, context, parent_name: "object" , error)) |
258 | return; |
259 | |
260 | if (!g_markup_collect_attributes (element_name, attribute_names: names, attribute_values: values, error, |
261 | first_type: G_MARKUP_COLLECT_INVALID, NULL, NULL, |
262 | G_MARKUP_COLLECT_INVALID)) |
263 | _gtk_builder_prefix_error (builder: data->builder, context, error); |
264 | } |
265 | else if (strcmp (s1: element_name, s2: "item" ) == 0) |
266 | { |
267 | gboolean translatable = FALSE; |
268 | const char *msg_context = NULL; |
269 | |
270 | if (!_gtk_builder_check_parent (builder: data->builder, context, parent_name: "items" , error)) |
271 | return; |
272 | |
273 | if (!g_markup_collect_attributes (element_name, attribute_names: names, attribute_values: values, error, |
274 | first_type: G_MARKUP_COLLECT_BOOLEAN|G_MARKUP_COLLECT_OPTIONAL, first_attr: "translatable" , &translatable, |
275 | G_MARKUP_COLLECT_STRING|G_MARKUP_COLLECT_OPTIONAL, "comments" , NULL, |
276 | G_MARKUP_COLLECT_STRING|G_MARKUP_COLLECT_OPTIONAL, "context" , &msg_context, |
277 | G_MARKUP_COLLECT_INVALID)) |
278 | { |
279 | _gtk_builder_prefix_error (builder: data->builder, context, error); |
280 | return; |
281 | } |
282 | |
283 | data->is_text = TRUE; |
284 | data->translatable = translatable; |
285 | data->context = g_strdup (str: msg_context); |
286 | } |
287 | else |
288 | { |
289 | _gtk_builder_error_unhandled_tag (builder: data->builder, context, |
290 | object: "GtkStringList" , element_name, |
291 | error); |
292 | } |
293 | } |
294 | |
295 | static void |
296 | item_text (GtkBuildableParseContext *context, |
297 | const char *text, |
298 | gsize text_len, |
299 | gpointer user_data, |
300 | GError **error) |
301 | { |
302 | ItemParserData *data = (ItemParserData*)user_data; |
303 | |
304 | if (data->is_text) |
305 | g_string_append_len (string: data->string, val: text, len: text_len); |
306 | } |
307 | |
308 | static void |
309 | item_end_element (GtkBuildableParseContext *context, |
310 | const char *element_name, |
311 | gpointer user_data, |
312 | GError **error) |
313 | { |
314 | ItemParserData *data = (ItemParserData*)user_data; |
315 | |
316 | /* Append the translated strings */ |
317 | if (data->string->len) |
318 | { |
319 | if (data->translatable) |
320 | { |
321 | const char *translated; |
322 | |
323 | translated = _gtk_builder_parser_translate (domain: data->domain, |
324 | context: data->context, |
325 | text: data->string->str); |
326 | g_string_assign (string: data->string, rval: translated); |
327 | } |
328 | |
329 | gtk_string_list_append (self: data->list, string: data->string->str); |
330 | } |
331 | |
332 | data->translatable = FALSE; |
333 | g_string_set_size (string: data->string, len: 0); |
334 | g_clear_pointer (&data->context, g_free); |
335 | data->is_text = FALSE; |
336 | } |
337 | |
338 | static const GtkBuildableParser item_parser = |
339 | { |
340 | item_start_element, |
341 | item_end_element, |
342 | item_text |
343 | }; |
344 | |
345 | static gboolean |
346 | gtk_string_list_buildable_custom_tag_start (GtkBuildable *buildable, |
347 | GtkBuilder *builder, |
348 | GObject *child, |
349 | const char *tagname, |
350 | GtkBuildableParser *parser, |
351 | gpointer *parser_data) |
352 | { |
353 | if (strcmp (s1: tagname, s2: "items" ) == 0) |
354 | { |
355 | ItemParserData *data; |
356 | |
357 | data = g_slice_new0 (ItemParserData); |
358 | data->builder = g_object_ref (builder); |
359 | data->list = g_object_ref (GTK_STRING_LIST (buildable)); |
360 | data->domain = gtk_builder_get_translation_domain (builder); |
361 | data->string = g_string_new (init: "" ); |
362 | |
363 | *parser = item_parser; |
364 | *parser_data = data; |
365 | |
366 | return TRUE; |
367 | } |
368 | |
369 | return FALSE; |
370 | } |
371 | |
372 | static void |
373 | gtk_string_list_buildable_custom_finished (GtkBuildable *buildable, |
374 | GtkBuilder *builder, |
375 | GObject *child, |
376 | const char *tagname, |
377 | gpointer user_data) |
378 | { |
379 | if (strcmp (s1: tagname, s2: "items" ) == 0) |
380 | { |
381 | ItemParserData *data; |
382 | |
383 | data = (ItemParserData*)user_data; |
384 | g_object_unref (object: data->list); |
385 | g_object_unref (object: data->builder); |
386 | g_string_free (string: data->string, TRUE); |
387 | g_slice_free (ItemParserData, data); |
388 | } |
389 | } |
390 | |
391 | static void |
392 | gtk_string_list_buildable_init (GtkBuildableIface *iface) |
393 | { |
394 | iface->custom_tag_start = gtk_string_list_buildable_custom_tag_start; |
395 | iface->custom_finished = gtk_string_list_buildable_custom_finished; |
396 | } |
397 | |
398 | G_DEFINE_TYPE_WITH_CODE (GtkStringList, gtk_string_list, G_TYPE_OBJECT, |
399 | G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE, |
400 | gtk_string_list_buildable_init) |
401 | G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, |
402 | gtk_string_list_model_init)) |
403 | |
404 | static void |
405 | gtk_string_list_dispose (GObject *object) |
406 | { |
407 | GtkStringList *self = GTK_STRING_LIST (ptr: object); |
408 | |
409 | objects_clear (self: &self->items); |
410 | |
411 | G_OBJECT_CLASS (gtk_string_list_parent_class)->dispose (object); |
412 | } |
413 | |
414 | static void |
415 | gtk_string_list_class_init (GtkStringListClass *class) |
416 | { |
417 | GObjectClass *gobject_class = G_OBJECT_CLASS (class); |
418 | |
419 | gobject_class->dispose = gtk_string_list_dispose; |
420 | } |
421 | |
422 | static void |
423 | gtk_string_list_init (GtkStringList *self) |
424 | { |
425 | objects_init (self: &self->items); |
426 | } |
427 | |
428 | /** |
429 | * gtk_string_list_new: |
430 | * @strings: (array zero-terminated=1) (nullable): The strings to put in the model |
431 | * |
432 | * Creates a new `GtkStringList` with the given @strings. |
433 | * |
434 | * Returns: a new `GtkStringList` |
435 | */ |
436 | GtkStringList * |
437 | gtk_string_list_new (const char * const *strings) |
438 | { |
439 | GtkStringList *self; |
440 | |
441 | self = g_object_new (GTK_TYPE_STRING_LIST, NULL); |
442 | |
443 | gtk_string_list_splice (self, position: 0, n_removals: 0, additions: strings); |
444 | |
445 | return self; |
446 | } |
447 | |
448 | /** |
449 | * gtk_string_list_splice: |
450 | * @self: a `GtkStringList` |
451 | * @position: the position at which to make the change |
452 | * @n_removals: the number of strings to remove |
453 | * @additions: (array zero-terminated=1) (nullable): The strings to add |
454 | * |
455 | * Changes @self by removing @n_removals strings and adding @additions |
456 | * to it. |
457 | * |
458 | * This function is more efficient than [method@Gtk.StringList.append] |
459 | * and [method@Gtk.StringList.remove], because it only emits the |
460 | * ::items-changed signal once for the change. |
461 | * |
462 | * This function copies the strings in @additions. |
463 | * |
464 | * The parameters @position and @n_removals must be correct (ie: |
465 | * @position + @n_removals must be less than or equal to the length |
466 | * of the list at the time this function is called). |
467 | */ |
468 | void |
469 | gtk_string_list_splice (GtkStringList *self, |
470 | guint position, |
471 | guint n_removals, |
472 | const char * const *additions) |
473 | { |
474 | guint i, n_additions; |
475 | |
476 | g_return_if_fail (GTK_IS_STRING_LIST (self)); |
477 | g_return_if_fail (position + n_removals >= position); /* overflow */ |
478 | g_return_if_fail (position + n_removals <= objects_get_size (&self->items)); |
479 | |
480 | if (additions) |
481 | n_additions = g_strv_length (str_array: (char **) additions); |
482 | else |
483 | n_additions = 0; |
484 | |
485 | objects_splice (self: &self->items, pos: position, removed: n_removals, FALSE, NULL, added: n_additions); |
486 | |
487 | for (i = 0; i < n_additions; i++) |
488 | { |
489 | *objects_index (self: &self->items, pos: position + i) = gtk_string_object_new (string: additions[i]); |
490 | } |
491 | |
492 | if (n_removals || n_additions) |
493 | g_list_model_items_changed (list: G_LIST_MODEL (ptr: self), position, removed: n_removals, added: n_additions); |
494 | } |
495 | |
496 | /** |
497 | * gtk_string_list_append: |
498 | * @self: a `GtkStringList` |
499 | * @string: the string to insert |
500 | * |
501 | * Appends @string to @self. |
502 | * |
503 | * The @string will be copied. See |
504 | * [method@Gtk.StringList.take] for a way to avoid that. |
505 | */ |
506 | void |
507 | gtk_string_list_append (GtkStringList *self, |
508 | const char *string) |
509 | { |
510 | g_return_if_fail (GTK_IS_STRING_LIST (self)); |
511 | |
512 | objects_append (self: &self->items, value: gtk_string_object_new (string)); |
513 | |
514 | g_list_model_items_changed (list: G_LIST_MODEL (ptr: self), position: objects_get_size (self: &self->items) - 1, removed: 0, added: 1); |
515 | } |
516 | |
517 | /** |
518 | * gtk_string_list_take: |
519 | * @self: a `GtkStringList` |
520 | * @string: (transfer full): the string to insert |
521 | * |
522 | * Adds @string to self at the end, and takes |
523 | * ownership of it. |
524 | * |
525 | * This variant of [method@Gtk.StringList.append] |
526 | * is convenient for formatting strings: |
527 | * |
528 | * ```c |
529 | * gtk_string_list_take (self, g_strdup_print ("%d dollars", lots)); |
530 | * ``` |
531 | */ |
532 | void |
533 | gtk_string_list_take (GtkStringList *self, |
534 | char *string) |
535 | { |
536 | g_return_if_fail (GTK_IS_STRING_LIST (self)); |
537 | |
538 | objects_append (self: &self->items, value: gtk_string_object_new_take (string)); |
539 | |
540 | g_list_model_items_changed (list: G_LIST_MODEL (ptr: self), position: objects_get_size (self: &self->items) - 1, removed: 0, added: 1); |
541 | } |
542 | |
543 | /** |
544 | * gtk_string_list_remove: |
545 | * @self: a `GtkStringList` |
546 | * @position: the position of the string that is to be removed |
547 | * |
548 | * Removes the string at @position from @self. |
549 | * |
550 | * @position must be smaller than the current |
551 | * length of the list. |
552 | */ |
553 | void |
554 | gtk_string_list_remove (GtkStringList *self, |
555 | guint position) |
556 | { |
557 | g_return_if_fail (GTK_IS_STRING_LIST (self)); |
558 | |
559 | gtk_string_list_splice (self, position, n_removals: 1, NULL); |
560 | } |
561 | |
562 | /** |
563 | * gtk_string_list_get_string: |
564 | * @self: a `GtkStringList` |
565 | * @position: the position to get the string for |
566 | * |
567 | * Gets the string that is at @position in @self. |
568 | * |
569 | * If @self does not contain @position items, %NULL is returned. |
570 | * |
571 | * This function returns the const char *. To get the |
572 | * object wrapping it, use g_list_model_get_item(). |
573 | * |
574 | * Returns: (nullable): the string at the given position |
575 | */ |
576 | const char * |
577 | gtk_string_list_get_string (GtkStringList *self, |
578 | guint position) |
579 | { |
580 | g_return_val_if_fail (GTK_IS_STRING_LIST (self), NULL); |
581 | |
582 | if (position >= objects_get_size (self: &self->items)) |
583 | return NULL; |
584 | |
585 | return objects_get (self: &self->items, pos: position)->string; |
586 | } |
587 | |