/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ #include "config.h" #include "shell-doc-system.h" #include "shell-global.h" /** * SECTION:shell-doc-system * @short_description: Track recently used documents * * Wraps #GtkRecentManager, caching recently used document information, and adds * APIs for asynchronous queries. */ enum { CHANGED, DELETED, LAST_SIGNAL }; static guint signals[LAST_SIGNAL] = { 0 }; struct _ShellDocSystemPrivate { GtkRecentManager *manager; GHashTable *infos_by_uri; GSList *infos_by_timestamp; guint idle_recent_changed_id; GHashTable *deleted_infos; guint idle_emit_deleted_id; }; G_DEFINE_TYPE(ShellDocSystem, shell_doc_system, G_TYPE_OBJECT); /** * shell_doc_system_get_all: * @system: A #ShellDocSystem * * Returns the currently cached set of recent files. Recent files are read initially * from the underlying #GtkRecentManager, and updated when it changes. * This function does not perform I/O. * * Returns: (transfer none) (element-type GtkRecentInfo): Cached recent file infos */ GSList * shell_doc_system_get_all (ShellDocSystem *self) { return self->priv->infos_by_timestamp; } /** * @self: A #ShellDocSystem * @uri: Url * * Returns: (transfer none): Recent file info corresponding to given @uri */ GtkRecentInfo * shell_doc_system_lookup_by_uri (ShellDocSystem *self, const char *uri) { return g_hash_table_lookup (self->priv->infos_by_uri, uri); } static gboolean shell_doc_system_idle_emit_deleted (gpointer data) { ShellDocSystem *self = SHELL_DOC_SYSTEM (data); GHashTableIter iter; gpointer key, value; self->priv->idle_emit_deleted_id = 0; g_hash_table_iter_init (&iter, self->priv->deleted_infos); while (g_hash_table_iter_next (&iter, &key, &value)) { GtkRecentInfo *info = key; g_signal_emit (self, signals[DELETED], 0, info); } g_signal_emit (self, signals[CHANGED], 0); return FALSE; } typedef struct { ShellDocSystem *self; GtkRecentInfo *info; } ShellDocSystemRecentQueryData; static void on_recent_file_query_result (GObject *source, GAsyncResult *result, gpointer user_data) { ShellDocSystemRecentQueryData *data = user_data; ShellDocSystem *self = data->self; GError *error = NULL; GFileInfo *fileinfo; fileinfo = g_file_query_info_finish (G_FILE (source), result, &error); if (fileinfo) g_object_unref (fileinfo); /* This is a strict error check; we don't want to cause recent files to * vanish for anything potentially transient. */ if (error != NULL && error->domain == G_IO_ERROR && error->code == G_IO_ERROR_NOT_FOUND) { self->priv->infos_by_timestamp = g_slist_remove (self->priv->infos_by_timestamp, data->info); g_hash_table_remove (self->priv->infos_by_uri, gtk_recent_info_get_uri (data->info)); g_hash_table_insert (self->priv->deleted_infos, gtk_recent_info_ref (data->info), NULL); if (self->priv->idle_emit_deleted_id == 0) self->priv->idle_emit_deleted_id = g_timeout_add (0, shell_doc_system_idle_emit_deleted, self); } g_clear_error (&error); gtk_recent_info_unref (data->info); g_free (data); } /** * shell_doc_system_queue_existence_check: * @system: A #ShellDocSystem * @n_items: Count of items to check for existence, starting from most recent * * Asynchronously start a check of a number of recent file for existence; * any deleted files will be emitted from the #ShellDocSystem::deleted * signal. Note that this function ignores non-local files; they * will simply always appear to exist (until they are removed from * the recent file list manually). * * The intent of this function is to be called after a #ShellDocSystem::changed * signal has been emitted, and a display has shown a subset of those files. */ void shell_doc_system_queue_existence_check (ShellDocSystem *self, guint n_items) { GSList *iter; guint i; for (i = 0, iter = self->priv->infos_by_timestamp; i < n_items && iter; i++, iter = iter->next) { GtkRecentInfo *info = iter->data; const char *uri; GFile *file; ShellDocSystemRecentQueryData *data; if (!gtk_recent_info_is_local (info)) continue; data = g_new0 (ShellDocSystemRecentQueryData, 1); data->self = self; data->info = gtk_recent_info_ref (info); uri = gtk_recent_info_get_uri (info); file = g_file_new_for_uri (uri); g_file_query_info_async (file, "standard::type", G_FILE_QUERY_INFO_NONE, G_PRIORITY_DEFAULT, NULL, on_recent_file_query_result, data); g_object_unref (file); } } static int sort_infos_by_timestamp_descending (gconstpointer a, gconstpointer b) { GtkRecentInfo *info_a = (GtkRecentInfo*)a; GtkRecentInfo *info_b = (GtkRecentInfo*)b; time_t modified_a, modified_b; modified_a = gtk_recent_info_get_modified (info_a); modified_b = gtk_recent_info_get_modified (info_b); return modified_b - modified_a; } static gboolean idle_handle_recent_changed (gpointer data) { ShellDocSystem *self = SHELL_DOC_SYSTEM (data); GList *items, *iter; self->priv->idle_recent_changed_id = 0; g_hash_table_remove_all (self->priv->deleted_infos); g_hash_table_remove_all (self->priv->infos_by_uri); g_slist_free (self->priv->infos_by_timestamp); self->priv->infos_by_timestamp = NULL; items = gtk_recent_manager_get_items (self->priv->manager); for (iter = items; iter; iter = iter->next) { GtkRecentInfo *info = iter->data; const char *uri = gtk_recent_info_get_uri (info); /* uri is owned by the info */ g_hash_table_insert (self->priv->infos_by_uri, (char*) uri, info); self->priv->infos_by_timestamp = g_slist_prepend (self->priv->infos_by_timestamp, info); } g_list_free (items); self->priv->infos_by_timestamp = g_slist_sort (self->priv->infos_by_timestamp, sort_infos_by_timestamp_descending); g_signal_emit (self, signals[CHANGED], 0); return FALSE; } static void shell_doc_system_on_recent_changed (GtkRecentManager *manager, ShellDocSystem *self) { if (self->priv->idle_recent_changed_id != 0) return; self->priv->idle_recent_changed_id = g_timeout_add (0, idle_handle_recent_changed, self); } /** * shell_doc_system_open: * @system: A #ShellDocSystem * @info: A #GtkRecentInfo * @workspace: Open on this workspace, or -1 for default * * Launch the default application associated with the mime type of * @info, using its uri. */ void shell_doc_system_open (ShellDocSystem *system, GtkRecentInfo *info, int workspace) { GFile *file; GAppInfo *app_info; gboolean needs_uri; GAppLaunchContext *context; context = shell_global_create_app_launch_context (shell_global_get ()); if (workspace != -1) gdk_app_launch_context_set_desktop ((GdkAppLaunchContext *)context, workspace); file = g_file_new_for_uri (gtk_recent_info_get_uri (info)); needs_uri = g_file_get_path (file) == NULL; g_object_unref (file); app_info = g_app_info_get_default_for_type (gtk_recent_info_get_mime_type (info), needs_uri); if (app_info != NULL) { GList *uris; uris = g_list_prepend (NULL, (gpointer)gtk_recent_info_get_uri (info)); g_app_info_launch_uris (app_info, uris, context, NULL); g_list_free (uris); } else { char *app_name; const char *app_exec; char *app_exec_quoted; guint count; time_t time; app_name = gtk_recent_info_last_application (info); if (gtk_recent_info_get_application_info (info, app_name, &app_exec, &count, &time)) { GRegex *regex; /* TODO: Change this once better support for creating GAppInfo is added to GtkRecentInfo, as right now this relies on the fact that the file uri is already a part of appExec, so we don't supply any files to app_info.launch(). The 'command line' passed to create_from_command_line is allowed to contain '%' macros that are expanded to file name / icon name, etc, so we need to escape % as %% */ regex = g_regex_new ("%", 0, 0, NULL); app_exec_quoted = g_regex_replace (regex, app_exec, -1, 0, "%%", 0, NULL); g_regex_unref (regex); app_info = g_app_info_create_from_commandline (app_exec_quoted, NULL, 0, NULL); g_free (app_exec_quoted); /* The point of passing an app launch context to launch() is mostly to get startup notification and associated benefits like the app appearing on the right desktop; but it doesn't really work for now because with the way we create the appInfo we aren't reading the application's desktop file, and thus don't find the StartupNotify=true in it. So, despite passing the app launch context, no startup notification occurs. */ g_app_info_launch (app_info, NULL, context, NULL); } g_free (app_name); } g_object_unref (context); } static void shell_doc_system_class_init(ShellDocSystemClass *klass) { GObjectClass *gobject_class = (GObjectClass *)klass; signals[CHANGED] = g_signal_new ("changed", SHELL_TYPE_DOC_SYSTEM, G_SIGNAL_RUN_LAST, 0, NULL, NULL, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0); signals[DELETED] = g_signal_new ("deleted", SHELL_TYPE_DOC_SYSTEM, G_SIGNAL_RUN_LAST, 0, NULL, NULL, g_cclosure_marshal_VOID__BOXED, G_TYPE_NONE, 1, GTK_TYPE_RECENT_INFO); g_type_class_add_private (gobject_class, sizeof (ShellDocSystemPrivate)); } static void shell_doc_system_init (ShellDocSystem *self) { ShellDocSystemPrivate *priv; self->priv = priv = G_TYPE_INSTANCE_GET_PRIVATE (self, SHELL_TYPE_DOC_SYSTEM, ShellDocSystemPrivate); self->priv->manager = gtk_recent_manager_get_default (); self->priv->deleted_infos = g_hash_table_new_full (NULL, NULL, (GDestroyNotify)gtk_recent_info_unref, NULL); self->priv->infos_by_uri = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, (GDestroyNotify)gtk_recent_info_unref); g_signal_connect (self->priv->manager, "changed", G_CALLBACK(shell_doc_system_on_recent_changed), self); shell_doc_system_on_recent_changed (self->priv->manager, self); } /** * shell_doc_system_get_default: * * Return Value: (transfer none): The global #ShellDocSystem singleton */ ShellDocSystem * shell_doc_system_get_default () { static ShellDocSystem *instance = NULL; if (instance == NULL) instance = g_object_new (SHELL_TYPE_DOC_SYSTEM, NULL); return instance; }