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
gtk_string_object_init(GtkStringObject * object)89 gtk_string_object_init (GtkStringObject *object)
90 {
91 }
92 
93 static void
gtk_string_object_finalize(GObject * object)94 gtk_string_object_finalize (GObject *object)
95 {
96   GtkStringObject *self = GTK_STRING_OBJECT (object);
97 
98   g_free (self->string);
99 
100   G_OBJECT_CLASS (gtk_string_object_parent_class)->finalize (object);
101 }
102 
103 static void
gtk_string_object_get_property(GObject * object,guint property_id,GValue * value,GParamSpec * pspec)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 (object);
110 
111   switch (property_id)
112     {
113     case PROP_STRING:
114       g_value_set_string (value, 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
gtk_string_object_class_init(GtkStringObjectClass * class)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 ("string", "String", "String",
138                                NULL,
139                                G_PARAM_READABLE |
140                                G_PARAM_STATIC_STRINGS);
141 
142   g_object_class_install_property (object_class, PROP_STRING, pspec);
143 
144 }
145 
146 static GtkStringObject *
gtk_string_object_new_take(char * string)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 *
gtk_string_object_new(const char * string)166 gtk_string_object_new (const char *string)
167 {
168   return gtk_string_object_new_take (g_strdup (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 *
gtk_string_object_get_string(GtkStringObject * self)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
gtk_string_list_get_item_type(GListModel * list)200 gtk_string_list_get_item_type (GListModel *list)
201 {
202   return G_TYPE_OBJECT;
203 }
204 
205 static guint
gtk_string_list_get_n_items(GListModel * list)206 gtk_string_list_get_n_items (GListModel *list)
207 {
208   GtkStringList *self = GTK_STRING_LIST (list);
209 
210   return objects_get_size (&self->items);
211 }
212 
213 static gpointer
gtk_string_list_get_item(GListModel * list,guint position)214 gtk_string_list_get_item (GListModel *list,
215                           guint       position)
216 {
217   GtkStringList *self = GTK_STRING_LIST (list);
218 
219   if (position >= objects_get_size (&self->items))
220     return NULL;
221 
222   return g_object_ref (objects_get (&self->items, position));
223 }
224 
225 static void
gtk_string_list_model_init(GListModelInterface * iface)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
item_start_element(GtkBuildableParseContext * context,const char * element_name,const char ** names,const char ** values,gpointer user_data,GError ** error)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 (element_name, "items") == 0)
256     {
257       if (!_gtk_builder_check_parent (data->builder, context, "object", error))
258         return;
259 
260       if (!g_markup_collect_attributes (element_name, names, values, error,
261                                         G_MARKUP_COLLECT_INVALID, NULL, NULL,
262                                         G_MARKUP_COLLECT_INVALID))
263         _gtk_builder_prefix_error (data->builder, context, error);
264     }
265   else if (strcmp (element_name, "item") == 0)
266     {
267       gboolean translatable = FALSE;
268       const char *msg_context = NULL;
269 
270       if (!_gtk_builder_check_parent (data->builder, context, "items", error))
271         return;
272 
273       if (!g_markup_collect_attributes (element_name, names, values, error,
274                                         G_MARKUP_COLLECT_BOOLEAN|G_MARKUP_COLLECT_OPTIONAL, "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 (data->builder, context, error);
280           return;
281         }
282 
283       data->is_text = TRUE;
284       data->translatable = translatable;
285       data->context = g_strdup (msg_context);
286     }
287   else
288     {
289       _gtk_builder_error_unhandled_tag (data->builder, context,
290                                         "GtkStringList", element_name,
291                                         error);
292     }
293 }
294 
295 static void
item_text(GtkBuildableParseContext * context,const char * text,gsize text_len,gpointer user_data,GError ** error)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 (data->string, text, text_len);
306 }
307 
308 static void
item_end_element(GtkBuildableParseContext * context,const char * element_name,gpointer user_data,GError ** error)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 (data->domain,
324                                                       data->context,
325                                                       data->string->str);
326           g_string_assign (data->string, translated);
327         }
328 
329       gtk_string_list_append (data->list, data->string->str);
330     }
331 
332   data->translatable = FALSE;
333   g_string_set_size (data->string, 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
gtk_string_list_buildable_custom_tag_start(GtkBuildable * buildable,GtkBuilder * builder,GObject * child,const char * tagname,GtkBuildableParser * parser,gpointer * parser_data)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 (tagname, "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 ("");
362 
363       *parser = item_parser;
364       *parser_data = data;
365 
366       return TRUE;
367     }
368 
369   return FALSE;
370 }
371 
372 static void
gtk_string_list_buildable_custom_finished(GtkBuildable * buildable,GtkBuilder * builder,GObject * child,const char * tagname,gpointer user_data)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 (tagname, "items") == 0)
380     {
381       ItemParserData *data;
382 
383       data = (ItemParserData*)user_data;
384       g_object_unref (data->list);
385       g_object_unref (data->builder);
386       g_string_free (data->string, TRUE);
387       g_slice_free (ItemParserData, data);
388     }
389 }
390 
391 static void
gtk_string_list_buildable_init(GtkBuildableIface * iface)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 
G_DEFINE_TYPE_WITH_CODE(GtkStringList,gtk_string_list,G_TYPE_OBJECT,G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE,gtk_string_list_buildable_init)G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL,gtk_string_list_model_init))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 (object);
408 
409   objects_clear (&self->items);
410 
411   G_OBJECT_CLASS (gtk_string_list_parent_class)->dispose (object);
412 }
413 
414 static void
gtk_string_list_class_init(GtkStringListClass * class)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
gtk_string_list_init(GtkStringList * self)423 gtk_string_list_init (GtkStringList *self)
424 {
425   objects_init (&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 *
gtk_string_list_new(const char * const * strings)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, 0, 0, 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
gtk_string_list_splice(GtkStringList * self,guint position,guint n_removals,const char * const * additions)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 ((char **) additions);
482   else
483     n_additions = 0;
484 
485   objects_splice (&self->items, position, n_removals, FALSE, NULL, n_additions);
486 
487   for (i = 0; i < n_additions; i++)
488     {
489       *objects_index (&self->items, position + i) = gtk_string_object_new (additions[i]);
490     }
491 
492   if (n_removals || n_additions)
493     g_list_model_items_changed (G_LIST_MODEL (self), position, n_removals, 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
gtk_string_list_append(GtkStringList * self,const char * string)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->items, gtk_string_object_new (string));
513 
514   g_list_model_items_changed (G_LIST_MODEL (self), objects_get_size (&self->items) - 1, 0, 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
gtk_string_list_take(GtkStringList * self,char * string)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->items, gtk_string_object_new_take (string));
539 
540   g_list_model_items_changed (G_LIST_MODEL (self), objects_get_size (&self->items) - 1, 0, 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
gtk_string_list_remove(GtkStringList * self,guint position)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, 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 *
gtk_string_list_get_string(GtkStringList * self,guint position)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->items))
583     return NULL;
584 
585   return objects_get (&self->items, position)->string;
586 }
587