gnome-shell/src/shell-app-system.c
Georges Basile Stavracas Neto c0047cd11d shell/app-system: Monitor for icon theme changes
Whenever an app is installed, the usual routine is
to run 'gtk-update-icon-cache' after installing all
of the app's files.

The side effect of that is that the .desktop file of
the application is installed before the icon theme
is updated. By the time GAppInfoMonitor emits the
'changed' signal, the icon theme is not yet updated,
leading to StIcon use the fallback icon.

Under some circumstances (e.g. on very slow spinning
disks) the app icon is never actually loaded, and we
see the fallback icon forever.

Monitor the icon theme for changes when an app is
installed. Try as many as 6 times before giving up
on detecting an icon theme update.

https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/661
2019-08-08 15:22:44 +00:00

513 lines
14 KiB
C

/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
#include "config.h"
#include "shell-app-system.h"
#include "shell-app-usage.h"
#include <string.h>
#include <gio/gio.h>
#include <glib/gi18n.h>
#include "shell-app-private.h"
#include "shell-window-tracker-private.h"
#include "shell-app-system-private.h"
#include "shell-global.h"
#include "shell-util.h"
#include "st.h"
/* Rescan for at most RESCAN_TIMEOUT_MS * MAX_RESCAN_RETRIES. That
* should be plenty of time for even a slow spinning drive to update
* the icon cache.
*/
#define RESCAN_TIMEOUT_MS 2500
#define MAX_RESCAN_RETRIES 6
/* Vendor prefixes are something that can be preprended to a .desktop
* file name. Undo this.
*/
static const char*const vendor_prefixes[] = { "gnome-",
"fedora-",
"mozilla-",
"debian-",
NULL };
enum {
PROP_0,
};
enum {
APP_STATE_CHANGED,
INSTALLED_CHANGED,
LAST_SIGNAL
};
static guint signals[LAST_SIGNAL] = { 0 };
typedef struct _ShellAppSystemPrivate ShellAppSystemPrivate;
struct _ShellAppSystem
{
GObject parent;
ShellAppSystemPrivate *priv;
};
struct _ShellAppSystemPrivate {
GHashTable *running_apps;
GHashTable *id_to_app;
GHashTable *startup_wm_class_to_id;
GList *installed_apps;
guint rescan_icons_timeout_id;
guint n_rescan_retries;
};
static void shell_app_system_finalize (GObject *object);
G_DEFINE_TYPE_WITH_PRIVATE (ShellAppSystem, shell_app_system, G_TYPE_OBJECT);
static void shell_app_system_class_init(ShellAppSystemClass *klass)
{
GObjectClass *gobject_class = (GObjectClass *)klass;
gobject_class->finalize = shell_app_system_finalize;
signals[APP_STATE_CHANGED] = g_signal_new ("app-state-changed",
SHELL_TYPE_APP_SYSTEM,
G_SIGNAL_RUN_LAST,
0,
NULL, NULL, NULL,
G_TYPE_NONE, 1,
SHELL_TYPE_APP);
signals[INSTALLED_CHANGED] =
g_signal_new ("installed-changed",
SHELL_TYPE_APP_SYSTEM,
G_SIGNAL_RUN_LAST,
0,
NULL, NULL, NULL,
G_TYPE_NONE, 0);
}
static void
scan_startup_wm_class_to_id (ShellAppSystem *self)
{
ShellAppSystemPrivate *priv = self->priv;
GList *l;
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 ();
for (l = priv->installed_apps; l != NULL; l = l->next)
{
GAppInfo *info = l->data;
const char *startup_wm_class, *id, *old_id;
id = g_app_info_get_id (info);
startup_wm_class = g_desktop_app_info_get_startup_wm_class (G_DESKTOP_APP_INFO (info));
if (startup_wm_class == NULL)
continue;
/* In case multiple .desktop files set the same StartupWMClass, prefer
* the one where ID and StartupWMClass match */
old_id = g_hash_table_lookup (priv->startup_wm_class_to_id, startup_wm_class);
if (old_id == NULL || strcmp (id, startup_wm_class) == 0)
g_hash_table_insert (priv->startup_wm_class_to_id,
g_strdup (startup_wm_class), g_strdup (id));
}
}
static gboolean
app_is_stale (ShellApp *app)
{
GDesktopAppInfo *info, *old;
GAppInfo *old_info, *new_info;
gboolean is_unchanged;
if (shell_app_is_window_backed (app))
return FALSE;
info = g_desktop_app_info_new (shell_app_get_id (app));
if (!info)
return TRUE;
old = shell_app_get_app_info (app);
old_info = G_APP_INFO (old);
new_info = G_APP_INFO (info);
is_unchanged =
g_app_info_should_show (old_info) == g_app_info_should_show (new_info) &&
strcmp (g_desktop_app_info_get_filename (old),
g_desktop_app_info_get_filename (info)) == 0 &&
g_strcmp0 (g_app_info_get_executable (old_info),
g_app_info_get_executable (new_info)) == 0 &&
g_strcmp0 (g_app_info_get_commandline (old_info),
g_app_info_get_commandline (new_info)) == 0 &&
strcmp (g_app_info_get_name (old_info),
g_app_info_get_name (new_info)) == 0 &&
g_strcmp0 (g_app_info_get_description (old_info),
g_app_info_get_description (new_info)) == 0 &&
strcmp (g_app_info_get_display_name (old_info),
g_app_info_get_display_name (new_info)) == 0 &&
g_icon_equal (g_app_info_get_icon (old_info),
g_app_info_get_icon (new_info));
g_object_unref (info);
return !is_unchanged;
}
static gboolean
stale_app_remove_func (gpointer key,
gpointer value,
gpointer user_data)
{
return app_is_stale (value);
}
static gboolean
rescan_icon_theme_cb (gpointer user_data)
{
ShellAppSystemPrivate *priv;
ShellAppSystem *self;
StTextureCache *texture_cache;
gboolean rescanned;
self = (ShellAppSystem *) user_data;
priv = self->priv;
texture_cache = st_texture_cache_get_default ();
rescanned = st_texture_cache_rescan_icon_theme (texture_cache);
priv->n_rescan_retries++;
if (rescanned || priv->n_rescan_retries >= MAX_RESCAN_RETRIES)
{
priv->n_rescan_retries = 0;
priv->rescan_icons_timeout_id = 0;
return G_SOURCE_REMOVE;
}
return G_SOURCE_CONTINUE;
}
static void
rescan_icon_theme (ShellAppSystem *self)
{
ShellAppSystemPrivate *priv = self->priv;
priv->n_rescan_retries = 0;
if (priv->rescan_icons_timeout_id > 0)
return;
priv->rescan_icons_timeout_id = g_timeout_add (RESCAN_TIMEOUT_MS,
rescan_icon_theme_cb,
self);
}
static void
installed_changed (GAppInfoMonitor *monitor,
gpointer user_data)
{
ShellAppSystem *self = user_data;
rescan_icon_theme (self);
scan_startup_wm_class_to_id (self);
g_hash_table_foreach_remove (self->priv->id_to_app, stale_app_remove_func, NULL);
g_signal_emit (self, signals[INSTALLED_CHANGED], 0, NULL);
}
static void
shell_app_system_init (ShellAppSystem *self)
{
ShellAppSystemPrivate *priv;
GAppInfoMonitor *monitor;
self->priv = priv = shell_app_system_get_instance_private (self);
priv->running_apps = g_hash_table_new_full (NULL, NULL, (GDestroyNotify) g_object_unref, NULL);
priv->id_to_app = g_hash_table_new_full (g_str_hash, g_str_equal,
NULL,
(GDestroyNotify)g_object_unref);
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);
}
static void
shell_app_system_finalize (GObject *object)
{
ShellAppSystem *self = SHELL_APP_SYSTEM (object);
ShellAppSystemPrivate *priv = self->priv;
g_hash_table_destroy (priv->running_apps);
g_hash_table_destroy (priv->id_to_app);
g_hash_table_destroy (priv->startup_wm_class_to_id);
g_list_free_full (priv->installed_apps, g_object_unref);
g_clear_handle_id (&priv->rescan_icons_timeout_id, g_source_remove);
G_OBJECT_CLASS (shell_app_system_parent_class)->finalize (object);
}
/**
* shell_app_system_get_default:
*
* Return Value: (transfer none): The global #ShellAppSystem singleton
*/
ShellAppSystem *
shell_app_system_get_default (void)
{
static ShellAppSystem *instance = NULL;
if (instance == NULL)
instance = g_object_new (SHELL_TYPE_APP_SYSTEM, NULL);
return instance;
}
/**
* shell_app_system_lookup_app:
*
* Find a #ShellApp corresponding to an id.
*
* Return value: (transfer none): The #ShellApp for id, or %NULL if none
*/
ShellApp *
shell_app_system_lookup_app (ShellAppSystem *self,
const char *id)
{
ShellAppSystemPrivate *priv = self->priv;
ShellApp *app;
GDesktopAppInfo *info;
app = g_hash_table_lookup (priv->id_to_app, id);
if (app)
return app;
info = g_desktop_app_info_new (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;
}
/**
* shell_app_system_lookup_heuristic_basename:
* @system: a #ShellAppSystem
* @id: Probable application identifier
*
* Find a valid application corresponding to a given
* heuristically determined application identifier
* string, or %NULL if none.
*
* Returns: (transfer none): A #ShellApp for @name
*/
ShellApp *
shell_app_system_lookup_heuristic_basename (ShellAppSystem *system,
const char *name)
{
ShellApp *result;
const char *const *prefix;
result = shell_app_system_lookup_app (system, name);
if (result != NULL)
return result;
for (prefix = vendor_prefixes; *prefix != NULL; prefix++)
{
char *tmpid = g_strconcat (*prefix, name, NULL);
result = shell_app_system_lookup_app (system, tmpid);
g_free (tmpid);
if (result != NULL)
return result;
}
return NULL;
}
/**
* shell_app_system_lookup_desktop_wmclass:
* @system: a #ShellAppSystem
* @wmclass: (nullable): A WM_CLASS value
*
* Find a valid application whose .desktop file, without the extension
* and properly canonicalized, matches @wmclass.
*
* Returns: (transfer none): A #ShellApp for @wmclass
*/
ShellApp *
shell_app_system_lookup_desktop_wmclass (ShellAppSystem *system,
const char *wmclass)
{
char *canonicalized;
char *desktop_file;
ShellApp *app;
if (wmclass == NULL)
return NULL;
/* First try without changing the case (this handles
org.example.Foo.Bar.desktop applications)
Note that is slightly wrong in that Gtk+ would set
the WM_CLASS to Org.example.Foo.Bar, but it also
sets the instance part to org.example.Foo.Bar, so we're ok
*/
desktop_file = g_strconcat (wmclass, ".desktop", NULL);
app = shell_app_system_lookup_heuristic_basename (system, desktop_file);
g_free (desktop_file);
if (app)
return app;
canonicalized = g_ascii_strdown (wmclass, -1);
/* This handles "Fedora Eclipse", probably others.
* Note g_strdelimit is modify-in-place. */
g_strdelimit (canonicalized, " ", '-');
desktop_file = g_strconcat (canonicalized, ".desktop", NULL);
app = shell_app_system_lookup_heuristic_basename (system, desktop_file);
g_free (canonicalized);
g_free (desktop_file);
return app;
}
/**
* shell_app_system_lookup_startup_wmclass:
* @system: a #ShellAppSystem
* @wmclass: (nullable): A WM_CLASS value
*
* Find a valid application whose .desktop file contains a
* StartupWMClass entry matching @wmclass.
*
* Returns: (transfer none): A #ShellApp for @wmclass
*/
ShellApp *
shell_app_system_lookup_startup_wmclass (ShellAppSystem *system,
const char *wmclass)
{
const char *id;
if (wmclass == NULL)
return NULL;
id = g_hash_table_lookup (system->priv->startup_wm_class_to_id, wmclass);
if (id == NULL)
return NULL;
return shell_app_system_lookup_app (system, id);
}
void
_shell_app_system_notify_app_state_changed (ShellAppSystem *self,
ShellApp *app)
{
ShellAppState state = shell_app_get_state (app);
switch (state)
{
case SHELL_APP_STATE_RUNNING:
g_hash_table_insert (self->priv->running_apps, g_object_ref (app), NULL);
break;
case SHELL_APP_STATE_STARTING:
break;
case SHELL_APP_STATE_STOPPED:
g_hash_table_remove (self->priv->running_apps, app);
break;
default:
g_warn_if_reached();
break;
}
g_signal_emit (self, signals[APP_STATE_CHANGED], 0, app);
}
/**
* shell_app_system_get_running:
* @self: A #ShellAppSystem
*
* Returns the set of applications which currently have at least one
* open window. The returned list will be sorted by shell_app_compare().
*
* Returns: (element-type ShellApp) (transfer container): Active applications
*/
GSList *
shell_app_system_get_running (ShellAppSystem *self)
{
gpointer key, value;
GSList *ret;
GHashTableIter iter;
g_hash_table_iter_init (&iter, self->priv->running_apps);
ret = NULL;
while (g_hash_table_iter_next (&iter, &key, &value))
{
ShellApp *app = key;
ret = g_slist_prepend (ret, app);
}
ret = g_slist_sort (ret, (GCompareFunc)shell_app_compare);
return ret;
}
/**
* shell_app_system_search:
* @search_string: the search string to use
*
* Wrapper around g_desktop_app_info_search() that replaces results that
* don't validate as UTF-8 with the empty string.
*
* Returns: (array zero-terminated=1) (element-type GStrv) (transfer full): a
* list of strvs. Free each item with g_strfreev() and free the outer
* list with g_free().
*/
char ***
shell_app_system_search (const char *search_string)
{
char ***results = g_desktop_app_info_search (search_string);
char ***groups, **ids;
for (groups = results; *groups; groups++)
for (ids = *groups; *ids; ids++)
if (!g_utf8_validate (*ids, -1, NULL))
**ids = '\0';
return results;
}
/**
* shell_app_system_get_installed:
* @self: the #ShellAppSystem
*
* Returns all installed apps, as a list of #GAppInfo
*
* Returns: (transfer none) (element-type GAppInfo): a list of #GAppInfo
* describing all known applications. This memory is owned by the
* #ShellAppSystem and should not be freed.
**/
GList *
shell_app_system_get_installed (ShellAppSystem *self)
{
ShellAppSystemPrivate *priv = self->priv;
return priv->installed_apps;
}