diff --git a/js/ui/appDisplay.js b/js/ui/appDisplay.js index cdd1756c2..461a48682 100644 --- a/js/ui/appDisplay.js +++ b/js/ui/appDisplay.js @@ -71,15 +71,9 @@ function _getFolderName(folder) { let name = folder.get_string('name'); if (folder.get_boolean('translate')) { - let keyfile = new GLib.KeyFile(); - let path = 'desktop-directories/%s'.format(name); - - try { - keyfile.load_from_data_dirs(path, GLib.KeyFileFlags.NONE); - name = keyfile.get_locale_string('Desktop Entry', 'Name', null); - } catch (e) { - return name; - } + let translated = Shell.util_get_translated_folder_name(name); + if (translated !== null) + return translated; } return name; @@ -120,15 +114,9 @@ function _findBestFolderName(apps) { }, commonCategories); for (let category of commonCategories) { - let keyfile = new GLib.KeyFile(); - let path = 'desktop-directories/%s.directory'.format(category); - - try { - keyfile.load_from_data_dirs(path, GLib.KeyFileFlags.NONE); - return keyfile.get_locale_string('Desktop Entry', 'Name', null); - } catch (e) { - continue; - } + let translated = Shell.util_get_translated_folder_name(category); + if (translated !== null) + return translated; } return null; diff --git a/src/meson.build b/src/meson.build index 0d607698a..8e18e3080 100644 --- a/src/meson.build +++ b/src/meson.build @@ -115,6 +115,7 @@ endif libshell_private_headers = [ 'shell-app-private.h', + 'shell-app-cache-private.h', 'shell-app-system-private.h', 'shell-global-private.h', 'shell-window-tracker-private.h', @@ -154,7 +155,9 @@ if have_networkmanager libshell_sources += 'shell-network-agent.c' endif -libshell_private_sources = [] +libshell_private_sources = [ + 'shell-app-cache.c', +] if enable_recorder libshell_sources += ['shell-recorder.c'] diff --git a/src/shell-app-cache-private.h b/src/shell-app-cache-private.h new file mode 100644 index 000000000..b73094ab1 --- /dev/null +++ b/src/shell-app-cache-private.h @@ -0,0 +1,19 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +#ifndef __SHELL_APP_CACHE_PRIVATE_H__ +#define __SHELL_APP_CACHE_PRIVATE_H__ + +#include +#include + +#define SHELL_TYPE_APP_CACHE (shell_app_cache_get_type()) + +G_DECLARE_FINAL_TYPE (ShellAppCache, shell_app_cache, SHELL, APP_CACHE, GObject) + +ShellAppCache *shell_app_cache_get_default (void); +GList *shell_app_cache_get_all (ShellAppCache *cache); +GDesktopAppInfo *shell_app_cache_get_info (ShellAppCache *cache, + const char *id); +char *shell_app_cache_translate_folder (ShellAppCache *cache, + const char *name); + +#endif /* __SHELL_APP_CACHE_PRIVATE_H__ */ diff --git a/src/shell-app-cache.c b/src/shell-app-cache.c new file mode 100644 index 000000000..15d4734d0 --- /dev/null +++ b/src/shell-app-cache.c @@ -0,0 +1,404 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ + +#include "config.h" + +#include "shell-app-cache-private.h" + +/** + * SECTION:shell-app-cache + * @title: ShellAppCache + * @short_description: application information cache + * + * The #ShellAppCache is responsible for caching information about #GAppInfo + * to ensure that the compositor thread never needs to perform disk reads to + * access them. All of the work is done off-thread. When the new data has + * been loaded, a #ShellAppCache::changed signal is emitted. + * + * Additionally, the #ShellAppCache caches information about translations for + * directories. This allows translation provided in [Desktop Entry] GKeyFiles + * to be available when building StLabel and other elements without performing + * costly disk reads. + * + * Various monitors are used to keep this information up to date while the + * Shell is running. + */ + +#define DEFAULT_TIMEOUT_SECONDS 5 + +struct _ShellAppCache +{ + GObject parent_instance; + + GAppInfoMonitor *monitor; + GPtrArray *dir_monitors; + GHashTable *folders; + GCancellable *cancellable; + GList *app_infos; + + guint queued_update; +}; + +typedef struct +{ + GList *app_infos; + GHashTable *folders; +} CacheState; + +G_DEFINE_TYPE (ShellAppCache, shell_app_cache, G_TYPE_OBJECT) + +enum { + CHANGED, + N_SIGNALS +}; + +static guint signals [N_SIGNALS]; + +static void +cache_state_free (CacheState *state) +{ + g_clear_pointer (&state->folders, g_hash_table_unref); + g_list_free_full (state->app_infos, g_object_unref); + g_slice_free (CacheState, state); +} + +static CacheState * +cache_state_new (void) +{ + CacheState *state; + + state = g_slice_new0 (CacheState); + state->folders = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); + + return g_steal_pointer (&state); +} + +/** + * shell_app_cache_get_default: + * + * Gets the default #ShellAppCache. + * + * Returns: (transfer none): a #ShellAppCache + */ +ShellAppCache * +shell_app_cache_get_default (void) +{ + static ShellAppCache *instance; + + if (instance == NULL) + { + instance = g_object_new (SHELL_TYPE_APP_CACHE, NULL); + g_object_add_weak_pointer (G_OBJECT (instance), (gpointer *)&instance); + } + + return instance; +} + +static void +load_folder (GHashTable *folders, + const char *path) +{ + g_autoptr(GDir) dir = NULL; + const char *name; + + g_assert (folders != NULL); + g_assert (path != NULL); + + dir = g_dir_open (path, 0, NULL); + if (dir == NULL) + return; + + while ((name = g_dir_read_name (dir))) + { + g_autofree gchar *filename = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + + /* First added wins */ + if (g_hash_table_contains (folders, name)) + continue; + + filename = g_build_filename (path, name, NULL); + keyfile = g_key_file_new (); + + if (g_key_file_load_from_file (keyfile, filename, G_KEY_FILE_NONE, NULL)) + { + gchar *translated; + + translated = g_key_file_get_locale_string (keyfile, + "Desktop Entry", "Name", + NULL, NULL); + + if (translated != NULL) + g_hash_table_insert (folders, g_strdup (name), translated); + } + } +} + +static void +load_folders (GHashTable *folders) +{ + const char * const *dirs; + g_autofree gchar *userdir = NULL; + guint i; + + g_assert (folders != NULL); + + userdir = g_build_filename (g_get_user_data_dir (), "desktop-directories", NULL); + load_folder (folders, userdir); + + dirs = g_get_system_data_dirs (); + for (i = 0; dirs[i] != NULL; i++) + { + g_autofree gchar *sysdir = g_build_filename (dirs[i], "desktop-directories", NULL); + load_folder (folders, sysdir); + } +} + +static void +shell_app_cache_worker (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + CacheState *state; + + g_assert (G_IS_TASK (task)); + g_assert (SHELL_IS_APP_CACHE (source_object)); + + state = cache_state_new (); + state->app_infos = g_app_info_get_all (); + load_folders (state->folders); + + g_task_return_pointer (task, state, (GDestroyNotify) cache_state_free); +} + +static void +apply_update_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + ShellAppCache *cache = (ShellAppCache *)object; + g_autoptr(GError) error = NULL; + CacheState *state; + + g_assert (SHELL_IS_APP_CACHE (cache)); + g_assert (G_IS_TASK (result)); + g_assert (user_data == NULL); + + state = g_task_propagate_pointer (G_TASK (result), &error); + + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + return; + + g_list_free_full (cache->app_infos, g_object_unref); + cache->app_infos = g_steal_pointer (&state->app_infos); + + g_clear_pointer (&cache->folders, g_hash_table_unref); + cache->folders = g_steal_pointer (&state->folders); + + g_signal_emit (cache, signals[CHANGED], 0); + + cache_state_free (state); +} + +static gboolean +shell_app_cache_do_update (gpointer user_data) +{ + ShellAppCache *cache = user_data; + g_autoptr(GTask) task = NULL; + + cache->queued_update = 0; + + /* Reset the cancellable state so we don't race with + * two updates coming back overlapped and applying the + * information in the wrong order. + */ + g_cancellable_cancel (cache->cancellable); + g_clear_object (&cache->cancellable); + cache->cancellable = g_cancellable_new (); + + task = g_task_new (cache, cache->cancellable, apply_update_cb, NULL); + g_task_set_source_tag (task, shell_app_cache_do_update); + g_task_run_in_thread (task, shell_app_cache_worker); + + return G_SOURCE_REMOVE; +} + +static void +shell_app_cache_queue_update (ShellAppCache *self) +{ + g_assert (SHELL_IS_APP_CACHE (self)); + + if (self->queued_update != 0) + g_source_remove (self->queued_update); + + self->queued_update = g_timeout_add_seconds (DEFAULT_TIMEOUT_SECONDS, + shell_app_cache_do_update, + self); +} + +static void +monitor_desktop_directories_for_data_dir (ShellAppCache *self, + const gchar *directory) +{ + g_autofree gchar *subdir = NULL; + g_autoptr(GFile) file = NULL; + g_autoptr(GFileMonitor) monitor = NULL; + + g_assert (SHELL_IS_APP_CACHE (self)); + + if (directory == NULL) + return; + + subdir = g_build_filename (directory, "desktop-directories", NULL); + file = g_file_new_for_path (subdir); + monitor = g_file_monitor_directory (file, G_FILE_MONITOR_NONE, NULL, NULL); + + if (monitor != NULL) + { + g_file_monitor_set_rate_limit (monitor, DEFAULT_TIMEOUT_SECONDS * 1000); + g_signal_connect_object (monitor, + "changed", + G_CALLBACK (shell_app_cache_queue_update), + self, + G_CONNECT_SWAPPED); + g_ptr_array_add (self->dir_monitors, g_steal_pointer (&monitor)); + } +} + +static void +shell_app_cache_finalize (GObject *object) +{ + ShellAppCache *self = (ShellAppCache *)object; + + g_clear_object (&self->monitor); + + if (self->queued_update) + { + g_source_remove (self->queued_update); + self->queued_update = 0; + } + + g_clear_pointer (&self->dir_monitors, g_ptr_array_unref); + g_clear_pointer (&self->folders, g_hash_table_unref); + g_list_free_full (self->app_infos, g_object_unref); + + G_OBJECT_CLASS (shell_app_cache_parent_class)->finalize (object); +} + +static void +shell_app_cache_class_init (ShellAppCacheClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = shell_app_cache_finalize; + + /** + * ShellAppCache::changed: + * + * The "changed" signal is emitted when the cache has updated + * information about installed applications. + */ + signals [CHANGED] = + g_signal_new ("changed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, 0); +} + +static void +shell_app_cache_init (ShellAppCache *self) +{ + const gchar * const *sysdirs; + guint i; + + /* Monitor directories for translation changes */ + self->dir_monitors = g_ptr_array_new_with_free_func (g_object_unref); + monitor_desktop_directories_for_data_dir (self, g_get_user_data_dir ()); + sysdirs = g_get_system_data_dirs (); + for (i = 0; sysdirs[i] != NULL; i++) + monitor_desktop_directories_for_data_dir (self, sysdirs[i]); + + /* Load translated directory names immediately */ + self->folders = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); + load_folders (self->folders); + + /* Setup AppMonitor to track changes */ + self->monitor = g_app_info_monitor_get (); + g_signal_connect_object (self->monitor, + "changed", + G_CALLBACK (shell_app_cache_queue_update), + self, + G_CONNECT_SWAPPED); + self->app_infos = g_app_info_get_all (); +} + +/** + * shell_app_cache_get_all: + * @cache: (nullable): a #ShellAppCache or %NULL + * + * Like g_app_info_get_all() but always returns a + * cached set of application info so the caller can be + * sure that I/O will not happen on the current thread. + * + * Returns: (transfer none) (element-type GAppInfo): + * a #GList of references to #GAppInfo. + */ +GList * +shell_app_cache_get_all (ShellAppCache *cache) +{ + g_return_val_if_fail (SHELL_IS_APP_CACHE (cache), NULL); + + return cache->app_infos; +} + +/** + * shell_app_cache_get_info: + * @cache: (nullable): a #ShellAppCache or %NULL + * @id: the application id + * + * A replacement for g_desktop_app_info_new() that will lookup the + * information from the cache instead of (re)loading from disk. + * + * Returns: (nullable) (transfer none): a #GDesktopAppInfo or %NULL + */ +GDesktopAppInfo * +shell_app_cache_get_info (ShellAppCache *cache, + const char *id) +{ + const GList *iter; + + g_return_val_if_fail (SHELL_IS_APP_CACHE (cache), NULL); + + for (iter = cache->app_infos; iter != NULL; iter = iter->next) + { + GAppInfo *info = iter->data; + + if (g_strcmp0 (id, g_app_info_get_id (info)) == 0) + return G_DESKTOP_APP_INFO (info); + } + + return NULL; +} + +/** + * shell_app_cache_translate_folder: + * @cache: (nullable): a #ShellAppCache or %NULL + * @name: the folder name + * + * Gets the translated folder name for @name if any exists. + * + * Returns: (nullable): the translated string or %NULL if there is no + * translation. + */ +char * +shell_app_cache_translate_folder (ShellAppCache *cache, + const char *name) +{ + g_return_val_if_fail (SHELL_IS_APP_CACHE (cache), NULL); + + if (name == NULL) + return NULL; + + return g_strdup (g_hash_table_lookup (cache->folders, name)); +} diff --git a/src/shell-app-system.c b/src/shell-app-system.c index 127f29ef0..828fa726a 100644 --- a/src/shell-app-system.c +++ b/src/shell-app-system.c @@ -9,6 +9,7 @@ #include #include +#include "shell-app-cache-private.h" #include "shell-app-private.h" #include "shell-window-tracker-private.h" #include "shell-app-system-private.h" @@ -94,14 +95,14 @@ static void scan_startup_wm_class_to_id (ShellAppSystem *self) { ShellAppSystemPrivate *priv = self->priv; - GList *l; + const GList *l; + GList *all; g_hash_table_remove_all (priv->startup_wm_class_to_id); - g_list_free_full (priv->installed_apps, g_object_unref); - priv->installed_apps = g_app_info_get_all (); + all = shell_app_cache_get_all (shell_app_cache_get_default ()); - for (l = priv->installed_apps; l != NULL; l = l->next) + for (l = all; l != NULL; l = l->next) { GAppInfo *info = l->data; const char *startup_wm_class, *id, *old_id; @@ -131,7 +132,8 @@ app_is_stale (ShellApp *app) if (shell_app_is_window_backed (app)) return FALSE; - info = g_desktop_app_info_new (shell_app_get_id (app)); + info = shell_app_cache_get_info (shell_app_cache_get_default (), + shell_app_get_id (app)); if (!info) return TRUE; @@ -156,7 +158,6 @@ app_is_stale (ShellApp *app) g_icon_equal (g_app_info_get_icon (old_info), g_app_info_get_icon (new_info)); - g_object_unref (info); return !is_unchanged; } @@ -210,11 +211,9 @@ rescan_icon_theme (ShellAppSystem *self) } static void -installed_changed (GAppInfoMonitor *monitor, - gpointer user_data) +installed_changed (ShellAppCache *cache, + ShellAppSystem *self) { - ShellAppSystem *self = user_data; - rescan_icon_theme (self); scan_startup_wm_class_to_id (self); @@ -227,7 +226,7 @@ static void shell_app_system_init (ShellAppSystem *self) { ShellAppSystemPrivate *priv; - GAppInfoMonitor *monitor; + ShellAppCache *cache; self->priv = priv = shell_app_system_get_instance_private (self); @@ -238,9 +237,9 @@ shell_app_system_init (ShellAppSystem *self) priv->startup_wm_class_to_id = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); - monitor = g_app_info_monitor_get (); - g_signal_connect (monitor, "changed", G_CALLBACK (installed_changed), self); - installed_changed (monitor, self); + cache = shell_app_cache_get_default (); + g_signal_connect (cache, "changed", G_CALLBACK (installed_changed), self); + installed_changed (cache, self); } static void @@ -293,13 +292,12 @@ shell_app_system_lookup_app (ShellAppSystem *self, if (app) return app; - info = g_desktop_app_info_new (id); + info = shell_app_cache_get_info (shell_app_cache_get_default (), id); if (!info) return NULL; app = _shell_app_new (info); g_hash_table_insert (priv->id_to_app, (char *) shell_app_get_id (app), app); - g_object_unref (info); return app; } @@ -506,7 +504,5 @@ shell_app_system_search (const char *search_string) GList * shell_app_system_get_installed (ShellAppSystem *self) { - ShellAppSystemPrivate *priv = self->priv; - - return priv->installed_apps; + return shell_app_cache_get_all (shell_app_cache_get_default ()); } diff --git a/src/shell-util.c b/src/shell-util.c index 918dcdf0b..26ff66e4e 100644 --- a/src/shell-util.c +++ b/src/shell-util.c @@ -16,6 +16,7 @@ #include #include +#include "shell-app-cache-private.h" #include "shell-util.h" #include #include @@ -665,3 +666,18 @@ shell_util_has_x11_display_extension (MetaDisplay *display, xdisplay = meta_x11_display_get_xdisplay (x11_display); return XQueryExtension (xdisplay, extension, &op, &event, &error); } + +/** + * shell_util_get_translated_folder_name: + * @name: the untranslated folder name + * + * Attempts to translate the folder @name using translations provided + * by .directory files. + * + * Returns: (nullable): a translated string or %NULL + */ +char * +shell_util_get_translated_folder_name (const char *name) +{ + return shell_app_cache_translate_folder (shell_app_cache_get_default (), name); +} diff --git a/src/shell-util.h b/src/shell-util.h index 220606de3..99b9350f9 100644 --- a/src/shell-util.h +++ b/src/shell-util.h @@ -69,6 +69,8 @@ void shell_util_sd_notify (void); gboolean shell_util_has_x11_display_extension (MetaDisplay *display, const char *extension); +char *shell_util_get_translated_folder_name (const char *name); + G_END_DECLS #endif /* __SHELL_UTIL_H__ */