/* * moohistorymgr.h * * Copyright (C) 2004-2010 by Yevgen Muntyan * * This file is part of medit. medit 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; either version 2.1 of the License, * or (at your option) any later version. * * You should have received a copy of the GNU Lesser General Public * License along with medit. If not, see . */ /** * class:MooHistoryMgr: (parent GObject) (moo.private 1) **/ #include "mooutils/moohistorymgr.h" #include "mooutils/moofileicon.h" #include "mooutils/mooapp-ipc.h" #include "mooutils/moofilewatch.h" #include "mooutils/mooutils-misc.h" #include "mooutils/mooutils-gobject.h" #include "mooutils/mooutils-fs.h" #include "mooutils/mooutils-treeview.h" #include "mooutils/mooprefs.h" #include "mooutils/moomarkup.h" #include "mooutils/mooutils-thread.h" #include "mooutils/moolist.h" #include "mooutils/mootype-macros.h" #include "marshals.h" #include #include #define N_MENU_ITEMS 10 #define MAX_ITEM_NUMBER 5000 #define IPC_ID "MooHistoryMgr" MOO_DEFINE_DLIST (WidgetList, widget_list, GtkWidget) MOO_DEFINE_QUEUE (MooHistoryItem, moo_history_item) MOO_DEFINE_QUARK (moo-history-mgr-parse-error, moo_history_mgr_parse_error_quark) struct _MooHistoryMgrPrivate { char *filename; char *basename; char *name; char *ipc_id; guint save_idle; guint update_widgets_idle; WidgetList *widgets; MooHistoryItemQueue *files; GHashTable *hash; guint loaded : 1; }; typedef struct { MooHistoryCallback callback; gpointer data; GDestroyNotify notify; } CallbackData; struct _MooHistoryItem { char *uri; GData *data; MooFileIcon *icon; }; typedef enum { UPDATE_ITEM_UPDATE, UPDATE_ITEM_REMOVE, UPDATE_ITEM_ADD } UpdateType; static GObject *moo_history_mgr_constructor (GType type, guint n_props, GObjectConstructParam *props); static void moo_history_mgr_dispose (GObject *object); static void moo_history_mgr_set_property(GObject *object, guint property_id, const GValue *value, GParamSpec *pspec); static void moo_history_mgr_get_property(GObject *object, guint property_id, GValue *value, GParamSpec *pspec); static const char *get_filename (MooHistoryMgr *mgr); static const char *get_basename (MooHistoryMgr *mgr); static void ensure_files (MooHistoryMgr *mgr); static void schedule_save (MooHistoryMgr *mgr); static void moo_history_mgr_save (MooHistoryMgr *mgr); static void populate_menu (MooHistoryMgr *mgr, GtkWidget *menu); static void schedule_update_widgets (MooHistoryMgr *mgr); static void moo_history_item_format (MooHistoryItem *item, GString *buffer); static gboolean moo_history_item_equal (MooHistoryItem *item1, MooHistoryItem *item2); static MooFileIcon *moo_history_item_get_icon (MooHistoryItem *item); static char *uri_get_basename (const char *uri); static char *uri_get_display_name (const char *uri); static void ipc_callback (GObject *obj, const char *data, gsize len); static void ipc_notify_add_file (MooHistoryMgr *mgr, MooHistoryItem *item); static void ipc_notify_update_file (MooHistoryMgr *mgr, MooHistoryItem *item); static void ipc_notify_remove_file (MooHistoryMgr *mgr, MooHistoryItem *item); G_DEFINE_TYPE (MooHistoryMgr, moo_history_mgr, G_TYPE_OBJECT) enum { PROP_0, PROP_NAME, PROP_EMPTY }; enum { CHANGED, N_SIGNALS }; static guint signals[N_SIGNALS]; static void moo_history_mgr_class_init (MooHistoryMgrClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); g_type_class_add_private (klass, sizeof (MooHistoryMgrPrivate)); object_class->constructor = moo_history_mgr_constructor; object_class->set_property = moo_history_mgr_set_property; object_class->get_property = moo_history_mgr_get_property; object_class->dispose = moo_history_mgr_dispose; g_object_class_install_property (object_class, PROP_NAME, g_param_spec_string ("name", "name", "name", NULL, (GParamFlags) (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY))); g_object_class_install_property (object_class, PROP_EMPTY, g_param_spec_boolean ("empty", "empty", "empty", TRUE, G_PARAM_READABLE)); signals[CHANGED] = _moo_signal_new_cb ("changed", MOO_TYPE_HISTORY_MGR, G_SIGNAL_RUN_LAST, G_CALLBACK (schedule_update_widgets), NULL, NULL, _moo_marshal_VOID__VOID, G_TYPE_NONE, 0); } static void moo_history_mgr_init (MooHistoryMgr *mgr) { mgr->priv = G_TYPE_INSTANCE_GET_PRIVATE (mgr, MOO_TYPE_HISTORY_MGR, MooHistoryMgrPrivate); mgr->priv->filename = NULL; mgr->priv->basename = NULL; mgr->priv->files = moo_history_item_queue_new (); mgr->priv->hash = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); mgr->priv->loaded = FALSE; } static GObject * moo_history_mgr_constructor (GType type, guint n_props, GObjectConstructParam *props) { GObject *object; MooHistoryMgr *mgr; object = G_OBJECT_CLASS (moo_history_mgr_parent_class)-> constructor (type, n_props, props); mgr = MOO_HISTORY_MGR (object); if (mgr->priv->name) { mgr->priv->ipc_id = g_strdup_printf (IPC_ID "/%s", mgr->priv->name); moo_ipc_register_client (G_OBJECT (mgr), mgr->priv->ipc_id, ipc_callback); } return object; } static void moo_history_mgr_dispose (GObject *object) { MooHistoryMgr *mgr = MOO_HISTORY_MGR (object); if (mgr->priv) { moo_history_mgr_shutdown (mgr); if (mgr->priv->files) { moo_history_item_queue_foreach (mgr->priv->files, (MooHistoryItemListFunc) moo_history_item_free, NULL); moo_history_item_queue_free_links (mgr->priv->files); g_hash_table_destroy (mgr->priv->hash); } g_free (mgr->priv->name); g_free (mgr->priv->filename); g_free (mgr->priv->basename); mgr->priv = NULL; } G_OBJECT_CLASS (moo_history_mgr_parent_class)->dispose (object); } void moo_history_mgr_shutdown (MooHistoryMgr *mgr) { g_return_if_fail (MOO_IS_HISTORY_MGR (mgr)); if (!mgr->priv) return; if (mgr->priv->ipc_id) { moo_ipc_unregister_client (G_OBJECT (mgr), mgr->priv->ipc_id); g_free (mgr->priv->ipc_id); mgr->priv->ipc_id = NULL; } if (mgr->priv->save_idle) { g_source_remove (mgr->priv->save_idle); mgr->priv->save_idle = 0; moo_history_mgr_save (mgr); } if (mgr->priv->update_widgets_idle) { g_source_remove (mgr->priv->update_widgets_idle); mgr->priv->update_widgets_idle = 0; } while (mgr->priv->widgets) gtk_widget_destroy (mgr->priv->widgets->data); } static void moo_history_mgr_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { MooHistoryMgr *mgr = MOO_HISTORY_MGR (object); switch (prop_id) { case PROP_NAME: MOO_ASSIGN_STRING (mgr->priv->name, g_value_get_string (value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static void moo_history_mgr_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { MooHistoryMgr *mgr = MOO_HISTORY_MGR (object); switch (prop_id) { case PROP_NAME: g_value_set_string (value, mgr->priv->name); break; case PROP_EMPTY: g_value_set_boolean (value, moo_history_mgr_get_n_items (mgr) == 0); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static const char * get_basename (MooHistoryMgr *mgr) { if (!mgr->priv->basename) { if (mgr->priv->name) { char *name = g_ascii_strdown (mgr->priv->name, -1); mgr->priv->basename = g_strdup_printf ("recent-files-%s.xml", name); g_free (name); } else { mgr->priv->basename = g_strdup ("recent-files.xml"); } } return mgr->priv->basename; } static const char * get_filename (MooHistoryMgr *mgr) { if (!mgr->priv->filename) mgr->priv->filename = moo_get_user_cache_file (get_basename (mgr)); return mgr->priv->filename; } char * _moo_history_mgr_get_filename (MooHistoryMgr *mgr) { g_return_val_if_fail (MOO_IS_HISTORY_MGR (mgr), NULL); return g_strdup (get_filename (mgr)); } /*****************************************************************/ /* Loading and saving */ #define ELM_ROOT "md-recent-files" #define ELM_UPDATE "md-recent-files-update" #define ELM_ITEM "item" #define ELM_DATA "data" #define PROP_VERSION "version" #define PROP_VERSION_VALUE "1.0" #define PROP_URI "uri" #define PROP_KEY "key" #define PROP_VALUE "value" #define PROP_TYPE "type" #define ELM_RECENT_ITEMS "recent-items" static void add_file (MooHistoryMgr *mgr, MooHistoryItem *item) { const char *uri; uri = moo_history_item_get_uri (item); if (g_hash_table_lookup (mgr->priv->hash, uri) != NULL) { g_critical ("duplicated uri '%s'", uri); moo_history_item_free (item); return; } moo_history_item_queue_push_tail (mgr->priv->files, item); g_hash_table_insert (mgr->priv->hash, g_strdup (uri), mgr->priv->files->tail); } static gboolean parse_element (const char *filename, MooMarkupNode *elm, MooHistoryItem **item_p) { const char *uri; MooHistoryItem *item; MooMarkupNode *child; if (strcmp (elm->name, ELM_ITEM) != 0) { g_critical ("in file '%s': invalid element '%s'", filename ? filename : "NONE", elm->name); return FALSE; } if (!(uri = moo_markup_get_prop (elm, PROP_URI))) { g_critical ("in file '%s': attribute '%s' missing", filename ? filename : "NONE", PROP_URI); return FALSE; } item = moo_history_item_new (uri, NULL); g_return_val_if_fail (item != NULL, FALSE); for (child = elm->children; child != NULL; child = child->next) { const char *key, *value; if (!MOO_MARKUP_IS_ELEMENT (child)) continue; if (strcmp (child->name, ELM_DATA) != 0) { g_critical ("in file '%s': invalid element '%s'", filename ? filename : "NONE", child->name); continue; } key = moo_markup_get_prop (child, PROP_KEY); value = moo_markup_get_prop (child, PROP_VALUE); if (!key || !key[0]) { g_critical ("in file '%s': attribute '%s' missing", filename ? filename : "NONE", PROP_KEY); continue; } else if (!value || !value[0]) { g_critical ("in file '%s': attribute '%s' missing", filename ? filename : "NONE", PROP_VALUE); continue; } moo_history_item_set (item, key, value); } *item_p = item; return TRUE; } typedef enum { ELEMENT_NONE = 0, ELEMENT_ROOT, ELEMENT_ITEM, ELEMENT_DATA } Element; typedef struct { gboolean seen_root; Element element; MooHistoryItem *item; MooHistoryMgr *mgr; } ParserData; static void start_element_root (const gchar *element_name, const gchar **attribute_names, const gchar **attribute_values, ParserData *data, GError **error) { gboolean seen_version = FALSE; const char **p, **v; if (data->seen_root) { g_set_error (error, MOO_HISTORY_MGR_PARSE_ERROR, MOO_HISTORY_MGR_PARSE_ERROR_INVALID_CONTENT, "invalid element '%s'", element_name); return; } if (strcmp (element_name, ELM_ROOT) != 0) { g_set_error (error, MOO_HISTORY_MGR_PARSE_ERROR, MOO_HISTORY_MGR_PARSE_ERROR_INVALID_CONTENT, "invalid element '%s'", element_name); return; } for (p = attribute_names, v = attribute_values; p && *p; p++, v++) { if (seen_version || strcmp (*p, PROP_VERSION) != 0) { g_set_error (error, MOO_HISTORY_MGR_PARSE_ERROR, MOO_HISTORY_MGR_PARSE_ERROR_INVALID_CONTENT, "invalid attribute '%s'", *p); return; } if (strcmp (*v, PROP_VERSION_VALUE) != 0) { g_set_error (error, MOO_HISTORY_MGR_PARSE_ERROR, MOO_HISTORY_MGR_PARSE_ERROR_INVALID_CONTENT, "invalid version value '%s'", *v); return; } seen_version = TRUE; } if (!seen_version) { g_set_error (error, MOO_HISTORY_MGR_PARSE_ERROR, MOO_HISTORY_MGR_PARSE_ERROR_INVALID_CONTENT, "version attribute missing"); return; } data->seen_root = TRUE; data->element = ELEMENT_ROOT; } static void start_element_item (const gchar *element_name, const gchar **attribute_names, const gchar **attribute_values, ParserData *data, GError **error) { MooHistoryItem *item = NULL; const char **p, **v; if (strcmp (element_name, ELM_ITEM) != 0) { g_set_error (error, MOO_HISTORY_MGR_PARSE_ERROR, MOO_HISTORY_MGR_PARSE_ERROR_INVALID_CONTENT, "invalid element '%s'", element_name); return; } data->element = ELEMENT_ITEM; for (p = attribute_names, v = attribute_values; p && *p; p++, v++) { if (strcmp (*p, PROP_URI) != 0) { g_set_error (error, MOO_HISTORY_MGR_PARSE_ERROR, MOO_HISTORY_MGR_PARSE_ERROR_INVALID_CONTENT, "invalid attribute '%s'", *v); return; } if (item != NULL) { g_set_error (error, MOO_HISTORY_MGR_PARSE_ERROR, MOO_HISTORY_MGR_PARSE_ERROR_INVALID_CONTENT, "duplicate attribute '%s'", *v); return; } item = moo_history_item_new (*v, NULL); g_return_if_fail (item != NULL); } if (!item) { g_set_error (error, MOO_HISTORY_MGR_PARSE_ERROR, MOO_HISTORY_MGR_PARSE_ERROR_INVALID_CONTENT, "missing attribute '%s'", PROP_URI); return; } data->item = item; } static void start_element_data (const gchar *element_name, const gchar **attribute_names, const gchar **attribute_values, ParserData *data, GError **error) { const char **p, **v; const char *key = NULL; const char *value = NULL; g_return_if_fail (data->item != NULL); if (strcmp (element_name, ELM_DATA) != 0) { g_set_error (error, MOO_HISTORY_MGR_PARSE_ERROR, MOO_HISTORY_MGR_PARSE_ERROR_INVALID_CONTENT, "invalid element '%s'", element_name); return; } data->element = ELEMENT_DATA; for (p = attribute_names, v = attribute_values; p && *p; p++, v++) { if (strcmp (*p, PROP_KEY) == 0) { key = *v; } else if (strcmp (*p, PROP_VALUE) == 0) { value = *v; } else { g_set_error (error, MOO_HISTORY_MGR_PARSE_ERROR, MOO_HISTORY_MGR_PARSE_ERROR_INVALID_CONTENT, "invalid attribute '%s'", *v); return; } } if (!key || !key[0]) { g_set_error (error, MOO_HISTORY_MGR_PARSE_ERROR, MOO_HISTORY_MGR_PARSE_ERROR_INVALID_CONTENT, "missing attribute '%s'", PROP_KEY); return; } if (!value) { g_set_error (error, MOO_HISTORY_MGR_PARSE_ERROR, MOO_HISTORY_MGR_PARSE_ERROR_INVALID_CONTENT, "missing attribute '%s'", PROP_VALUE); return; } moo_history_item_set (data->item, key, value); } static void parser_start_element (G_GNUC_UNUSED GMarkupParseContext *context, const gchar *element_name, const gchar **attribute_names, const gchar **attribute_values, ParserData *data, GError **error) { switch (data->element) { case ELEMENT_NONE: start_element_root (element_name, attribute_names, attribute_values, data, error); break; case ELEMENT_ROOT: start_element_item (element_name, attribute_names, attribute_values, data, error); break; case ELEMENT_ITEM: start_element_data (element_name, attribute_names, attribute_values, data, error); break; case ELEMENT_DATA: g_set_error (error, MOO_HISTORY_MGR_PARSE_ERROR, MOO_HISTORY_MGR_PARSE_ERROR_INVALID_CONTENT, "invalid element '%s'", element_name); break; } } static void parser_end_element (G_GNUC_UNUSED GMarkupParseContext *context, G_GNUC_UNUSED const gchar *element_name, ParserData *data, G_GNUC_UNUSED GError **error) { switch (data->element) { case ELEMENT_ROOT: data->element = ELEMENT_NONE; break; case ELEMENT_ITEM: data->element = ELEMENT_ROOT; if (data->item) { add_file (data->mgr, data->item); data->item = NULL; } break; case ELEMENT_DATA: data->element = ELEMENT_ITEM; break; case ELEMENT_NONE: g_assert_not_reached (); break; } } static void load_file (MooHistoryMgr *mgr) { const char *filename; GMarkupParser parser = {0}; ParserData data = {0}; GError *error = NULL; mgr->priv->loaded = TRUE; filename = get_filename (mgr); g_return_if_fail (filename != NULL); if (!g_file_test (filename, G_FILE_TEST_EXISTS)) return; parser.start_element = (MooMarkupStartElementFunc) parser_start_element; parser.end_element = (MooMarkupEndElementFunc) parser_end_element; data.seen_root = FALSE; data.element = ELEMENT_NONE; data.item = NULL; data.mgr = mgr; if (!moo_parse_markup_file (filename, &parser, &data, &error)) { g_critical ("could not load file '%s': %s", filename, moo_error_message (error)); g_error_free (error); } if (data.item) moo_history_item_free (data.item); } static gboolean parse_update_item (MooMarkupDoc *xml, MooHistoryItem **item, UpdateType *type) { const char *version; const char *update_type_string; MooMarkupNode *root, *child; if (!(root = moo_markup_get_root_element (xml, ELM_UPDATE))) { g_critical ("missing element %s", ELM_UPDATE); return FALSE; } if (!(version = moo_markup_get_prop (root, PROP_VERSION)) || strcmp (version, PROP_VERSION_VALUE) != 0) { g_critical ("invalid version value '%s'", version ? version : "(null)"); return FALSE; } if (!(update_type_string = moo_markup_get_prop (root, PROP_TYPE))) { g_critical ("attribute '%s' missing", PROP_TYPE); return FALSE; } if (strcmp (update_type_string, "add") == 0) *type = UPDATE_ITEM_ADD; else if (strcmp (update_type_string, "remove") == 0) *type = UPDATE_ITEM_REMOVE; else if (strcmp (update_type_string, "update") == 0) *type = UPDATE_ITEM_UPDATE; else { g_critical ("invalid value '%s' for attribute '%s'", update_type_string, PROP_TYPE); return FALSE; } for (child = root->children; child != NULL; child = child->next) if (MOO_MARKUP_IS_ELEMENT (child)) return parse_element (NULL, child, item); g_critical ("element '%s' missing", ELM_ITEM); return FALSE; } static void ensure_files (MooHistoryMgr *mgr) { if (!mgr->priv->loaded) load_file (mgr); } static gboolean save_in_idle (MooHistoryMgr *mgr) { mgr->priv->save_idle = 0; moo_history_mgr_save (mgr); return FALSE; } static void schedule_save (MooHistoryMgr *mgr) { if (!mgr->priv->save_idle) mgr->priv->save_idle = g_idle_add ((GSourceFunc) save_in_idle, mgr); } static void moo_history_mgr_save (MooHistoryMgr *mgr) { const char *filename; GError *error = NULL; MooFileWriter *writer; g_return_if_fail (MOO_IS_HISTORY_MGR (mgr)); if (!mgr->priv->files) return; filename = get_filename (mgr); if (!mgr->priv->files->length) { _moo_unlink (filename); return; } if ((writer = moo_config_writer_new (filename, FALSE, &error))) { GString *string; MooHistoryItemList *l; moo_file_writer_write (writer, "\n", -1); moo_file_writer_write (writer, "<" ELM_ROOT " " PROP_VERSION "=\"" PROP_VERSION_VALUE "\">\n", -1); string = g_string_new (NULL); for (l = mgr->priv->files->head; l != NULL; l = l->next) { MooHistoryItem *item = l->data; g_string_truncate (string, 0); moo_history_item_format (item, string); if (!moo_file_writer_write (writer, string->str, -1)) break; } g_string_free (string, TRUE); moo_file_writer_write (writer, "\n", -1); moo_file_writer_close (writer, &error); } if (error) { g_critical ("could not save file '%s': %s", filename, moo_error_message (error)); g_error_free (error); } } static char * format_for_update (MooHistoryItem *item, UpdateType type) { GString *buffer; const char *update_types[3] = {"update", "remove", "add"}; g_return_val_if_fail (type < 3, NULL); buffer = g_string_new (NULL); g_string_append_printf (buffer, "<%s %s=\"%s\" %s=\"%s\">\n", ELM_UPDATE, PROP_VERSION, PROP_VERSION_VALUE, PROP_TYPE, update_types[type]); moo_history_item_format (item, buffer); g_string_append (buffer, "\n"); return g_string_free (buffer, FALSE); } guint moo_history_mgr_get_n_items (MooHistoryMgr *mgr) { g_return_val_if_fail (MOO_IS_HISTORY_MGR (mgr), 0); ensure_files (mgr); return mgr->priv->files->length; } void moo_history_mgr_add_uri (MooHistoryMgr *mgr, const char *uri) { MooHistoryItem *freeme = NULL; MooHistoryItem *item; g_return_if_fail (MOO_IS_HISTORY_MGR (mgr)); g_return_if_fail (uri && uri[0]); if (!(item = moo_history_mgr_find_uri (mgr, uri))) { freeme = moo_history_item_new (uri, NULL); item = freeme; } moo_history_mgr_add_file (mgr, item); moo_history_item_free (freeme); } static void moo_history_mgr_add_file_real (MooHistoryMgr *mgr, MooHistoryItem *item, gboolean notify) { const char *uri; MooHistoryItemList *link; MooHistoryItem *new_item = NULL; g_return_if_fail (MOO_IS_HISTORY_MGR (mgr)); g_return_if_fail (item != NULL); ensure_files (mgr); uri = moo_history_item_get_uri (item); link = (MooHistoryItemList*) g_hash_table_lookup (mgr->priv->hash, uri); if (!link) { MooHistoryItem *copy = moo_history_item_copy (item); moo_history_item_queue_push_head (mgr->priv->files, copy); new_item = copy; g_hash_table_insert (mgr->priv->hash, g_strdup (uri), mgr->priv->files->head); } else if (link != mgr->priv->files->head || !moo_history_item_equal (item, link->data)) { MooHistoryItem *tmp = link->data; moo_history_item_queue_unlink (mgr->priv->files, link); moo_history_item_queue_push_head_link (mgr->priv->files, link); new_item = link->data = moo_history_item_copy (item); moo_history_item_free (tmp); } if (new_item) { g_signal_emit (mgr, signals[CHANGED], 0); if (notify) { schedule_save (mgr); ipc_notify_add_file (mgr, new_item); } if (mgr->priv->files->length == 1) g_object_notify (G_OBJECT (mgr), "empty"); } if (mgr->priv->files->length > MAX_ITEM_NUMBER) moo_history_mgr_remove_uri (mgr, moo_history_item_get_uri (mgr->priv->files->tail->data)); } void moo_history_mgr_add_file (MooHistoryMgr *mgr, MooHistoryItem *item) { moo_history_mgr_add_file_real (mgr, item, TRUE); } static void moo_history_mgr_update_file_real (MooHistoryMgr *mgr, MooHistoryItem *file, gboolean notify) { const char *uri; MooHistoryItemList *link; g_return_if_fail (MOO_IS_HISTORY_MGR (mgr)); g_return_if_fail (file != NULL); ensure_files (mgr); uri = moo_history_item_get_uri (file); link = (MooHistoryItemList*) g_hash_table_lookup (mgr->priv->hash, uri); if (!link) { moo_history_mgr_add_file (mgr, file); } else if (!moo_history_item_equal (link->data, file)) { MooHistoryItem *tmp = link->data; link->data = moo_history_item_copy (file); moo_history_item_free (tmp); g_signal_emit (mgr, signals[CHANGED], 0); if (notify) { schedule_save (mgr); ipc_notify_update_file (mgr, link->data); } } } void moo_history_mgr_update_file (MooHistoryMgr *mgr, MooHistoryItem *file) { moo_history_mgr_update_file_real (mgr, file, TRUE); } MooHistoryItem * moo_history_mgr_find_uri (MooHistoryMgr *mgr, const char *uri) { MooHistoryItemList *link; g_return_val_if_fail (MOO_IS_HISTORY_MGR (mgr), NULL); g_return_val_if_fail (uri != NULL, NULL); ensure_files (mgr); link = (MooHistoryItemList*) g_hash_table_lookup (mgr->priv->hash, uri); return link ? link->data : NULL; } static void moo_history_mgr_remove_uri_real (MooHistoryMgr *mgr, const char *uri, gboolean notify) { MooHistoryItemList *link; MooHistoryItem *item; g_return_if_fail (MOO_IS_HISTORY_MGR (mgr)); g_return_if_fail (uri != NULL); ensure_files (mgr); link = (MooHistoryItemList*) g_hash_table_lookup (mgr->priv->hash, uri); if (!link) return; item = link->data; g_hash_table_remove (mgr->priv->hash, uri); moo_history_item_queue_delete_link (mgr->priv->files, link); g_signal_emit (mgr, signals[CHANGED], 0); if (notify) { schedule_save (mgr); ipc_notify_remove_file (mgr, item); } if (mgr->priv->files->length == 0) g_object_notify (G_OBJECT (mgr), "empty"); moo_history_item_free (item); } void moo_history_mgr_remove_uri (MooHistoryMgr *mgr, const char *uri) { moo_history_mgr_remove_uri_real (mgr, uri, TRUE); } static void ipc_callback (GObject *obj, const char *data, gsize len) { MooHistoryMgr *mgr; MooMarkupDoc *xml; GError *error = NULL; MooHistoryItem *item; UpdateType type; g_return_if_fail (MOO_IS_HISTORY_MGR (obj)); mgr = MOO_HISTORY_MGR (obj); ensure_files (mgr); xml = moo_markup_parse_memory (data, len, &error); if (!xml) { g_critical ("got invalid data: %.*s", (int) len, data); return; } #if 0 g_print ("%s: got data: %.*s\n", G_STRLOC, (int) len, data); #endif if (parse_update_item (xml, &item, &type)) { switch (type) { case UPDATE_ITEM_UPDATE: moo_history_mgr_update_file_real (mgr, item, FALSE); break; case UPDATE_ITEM_ADD: moo_history_mgr_add_file_real (mgr, item, FALSE); break; case UPDATE_ITEM_REMOVE: moo_history_mgr_remove_uri_real (mgr, moo_history_item_get_uri (item), FALSE); break; } moo_history_item_free (item); } moo_markup_doc_unref (xml); } static void ipc_notify (MooHistoryMgr *mgr, MooHistoryItem *item, UpdateType type) { if (mgr->priv->ipc_id) { char *string = format_for_update (item, type); moo_ipc_send (G_OBJECT (mgr), mgr->priv->ipc_id, string, -1); g_free (string); } } static void ipc_notify_add_file (MooHistoryMgr *mgr, MooHistoryItem *item) { ipc_notify (mgr, item, UPDATE_ITEM_ADD); } static void ipc_notify_update_file (MooHistoryMgr *mgr, MooHistoryItem *item) { ipc_notify (mgr, item, UPDATE_ITEM_UPDATE); } static void ipc_notify_remove_file (MooHistoryMgr *mgr, MooHistoryItem *item) { ipc_notify (mgr, item, UPDATE_ITEM_REMOVE); } /*****************************************************************/ /* Menu */ static void callback_data_free (CallbackData *data) { if (data) { if (data->notify) data->notify (data->data); g_slice_free (CallbackData, data); } } static void view_destroyed (GtkWidget *widget, MooHistoryMgr *mgr) { g_object_set_data (G_OBJECT (widget), "moo-history-mgr-callback-data", NULL); mgr->priv->widgets = widget_list_remove (mgr->priv->widgets, widget); } static void update_menu (MooHistoryMgr *mgr, GtkWidget *menu) { WidgetList *children; children = widget_list_from_glist (gtk_container_get_children (GTK_CONTAINER (menu))); while (children) { GtkWidget *item = children->data; if (g_object_get_data (G_OBJECT (item), "moo-history-menu-item-file")) gtk_widget_destroy (item); children = widget_list_delete_link (children, children); } populate_menu (mgr, menu); } GtkWidget * moo_history_mgr_create_menu (MooHistoryMgr *mgr, MooHistoryCallback callback, gpointer data, GDestroyNotify notify) { GtkWidget *menu; CallbackData *cb_data; g_return_val_if_fail (MOO_IS_HISTORY_MGR (mgr), NULL); g_return_val_if_fail (callback != NULL, NULL); menu = gtk_menu_new (); gtk_widget_show (menu); g_signal_connect (menu, "destroy", G_CALLBACK (view_destroyed), mgr); cb_data = g_slice_new0 (CallbackData); cb_data->callback = callback; cb_data->data = data; cb_data->notify = notify; g_object_set_data_full (G_OBJECT (menu), "moo-history-mgr-callback-data", cb_data, (GDestroyNotify) callback_data_free); populate_menu (mgr, menu); mgr->priv->widgets = widget_list_prepend (mgr->priv->widgets, menu); return menu; } static void menu_item_activated (GtkWidget *menu_item) { GtkWidget *parent = menu_item->parent; CallbackData *data; MooHistoryItem *item; GSList *list; g_return_if_fail (parent != NULL); data = (CallbackData*) g_object_get_data (G_OBJECT (parent), "moo-history-mgr-callback-data"); item = (MooHistoryItem*) g_object_get_data (G_OBJECT (menu_item), "moo-history-menu-item-file"); g_return_if_fail (data && item); list = g_slist_prepend (NULL, moo_history_item_copy (item)); data->callback (list, data->data); moo_history_item_free ((MooHistoryItem*) list->data); g_slist_free (list); } static void populate_menu (MooHistoryMgr *mgr, GtkWidget *menu) { guint n_items, i; MooHistoryItemList *l; ensure_files (mgr); n_items = MIN (mgr->priv->files->length, N_MENU_ITEMS); for (i = 0, l = mgr->priv->files->head; i < n_items; i++, l = l->next) { GtkWidget *item, *image; MooHistoryItem *hist_item = l->data; char *display_name, *display_basename; GdkPixbuf *pixbuf; display_basename = uri_get_basename (hist_item->uri); display_name = uri_get_display_name (hist_item->uri); item = gtk_image_menu_item_new_with_label (display_basename); _moo_widget_set_tooltip (item, display_name); gtk_widget_show (item); gtk_menu_shell_insert (GTK_MENU_SHELL (menu), item, i); /* XXX */ pixbuf = moo_file_icon_get_pixbuf (moo_history_item_get_icon (hist_item), GTK_WIDGET (item), GTK_ICON_SIZE_MENU); image = gtk_image_new_from_pixbuf (pixbuf); gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (item), image); g_object_set_data_full (G_OBJECT (item), "moo-history-menu-item-file", moo_history_item_copy (hist_item), (GDestroyNotify) moo_history_item_free); g_signal_connect (item, "activate", G_CALLBACK (menu_item_activated), NULL); g_free (display_basename); g_free (display_name); } } enum { COLUMN_PIXBUF, COLUMN_NAME, COLUMN_TOOLTIP, COLUMN_URI, N_COLUMNS }; static void open_selected (GtkTreeView *tree_view) { CallbackData *data; GtkTreeIter iter; GtkTreeModel *model; GtkTreeSelection *selection; MooHistoryMgr *mgr; GList *selected; GSList *items; mgr = (MooHistoryMgr*) g_object_get_data (G_OBJECT (tree_view), "moo-history-mgr"); g_return_if_fail (MOO_IS_HISTORY_MGR (mgr)); data = (CallbackData*) g_object_get_data (G_OBJECT (tree_view), "moo-history-mgr-callback-data"); g_return_if_fail (data != NULL); selection = gtk_tree_view_get_selection (tree_view); selected = gtk_tree_selection_get_selected_rows (selection, &model); for (items = NULL; selected != NULL; ) { char *uri = NULL; MooHistoryItem *item; GtkTreePath *path = (GtkTreePath*) selected->data; gtk_tree_model_get_iter (model, &iter, path); gtk_tree_model_get (model, &iter, COLUMN_URI, &uri, -1); item = moo_history_mgr_find_uri (mgr, uri); if (item) items = g_slist_prepend (items, moo_history_item_copy (item)); g_free (uri); gtk_tree_path_free (path); selected = g_list_delete_link (selected, selected); } items = g_slist_reverse (items); if (items) data->callback (items, data->data); g_slist_foreach (items, (GFunc) moo_history_item_free, NULL); g_slist_free (items); } static GtkWidget * create_tree_view (void) { GtkWidget *tree_view; GtkTreeViewColumn *column; GtkCellRenderer *cell; GtkListStore *store; tree_view = gtk_tree_view_new (); store = gtk_list_store_new (N_COLUMNS, GDK_TYPE_PIXBUF, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING); gtk_tree_view_set_model (GTK_TREE_VIEW (tree_view), GTK_TREE_MODEL (store)); gtk_tree_view_set_headers_visible (GTK_TREE_VIEW (tree_view), FALSE); gtk_tree_view_set_tooltip_column (GTK_TREE_VIEW (tree_view), COLUMN_TOOLTIP); gtk_tree_selection_set_mode (gtk_tree_view_get_selection (GTK_TREE_VIEW (tree_view)), GTK_SELECTION_MULTIPLE); column = gtk_tree_view_column_new (); gtk_tree_view_append_column (GTK_TREE_VIEW (tree_view), column); cell = gtk_cell_renderer_pixbuf_new (); gtk_tree_view_column_pack_start (column, cell, FALSE); gtk_tree_view_column_set_attributes (column, cell, "pixbuf", COLUMN_PIXBUF, NULL); cell = gtk_cell_renderer_text_new (); gtk_tree_view_column_pack_start (column, cell, TRUE); gtk_tree_view_column_set_attributes (column, cell, "text", COLUMN_NAME, NULL); return tree_view; } #define FIRST_CHUNK_SIZE 100 #define CHUNK_SIZE 100 #define LOADING_TIMEOUT 40 #define LOADING_PRIORITY G_PRIORITY_DEFAULT_IDLE typedef struct { MooHistoryItemList *items; guint idle; GtkWidget *tree_view; GtkListStore *store; } IdleLoader; static void cancel_idle_loading (GtkWidget *tree_view) { g_object_set_data (G_OBJECT (tree_view), "moo-history-mgr-idle-loader", NULL); } static void idle_loader_free (IdleLoader *data) { if (data->idle) g_source_remove (data->idle); moo_history_item_list_foreach (data->items, (MooHistoryItemListFunc) moo_history_item_free, NULL); moo_history_item_list_free_links (data->items); g_free (data); } static void add_entry (MooHistoryItem *item, GtkListStore *store, GtkWidget *tree_view) { char *display_name, *display_basename; GdkPixbuf *pixbuf; GtkTreeIter iter; display_basename = uri_get_basename (item->uri); display_name = uri_get_display_name (item->uri); pixbuf = moo_file_icon_get_pixbuf (moo_history_item_get_icon (item), tree_view, GTK_ICON_SIZE_MENU); gtk_list_store_append (store, &iter); gtk_list_store_set (store, &iter, COLUMN_PIXBUF, pixbuf, COLUMN_NAME, display_basename, COLUMN_TOOLTIP, display_name, COLUMN_URI, moo_history_item_get_uri (item), -1); g_free (display_basename); g_free (display_name); } static gboolean idle_loader (IdleLoader *data) { int count; for (count = 0; data->items != NULL && count < CHUNK_SIZE; count++) { add_entry (data->items->data, data->store, data->tree_view); moo_history_item_free (data->items->data); data->items = moo_history_item_list_delete_link (data->items, data->items); } if (!data->items) { data->idle = 0; g_object_set_data (G_OBJECT (data->tree_view), "moo-history-mgr-idle-loader", NULL); return FALSE; } else { return TRUE; } } static void populate_tree_view (MooHistoryMgr *mgr, GtkWidget *tree_view) { GtkListStore *store; GtkTreeModel *model; MooHistoryItemList *l; int count; ensure_files (mgr); cancel_idle_loading (tree_view); model = gtk_tree_view_get_model (GTK_TREE_VIEW (tree_view)); store = GTK_LIST_STORE (model); for (l = mgr->priv->files->head, count = 0; l != NULL && count < FIRST_CHUNK_SIZE; l = l->next, count++) { add_entry (l->data, store, tree_view); } if (l != NULL) { IdleLoader *data = g_new0 (IdleLoader, 1); while (l != NULL) { data->items = moo_history_item_list_prepend (data->items, moo_history_item_copy (l->data)); l = l->next; } data->items = moo_history_item_list_reverse (data->items); data->idle = g_timeout_add_full (LOADING_PRIORITY, LOADING_TIMEOUT, (GSourceFunc) idle_loader, data, NULL); data->tree_view = tree_view; data->store = store; g_object_set_data_full (G_OBJECT (tree_view), "moo-history-mgr-idle-loader", data, (GDestroyNotify) idle_loader_free); } } static void update_tree_view (MooHistoryMgr *mgr, GtkWidget *tree_view) { GtkTreeModel *model = gtk_tree_view_get_model (GTK_TREE_VIEW (tree_view)); gtk_list_store_clear (GTK_LIST_STORE (model)); populate_tree_view (mgr, tree_view); } static GtkWidget * moo_history_mgr_create_tree_view (MooHistoryMgr *mgr, MooHistoryCallback callback, gpointer data, GDestroyNotify notify) { GtkWidget *tree_view; CallbackData *cb_data; g_return_val_if_fail (MOO_IS_HISTORY_MGR (mgr), NULL); g_return_val_if_fail (callback != NULL, NULL); tree_view = create_tree_view (); gtk_widget_show (tree_view); g_signal_connect (tree_view, "destroy", G_CALLBACK (view_destroyed), mgr); cb_data = g_slice_new0 (CallbackData); cb_data->callback = callback; cb_data->data = data; cb_data->notify = notify; g_object_set_data_full (G_OBJECT (tree_view), "moo-history-mgr-callback-data", cb_data, (GDestroyNotify) callback_data_free); g_object_set_data (G_OBJECT (tree_view), "moo-history-mgr", mgr); populate_tree_view (mgr, tree_view); if (mgr->priv->files->head) _moo_tree_view_select_first (GTK_TREE_VIEW (tree_view)); mgr->priv->widgets = widget_list_prepend (mgr->priv->widgets, tree_view); return tree_view; } static void dialog_response (GtkTreeView *tree_view, int response) { if (response == GTK_RESPONSE_OK) open_selected (tree_view); } static void row_activated (GtkDialog *dialog) { gtk_dialog_response (dialog, GTK_RESPONSE_OK); } GtkWidget * moo_history_mgr_create_dialog (MooHistoryMgr *mgr, MooHistoryCallback callback, gpointer data, GDestroyNotify notify) { GtkWidget *dialog, *swin, *tree_view; g_return_val_if_fail (MOO_IS_HISTORY_MGR (mgr), NULL); g_return_val_if_fail (callback != NULL, NULL); dialog = gtk_dialog_new_with_buttons ("", NULL, GTK_DIALOG_DESTROY_WITH_PARENT, GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, GTK_STOCK_OPEN, GTK_RESPONSE_OK, NULL); gtk_dialog_set_alternative_button_order (GTK_DIALOG (dialog), GTK_RESPONSE_OK, GTK_RESPONSE_CANCEL, -1); swin = gtk_scrolled_window_new (NULL, NULL); gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (swin), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC); tree_view = moo_history_mgr_create_tree_view (mgr, callback, data, notify); gtk_container_add (GTK_CONTAINER (swin), tree_view); gtk_widget_show_all (swin); gtk_box_pack_start (GTK_BOX (GTK_DIALOG (dialog)->vbox), swin, TRUE, TRUE, 0); g_signal_connect_swapped (tree_view, "row-activated", G_CALLBACK (row_activated), dialog); g_signal_connect_swapped (dialog, "response", G_CALLBACK (dialog_response), tree_view); return dialog; } static gboolean do_update_widgets (MooHistoryMgr *mgr) { WidgetList *l; mgr->priv->update_widgets_idle = 0; for (l = mgr->priv->widgets; l != NULL; l = l->next) { GtkWidget *widget = l->data; if (GTK_IS_MENU (widget)) update_menu (mgr, widget); else if (GTK_IS_TREE_VIEW (widget)) update_tree_view (mgr, widget); else g_critical ("%s: oops", G_STRFUNC); } return FALSE; } static void schedule_update_widgets (MooHistoryMgr *mgr) { if (!mgr->priv->update_widgets_idle && mgr->priv->widgets) mgr->priv->update_widgets_idle = g_idle_add ((GSourceFunc) do_update_widgets, mgr); } void moo_history_item_free (MooHistoryItem *item) { if (item) { g_free (item->uri); g_datalist_clear (&item->data); moo_file_icon_free (item->icon); g_slice_free (MooHistoryItem, item); } } static MooHistoryItem * moo_history_item_new_uri (const char *uri) { MooHistoryItem *item = g_slice_new (MooHistoryItem); item->uri = g_strdup (uri); item->data = NULL; item->icon = NULL; return item; } static MooHistoryItem * moo_history_item_newv (const char *uri, const char *first_key, va_list args) { const char *key; MooHistoryItem *item; item = moo_history_item_new_uri (uri); for (key = first_key; key != NULL; ) { const char *value = va_arg (args, const char *); moo_history_item_set (item, key, value); key = va_arg (args, const char *); } return item; } MooHistoryItem * moo_history_item_new (const char *uri, const char *first_key, ...) { va_list args; MooHistoryItem *item; g_return_val_if_fail (uri != NULL, NULL); va_start (args, first_key); item = moo_history_item_newv (uri, first_key, args); va_end (args); return item; } static void copy_data (GQuark key, const char *value, MooHistoryItem *dest) { g_datalist_id_set_data_full (&dest->data, key, g_strdup (value), g_free); } MooHistoryItem * moo_history_item_copy (MooHistoryItem *item) { MooHistoryItem *copy; if (!item) return NULL; copy = moo_history_item_new_uri (item->uri); g_datalist_foreach (&item->data, (GDataForeachFunc) copy_data, copy); copy->icon = moo_file_icon_copy (item->icon); return copy; } typedef struct { MooHistoryItem *item; gboolean equal; } CmpData; static void cmp_data (GQuark key, const char *value, CmpData *data) { const char *value2; if (!data->equal) return; value2 = (const char*) g_datalist_id_get_data (&data->item->data, key); if (!value2 || strcmp (value2, value) != 0) data->equal = FALSE; } static gboolean moo_history_item_equal (MooHistoryItem *item1, MooHistoryItem *item2) { CmpData data; g_return_val_if_fail (item1 && item2, FALSE); if (item1 == item2) return TRUE; if (strcmp (item1->uri, item2->uri) != 0) return FALSE; data.equal = TRUE; data.item = item1; g_datalist_foreach (&item2->data, (GDataForeachFunc) cmp_data, &data); if (data.equal) { data.item = item2; g_datalist_foreach (&item1->data, (GDataForeachFunc) cmp_data, &data); } return data.equal; } void moo_history_item_set (MooHistoryItem *item, const char *key, const char *value) { g_return_if_fail (item != NULL); g_return_if_fail (key != NULL); if (value) g_datalist_set_data_full (&item->data, key, g_strdup (value), g_free); else g_datalist_remove_data (&item->data, key); } const char * moo_history_item_get (MooHistoryItem *item, const char *key) { g_return_val_if_fail (item != NULL, NULL); g_return_val_if_fail (key != NULL, NULL); return (const char*) g_datalist_get_data (&item->data, key); } const char * moo_history_item_get_uri (MooHistoryItem *item) { g_return_val_if_fail (item != NULL, NULL); return item->uri; } static MooFileIcon * moo_history_item_get_icon (MooHistoryItem *item) { g_return_val_if_fail (item != NULL, NULL); if (!item->icon) { char *display_name; item->icon = moo_file_icon_new (); display_name = uri_get_display_name (item->uri); /* XXX */ moo_file_icon_for_file (item->icon, display_name); g_free (display_name); } return item->icon; } static void format_data (GQuark key_id, const char *value, GString *dest) { const char *key = g_quark_to_string (key_id); char *key_escaped = g_markup_escape_text (key, -1); char *value_escaped = g_markup_escape_text (value, -1); g_string_append_printf (dest, " \n", key_escaped, value_escaped); g_free (value_escaped); g_free (key_escaped); } static void moo_history_item_format (MooHistoryItem *item, GString *dest) { char *uri_escaped; g_return_if_fail (item != NULL); g_return_if_fail (dest != NULL); uri_escaped = g_markup_escape_text (item->uri, -1); if (item->data) { g_string_append_printf (dest, " \n", uri_escaped); g_datalist_foreach (&item->data, (GDataForeachFunc) format_data, dest); g_string_append (dest, " \n"); } else { g_string_append_printf (dest, " \n", uri_escaped); } g_free (uri_escaped); } void moo_history_item_foreach (MooHistoryItem *item, GDataForeachFunc func, gpointer user_data) { g_return_if_fail (item != NULL); g_return_if_fail (func != NULL); g_datalist_foreach (&item->data, func, user_data); } static char * uri_get_basename (const char *uri) { const char *last_slash; g_return_val_if_fail (uri != NULL, NULL); if (g_str_has_prefix (uri, "file://")) { char *filename = g_filename_from_uri (uri, NULL, NULL); if (filename) { char *display_name = g_filename_display_basename (filename); if (display_name) { g_free (filename); return display_name; } g_free (filename); } } /* XXX percent encoding */ last_slash = strrchr (uri, '/'); if (last_slash) return g_strdup (last_slash + 1); else return g_strdup (uri); } static char * uri_get_display_name (const char *uri) { g_return_val_if_fail (uri != NULL, NULL); if (g_str_has_prefix (uri, "file://")) { char *filename = g_filename_from_uri (uri, NULL, NULL); if (filename) { char *display_name = g_filename_display_name (filename); if (display_name) { g_free (filename); return display_name; } g_free (filename); } } return g_strdup (uri); }