summaryrefslogtreecommitdiff
path: root/src/nautilus-tag-manager.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/nautilus-tag-manager.c')
-rw-r--r--src/nautilus-tag-manager.c887
1 files changed, 887 insertions, 0 deletions
diff --git a/src/nautilus-tag-manager.c b/src/nautilus-tag-manager.c
new file mode 100644
index 000000000..9fdad5b52
--- /dev/null
+++ b/src/nautilus-tag-manager.c
@@ -0,0 +1,887 @@
+/* nautilus-tag-manager.c
+ *
+ * Copyright (C) 2017 Alexandru Pandelea <alexandru.pandelea@gmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "nautilus-tag-manager.h"
+#include "nautilus-file.h"
+#include "nautilus-file-undo-operations.h"
+#include "nautilus-file-undo-manager.h"
+
+#include <tracker-sparql.h>
+
+struct _NautilusTagManager
+{
+ GObject object;
+
+ TrackerNotifier *notifier;
+ GError *notifier_error;
+
+ GHashTable *favorite_files;
+ GCancellable *cancellable;
+};
+
+G_DEFINE_TYPE (NautilusTagManager, nautilus_tag_manager, G_TYPE_OBJECT);
+
+static NautilusTagManager *tag_manager = NULL;
+
+typedef enum
+{
+ GET_FAVORITE_FILES,
+ GET_IDS_FOR_URLS
+} OperationType;
+
+typedef struct
+{
+ GTask *task;
+ GList *selection;
+ GHashTable *ids;
+ GObject *object;
+ GAsyncReadyCallback callback;
+ GCancellable *cancellable;
+} InsertTaskData;
+
+typedef struct
+{
+ NautilusTagManager *tag_manager;
+ GTask *task;
+ GList *selection;
+ gboolean star;
+ GHashTable *ids;
+} UpdateData;
+
+enum
+{
+ FAVORITES_CHANGED,
+ LAST_SIGNAL
+};
+
+static guint signals[LAST_SIGNAL];
+
+static const gchar*
+nautilus_tag_manager_file_with_id_changed_url (GHashTable *hash_table,
+ gint64 id,
+ const gchar *url)
+{
+ GHashTableIter iter;
+ gpointer key, value;
+
+ g_hash_table_iter_init (&iter, hash_table);
+ while (g_hash_table_iter_next (&iter, &key, &value))
+ {
+ if ((gint64) value == id && g_strcmp0 (url, key) != 0)
+ {
+ return key;
+ }
+ }
+
+ return NULL;
+}
+
+void
+destroy_insert_task_data (gpointer data)
+{
+ InsertTaskData *task_data;
+
+ task_data = data;
+
+ nautilus_file_list_free (task_data->selection);
+ g_free (data);
+}
+
+static GString*
+add_selection_filter (GList *selection,
+ GString *query)
+{
+ NautilusFile *file;
+ GList *l;
+ gchar *uri;
+
+ g_string_append (query, " FILTER(?url IN (");
+
+ for (l = selection; l != NULL; l = l->next)
+ {
+ file = l->data;
+
+ uri = nautilus_file_get_uri (file);
+
+ g_string_append_printf (query, "'%s'", uri);
+
+ if (l->next != NULL)
+ {
+ g_string_append (query, ", ");
+ }
+
+ g_free (uri);
+ }
+
+ g_string_append (query, "))");
+
+ return query;
+}
+
+static void
+start_query_or_update (GString *query,
+ GAsyncReadyCallback callback,
+ gpointer user_data,
+ gboolean is_query,
+ GCancellable *cancellable)
+{
+ g_autoptr (GError) error = NULL;
+ TrackerSparqlConnection *connection;
+
+ connection = tracker_sparql_connection_get (cancellable, &error);
+ if (!connection)
+ {
+ if (error)
+ {
+ g_warning ("Error on getting connection: %s", error->message);
+ }
+
+ return;
+ }
+
+ if (is_query)
+ {
+ tracker_sparql_connection_query_async (connection,
+ query->str,
+ cancellable,
+ callback,
+ user_data);
+ }
+ else
+ {
+ tracker_sparql_connection_update_async (connection,
+ query->str,
+ G_PRIORITY_DEFAULT,
+ cancellable,
+ callback,
+ user_data);
+ }
+
+ g_object_unref (connection);
+}
+
+static void
+on_query_callback (GObject *object,
+ GAsyncResult *result,
+ gpointer user_data,
+ GAsyncReadyCallback callback,
+ OperationType op_type,
+ GCancellable *cancellable)
+{
+ TrackerSparqlCursor *cursor;
+ g_autoptr (GError) error = NULL;
+ TrackerSparqlConnection *connection;
+ GTask *task;
+
+ task = user_data;
+
+ connection = TRACKER_SPARQL_CONNECTION (object);
+
+ cursor = tracker_sparql_connection_query_finish (connection,
+ result,
+ &error);
+
+ if (error != NULL)
+ {
+ if (error->code != G_IO_ERROR_CANCELLED)
+ {
+ if (op_type == GET_FAVORITE_FILES)
+ {
+ g_warning ("Error on getting favorite files: %s", error->message);
+ }
+ else if (op_type == GET_IDS_FOR_URLS)
+ {
+ g_warning ("Error on getting id for url: %s", error->message);
+ g_task_return_pointer (task, g_task_get_task_data (task), NULL);
+ g_object_unref (task);
+ }
+ else
+ {
+ g_warning ("Error on getting query callback: %s", error->message);
+ }
+ }
+ }
+ else
+ {
+ tracker_sparql_cursor_next_async (cursor,
+ cancellable,
+ callback,
+ user_data);
+ }
+}
+
+static void
+on_update_callback (GObject *object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ TrackerSparqlConnection *connection;
+ GError *error;
+ UpdateData *data;
+ gint64 *id;
+ GList *l;
+ gchar *uri;
+
+ data = user_data;
+
+ error = NULL;
+
+ connection = TRACKER_SPARQL_CONNECTION (object);
+
+ tracker_sparql_connection_update_finish (connection, result, &error);
+
+ if (error == NULL ||
+ (error != NULL && error->code != G_IO_ERROR_CANCELLED))
+ {
+ for (l = data->selection; l != NULL; l = l->next)
+ {
+ uri = nautilus_file_get_uri (NAUTILUS_FILE (l->data));
+
+ if (data->star)
+ {
+ if (g_hash_table_contains (data->ids, uri))
+ {
+ id = g_new0 (gint64, 1);
+
+ *id = (gint64) g_hash_table_lookup (data->ids, uri);
+ g_hash_table_insert (data->tag_manager->favorite_files,
+ nautilus_file_get_uri (NAUTILUS_FILE (l->data)),
+ id);
+ }
+ }
+ else
+ {
+ g_hash_table_remove (data->tag_manager->favorite_files, uri);
+ }
+
+ g_free (uri);
+ }
+
+ if (!nautilus_file_undo_manager_is_operating ())
+ {
+ NautilusFileUndoInfo *undo_info;
+
+ undo_info = nautilus_file_undo_info_favorites_new (data->selection, data->star);
+ nautilus_file_undo_manager_set_action (undo_info);
+
+ g_object_unref (undo_info);
+ }
+
+ g_signal_emit_by_name (data->tag_manager, "favorites-changed", nautilus_file_list_copy (data->selection));
+
+ g_task_return_boolean (data->task, TRUE);
+ g_object_unref (data->task);
+ }
+ else if (error && error->code == G_IO_ERROR_CANCELLED)
+ {
+ g_error_free (error);
+ }
+ else
+ {
+ g_task_return_error (data->task, error);
+ g_object_unref (data->task);
+ g_warning ("error updating tags: %s", error->message);
+ }
+
+ if (data->ids)
+ {
+ g_hash_table_destroy (data->ids);
+ }
+ nautilus_file_list_free (data->selection);
+ g_free (data);
+}
+
+static gboolean
+get_query_status (TrackerSparqlCursor *cursor,
+ GAsyncResult *result,
+ OperationType op_type,
+ gpointer user_data)
+{
+ gboolean success;
+ GTask *task;
+ g_autoptr (GError) error = NULL;
+
+ task = user_data;
+
+ success = tracker_sparql_cursor_next_finish (cursor, result, &error);
+
+ if (!success)
+ {
+ if (error)
+ {
+ g_warning ("Error on getting all tags cursor callback: %s", error->message);
+ }
+
+ g_clear_object (&cursor);
+
+ if (error == NULL ||
+ (error != NULL && error->code != G_IO_ERROR_CANCELLED))
+ {
+ if (op_type == GET_IDS_FOR_URLS)
+ {
+ g_task_return_pointer (task, g_task_get_task_data (task), NULL);
+ g_object_unref (task);
+ }
+ }
+ }
+
+ return success;
+}
+
+GList*
+nautilus_tag_manager_get_favorite_files (NautilusTagManager *self)
+{
+ return g_hash_table_get_keys (self->favorite_files);
+}
+
+static void
+on_get_favorite_files_cursor_callback (GObject *object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ TrackerSparqlCursor *cursor;
+ const gchar *url;
+ gint64 *id;
+ gboolean success;
+ NautilusTagManager *self;
+ GList *changed_files;
+ NautilusFile *file;
+
+ cursor = TRACKER_SPARQL_CURSOR (object);
+
+ self = NAUTILUS_TAG_MANAGER (user_data);
+
+ success = get_query_status (cursor, result, GET_FAVORITE_FILES, NULL);
+ if (!success)
+ {
+ return;
+ }
+
+ id = g_new0 (gint64, 1);
+
+ url = tracker_sparql_cursor_get_string (cursor, 0, NULL);
+ *id = tracker_sparql_cursor_get_integer (cursor, 1);
+
+ g_hash_table_insert (self->favorite_files,
+ g_strdup (url),
+ id);
+
+ file = nautilus_file_get_by_uri (url);
+ changed_files = g_list_prepend (NULL, file);
+
+ g_signal_emit_by_name (self, "favorites-changed", changed_files);
+
+ nautilus_file_list_free (changed_files);
+
+ tracker_sparql_cursor_next_async (cursor,
+ self->cancellable,
+ on_get_favorite_files_cursor_callback,
+ self);
+}
+
+static void
+on_get_favorite_files_query_callback (GObject *object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ NautilusTagManager *self;
+
+ self = NAUTILUS_TAG_MANAGER (user_data);
+
+ on_query_callback (object,
+ result,
+ user_data,
+ on_get_favorite_files_cursor_callback,
+ GET_FAVORITE_FILES,
+ self->cancellable);
+}
+
+static void
+nautilus_tag_manager_query_favorite_files (NautilusTagManager *self,
+ GCancellable *cancellable)
+{
+ GString *query;
+
+ self->cancellable = cancellable;
+
+ query = g_string_new ("SELECT ?url tracker:id(?urn) WHERE { ?urn nie:url ?url ; nao:hasTag nao:predefined-tag-favorite}");
+
+ start_query_or_update (query,
+ on_get_favorite_files_query_callback,
+ self,
+ TRUE,
+ cancellable);
+
+ g_string_free (query, TRUE);
+}
+
+static gpointer
+nautilus_tag_manager_gpointer_task_finish (GObject *source_object,
+ GAsyncResult *res,
+ GError **error)
+{
+ g_return_val_if_fail (g_task_is_valid (res, source_object), FALSE);
+
+ return g_task_propagate_pointer (G_TASK (res), error);
+}
+
+static GString*
+nautilus_tag_manager_delete_tag (NautilusTagManager *self,
+ GList *selection,
+ GString *query)
+{
+
+ g_string_append (query,
+ "DELETE { ?urn nao:hasTag nao:predefined-tag-favorite }"
+ "WHERE { ?urn a nfo:FileDataObject ; nie:url ?url .");
+
+ query = add_selection_filter (selection, query);
+
+ g_string_append (query, "}\n");
+
+ return query;
+}
+
+static GString*
+nautilus_tag_manager_insert_tag (NautilusTagManager *self,
+ GList *selection,
+ GString *query)
+{
+ g_string_append (query,
+ "INSERT { ?urn nao:hasTag nao:predefined-tag-favorite }"
+ "WHERE { ?urn a nfo:FileDataObject ; nie:url ?url .");
+
+ query = add_selection_filter (selection, query);
+
+ g_string_append (query, "}\n");
+
+ return query;
+}
+
+gboolean
+nautilus_tag_manager_file_is_favorite (NautilusTagManager *self,
+ const gchar *file_name)
+{
+ return g_hash_table_contains (self->favorite_files, file_name);
+}
+
+static void
+on_get_file_ids_for_urls_cursor_callback (GObject *object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ TrackerSparqlCursor *cursor;
+ GTask *task;
+ gint64 *id;
+ const gchar *url;
+ gboolean success;
+ GList *l;
+ gchar *file_url;
+ InsertTaskData *data;
+
+ task = user_data;
+ data = g_task_get_task_data (task);
+
+ cursor = TRACKER_SPARQL_CURSOR (object);
+
+ success = get_query_status (cursor, result, GET_IDS_FOR_URLS, task);
+ if (!success)
+ {
+ return;
+ }
+
+ id = g_new0 (gint64, 1);
+
+ url = tracker_sparql_cursor_get_string (cursor, 0, NULL);
+ *id = tracker_sparql_cursor_get_integer (cursor, 1);
+
+ for (l = data->selection; l != NULL; l = l->next)
+ {
+ file_url = nautilus_file_get_uri (NAUTILUS_FILE (l->data));
+
+ if (g_strcmp0 (file_url, url) == 0)
+ {
+ g_hash_table_insert (data->ids,
+ g_strdup (url),
+ id);
+
+ g_free (file_url);
+
+ break;
+ }
+
+ g_free (file_url);
+ }
+
+ tracker_sparql_cursor_next_async (cursor,
+ g_task_get_cancellable (task),
+ on_get_file_ids_for_urls_cursor_callback,
+ task);
+
+}
+
+
+static void
+on_get_file_ids_for_urls_query_callback (GObject *object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ GTask *task;
+
+ task = user_data;
+
+ on_query_callback (object,
+ result,
+ user_data,
+ on_get_file_ids_for_urls_cursor_callback,
+ GET_IDS_FOR_URLS,
+ g_task_get_cancellable (task));
+}
+
+static void
+nautilus_tag_manager_get_file_ids_for_urls (GObject *object,
+ GList *selection,
+ GTask *task)
+{
+ GString *query;
+
+ query = g_string_new ("SELECT ?url tracker:id(?urn) WHERE { ?urn nie:url ?url; .");
+
+ query = add_selection_filter (selection, query);
+
+ g_string_append (query, "}\n");
+
+ start_query_or_update (query,
+ on_get_file_ids_for_urls_query_callback,
+ task,
+ TRUE,
+ g_task_get_cancellable (task));
+
+ g_string_free (query, TRUE);
+}
+
+static void
+on_star_files_callback (GObject *object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ NautilusTagManager *self;
+ GString *query;
+ InsertTaskData *data;
+ g_autoptr (GError) error = NULL;
+ GTask *task;
+ UpdateData *update_data;
+
+ self = NAUTILUS_TAG_MANAGER (object);
+
+ data = nautilus_tag_manager_gpointer_task_finish (object, res, &error);
+
+ task = g_task_new (data->object, data->cancellable, data->callback, NULL);
+
+ query = g_string_new ("");
+
+ query = nautilus_tag_manager_insert_tag (self,
+ data->selection,
+ query);
+
+ update_data = g_new0 (UpdateData, 1);
+ update_data->task = task;
+ update_data->tag_manager = self;
+ update_data->selection = nautilus_file_list_copy (data->selection);
+ update_data->star = TRUE;
+ update_data->ids = data->ids;
+
+ /* the ids hash table is now owned by the update_data,
+ * so it will be freed by it.
+ */
+ destroy_insert_task_data (data);
+
+ start_query_or_update (query,
+ on_update_callback,
+ update_data,
+ FALSE,
+ g_task_get_cancellable (task));
+
+ g_string_free (query, TRUE);
+}
+
+void
+nautilus_tag_manager_star_files (NautilusTagManager *self,
+ GObject *object,
+ GList *selection,
+ GAsyncReadyCallback callback,
+ GCancellable *cancellable)
+{
+ GTask *task;
+ InsertTaskData *data;
+
+ data = g_new0 (InsertTaskData, 1);
+ data->selection = nautilus_file_list_copy (selection);
+ data->ids = g_hash_table_new_full (g_str_hash,
+ g_str_equal,
+ (GDestroyNotify) g_free,
+ (GDestroyNotify) g_free);
+ data->callback = callback;
+ data->object = object;
+ data->cancellable = cancellable;
+
+ task = g_task_new (self, cancellable, on_star_files_callback, NULL);
+ g_task_set_task_data (task,
+ data,
+ NULL);
+
+ nautilus_tag_manager_get_file_ids_for_urls (G_OBJECT (self), selection, task);
+}
+
+void
+nautilus_tag_manager_unstar_files (NautilusTagManager *self,
+ GObject *object,
+ GList *selection,
+ GAsyncReadyCallback callback,
+ GCancellable *cancellable)
+{
+ GString *query;
+ GTask *task;
+ NautilusFile *file;
+ UpdateData *update_data;
+
+ file = NAUTILUS_FILE (selection->data);
+
+ task = g_task_new (object, cancellable, callback, NULL);
+
+ query = g_string_new ("");
+
+ query = nautilus_tag_manager_delete_tag (self,
+ selection,
+ query);
+
+ update_data = g_new0 (UpdateData, 1);
+ update_data->task = task;
+ update_data->tag_manager = self;
+ update_data->selection = nautilus_file_list_copy (selection);
+ update_data->star = FALSE;
+
+ start_query_or_update (query,
+ on_update_callback,
+ update_data,
+ FALSE,
+ cancellable);
+
+ g_string_free (query, TRUE);
+}
+
+void
+on_tracker_notifier_events(TrackerNotifier *notifier,
+ GPtrArray *events,
+ gpointer user_data)
+{
+ TrackerNotifierEvent *event;
+ NautilusTagManager *self;
+ int i;
+ const gchar *location_uri;
+ const gchar *new_location_uri;
+ GError *error = NULL;
+ TrackerSparqlConnection *connection;
+ TrackerSparqlCursor *cursor;
+ GString *query;
+ gboolean query_has_results;
+ gint64 *id;
+ GList *changed_files;
+ NautilusFile *file;
+
+ self = NAUTILUS_TAG_MANAGER (user_data);
+
+ for (i = 0; i < events->len; i++)
+ {
+ event = g_ptr_array_index (events, i);
+
+ location_uri = tracker_notifier_event_get_location (event);
+
+ query = g_string_new ("");
+ g_string_append_printf (query,
+ "SELECT ?url WHERE { ?urn nie:url ?url; nao:hasTag nao:predefined-tag-favorite . FILTER (tracker:id(?urn) = %ld)}",
+ tracker_notifier_event_get_id (event));
+
+ /* check if the file changed it's location and update hash table if so */
+ new_location_uri = nautilus_tag_manager_file_with_id_changed_url (self->favorite_files,
+ tracker_notifier_event_get_id (event),
+ location_uri);
+ if (new_location_uri)
+ {
+ id = g_new0 (gint64, 1);
+ *id = tracker_notifier_event_get_id (event);
+
+ g_hash_table_remove (self->favorite_files, new_location_uri);
+ g_hash_table_insert (self->favorite_files,
+ g_strdup (location_uri),
+ id);
+
+ file = nautilus_file_get_by_uri (location_uri);
+ changed_files = g_list_prepend (NULL, file);
+
+ g_signal_emit_by_name (self, "favorites-changed", changed_files);
+
+ nautilus_file_list_free (changed_files);
+ }
+
+ connection = tracker_sparql_connection_get (NULL, &error);
+
+ if (!connection)
+ {
+ g_printerr ("Couldn't obtain a direct connection to the Tracker store: %s",
+ error ? error->message : "unknown error");
+ g_clear_error (&error);
+
+ return;
+ }
+
+ cursor = tracker_sparql_connection_query (connection,
+ query->str,
+ NULL,
+ &error);
+
+ if (error)
+ {
+ g_printerr ("Couldn't query the Tracker Store: '%s'", error->message);
+
+ g_clear_error (&error);
+
+ return;
+ }
+
+ if (cursor)
+ {
+ query_has_results = tracker_sparql_cursor_next (cursor, NULL, &error);
+
+ /* if no results are found, then the file isn't marked as favorite.
+ * If needed, update the hashtable.
+ */
+ if (!query_has_results && location_uri && g_hash_table_contains (self->favorite_files, location_uri))
+ {
+ g_hash_table_remove (self->favorite_files, location_uri);
+
+ file = nautilus_file_get_by_uri (location_uri);
+ changed_files = g_list_prepend (NULL, file);
+
+ g_signal_emit_by_name (self, "favorites-changed", changed_files);
+
+ nautilus_file_list_free (changed_files);
+ }
+ else if (query_has_results && location_uri && !g_hash_table_contains (self->favorite_files, location_uri))
+ {
+ id = g_new0 (gint64, 1);
+ *id = tracker_notifier_event_get_id (event);
+
+ g_hash_table_insert (self->favorite_files,
+ g_strdup (location_uri),
+ id);
+
+ file = nautilus_file_get_by_uri (location_uri);
+ changed_files = g_list_prepend (NULL, file);
+
+ g_signal_emit_by_name (self, "favorites-changed", changed_files);
+
+ nautilus_file_list_free (changed_files);
+ }
+
+ g_object_unref (cursor);
+ }
+
+ g_object_unref (connection);
+
+ g_string_free (query, TRUE);
+ }
+
+}
+
+static void
+nautilus_tag_manager_finalize (GObject *object)
+{
+ NautilusTagManager *self;
+
+ self = NAUTILUS_TAG_MANAGER (object);
+
+ g_signal_handlers_disconnect_by_func (self->notifier,
+ G_CALLBACK (on_tracker_notifier_events),
+ self);
+ g_clear_object (&self->notifier);
+
+ g_hash_table_destroy (self->favorite_files);
+
+ G_OBJECT_CLASS (nautilus_tag_manager_parent_class)->finalize (object);
+}
+
+static void
+nautilus_tag_manager_class_init (NautilusTagManagerClass *klass)
+{
+ GObjectClass *oclass;
+
+ oclass = G_OBJECT_CLASS (klass);
+
+ oclass->finalize = nautilus_tag_manager_finalize;
+
+ signals[FAVORITES_CHANGED] = g_signal_new ("favorites-changed",
+ NAUTILUS_TYPE_TAG_MANAGER,
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL,
+ NULL,
+ g_cclosure_marshal_VOID__POINTER,
+ G_TYPE_NONE,
+ 1,
+ G_TYPE_POINTER);
+}
+
+NautilusTagManager* nautilus_tag_manager_get ()
+{
+ if (tag_manager != NULL)
+ {
+ return g_object_ref (tag_manager);
+ }
+
+ tag_manager = g_object_new (NAUTILUS_TYPE_TAG_MANAGER, NULL);
+ g_object_add_weak_pointer (G_OBJECT (tag_manager), (gpointer)&tag_manager);
+
+ return tag_manager;
+}
+
+void nautilus_tag_manager_set_cancellable (NautilusTagManager *tag_manager,
+ GCancellable *cancellable)
+{
+ nautilus_tag_manager_query_favorite_files (tag_manager, cancellable);
+
+ tag_manager->notifier = tracker_notifier_new (NULL,
+ TRACKER_NOTIFIER_FLAG_QUERY_LOCATION,
+ cancellable,
+ &tag_manager->notifier_error);
+
+ g_signal_connect (tag_manager->notifier,
+ "events",
+ G_CALLBACK (on_tracker_notifier_events),
+ tag_manager);
+
+}
+
+static void
+nautilus_tag_manager_init (NautilusTagManager *self)
+{
+ self->favorite_files = g_hash_table_new_full (g_str_hash,
+ g_str_equal,
+ (GDestroyNotify) g_free,
+ (GDestroyNotify) g_free);
+}