gnome-shell/src/shell-app-system.c
Jasper St. Pierre 40c966fcd6 app-system: Remove lookup_app_for_path
It's absurdly silly. Just modify the one place that uses it
to be better.

https://bugzilla.gnome.org/show_bug.cgi?id=698486
2013-11-02 20:12:37 -04:00

602 lines
17 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"
/* 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 };
struct _ShellAppSystemPrivate {
GMenuTree *apps_tree;
GHashTable *running_apps;
GHashTable *visible_id_to_app;
GHashTable *id_to_app;
GHashTable *startup_wm_class_to_id;
};
static void shell_app_system_finalize (GObject *object);
static void on_apps_tree_changed_cb (GMenuTree *tree, gpointer user_data);
G_DEFINE_TYPE(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,
G_STRUCT_OFFSET (ShellAppSystemClass, installed_changed),
NULL, NULL, NULL,
G_TYPE_NONE, 0);
g_type_class_add_private (gobject_class, sizeof (ShellAppSystemPrivate));
}
static void
shell_app_system_init (ShellAppSystem *self)
{
ShellAppSystemPrivate *priv;
self->priv = priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
SHELL_TYPE_APP_SYSTEM,
ShellAppSystemPrivate);
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);
/* All the objects in this hash table are owned by id_to_app */
priv->visible_id_to_app = g_hash_table_new (g_str_hash, g_str_equal);
priv->startup_wm_class_to_id = g_hash_table_new (g_str_hash, g_str_equal);
/* We want to track NoDisplay apps, so we add INCLUDE_NODISPLAY. We'll
* filter NoDisplay apps out when showing them to the user. */
priv->apps_tree = gmenu_tree_new ("applications.menu", GMENU_TREE_FLAGS_INCLUDE_NODISPLAY);
g_signal_connect (priv->apps_tree, "changed", G_CALLBACK (on_apps_tree_changed_cb), self);
on_apps_tree_changed_cb (priv->apps_tree, self);
}
static void
shell_app_system_finalize (GObject *object)
{
ShellAppSystem *self = SHELL_APP_SYSTEM (object);
ShellAppSystemPrivate *priv = self->priv;
g_object_unref (priv->apps_tree);
g_hash_table_destroy (priv->running_apps);
g_hash_table_destroy (priv->id_to_app);
g_hash_table_destroy (priv->visible_id_to_app);
g_hash_table_destroy (priv->startup_wm_class_to_id);
G_OBJECT_CLASS (shell_app_system_parent_class)->finalize (object);
}
static void
get_flattened_entries_recurse (GMenuTreeDirectory *dir,
GHashTable *entry_set)
{
GMenuTreeIter *iter = gmenu_tree_directory_iter (dir);
GMenuTreeItemType next_type;
while ((next_type = gmenu_tree_iter_next (iter)) != GMENU_TREE_ITEM_INVALID)
{
gpointer item = NULL;
switch (next_type)
{
case GMENU_TREE_ITEM_ENTRY:
{
GMenuTreeEntry *entry;
item = entry = gmenu_tree_iter_get_entry (iter);
/* Key is owned by entry */
g_hash_table_replace (entry_set,
(char*)gmenu_tree_entry_get_desktop_file_id (entry),
gmenu_tree_item_ref (entry));
}
break;
case GMENU_TREE_ITEM_DIRECTORY:
{
item = gmenu_tree_iter_get_directory (iter);
get_flattened_entries_recurse ((GMenuTreeDirectory*)item, entry_set);
}
break;
default:
break;
}
if (item != NULL)
gmenu_tree_item_unref (item);
}
gmenu_tree_iter_unref (iter);
}
static GHashTable *
get_flattened_entries_from_tree (GMenuTree *tree)
{
GHashTable *table;
GMenuTreeDirectory *root;
table = g_hash_table_new_full (g_str_hash, g_str_equal,
(GDestroyNotify) NULL,
(GDestroyNotify) gmenu_tree_item_unref);
root = gmenu_tree_get_root_directory (tree);
if (root != NULL)
get_flattened_entries_recurse (root, table);
gmenu_tree_item_unref (root);
return table;
}
static void
on_apps_tree_changed_cb (GMenuTree *tree,
gpointer user_data)
{
ShellAppSystem *self = SHELL_APP_SYSTEM (user_data);
GError *error = NULL;
GHashTable *new_apps;
GHashTableIter iter;
gpointer key, value;
g_assert (tree == self->priv->apps_tree);
g_hash_table_remove_all (self->priv->visible_id_to_app);
if (!gmenu_tree_load_sync (self->priv->apps_tree, &error))
{
if (error)
{
g_warning ("Failed to load apps: %s", error->message);
g_error_free (error);
}
else
{
g_warning ("Failed to load apps");
}
return;
}
new_apps = get_flattened_entries_from_tree (self->priv->apps_tree);
g_hash_table_iter_init (&iter, new_apps);
while (g_hash_table_iter_next (&iter, &key, &value))
{
const char *id = key;
GMenuTreeEntry *entry = value;
ShellApp *app;
GDesktopAppInfo *info;
const char *startup_wm_class;
info = g_desktop_app_info_new (gmenu_tree_entry_get_desktop_file_id (entry));
app = g_hash_table_lookup (self->priv->id_to_app, id);
if (app != NULL)
{
GDesktopAppInfo *old_info;
const gchar *old_startup_wm_class;
old_info = shell_app_get_app_info (app);
old_startup_wm_class = g_desktop_app_info_get_startup_wm_class (old_info);
if (old_startup_wm_class)
g_hash_table_remove (self->priv->startup_wm_class_to_id, old_startup_wm_class);
_shell_app_set_app_info (app, info);
g_object_ref (app); /* Extra ref, removed in _replace below */
}
else
{
app = _shell_app_new (info);
}
g_object_unref (info);
g_hash_table_replace (self->priv->id_to_app, (char*)id, app);
if (!gmenu_tree_entry_get_is_nodisplay_recurse (entry))
g_hash_table_replace (self->priv->visible_id_to_app, (char*)id, app);
startup_wm_class = g_desktop_app_info_get_startup_wm_class (info);
if (startup_wm_class)
g_hash_table_replace (self->priv->startup_wm_class_to_id,
(char*)startup_wm_class, (char*)id);
}
/* Now iterate over the apps again; we need to unreference any apps
* which have been removed. The JS code may still be holding a
* reference; that's fine.
*/
g_hash_table_iter_init (&iter, self->priv->id_to_app);
while (g_hash_table_iter_next (&iter, &key, &value))
{
const char *id = key;
if (!g_hash_table_lookup (new_apps, id))
g_hash_table_iter_remove (&iter);
}
g_hash_table_destroy (new_apps);
g_signal_emit (self, signals[INSTALLED_CHANGED], 0);
}
/**
* shell_app_system_get_default:
*
* Return Value: (transfer none): The global #ShellAppSystem singleton
*/
ShellAppSystem *
shell_app_system_get_default ()
{
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)
{
return g_hash_table_lookup (self->priv->id_to_app, id);
}
/**
* 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: (allow-none): 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: (allow-none): 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:
case SHELL_APP_STATE_BUSY:
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;
}
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 in the given context. 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;
}
static gint
compare_apps_by_usage (gconstpointer a,
gconstpointer b,
gpointer data)
{
ShellAppUsage *usage = shell_app_usage_get_default ();
ShellApp *app_a = (ShellApp*)a;
ShellApp *app_b = (ShellApp*)b;
return shell_app_usage_compare (usage, "", app_a, app_b);
}
static GSList *
sort_and_concat_results (ShellAppSystem *system,
GSList *prefix_matches,
GSList *substring_matches)
{
GSList *matches = NULL;
GSList *l;
prefix_matches = g_slist_sort_with_data (prefix_matches,
compare_apps_by_usage,
system);
substring_matches = g_slist_sort_with_data (substring_matches,
compare_apps_by_usage,
system);
for (l = substring_matches; l != NULL; l = l->next)
matches = g_slist_prepend (matches, (char *) shell_app_get_id (SHELL_APP (l->data)));
for (l = prefix_matches; l != NULL; l = l->next)
matches = g_slist_prepend (matches, (char *) shell_app_get_id (SHELL_APP (l->data)));
return g_slist_reverse (matches);
}
/**
* normalize_terms:
* @terms: (element-type utf8): Input search terms
*
* Returns: (element-type utf8) (transfer full): Unicode-normalized and lowercased terms
*/
static GSList *
normalize_terms (GSList *terms)
{
GSList *normalized_terms = NULL;
GSList *iter;
for (iter = terms; iter; iter = iter->next)
{
const char *term = iter->data;
normalized_terms = g_slist_prepend (normalized_terms,
shell_util_normalize_casefold_and_unaccent (term));
}
return normalized_terms;
}
static GSList *
search_tree (ShellAppSystem *self,
GSList *terms,
GHashTable *apps)
{
GSList *prefix_results = NULL;
GSList *substring_results = NULL;
GSList *normalized_terms;
GHashTableIter iter;
gpointer key, value;
normalized_terms = normalize_terms (terms);
g_hash_table_iter_init (&iter, apps);
while (g_hash_table_iter_next (&iter, &key, &value))
{
ShellApp *app = value;
_shell_app_do_match (app, normalized_terms,
&prefix_results,
&substring_results);
}
g_slist_free_full (normalized_terms, g_free);
return sort_and_concat_results (self, prefix_results, substring_results);
}
/**
* shell_app_system_initial_search:
* @system: A #ShellAppSystem
* @terms: (element-type utf8): List of terms, logical AND
*
* Search through applications for the given search terms.
*
* Returns: (transfer container) (element-type utf8): List of applications
*/
GSList *
shell_app_system_initial_search (ShellAppSystem *self,
GSList *terms)
{
return search_tree (self, terms, self->priv->visible_id_to_app);
}
/**
* shell_app_system_subsearch:
* @system: A #ShellAppSystem
* @previous_results: (element-type utf8): List of previous results
* @terms: (element-type utf8): List of terms, logical AND
*
* Search through a previous result set; for more information, see
* js/ui/search.js. Note that returned strings are only valid until
* a return to the main loop.
*
* Returns: (transfer container) (element-type utf8): List of application identifiers
*/
GSList *
shell_app_system_subsearch (ShellAppSystem *system,
GSList *previous_results,
GSList *terms)
{
GSList *iter;
GSList *prefix_results = NULL;
GSList *substring_results = NULL;
GSList *normalized_terms = normalize_terms (terms);
previous_results = g_slist_reverse (previous_results);
for (iter = previous_results; iter; iter = iter->next)
{
ShellApp *app = shell_app_system_lookup_app (system, iter->data);
_shell_app_do_match (app, normalized_terms,
&prefix_results,
&substring_results);
}
g_slist_free_full (normalized_terms, g_free);
/* Note that a shorter term might have matched as a prefix, but
when extended only as a substring, so we have to redo the
sort rather than reusing the existing ordering */
return sort_and_concat_results (system, prefix_results, substring_results);
}