1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
2 /*
3  * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com)
4  *
5  * This library is free software: you can redistribute it and/or modify it
6  * under the terms of the GNU Lesser General Public License as published by
7  * the Free Software Foundation.
8  *
9  * This library is distributed in the hope that it will be useful, but
10  * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
11  * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
12  * for more details.
13  *
14  * You should have received a copy of the GNU Lesser General Public License
15  * along with this library. If not, see <http://www.gnu.org/licenses/>.
16  *
17  */
18 
19 #include "evolution-data-server-config.h"
20 
21 #include <errno.h>
22 #include <string.h>
23 #include <libxml/parser.h>
24 #include <glib/gstdio.h>
25 #include <glib/gi18n-lib.h>
26 #include "e-data-server-util.h"
27 #include "e-categories.h"
28 
29 #include "libedataserver-private.h"
30 
31 #define d(x)
32 
33 typedef struct {
34 	gchar *display_name;  /* localized category name */
35 	gchar *clocale_name;  /* only for default categories */
36 	gchar *icon_file;
37 	gboolean is_default;
38 	gboolean is_searchable;
39 } CategoryInfo;
40 
41 typedef struct {
42 	const gchar *category;
43 	const gchar *icon_file;
44 } DefaultCategory;
45 
46 static DefaultCategory default_categories[] = {
47 	{ NC_("CategoryName", "Anniversary") },
48 	{ NC_("CategoryName", "Birthday"), "category_birthday_16.png" },
49 	{ NC_("CategoryName", "Business"), "category_business_16.png" },
50 	{ NC_("CategoryName", "Competition") },
51 	{ NC_("CategoryName", "Favorites"), "category_favorites_16.png" },
52 	{ NC_("CategoryName", "Gifts"), "category_gifts_16.png" },
53 	{ NC_("CategoryName", "Goals/Objectives"), "category_goals_16.png" },
54 	{ NC_("CategoryName", "Holiday"), "category_holiday_16.png" },
55 	{ NC_("CategoryName", "Holiday Cards"), "category_holiday-cards_16.png" },
56 	/* important people (e.g. new business partners) */
57 	{ NC_("CategoryName", "Hot Contacts"), "category_hot-contacts_16.png" },
58 	{ NC_("CategoryName", "Ideas"), "category_ideas_16.png" },
59 	{ NC_("CategoryName", "International"), "category_international_16.png" },
60 	{ NC_("CategoryName", "Key Customer"), "category_key-customer_16.png" },
61 	{ NC_("CategoryName", "Miscellaneous"), "category_miscellaneous_16.png" },
62 	{ NC_("CategoryName", "Personal"), "category_personal_16.png" },
63 	{ NC_("CategoryName", "Phone Calls"), "category_phonecalls_16.png" },
64 	/* Translators: "Status" is a category name; it can mean anything user wants to */
65 	{ NC_("CategoryName", "Status"), "category_status_16.png" },
66 	{ NC_("CategoryName", "Strategies"), "category_strategies_16.png" },
67 	{ NC_("CategoryName", "Suppliers"), "category_suppliers_16.png" },
68 	{ NC_("CategoryName", "Time & Expenses"), "category_time-and-expenses_16.png" },
69 	{ NC_("CategoryName", "VIP") },
70 	{ NC_("CategoryName", "Waiting") },
71 	{ NULL }
72 };
73 
74 /* ------------------------------------------------------------------------- */
75 
76 typedef struct {
77 	GObject object;
78 } EChangedListener;
79 
80 typedef struct {
81 	GObjectClass parent_class;
82 
83 	void (* changed) (void);
84 } EChangedListenerClass;
85 
86 static GType e_changed_listener_get_type (void);
87 
88 G_DEFINE_TYPE (EChangedListener, e_changed_listener, G_TYPE_OBJECT)
89 
90 enum {
91 	CHANGED,
92 	LAST_SIGNAL
93 };
94 
95 static guint changed_listener_signals[LAST_SIGNAL];
96 
97 static void
e_changed_listener_class_init(EChangedListenerClass * class)98 e_changed_listener_class_init (EChangedListenerClass *class)
99 {
100 	changed_listener_signals[CHANGED] = g_signal_new (
101 		"changed",
102 		G_TYPE_FROM_CLASS (class),
103 		G_SIGNAL_RUN_FIRST,
104 		G_STRUCT_OFFSET (EChangedListenerClass, changed),
105 		NULL, NULL, NULL,
106 		G_TYPE_NONE, 0);
107 }
108 
109 static void
e_changed_listener_init(EChangedListener * listener)110 e_changed_listener_init (EChangedListener *listener)
111 {
112 }
113 
114 /* ------------------------------------------------------------------------- */
115 
116 /* All the static variables below are protected by a global categories lock. */
117 G_LOCK_DEFINE_STATIC (categories);
118 
119 static gboolean initialized = FALSE;
120 static GHashTable *categories_table = NULL;
121 static gboolean save_is_pending = FALSE;
122 static guint idle_id = 0;
123 static EChangedListener *listeners = NULL;
124 static gboolean changed = FALSE;
125 
126 static gchar *
build_categories_filename(void)127 build_categories_filename (void)
128 {
129 	const gchar *user_data_dir;
130 	gchar *filename;
131 
132 	user_data_dir = e_get_user_data_dir ();
133 	filename = g_build_filename (user_data_dir, "categories.xml", NULL);
134 
135 	if (!g_file_test (filename, G_FILE_TEST_IS_REGULAR)) {
136 		gchar *old_filename;
137 
138 		/* Try moving the file from its old 2.x location.
139 		 * This is best effort; don't worry about errors. */
140 		old_filename = g_build_filename (
141 			g_get_home_dir (), ".evolution",
142 			"categories.xml", NULL);
143 		if (g_rename (old_filename, filename) == -1 && errno != ENOENT) {
144 			g_warning ("%s: Failed to rename '%s' to '%s': %s", G_STRFUNC, old_filename, filename, g_strerror (errno));
145 		}
146 		g_free (old_filename);
147 	}
148 
149 	return filename;
150 }
151 
152 static void
free_category_info(CategoryInfo * cat_info)153 free_category_info (CategoryInfo *cat_info)
154 {
155 	g_free (cat_info->display_name);
156 	g_free (cat_info->clocale_name);
157 	g_free (cat_info->icon_file);
158 
159 	g_slice_free (CategoryInfo, cat_info);
160 }
161 
162 static gboolean
category_info_equal(const CategoryInfo * cat_info1,const CategoryInfo * cat_info2)163 category_info_equal (const CategoryInfo *cat_info1,
164 		     const CategoryInfo *cat_info2)
165 {
166 	if (!cat_info1 || !cat_info2 || cat_info1 == cat_info2)
167 		return cat_info1 == cat_info2;
168 
169 	return g_strcmp0 (cat_info1->display_name, cat_info2->display_name) == 0 &&
170 		g_strcmp0 (cat_info1->clocale_name, cat_info2->clocale_name) == 0 &&
171 		g_strcmp0 (cat_info1->icon_file, cat_info2->icon_file) == 0 &&
172 		(cat_info1->is_default ? 1 : 0) == (cat_info2->is_default ? 1 : 0) &&
173 		(cat_info1->is_searchable ? 1 : 0) == (cat_info2->is_searchable ? 1 : 0);
174 }
175 
176 static gchar *
escape_string(const gchar * source)177 escape_string (const gchar *source)
178 {
179 	GString *buffer;
180 
181 	buffer = g_string_sized_new (strlen (source));
182 
183 	while (*source) {
184 		switch (*source) {
185 		case '<':
186 			g_string_append_len (buffer, "&lt;", 4);
187 			break;
188 		case '>':
189 			g_string_append_len (buffer, "&gt;", 4);
190 			break;
191 		case '&':
192 			g_string_append_len (buffer, "&amp;", 5);
193 			break;
194 		case '"':
195 			g_string_append_len (buffer, "&quot;", 6);
196 			break;
197 		default:
198 			g_string_append_c (buffer, *source);
199 			break;
200 		}
201 		source++;
202 	}
203 
204 	return g_string_free (buffer, FALSE);
205 }
206 
207 /* This must be called with the @categories lock held. */
208 static void
hash_to_xml_string(gpointer key,gpointer value,gpointer user_data)209 hash_to_xml_string (gpointer key,
210                     gpointer value,
211                     gpointer user_data)
212 {
213 	CategoryInfo *cat_info = value;
214 	GString *string = user_data;
215 	gchar *category;
216 
217 	g_string_append_len (string, "  <category", 11);
218 
219 	if (cat_info->is_default && cat_info->clocale_name && *cat_info->clocale_name)
220 		category = escape_string (cat_info->clocale_name);
221 	else
222 		category = escape_string (cat_info->display_name);
223 	g_string_append_printf (string, " a=\"%s\"", category);
224 	g_free (category);
225 
226 	if (cat_info->icon_file != NULL)
227 		g_string_append_printf (
228 			string, " icon=\"%s\"", cat_info->icon_file);
229 
230 	g_string_append_printf (
231 		string, " default=\"%d\"", cat_info->is_default ? 1 : 0);
232 
233 	g_string_append_printf (
234 		string, " searchable=\"%d\"", cat_info->is_searchable ? 1 : 0);
235 
236 	g_string_append_len (string, "/>\n", 3);
237 }
238 
239 /* Called with the @categories lock locked */
240 static void
idle_saver_save(void)241 idle_saver_save (void)
242 {
243 	GString *buffer;
244 	gchar *contents;
245 	gchar *filename;
246 	gchar *pathname;
247 	EChangedListener *emit_listeners = NULL;  /* owned */
248 	GError *error = NULL;
249 
250 	if (!save_is_pending)
251 		goto exit;
252 
253 	filename = build_categories_filename ();
254 
255 	d (g_debug ("Saving categories to \"%s\"", filename));
256 
257 	/* Build the file contents. */
258 	buffer = g_string_new ("<categories>\n");
259 	g_hash_table_foreach (categories_table, hash_to_xml_string, buffer);
260 	g_string_append_len (buffer, "</categories>\n", 14);
261 	contents = g_string_free (buffer, FALSE);
262 
263 	pathname = g_path_get_dirname (filename);
264 	g_mkdir_with_parents (pathname, 0700);
265 
266 	if (!g_file_set_contents (filename, contents, -1, &error)) {
267 		g_warning ("Unable to save categories: %s", error->message);
268 		g_error_free (error);
269 	}
270 
271 	g_free (pathname);
272 	g_free (contents);
273 	g_free (filename);
274 	save_is_pending = FALSE;
275 
276 	if (changed)
277 		emit_listeners = g_object_ref (listeners);
278 
279 	changed = FALSE;
280 exit:
281 	idle_id = 0;
282 
283 	/* Emit the signal with the lock released to avoid re-entrancy
284 	 * deadlocks. Hold a reference to @listeners until this is complete. */
285 	if (emit_listeners) {
286 		G_UNLOCK (categories);
287 
288 		g_signal_emit_by_name (emit_listeners, "changed");
289 		g_object_unref (emit_listeners);
290 
291 		G_LOCK (categories);
292 	}
293 }
294 
295 static gboolean
idle_saver_cb(gpointer user_data)296 idle_saver_cb (gpointer user_data)
297 {
298 	G_LOCK (categories);
299 
300 	idle_saver_save ();
301 
302 	G_UNLOCK (categories);
303 
304 	return FALSE;
305 }
306 
307 /* This must be called with the @categories lock held. */
308 static void
save_categories(void)309 save_categories (void)
310 {
311 	save_is_pending = TRUE;
312 
313 	if (idle_id == 0)
314 		idle_id = g_idle_add (idle_saver_cb, NULL);
315 }
316 
317 static gchar *
get_collation_key(const gchar * category)318 get_collation_key (const gchar *category)
319 {
320 	gchar *casefolded, *key;
321 
322 	g_return_val_if_fail (category != NULL, NULL);
323 
324 	casefolded = g_utf8_casefold (category, -1);
325 	g_return_val_if_fail (casefolded != NULL, NULL);
326 
327 	key = g_utf8_collate_key (casefolded, -1);
328 	g_free (casefolded);
329 
330 	return key;
331 }
332 
333 /* This must be called with the @categories lock held. */
334 static void
categories_add_full(const gchar * category,const gchar * icon_file,gboolean is_default,gboolean is_searchable)335 categories_add_full (const gchar *category,
336                      const gchar *icon_file,
337                      gboolean is_default,
338                      gboolean is_searchable)
339 {
340 	CategoryInfo *cat_info, *existing_cat_info;
341 	gchar *collation_key;
342 
343 	cat_info = g_slice_new (CategoryInfo);
344 	if (is_default) {
345 		const gchar *display_name;
346 		display_name = g_dpgettext2 (
347 			GETTEXT_PACKAGE, "CategoryName", category);
348 		cat_info->display_name = g_strdup (display_name);
349 		cat_info->clocale_name = g_strdup (category);
350 	} else {
351 		cat_info->display_name = g_strdup (category);
352 		cat_info->clocale_name = NULL;
353 	}
354 	cat_info->icon_file = g_strdup (icon_file);
355 	cat_info->is_default = is_default;
356 	cat_info->is_searchable = is_default || is_searchable;
357 
358 	collation_key = get_collation_key (cat_info->display_name);
359 	existing_cat_info = g_hash_table_lookup (categories_table, collation_key);
360 	if (category_info_equal (existing_cat_info, cat_info)) {
361 		free_category_info (cat_info);
362 		g_free (collation_key);
363 	} else {
364 		g_hash_table_insert (categories_table, collation_key, cat_info);
365 		changed = TRUE;
366 		save_categories ();
367 	}
368 }
369 
370 /* This must be called with the @categories lock held. */
371 static CategoryInfo *
categories_lookup(const gchar * category)372 categories_lookup (const gchar *category)
373 {
374 	CategoryInfo *cat_info;
375 	gchar *collation_key;
376 
377 	collation_key = get_collation_key (category);
378 	cat_info = g_hash_table_lookup (categories_table, collation_key);
379 	g_free (collation_key);
380 
381 	return cat_info;
382 }
383 
384 /* This must be called with the @categories lock held. */
385 static gint
parse_categories(const gchar * contents,gsize length)386 parse_categories (const gchar *contents,
387                   gsize length)
388 {
389 	xmlDocPtr doc;
390 	xmlNodePtr node;
391 	gint n_added = 0;
392 
393 	doc = xmlParseMemory (contents, length);
394 	if (doc == NULL) {
395 		g_warning ("Unable to parse categories");
396 		return 0;
397 	}
398 
399 	node = xmlDocGetRootElement (doc);
400 	if (node == NULL) {
401 		g_warning ("Unable to parse categories");
402 		xmlFreeDoc (doc);
403 		return 0;
404 	}
405 
406 	for (node = node->xmlChildrenNode; node != NULL; node = node->next) {
407 		xmlChar *category, *icon_file, *is_default, *is_searchable;
408 
409 		category = xmlGetProp (node, (xmlChar *) "a");
410 		icon_file = xmlGetProp (node, (xmlChar *) "icon");
411 		is_default = xmlGetProp (node, (xmlChar *) "default");
412 		is_searchable = xmlGetProp (node, (xmlChar *) "searchable");
413 
414 		if (category != NULL && *category) {
415 			categories_add_full (
416 				(gchar *) category, (gchar *) icon_file,
417 				g_strcmp0 ((gchar *) is_default, "1") == 0,
418 				g_strcmp0 ((gchar *) is_searchable, "1") == 0);
419 			n_added++;
420 		}
421 
422 		xmlFree (category);
423 		xmlFree (icon_file);
424 		xmlFree (is_default);
425 		xmlFree (is_searchable);
426 	}
427 
428 	xmlFreeDoc (doc);
429 
430 	return n_added;
431 }
432 
433 /* This must be called with the @categories lock held. */
434 static gint
load_categories(void)435 load_categories (void)
436 {
437 	gchar *contents;
438 	gchar *filename;
439 	gsize length;
440 	gint n_added = 0;
441 	GError *error = NULL;
442 
443 	contents = NULL;
444 	filename = build_categories_filename ();
445 
446 	if (!g_file_test (filename, G_FILE_TEST_EXISTS))
447 		goto exit;
448 
449 	d (g_debug ("Loading categories from \"%s\"", filename));
450 
451 	if (!g_file_get_contents (filename, &contents, &length, &error)) {
452 		g_warning ("Unable to load categories: %s", error->message);
453 		g_error_free (error);
454 		goto exit;
455 	}
456 
457 	n_added = parse_categories (contents, length);
458 
459 exit:
460 	g_free (contents);
461 	g_free (filename);
462 
463 	return n_added;
464 }
465 
466 /* This must be called with the @categories lock held. */
467 static void
load_default_categories(void)468 load_default_categories (void)
469 {
470 	DefaultCategory *cat_info = default_categories;
471 
472 	while (cat_info->category != NULL) {
473 		gchar *icon_file = NULL;
474 
475 		if (cat_info->icon_file != NULL)
476 			icon_file = g_build_filename (
477 				E_DATA_SERVER_IMAGESDIR,
478 				cat_info->icon_file, NULL);
479 
480 		categories_add_full (cat_info->category, icon_file, TRUE, TRUE);
481 
482 		g_free (icon_file);
483 		cat_info++;
484 	}
485 }
486 
487 static void
finalize_categories(void)488 finalize_categories (void)
489 {
490 	G_LOCK (categories);
491 
492 	if (save_is_pending)
493 		idle_saver_save ();
494 
495 	if (idle_id > 0) {
496 		g_source_remove (idle_id);
497 		idle_id = 0;
498 	}
499 
500 	g_clear_pointer (&categories_table, g_hash_table_destroy);
501 	g_clear_object (&listeners);
502 
503 	initialized = FALSE;
504 
505 	G_UNLOCK (categories);
506 }
507 
508 /* This must be called with the @categories lock held. */
509 static void
initialize_categories(void)510 initialize_categories (void)
511 {
512 	gint n_added;
513 
514 	if (initialized)
515 		return;
516 
517 	initialized = TRUE;
518 
519 	bindtextdomain (GETTEXT_PACKAGE, E_DATA_SERVER_LOCALEDIR);
520 
521 	categories_table = g_hash_table_new_full (
522 		g_str_hash, g_str_equal,
523 		(GDestroyNotify) g_free,
524 		(GDestroyNotify) free_category_info);
525 
526 	listeners = g_object_new (e_changed_listener_get_type (), NULL);
527 
528 	atexit (finalize_categories);
529 
530 	n_added = load_categories ();
531 	if (n_added > 0) {
532 		d (g_debug ("Loaded %d categories", n_added));
533 		save_is_pending = FALSE;
534 		return;
535 	}
536 
537 	load_default_categories ();
538 	d (g_debug ("Loaded default categories"));
539 	save_categories ();
540 }
541 
542 /**
543  * e_categories_get_list:
544  *
545  * Returns a sorted list of all the category names currently configured.
546  *
547  * This function is mostly thread safe, but as the category names are not
548  * copied, they may be freed by another thread after being returned by this
549  * function. Use e_categories_dup_list() instead.
550  *
551  * Returns: (transfer container) (element-type utf8): a sorted GList containing
552  * the names of the categories. The list should be freed using g_list_free(),
553  * but the names of the categories should not be touched at all, they are
554  * internal strings.
555  *
556  * Deprecated: 3.16: This function is not entirely thread safe. Use
557  * e_categories_dup_list() instead.
558  */
559 GList *
e_categories_get_list(void)560 e_categories_get_list (void)
561 {
562 	GHashTableIter iter;
563 	GList *list = NULL;
564 	gpointer key, value;
565 
566 	G_LOCK (categories);
567 
568 	if (!initialized)
569 		initialize_categories ();
570 
571 	g_hash_table_iter_init (&iter, categories_table);
572 
573 	while (g_hash_table_iter_next (&iter, &key, &value)) {
574 		CategoryInfo *cat_info = value;
575 		list = g_list_prepend (list, cat_info->display_name);
576 	}
577 
578 	G_UNLOCK (categories);
579 
580 	return g_list_sort (list, (GCompareFunc) g_utf8_collate);
581 }
582 
583 /**
584  * e_categories_dup_list:
585  *
586  * Returns a sorted list of all the category names currently configured.
587  *
588  * This function is thread safe.
589  *
590  * Returns: (transfer full) (element-type utf8): a sorted #GList containing
591  * the names of the categories. The list should be freed using g_list_free(),
592  * and the names of the categories should be freed using g_free(). Everything
593  * can be freed simultaneously using g_list_free_full().
594  *
595  * Since: 3.16
596  */
597 GList *
e_categories_dup_list(void)598 e_categories_dup_list (void)
599 {
600 	GHashTableIter iter;
601 	GList *list = NULL;
602 	gpointer key, value;
603 
604 	G_LOCK (categories);
605 
606 	if (!initialized)
607 		initialize_categories ();
608 
609 	g_hash_table_iter_init (&iter, categories_table);
610 
611 	while (g_hash_table_iter_next (&iter, &key, &value)) {
612 		CategoryInfo *cat_info = value;
613 		list = g_list_prepend (list, g_strdup (cat_info->display_name));
614 	}
615 
616 	G_UNLOCK (categories);
617 
618 	return g_list_sort (list, (GCompareFunc) g_utf8_collate);
619 }
620 
621 /**
622  * e_categories_add:
623  * @category: name of category to add.
624  * @unused: DEPRECATED! associated color. DEPRECATED!
625  * @icon_file: full path of the icon associated to the category.
626  * @searchable: whether the category can be used for searching in the GUI.
627  *
628  * Adds a new category, with its corresponding icon, to the
629  * configuration database.
630  *
631  * This function is thread safe.
632  */
633 void
e_categories_add(const gchar * category,const gchar * unused,const gchar * icon_file,gboolean searchable)634 e_categories_add (const gchar *category,
635                   const gchar *unused,
636                   const gchar *icon_file,
637                   gboolean searchable)
638 {
639 	g_return_if_fail (category != NULL);
640 	g_return_if_fail (*category);
641 
642 	G_LOCK (categories);
643 
644 	if (!initialized)
645 		initialize_categories ();
646 
647 	categories_add_full (category, icon_file, FALSE, searchable);
648 
649 	G_UNLOCK (categories);
650 }
651 
652 /**
653  * e_categories_remove:
654  * @category: category to be removed.
655  *
656  * Removes the given category from the configuration.
657  *
658  * This function is thread safe.
659  */
660 void
e_categories_remove(const gchar * category)661 e_categories_remove (const gchar *category)
662 {
663 	gchar *collation_key;
664 
665 	g_return_if_fail (category != NULL);
666 
667 	G_LOCK (categories);
668 
669 	if (!initialized)
670 		initialize_categories ();
671 
672 	collation_key = get_collation_key (category);
673 
674 	if (g_hash_table_remove (categories_table, collation_key)) {
675 		changed = TRUE;
676 		save_categories ();
677 	}
678 
679 	g_free (collation_key);
680 
681 	G_UNLOCK (categories);
682 }
683 
684 /**
685  * e_categories_exist:
686  * @category: category to be searched.
687  *
688  * Checks whether the given category is available in the configuration.
689  *
690  * This function is thread safe.
691  *
692  * Returns: %TRUE if the category is available, %FALSE otherwise.
693  */
694 gboolean
e_categories_exist(const gchar * category)695 e_categories_exist (const gchar *category)
696 {
697 	gboolean exists;
698 
699 	g_return_val_if_fail (category != NULL, FALSE);
700 
701 	G_LOCK (categories);
702 
703 	if (!initialized)
704 		initialize_categories ();
705 
706 	exists = (!*category) || (categories_lookup (category) != NULL);
707 
708 	G_UNLOCK (categories);
709 
710 	return exists;
711 }
712 
713 /**
714  * e_categories_get_icon_file_for:
715  * @category: category to retrieve the icon file for.
716  *
717  * Gets the icon file associated with the given category.
718  *
719  * This function is mostly thread safe, but as the icon file name is not
720  * copied, it may be freed by another thread after being returned by this
721  * function. Use e_categories_dup_icon_file_for() instead.
722  *
723  * Deprecated: 3.16: This function is not entirely thread safe. Use
724  * e_categories_dup_icon_file_for() instead.
725  *
726  * Returns: icon file name.
727  */
728 const gchar *
e_categories_get_icon_file_for(const gchar * category)729 e_categories_get_icon_file_for (const gchar *category)
730 {
731 	CategoryInfo *cat_info;
732 
733 	g_return_val_if_fail (category != NULL, NULL);
734 
735 	G_LOCK (categories);
736 
737 	if (!initialized)
738 		initialize_categories ();
739 
740 	cat_info = categories_lookup (category);
741 
742 	G_UNLOCK (categories);
743 
744 	if (cat_info == NULL)
745 		return NULL;
746 
747 	return cat_info->icon_file;
748 }
749 
750 /**
751  * e_categories_dup_icon_file_for:
752  * @category: category to retrieve the icon file for.
753  *
754  * Gets the icon file associated with the given category and returns a copy of
755  * it.
756  *
757  * This function is thread safe.
758  *
759  * Returns: (transfer full): icon file name; free with g_free().
760  *
761  * Since: 3.16
762  */
763 gchar *
e_categories_dup_icon_file_for(const gchar * category)764 e_categories_dup_icon_file_for (const gchar *category)
765 {
766 	CategoryInfo *cat_info;
767 	gchar *icon_file = NULL;
768 
769 	g_return_val_if_fail (category != NULL, NULL);
770 
771 	G_LOCK (categories);
772 
773 	if (!initialized)
774 		initialize_categories ();
775 
776 	cat_info = categories_lookup (category);
777 
778 	if (cat_info != NULL)
779 		icon_file = g_strdup (cat_info->icon_file);
780 
781 	G_UNLOCK (categories);
782 
783 	return icon_file;
784 }
785 
786 /**
787  * e_categories_set_icon_file_for:
788  * @category: category to set the icon file for.
789  * @icon_file: icon file.
790  *
791  * Sets the icon file associated with the given category.
792  *
793  * This function is thread safe.
794  */
795 void
e_categories_set_icon_file_for(const gchar * category,const gchar * icon_file)796 e_categories_set_icon_file_for (const gchar *category,
797                                 const gchar *icon_file)
798 {
799 	CategoryInfo *cat_info;
800 
801 	g_return_if_fail (category != NULL);
802 
803 	G_LOCK (categories);
804 
805 	if (!initialized)
806 		initialize_categories ();
807 
808 	cat_info = categories_lookup (category);
809 	g_return_if_fail (cat_info != NULL);
810 
811 	g_free (cat_info->icon_file);
812 	cat_info->icon_file = g_strdup (icon_file);
813 
814 	changed = TRUE;
815 	save_categories ();
816 
817 	G_UNLOCK (categories);
818 }
819 
820 /**
821  * e_categories_is_searchable:
822  * @category: category name.
823  *
824  * Gets whether the given calendar is to be used for searches in the GUI.
825  *
826  * This function is thread safe.
827  *
828  * Return value; %TRUE% if the category is searchable, %FALSE% if not.
829  */
830 gboolean
e_categories_is_searchable(const gchar * category)831 e_categories_is_searchable (const gchar *category)
832 {
833 	CategoryInfo *cat_info;
834 	gboolean is_searchable = FALSE;
835 
836 	g_return_val_if_fail (category != NULL, FALSE);
837 
838 	G_LOCK (categories);
839 
840 	if (!initialized)
841 		initialize_categories ();
842 
843 	cat_info = categories_lookup (category);
844 
845 	if (cat_info != NULL)
846 		is_searchable = cat_info->is_searchable;
847 
848 	G_UNLOCK (categories);
849 
850 	return is_searchable;
851 }
852 
853 /**
854  * e_categories_register_change_listener:
855  * @listener: (scope async): the callback to be called on any category change.
856  * @user_data: used data passed to the @listener when called.
857  *
858  * Registers callback to be called on change of any category.
859  * Pair listener and user_data is used to distinguish between listeners.
860  * Listeners can be unregistered with @e_categories_unregister_change_listener.
861  *
862  * This function is thread safe.
863  *
864  * Since: 2.24
865  **/
866 void
e_categories_register_change_listener(GCallback listener,gpointer user_data)867 e_categories_register_change_listener (GCallback listener,
868                                        gpointer user_data)
869 {
870 	G_LOCK (categories);
871 
872 	if (!initialized)
873 		initialize_categories ();
874 
875 	g_signal_connect (listeners, "changed", listener, user_data);
876 
877 	G_UNLOCK (categories);
878 }
879 
880 /**
881  * e_categories_unregister_change_listener:
882  * @listener: (scope async): Callback to be removed.
883  * @user_data: User data as passed with call to @e_categories_register_change_listener.
884  *
885  * Removes previously registered callback from the list of listeners on changes.
886  * If it was not registered, then does nothing.
887  *
888  * This function is thread safe.
889  *
890  * Since: 2.24
891  **/
892 void
e_categories_unregister_change_listener(GCallback listener,gpointer user_data)893 e_categories_unregister_change_listener (GCallback listener,
894                                          gpointer user_data)
895 {
896 	G_LOCK (categories);
897 
898 	if (initialized)
899 		g_signal_handlers_disconnect_by_func (listeners, listener, user_data);
900 
901 	G_UNLOCK (categories);
902 }
903