1 | /* GTK - The GIMP Toolkit |
2 | * Copyright (C) 1995-1997 Peter Mattis, Spencer Kimball and Josh MacDonald |
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 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 | |
18 | /* |
19 | * Modified by the GTK+ Team and others 1997-2000. See the AUTHORS |
20 | * file for a list of people on the GTK+ Team. See the ChangeLog |
21 | * files for a list of changes. These files are distributed with |
22 | * GTK+ at ftp://ftp.gtk.org/pub/gtk/. |
23 | */ |
24 | |
25 | #include "config.h" |
26 | |
27 | #include "gtktexttagtable.h" |
28 | #include "gtktexttagtableprivate.h" |
29 | |
30 | #include "gtkbuildable.h" |
31 | #include "gtktexttagprivate.h" |
32 | #include "gtkmarshalers.h" |
33 | #include "gtktextbufferprivate.h" /* just for the lame notify_will_remove_tag hack */ |
34 | #include "gtkintl.h" |
35 | |
36 | #include <stdlib.h> |
37 | |
38 | |
39 | /** |
40 | * GtkTextTagTable: |
41 | * |
42 | * The collection of tags in a `GtkTextBuffer` |
43 | * |
44 | * You may wish to begin by reading the |
45 | * [text widget conceptual overview](section-text-widget.html), |
46 | * which gives an overview of all the objects and data types |
47 | * related to the text widget and how they work together. |
48 | * |
49 | * # GtkTextTagTables as GtkBuildable |
50 | * |
51 | * The `GtkTextTagTable` implementation of the `GtkBuildable` interface |
52 | * supports adding tags by specifying “tag” as the “type” attribute |
53 | * of a <child> element. |
54 | * |
55 | * An example of a UI definition fragment specifying tags: |
56 | * ```xml |
57 | * <object class="GtkTextTagTable"> |
58 | * <child type="tag"> |
59 | * <object class="GtkTextTag"/> |
60 | * </child> |
61 | * </object> |
62 | * ``` |
63 | */ |
64 | |
65 | typedef struct _GtkTextTagTablePrivate GtkTextTagTablePrivate; |
66 | typedef struct _GtkTextTagTableClass GtkTextTagTableClass; |
67 | |
68 | struct _GtkTextTagTable |
69 | { |
70 | GObject parent_instance; |
71 | |
72 | GtkTextTagTablePrivate *priv; |
73 | }; |
74 | |
75 | struct _GtkTextTagTableClass |
76 | { |
77 | GObjectClass parent_class; |
78 | |
79 | void (* tag_changed) (GtkTextTagTable *table, GtkTextTag *tag, gboolean size_changed); |
80 | void (* tag_added) (GtkTextTagTable *table, GtkTextTag *tag); |
81 | void (* tag_removed) (GtkTextTagTable *table, GtkTextTag *tag); |
82 | }; |
83 | |
84 | struct _GtkTextTagTablePrivate |
85 | { |
86 | GHashTable *hash; |
87 | GSList *anonymous; |
88 | GSList *buffers; |
89 | |
90 | int anon_count; |
91 | |
92 | guint seen_invisible : 1; |
93 | }; |
94 | |
95 | enum { |
96 | TAG_CHANGED, |
97 | TAG_ADDED, |
98 | TAG_REMOVED, |
99 | LAST_SIGNAL |
100 | }; |
101 | |
102 | static void gtk_text_tag_table_finalize (GObject *object); |
103 | |
104 | static void gtk_text_tag_table_buildable_interface_init (GtkBuildableIface *iface); |
105 | static void gtk_text_tag_table_buildable_add_child (GtkBuildable *buildable, |
106 | GtkBuilder *builder, |
107 | GObject *child, |
108 | const char *type); |
109 | |
110 | static guint signals[LAST_SIGNAL] = { 0 }; |
111 | |
112 | G_DEFINE_TYPE_WITH_CODE (GtkTextTagTable, gtk_text_tag_table, G_TYPE_OBJECT, |
113 | G_ADD_PRIVATE (GtkTextTagTable) |
114 | G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE, |
115 | gtk_text_tag_table_buildable_interface_init)) |
116 | |
117 | static void |
118 | gtk_text_tag_table_class_init (GtkTextTagTableClass *klass) |
119 | { |
120 | GObjectClass *object_class = G_OBJECT_CLASS (klass); |
121 | |
122 | object_class->finalize = gtk_text_tag_table_finalize; |
123 | |
124 | /** |
125 | * GtkTextTagTable::tag-changed: |
126 | * @texttagtable: the object which received the signal. |
127 | * @tag: the changed tag. |
128 | * @size_changed: whether the change affects the `GtkTextView` layout. |
129 | * |
130 | * Emitted every time a tag in the `GtkTextTagTable` changes. |
131 | */ |
132 | signals[TAG_CHANGED] = |
133 | g_signal_new (I_("tag-changed" ), |
134 | G_OBJECT_CLASS_TYPE (object_class), |
135 | signal_flags: G_SIGNAL_RUN_LAST, |
136 | G_STRUCT_OFFSET (GtkTextTagTableClass, tag_changed), |
137 | NULL, NULL, |
138 | c_marshaller: _gtk_marshal_VOID__OBJECT_BOOLEAN, |
139 | G_TYPE_NONE, |
140 | n_params: 2, |
141 | GTK_TYPE_TEXT_TAG, |
142 | G_TYPE_BOOLEAN); |
143 | g_signal_set_va_marshaller (signal_id: signals[TAG_CHANGED], |
144 | G_OBJECT_CLASS_TYPE (object_class), |
145 | va_marshaller: _gtk_marshal_VOID__OBJECT_BOOLEANv); |
146 | |
147 | /** |
148 | * GtkTextTagTable::tag-added: |
149 | * @texttagtable: the object which received the signal. |
150 | * @tag: the added tag. |
151 | * |
152 | * Emitted every time a new tag is added in the `GtkTextTagTable`. |
153 | */ |
154 | signals[TAG_ADDED] = |
155 | g_signal_new (I_("tag-added" ), |
156 | G_OBJECT_CLASS_TYPE (object_class), |
157 | signal_flags: G_SIGNAL_RUN_LAST, |
158 | G_STRUCT_OFFSET (GtkTextTagTableClass, tag_added), |
159 | NULL, NULL, |
160 | NULL, |
161 | G_TYPE_NONE, |
162 | n_params: 1, |
163 | GTK_TYPE_TEXT_TAG); |
164 | |
165 | /** |
166 | * GtkTextTagTable::tag-removed: |
167 | * @texttagtable: the object which received the signal. |
168 | * @tag: the removed tag. |
169 | * |
170 | * Emitted every time a tag is removed from the `GtkTextTagTable`. |
171 | * |
172 | * The @tag is still valid by the time the signal is emitted, but |
173 | * it is not associated with a tag table any more. |
174 | */ |
175 | signals[TAG_REMOVED] = |
176 | g_signal_new (I_("tag-removed" ), |
177 | G_OBJECT_CLASS_TYPE (object_class), |
178 | signal_flags: G_SIGNAL_RUN_LAST, |
179 | G_STRUCT_OFFSET (GtkTextTagTableClass, tag_removed), |
180 | NULL, NULL, |
181 | NULL, |
182 | G_TYPE_NONE, |
183 | n_params: 1, |
184 | GTK_TYPE_TEXT_TAG); |
185 | } |
186 | |
187 | static void |
188 | gtk_text_tag_table_init (GtkTextTagTable *table) |
189 | { |
190 | table->priv = gtk_text_tag_table_get_instance_private (self: table); |
191 | table->priv->hash = g_hash_table_new (hash_func: g_str_hash, key_equal_func: g_str_equal); |
192 | } |
193 | |
194 | static void |
195 | check_visible (GtkTextTagTable *table, |
196 | GtkTextTag *tag) |
197 | { |
198 | if (table->priv->seen_invisible) |
199 | return; |
200 | |
201 | if (tag->priv->invisible_set) |
202 | { |
203 | gboolean invisible; |
204 | |
205 | g_object_get (object: tag, first_property_name: "invisible" , &invisible, NULL); |
206 | table->priv->seen_invisible = invisible; |
207 | } |
208 | } |
209 | |
210 | /** |
211 | * gtk_text_tag_table_new: |
212 | * |
213 | * Creates a new `GtkTextTagTable`. |
214 | * |
215 | * The table contains no tags by default. |
216 | * |
217 | * Returns: a new `GtkTextTagTable` |
218 | */ |
219 | GtkTextTagTable* |
220 | gtk_text_tag_table_new (void) |
221 | { |
222 | GtkTextTagTable *table; |
223 | |
224 | table = g_object_new (GTK_TYPE_TEXT_TAG_TABLE, NULL); |
225 | |
226 | return table; |
227 | } |
228 | |
229 | static void |
230 | foreach_unref (GtkTextTag *tag, gpointer data) |
231 | { |
232 | GtkTextTagTable *table = GTK_TEXT_TAG_TABLE (tag->priv->table); |
233 | GtkTextTagTablePrivate *priv = table->priv; |
234 | GSList *l; |
235 | |
236 | /* We don't want to emit the remove signal here; so we just unparent |
237 | * and unref the tag. |
238 | */ |
239 | |
240 | for (l = priv->buffers; l != NULL; l = l->next) |
241 | _gtk_text_buffer_notify_will_remove_tag (GTK_TEXT_BUFFER (l->data), |
242 | tag); |
243 | |
244 | tag->priv->table = NULL; |
245 | g_object_unref (object: tag); |
246 | } |
247 | |
248 | static void |
249 | gtk_text_tag_table_finalize (GObject *object) |
250 | { |
251 | GtkTextTagTable *table = GTK_TEXT_TAG_TABLE (object); |
252 | GtkTextTagTablePrivate *priv = table->priv; |
253 | |
254 | gtk_text_tag_table_foreach (table, func: foreach_unref, NULL); |
255 | |
256 | g_hash_table_destroy (hash_table: priv->hash); |
257 | g_slist_free (list: priv->anonymous); |
258 | g_slist_free (list: priv->buffers); |
259 | |
260 | G_OBJECT_CLASS (gtk_text_tag_table_parent_class)->finalize (object); |
261 | } |
262 | |
263 | static void |
264 | gtk_text_tag_table_buildable_interface_init (GtkBuildableIface *iface) |
265 | { |
266 | iface->add_child = gtk_text_tag_table_buildable_add_child; |
267 | } |
268 | |
269 | static void |
270 | gtk_text_tag_table_buildable_add_child (GtkBuildable *buildable, |
271 | GtkBuilder *builder, |
272 | GObject *child, |
273 | const char *type) |
274 | { |
275 | if (type && strcmp (s1: type, s2: "tag" ) == 0) |
276 | gtk_text_tag_table_add (GTK_TEXT_TAG_TABLE (buildable), |
277 | GTK_TEXT_TAG (child)); |
278 | } |
279 | |
280 | /** |
281 | * gtk_text_tag_table_add: |
282 | * @table: a `GtkTextTagTable` |
283 | * @tag: a `GtkTextTag` |
284 | * |
285 | * Add a tag to the table. |
286 | * |
287 | * The tag is assigned the highest priority in the table. |
288 | * |
289 | * @tag must not be in a tag table already, and may not have |
290 | * the same name as an already-added tag. |
291 | * |
292 | * Returns: %TRUE on success. |
293 | */ |
294 | gboolean |
295 | gtk_text_tag_table_add (GtkTextTagTable *table, |
296 | GtkTextTag *tag) |
297 | { |
298 | GtkTextTagTablePrivate *priv; |
299 | guint size; |
300 | |
301 | g_return_val_if_fail (GTK_IS_TEXT_TAG_TABLE (table), FALSE); |
302 | g_return_val_if_fail (GTK_IS_TEXT_TAG (tag), FALSE); |
303 | g_return_val_if_fail (tag->priv->table == NULL, FALSE); |
304 | |
305 | priv = table->priv; |
306 | |
307 | if (tag->priv->name && g_hash_table_lookup (hash_table: priv->hash, key: tag->priv->name)) |
308 | { |
309 | g_warning ("A tag named '%s' is already in the tag table." , |
310 | tag->priv->name); |
311 | return FALSE; |
312 | } |
313 | |
314 | g_object_ref (tag); |
315 | |
316 | if (tag->priv->name) |
317 | g_hash_table_insert (hash_table: priv->hash, key: tag->priv->name, value: tag); |
318 | else |
319 | { |
320 | priv->anonymous = g_slist_prepend (list: priv->anonymous, data: tag); |
321 | priv->anon_count++; |
322 | } |
323 | |
324 | tag->priv->table = table; |
325 | |
326 | /* We get the highest tag priority, as the most-recently-added |
327 | tag. Note that we do NOT use gtk_text_tag_set_priority, |
328 | as it assumes the tag is already in the table. */ |
329 | size = gtk_text_tag_table_get_size (table); |
330 | g_assert (size > 0); |
331 | tag->priv->priority = size - 1; |
332 | |
333 | check_visible (table, tag); |
334 | |
335 | g_signal_emit (instance: table, signal_id: signals[TAG_ADDED], detail: 0, tag); |
336 | return TRUE; |
337 | } |
338 | |
339 | /** |
340 | * gtk_text_tag_table_lookup: |
341 | * @table: a `GtkTextTagTable` |
342 | * @name: name of a tag |
343 | * |
344 | * Look up a named tag. |
345 | * |
346 | * Returns: (nullable) (transfer none): The tag |
347 | */ |
348 | GtkTextTag* |
349 | gtk_text_tag_table_lookup (GtkTextTagTable *table, |
350 | const char *name) |
351 | { |
352 | GtkTextTagTablePrivate *priv; |
353 | |
354 | g_return_val_if_fail (GTK_IS_TEXT_TAG_TABLE (table), NULL); |
355 | g_return_val_if_fail (name != NULL, NULL); |
356 | |
357 | priv = table->priv; |
358 | |
359 | return g_hash_table_lookup (hash_table: priv->hash, key: name); |
360 | } |
361 | |
362 | /** |
363 | * gtk_text_tag_table_remove: |
364 | * @table: a `GtkTextTagTable` |
365 | * @tag: a `GtkTextTag` |
366 | * |
367 | * Remove a tag from the table. |
368 | * |
369 | * If a `GtkTextBuffer` has @table as its tag table, the tag is |
370 | * removed from the buffer. The table’s reference to the tag is |
371 | * removed, so the tag will end up destroyed if you don’t have |
372 | * a reference to it. |
373 | */ |
374 | void |
375 | gtk_text_tag_table_remove (GtkTextTagTable *table, |
376 | GtkTextTag *tag) |
377 | { |
378 | GtkTextTagTablePrivate *priv; |
379 | GSList *l; |
380 | |
381 | g_return_if_fail (GTK_IS_TEXT_TAG_TABLE (table)); |
382 | g_return_if_fail (GTK_IS_TEXT_TAG (tag)); |
383 | g_return_if_fail (tag->priv->table == table); |
384 | |
385 | priv = table->priv; |
386 | |
387 | /* Our little bad hack to be sure buffers don't still have the tag |
388 | * applied to text in the buffer |
389 | */ |
390 | for (l = priv->buffers; l != NULL; l = l->next) |
391 | _gtk_text_buffer_notify_will_remove_tag (GTK_TEXT_BUFFER (l->data), |
392 | tag); |
393 | |
394 | /* Set ourselves to the highest priority; this means |
395 | when we're removed, there won't be any gaps in the |
396 | priorities of the tags in the table. */ |
397 | gtk_text_tag_set_priority (tag, priority: gtk_text_tag_table_get_size (table) - 1); |
398 | |
399 | tag->priv->table = NULL; |
400 | |
401 | if (tag->priv->name) |
402 | g_hash_table_remove (hash_table: priv->hash, key: tag->priv->name); |
403 | else |
404 | { |
405 | priv->anonymous = g_slist_remove (list: priv->anonymous, data: tag); |
406 | priv->anon_count--; |
407 | } |
408 | |
409 | g_signal_emit (instance: table, signal_id: signals[TAG_REMOVED], detail: 0, tag); |
410 | |
411 | g_object_unref (object: tag); |
412 | } |
413 | |
414 | struct ForeachData |
415 | { |
416 | GtkTextTagTableForeach func; |
417 | gpointer data; |
418 | }; |
419 | |
420 | static void |
421 | hash_foreach (gpointer key, gpointer value, gpointer data) |
422 | { |
423 | struct ForeachData *fd = data; |
424 | |
425 | g_return_if_fail (GTK_IS_TEXT_TAG (value)); |
426 | |
427 | (* fd->func) (value, fd->data); |
428 | } |
429 | |
430 | static void |
431 | list_foreach (gpointer data, gpointer user_data) |
432 | { |
433 | struct ForeachData *fd = user_data; |
434 | |
435 | g_return_if_fail (GTK_IS_TEXT_TAG (data)); |
436 | |
437 | (* fd->func) (data, fd->data); |
438 | } |
439 | |
440 | /** |
441 | * gtk_text_tag_table_foreach: |
442 | * @table: a `GtkTextTagTable` |
443 | * @func: (scope call): a function to call on each tag |
444 | * @data: user data |
445 | * |
446 | * Calls @func on each tag in @table, with user data @data. |
447 | * |
448 | * Note that the table may not be modified while iterating |
449 | * over it (you can’t add/remove tags). |
450 | */ |
451 | void |
452 | gtk_text_tag_table_foreach (GtkTextTagTable *table, |
453 | GtkTextTagTableForeach func, |
454 | gpointer data) |
455 | { |
456 | GtkTextTagTablePrivate *priv; |
457 | struct ForeachData d; |
458 | |
459 | g_return_if_fail (GTK_IS_TEXT_TAG_TABLE (table)); |
460 | g_return_if_fail (func != NULL); |
461 | |
462 | priv = table->priv; |
463 | |
464 | d.func = func; |
465 | d.data = data; |
466 | |
467 | g_hash_table_foreach (hash_table: priv->hash, func: hash_foreach, user_data: &d); |
468 | g_slist_foreach (list: priv->anonymous, func: list_foreach, user_data: &d); |
469 | } |
470 | |
471 | /** |
472 | * gtk_text_tag_table_get_size: |
473 | * @table: a `GtkTextTagTable` |
474 | * |
475 | * Returns the size of the table (number of tags) |
476 | * |
477 | * Returns: number of tags in @table |
478 | */ |
479 | int |
480 | gtk_text_tag_table_get_size (GtkTextTagTable *table) |
481 | { |
482 | GtkTextTagTablePrivate *priv; |
483 | |
484 | g_return_val_if_fail (GTK_IS_TEXT_TAG_TABLE (table), 0); |
485 | |
486 | priv = table->priv; |
487 | |
488 | return g_hash_table_size (hash_table: priv->hash) + priv->anon_count; |
489 | } |
490 | |
491 | void |
492 | _gtk_text_tag_table_add_buffer (GtkTextTagTable *table, |
493 | gpointer buffer) |
494 | { |
495 | GtkTextTagTablePrivate *priv = table->priv; |
496 | |
497 | priv->buffers = g_slist_prepend (list: priv->buffers, data: buffer); |
498 | } |
499 | |
500 | static void |
501 | foreach_remove_tag (GtkTextTag *tag, gpointer data) |
502 | { |
503 | GtkTextBuffer *buffer; |
504 | |
505 | buffer = GTK_TEXT_BUFFER (data); |
506 | |
507 | _gtk_text_buffer_notify_will_remove_tag (buffer, tag); |
508 | } |
509 | |
510 | void |
511 | _gtk_text_tag_table_remove_buffer (GtkTextTagTable *table, |
512 | gpointer buffer) |
513 | { |
514 | GtkTextTagTablePrivate *priv = table->priv; |
515 | |
516 | gtk_text_tag_table_foreach (table, func: foreach_remove_tag, data: buffer); |
517 | |
518 | priv->buffers = g_slist_remove (list: priv->buffers, data: buffer); |
519 | } |
520 | |
521 | void |
522 | _gtk_text_tag_table_tag_changed (GtkTextTagTable *table, |
523 | GtkTextTag *tag, |
524 | gboolean size_changed) |
525 | { |
526 | check_visible (table, tag); |
527 | g_signal_emit (instance: table, signal_id: signals[TAG_CHANGED], detail: 0, tag, size_changed); |
528 | } |
529 | |
530 | gboolean |
531 | _gtk_text_tag_table_affects_visibility (GtkTextTagTable *table) |
532 | { |
533 | return table->priv->seen_invisible; |
534 | } |
535 | |