/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ /* * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) * * This library is free software: you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License * for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this library. If not, see . * */ #include "evolution-data-server-config.h" #include #include #include #include #include #include "e-data-server-util.h" #include "e-categories.h" #include "libedataserver-private.h" #define d(x) typedef struct { gchar *display_name; /* localized category name */ gchar *clocale_name; /* only for default categories */ gchar *icon_file; gboolean is_default; gboolean is_searchable; } CategoryInfo; typedef struct { const gchar *category; const gchar *icon_file; } DefaultCategory; static DefaultCategory default_categories[] = { { NC_("CategoryName", "Anniversary") }, { NC_("CategoryName", "Birthday"), "category_birthday_16.png" }, { NC_("CategoryName", "Business"), "category_business_16.png" }, { NC_("CategoryName", "Competition") }, { NC_("CategoryName", "Favorites"), "category_favorites_16.png" }, { NC_("CategoryName", "Gifts"), "category_gifts_16.png" }, { NC_("CategoryName", "Goals/Objectives"), "category_goals_16.png" }, { NC_("CategoryName", "Holiday"), "category_holiday_16.png" }, { NC_("CategoryName", "Holiday Cards"), "category_holiday-cards_16.png" }, /* important people (e.g. new business partners) */ { NC_("CategoryName", "Hot Contacts"), "category_hot-contacts_16.png" }, { NC_("CategoryName", "Ideas"), "category_ideas_16.png" }, { NC_("CategoryName", "International"), "category_international_16.png" }, { NC_("CategoryName", "Key Customer"), "category_key-customer_16.png" }, { NC_("CategoryName", "Miscellaneous"), "category_miscellaneous_16.png" }, { NC_("CategoryName", "Personal"), "category_personal_16.png" }, { NC_("CategoryName", "Phone Calls"), "category_phonecalls_16.png" }, /* Translators: "Status" is a category name; it can mean anything user wants to */ { NC_("CategoryName", "Status"), "category_status_16.png" }, { NC_("CategoryName", "Strategies"), "category_strategies_16.png" }, { NC_("CategoryName", "Suppliers"), "category_suppliers_16.png" }, { NC_("CategoryName", "Time & Expenses"), "category_time-and-expenses_16.png" }, { NC_("CategoryName", "VIP") }, { NC_("CategoryName", "Waiting") }, { NULL } }; /* ------------------------------------------------------------------------- */ typedef struct { GObject object; } EChangedListener; typedef struct { GObjectClass parent_class; void (* changed) (void); } EChangedListenerClass; static GType e_changed_listener_get_type (void); G_DEFINE_TYPE (EChangedListener, e_changed_listener, G_TYPE_OBJECT) enum { CHANGED, LAST_SIGNAL }; static guint changed_listener_signals[LAST_SIGNAL]; static void e_changed_listener_class_init (EChangedListenerClass *class) { changed_listener_signals[CHANGED] = g_signal_new ( "changed", G_TYPE_FROM_CLASS (class), G_SIGNAL_RUN_FIRST, G_STRUCT_OFFSET (EChangedListenerClass, changed), NULL, NULL, NULL, G_TYPE_NONE, 0); } static void e_changed_listener_init (EChangedListener *listener) { } /* ------------------------------------------------------------------------- */ /* All the static variables below are protected by a global categories lock. */ G_LOCK_DEFINE_STATIC (categories); static gboolean initialized = FALSE; static GHashTable *categories_table = NULL; static gboolean save_is_pending = FALSE; static guint idle_id = 0; static EChangedListener *listeners = NULL; static gboolean changed = FALSE; static gchar * build_categories_filename (void) { const gchar *user_data_dir; gchar *filename; user_data_dir = e_get_user_data_dir (); filename = g_build_filename (user_data_dir, "categories.xml", NULL); if (!g_file_test (filename, G_FILE_TEST_IS_REGULAR)) { gchar *old_filename; /* Try moving the file from its old 2.x location. * This is best effort; don't worry about errors. */ old_filename = g_build_filename ( g_get_home_dir (), ".evolution", "categories.xml", NULL); if (g_rename (old_filename, filename) == -1 && errno != ENOENT) { g_warning ("%s: Failed to rename '%s' to '%s': %s", G_STRFUNC, old_filename, filename, g_strerror (errno)); } g_free (old_filename); } return filename; } static void free_category_info (CategoryInfo *cat_info) { g_free (cat_info->display_name); g_free (cat_info->clocale_name); g_free (cat_info->icon_file); g_slice_free (CategoryInfo, cat_info); } static gboolean category_info_equal (const CategoryInfo *cat_info1, const CategoryInfo *cat_info2) { if (!cat_info1 || !cat_info2 || cat_info1 == cat_info2) return cat_info1 == cat_info2; return g_strcmp0 (cat_info1->display_name, cat_info2->display_name) == 0 && g_strcmp0 (cat_info1->clocale_name, cat_info2->clocale_name) == 0 && g_strcmp0 (cat_info1->icon_file, cat_info2->icon_file) == 0 && (cat_info1->is_default ? 1 : 0) == (cat_info2->is_default ? 1 : 0) && (cat_info1->is_searchable ? 1 : 0) == (cat_info2->is_searchable ? 1 : 0); } static gchar * escape_string (const gchar *source) { GString *buffer; buffer = g_string_sized_new (strlen (source)); while (*source) { switch (*source) { case '<': g_string_append_len (buffer, "<", 4); break; case '>': g_string_append_len (buffer, ">", 4); break; case '&': g_string_append_len (buffer, "&", 5); break; case '"': g_string_append_len (buffer, """, 6); break; default: g_string_append_c (buffer, *source); break; } source++; } return g_string_free (buffer, FALSE); } /* This must be called with the @categories lock held. */ static void hash_to_xml_string (gpointer key, gpointer value, gpointer user_data) { CategoryInfo *cat_info = value; GString *string = user_data; gchar *category; g_string_append_len (string, " is_default && cat_info->clocale_name && *cat_info->clocale_name) category = escape_string (cat_info->clocale_name); else category = escape_string (cat_info->display_name); g_string_append_printf (string, " a=\"%s\"", category); g_free (category); if (cat_info->icon_file != NULL) g_string_append_printf ( string, " icon=\"%s\"", cat_info->icon_file); g_string_append_printf ( string, " default=\"%d\"", cat_info->is_default ? 1 : 0); g_string_append_printf ( string, " searchable=\"%d\"", cat_info->is_searchable ? 1 : 0); g_string_append_len (string, "/>\n", 3); } /* Called with the @categories lock locked */ static void idle_saver_save (void) { GString *buffer; gchar *contents; gchar *filename; gchar *pathname; EChangedListener *emit_listeners = NULL; /* owned */ GError *error = NULL; if (!save_is_pending) goto exit; filename = build_categories_filename (); d (g_debug ("Saving categories to \"%s\"", filename)); /* Build the file contents. */ buffer = g_string_new ("\n"); g_hash_table_foreach (categories_table, hash_to_xml_string, buffer); g_string_append_len (buffer, "\n", 14); contents = g_string_free (buffer, FALSE); pathname = g_path_get_dirname (filename); g_mkdir_with_parents (pathname, 0700); if (!g_file_set_contents (filename, contents, -1, &error)) { g_warning ("Unable to save categories: %s", error->message); g_error_free (error); } g_free (pathname); g_free (contents); g_free (filename); save_is_pending = FALSE; if (changed) emit_listeners = g_object_ref (listeners); changed = FALSE; exit: idle_id = 0; /* Emit the signal with the lock released to avoid re-entrancy * deadlocks. Hold a reference to @listeners until this is complete. */ if (emit_listeners) { G_UNLOCK (categories); g_signal_emit_by_name (emit_listeners, "changed"); g_object_unref (emit_listeners); G_LOCK (categories); } } static gboolean idle_saver_cb (gpointer user_data) { G_LOCK (categories); idle_saver_save (); G_UNLOCK (categories); return FALSE; } /* This must be called with the @categories lock held. */ static void save_categories (void) { save_is_pending = TRUE; if (idle_id == 0) idle_id = g_idle_add (idle_saver_cb, NULL); } static gchar * get_collation_key (const gchar *category) { gchar *casefolded, *key; g_return_val_if_fail (category != NULL, NULL); casefolded = g_utf8_casefold (category, -1); g_return_val_if_fail (casefolded != NULL, NULL); key = g_utf8_collate_key (casefolded, -1); g_free (casefolded); return key; } /* This must be called with the @categories lock held. */ static void categories_add_full (const gchar *category, const gchar *icon_file, gboolean is_default, gboolean is_searchable) { CategoryInfo *cat_info, *existing_cat_info; gchar *collation_key; cat_info = g_slice_new (CategoryInfo); if (is_default) { const gchar *display_name; display_name = g_dpgettext2 ( GETTEXT_PACKAGE, "CategoryName", category); cat_info->display_name = g_strdup (display_name); cat_info->clocale_name = g_strdup (category); } else { cat_info->display_name = g_strdup (category); cat_info->clocale_name = NULL; } cat_info->icon_file = g_strdup (icon_file); cat_info->is_default = is_default; cat_info->is_searchable = is_default || is_searchable; collation_key = get_collation_key (cat_info->display_name); existing_cat_info = g_hash_table_lookup (categories_table, collation_key); if (category_info_equal (existing_cat_info, cat_info)) { free_category_info (cat_info); g_free (collation_key); } else { g_hash_table_insert (categories_table, collation_key, cat_info); changed = TRUE; save_categories (); } } /* This must be called with the @categories lock held. */ static CategoryInfo * categories_lookup (const gchar *category) { CategoryInfo *cat_info; gchar *collation_key; collation_key = get_collation_key (category); cat_info = g_hash_table_lookup (categories_table, collation_key); g_free (collation_key); return cat_info; } /* This must be called with the @categories lock held. */ static gint parse_categories (const gchar *contents, gsize length) { xmlDocPtr doc; xmlNodePtr node; gint n_added = 0; doc = xmlParseMemory (contents, length); if (doc == NULL) { g_warning ("Unable to parse categories"); return 0; } node = xmlDocGetRootElement (doc); if (node == NULL) { g_warning ("Unable to parse categories"); xmlFreeDoc (doc); return 0; } for (node = node->xmlChildrenNode; node != NULL; node = node->next) { xmlChar *category, *icon_file, *is_default, *is_searchable; category = xmlGetProp (node, (xmlChar *) "a"); icon_file = xmlGetProp (node, (xmlChar *) "icon"); is_default = xmlGetProp (node, (xmlChar *) "default"); is_searchable = xmlGetProp (node, (xmlChar *) "searchable"); if (category != NULL && *category) { categories_add_full ( (gchar *) category, (gchar *) icon_file, g_strcmp0 ((gchar *) is_default, "1") == 0, g_strcmp0 ((gchar *) is_searchable, "1") == 0); n_added++; } xmlFree (category); xmlFree (icon_file); xmlFree (is_default); xmlFree (is_searchable); } xmlFreeDoc (doc); return n_added; } /* This must be called with the @categories lock held. */ static gint load_categories (void) { gchar *contents; gchar *filename; gsize length; gint n_added = 0; GError *error = NULL; contents = NULL; filename = build_categories_filename (); if (!g_file_test (filename, G_FILE_TEST_EXISTS)) goto exit; d (g_debug ("Loading categories from \"%s\"", filename)); if (!g_file_get_contents (filename, &contents, &length, &error)) { g_warning ("Unable to load categories: %s", error->message); g_error_free (error); goto exit; } n_added = parse_categories (contents, length); exit: g_free (contents); g_free (filename); return n_added; } /* This must be called with the @categories lock held. */ static void load_default_categories (void) { DefaultCategory *cat_info = default_categories; while (cat_info->category != NULL) { gchar *icon_file = NULL; if (cat_info->icon_file != NULL) icon_file = g_build_filename ( E_DATA_SERVER_IMAGESDIR, cat_info->icon_file, NULL); categories_add_full (cat_info->category, icon_file, TRUE, TRUE); g_free (icon_file); cat_info++; } } static void finalize_categories (void) { G_LOCK (categories); if (save_is_pending) idle_saver_save (); if (idle_id > 0) { g_source_remove (idle_id); idle_id = 0; } g_clear_pointer (&categories_table, g_hash_table_destroy); g_clear_object (&listeners); initialized = FALSE; G_UNLOCK (categories); } /* This must be called with the @categories lock held. */ static void initialize_categories (void) { gint n_added; if (initialized) return; initialized = TRUE; bindtextdomain (GETTEXT_PACKAGE, E_DATA_SERVER_LOCALEDIR); categories_table = g_hash_table_new_full ( g_str_hash, g_str_equal, (GDestroyNotify) g_free, (GDestroyNotify) free_category_info); listeners = g_object_new (e_changed_listener_get_type (), NULL); atexit (finalize_categories); n_added = load_categories (); if (n_added > 0) { d (g_debug ("Loaded %d categories", n_added)); save_is_pending = FALSE; return; } load_default_categories (); d (g_debug ("Loaded default categories")); save_categories (); } /** * e_categories_get_list: * * Returns a sorted list of all the category names currently configured. * * This function is mostly thread safe, but as the category names are not * copied, they may be freed by another thread after being returned by this * function. Use e_categories_dup_list() instead. * * Returns: (transfer container) (element-type utf8): a sorted GList containing * the names of the categories. The list should be freed using g_list_free(), * but the names of the categories should not be touched at all, they are * internal strings. * * Deprecated: 3.16: This function is not entirely thread safe. Use * e_categories_dup_list() instead. */ GList * e_categories_get_list (void) { GHashTableIter iter; GList *list = NULL; gpointer key, value; G_LOCK (categories); if (!initialized) initialize_categories (); g_hash_table_iter_init (&iter, categories_table); while (g_hash_table_iter_next (&iter, &key, &value)) { CategoryInfo *cat_info = value; list = g_list_prepend (list, cat_info->display_name); } G_UNLOCK (categories); return g_list_sort (list, (GCompareFunc) g_utf8_collate); } /** * e_categories_dup_list: * * Returns a sorted list of all the category names currently configured. * * This function is thread safe. * * Returns: (transfer full) (element-type utf8): a sorted #GList containing * the names of the categories. The list should be freed using g_list_free(), * and the names of the categories should be freed using g_free(). Everything * can be freed simultaneously using g_list_free_full(). * * Since: 3.16 */ GList * e_categories_dup_list (void) { GHashTableIter iter; GList *list = NULL; gpointer key, value; G_LOCK (categories); if (!initialized) initialize_categories (); g_hash_table_iter_init (&iter, categories_table); while (g_hash_table_iter_next (&iter, &key, &value)) { CategoryInfo *cat_info = value; list = g_list_prepend (list, g_strdup (cat_info->display_name)); } G_UNLOCK (categories); return g_list_sort (list, (GCompareFunc) g_utf8_collate); } /** * e_categories_add: * @category: name of category to add. * @unused: DEPRECATED! associated color. DEPRECATED! * @icon_file: full path of the icon associated to the category. * @searchable: whether the category can be used for searching in the GUI. * * Adds a new category, with its corresponding icon, to the * configuration database. * * This function is thread safe. */ void e_categories_add (const gchar *category, const gchar *unused, const gchar *icon_file, gboolean searchable) { g_return_if_fail (category != NULL); g_return_if_fail (*category); G_LOCK (categories); if (!initialized) initialize_categories (); categories_add_full (category, icon_file, FALSE, searchable); G_UNLOCK (categories); } /** * e_categories_remove: * @category: category to be removed. * * Removes the given category from the configuration. * * This function is thread safe. */ void e_categories_remove (const gchar *category) { gchar *collation_key; g_return_if_fail (category != NULL); G_LOCK (categories); if (!initialized) initialize_categories (); collation_key = get_collation_key (category); if (g_hash_table_remove (categories_table, collation_key)) { changed = TRUE; save_categories (); } g_free (collation_key); G_UNLOCK (categories); } /** * e_categories_exist: * @category: category to be searched. * * Checks whether the given category is available in the configuration. * * This function is thread safe. * * Returns: %TRUE if the category is available, %FALSE otherwise. */ gboolean e_categories_exist (const gchar *category) { gboolean exists; g_return_val_if_fail (category != NULL, FALSE); G_LOCK (categories); if (!initialized) initialize_categories (); exists = (!*category) || (categories_lookup (category) != NULL); G_UNLOCK (categories); return exists; } /** * e_categories_get_icon_file_for: * @category: category to retrieve the icon file for. * * Gets the icon file associated with the given category. * * This function is mostly thread safe, but as the icon file name is not * copied, it may be freed by another thread after being returned by this * function. Use e_categories_dup_icon_file_for() instead. * * Deprecated: 3.16: This function is not entirely thread safe. Use * e_categories_dup_icon_file_for() instead. * * Returns: icon file name. */ const gchar * e_categories_get_icon_file_for (const gchar *category) { CategoryInfo *cat_info; g_return_val_if_fail (category != NULL, NULL); G_LOCK (categories); if (!initialized) initialize_categories (); cat_info = categories_lookup (category); G_UNLOCK (categories); if (cat_info == NULL) return NULL; return cat_info->icon_file; } /** * e_categories_dup_icon_file_for: * @category: category to retrieve the icon file for. * * Gets the icon file associated with the given category and returns a copy of * it. * * This function is thread safe. * * Returns: (transfer full): icon file name; free with g_free(). * * Since: 3.16 */ gchar * e_categories_dup_icon_file_for (const gchar *category) { CategoryInfo *cat_info; gchar *icon_file = NULL; g_return_val_if_fail (category != NULL, NULL); G_LOCK (categories); if (!initialized) initialize_categories (); cat_info = categories_lookup (category); if (cat_info != NULL) icon_file = g_strdup (cat_info->icon_file); G_UNLOCK (categories); return icon_file; } /** * e_categories_set_icon_file_for: * @category: category to set the icon file for. * @icon_file: icon file. * * Sets the icon file associated with the given category. * * This function is thread safe. */ void e_categories_set_icon_file_for (const gchar *category, const gchar *icon_file) { CategoryInfo *cat_info; g_return_if_fail (category != NULL); G_LOCK (categories); if (!initialized) initialize_categories (); cat_info = categories_lookup (category); g_return_if_fail (cat_info != NULL); g_free (cat_info->icon_file); cat_info->icon_file = g_strdup (icon_file); changed = TRUE; save_categories (); G_UNLOCK (categories); } /** * e_categories_is_searchable: * @category: category name. * * Gets whether the given calendar is to be used for searches in the GUI. * * This function is thread safe. * * Return value; %TRUE% if the category is searchable, %FALSE% if not. */ gboolean e_categories_is_searchable (const gchar *category) { CategoryInfo *cat_info; gboolean is_searchable = FALSE; g_return_val_if_fail (category != NULL, FALSE); G_LOCK (categories); if (!initialized) initialize_categories (); cat_info = categories_lookup (category); if (cat_info != NULL) is_searchable = cat_info->is_searchable; G_UNLOCK (categories); return is_searchable; } /** * e_categories_register_change_listener: * @listener: (scope async): the callback to be called on any category change. * @user_data: used data passed to the @listener when called. * * Registers callback to be called on change of any category. * Pair listener and user_data is used to distinguish between listeners. * Listeners can be unregistered with @e_categories_unregister_change_listener. * * This function is thread safe. * * Since: 2.24 **/ void e_categories_register_change_listener (GCallback listener, gpointer user_data) { G_LOCK (categories); if (!initialized) initialize_categories (); g_signal_connect (listeners, "changed", listener, user_data); G_UNLOCK (categories); } /** * e_categories_unregister_change_listener: * @listener: (scope async): Callback to be removed. * @user_data: User data as passed with call to @e_categories_register_change_listener. * * Removes previously registered callback from the list of listeners on changes. * If it was not registered, then does nothing. * * This function is thread safe. * * Since: 2.24 **/ void e_categories_unregister_change_listener (GCallback listener, gpointer user_data) { G_LOCK (categories); if (initialized) g_signal_handlers_disconnect_by_func (listeners, listener, user_data); G_UNLOCK (categories); }