gnome-shell/src/shell-app.c
Colin Walters c5de239e25 shell_util_normalize_and_casefold: New utility function
Merge the duplicated copies into shell-util.
2011-08-10 13:00:06 -04:00

1352 lines
38 KiB
C

/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
#include "config.h"
#include <string.h>
#include <glib/gi18n-lib.h>
#include <meta/display.h>
#include "shell-app-private.h"
#include "shell-enum-types.h"
#include "shell-global.h"
#include "shell-util.h"
#include "shell-window-tracker-private.h"
#include "st.h"
typedef enum {
MATCH_NONE,
MATCH_SUBSTRING, /* Not prefix, substring */
MATCH_MULTIPLE_SUBSTRING, /* Matches multiple criteria with substrings */
MATCH_PREFIX, /* Strict prefix */
MATCH_MULTIPLE_PREFIX, /* Matches multiple criteria, at least one prefix */
} ShellAppSearchMatch;
/* This is mainly a memory usage optimization - the user is going to
* be running far fewer of the applications at one time than they have
* installed. But it also just helps keep the code more logically
* separated.
*/
typedef struct {
guint refcount;
/* Last time the user interacted with any of this application's windows */
guint32 last_user_time;
/* Signal connection to dirty window sort list on workspace changes */
guint workspace_switch_id;
GSList *windows;
/* Whether or not we need to resort the windows; this is done on demand */
gboolean window_sort_stale : 1;
} ShellAppRunningState;
/**
* SECTION:shell-app
* @short_description: Object representing an application
*
* This object wraps a #GMenuTreeEntry, providing methods and signals
* primarily useful for running applications.
*/
struct _ShellApp
{
GObject parent;
int started_on_workspace;
ShellAppState state;
GMenuTreeEntry *entry; /* If NULL, this app is backend by exactly one MetaWindow */
ShellAppRunningState *running_state;
char *window_id_string;
char *casefolded_name;
char *name_collation_key;
char *casefolded_description;
char *casefolded_exec;
};
G_DEFINE_TYPE (ShellApp, shell_app, G_TYPE_OBJECT);
enum {
PROP_0,
PROP_STATE
};
enum {
WINDOWS_CHANGED,
LAST_SIGNAL
};
static guint shell_app_signals[LAST_SIGNAL] = { 0 };
static void create_running_state (ShellApp *app);
static void unref_running_state (ShellAppRunningState *state);
static void
shell_app_get_property (GObject *gobject,
guint prop_id,
GValue *value,
GParamSpec *pspec)
{
ShellApp *app = SHELL_APP (gobject);
switch (prop_id)
{
case PROP_STATE:
g_value_set_enum (value, app->state);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec);
break;
}
}
const char *
shell_app_get_id (ShellApp *app)
{
if (app->entry)
return gmenu_tree_entry_get_desktop_file_id (app->entry);
return app->window_id_string;
}
static MetaWindow *
window_backed_app_get_window (ShellApp *app)
{
g_assert (app->entry == NULL);
g_assert (app->running_state);
g_assert (app->running_state->windows);
return app->running_state->windows->data;
}
/**
* shell_app_create_icon_texture:
*
* Look up the icon for this application, and create a #ClutterTexture
* for it at the given size.
*
* Return value: (transfer none): A floating #ClutterActor
*/
ClutterActor *
shell_app_create_icon_texture (ShellApp *app,
float size)
{
GIcon *icon;
ClutterActor *ret;
ret = NULL;
if (app->entry == NULL)
{
MetaWindow *window = window_backed_app_get_window (app);
return st_texture_cache_bind_pixbuf_property (st_texture_cache_get_default (),
G_OBJECT (window),
"icon");
}
icon = g_app_info_get_icon (G_APP_INFO (gmenu_tree_entry_get_app_info (app->entry)));
if (icon != NULL)
ret = st_texture_cache_load_gicon (st_texture_cache_get_default (), NULL, icon, (int)size);
if (ret == NULL)
{
icon = g_themed_icon_new ("application-x-executable");
ret = st_texture_cache_load_gicon (st_texture_cache_get_default (), NULL, icon, (int)size);
g_object_unref (icon);
}
return ret;
}
typedef struct {
ShellApp *app;
int size;
} CreateFadedIconData;
static CoglHandle
shell_app_create_faded_icon_cpu (StTextureCache *cache,
const char *key,
void *datap,
GError **error)
{
CreateFadedIconData *data = datap;
ShellApp *app;
GdkPixbuf *pixbuf;
int size;
CoglHandle texture;
gint width, height, rowstride;
guint8 n_channels;
gboolean have_alpha;
gint fade_start;
gint fade_range;
guint i, j;
guint pixbuf_byte_size;
guint8 *orig_pixels;
guint8 *pixels;
GIcon *icon;
GtkIconInfo *info;
app = data->app;
size = data->size;
info = NULL;
icon = g_app_info_get_icon (G_APP_INFO (gmenu_tree_entry_get_app_info (app->entry)));
if (icon != NULL)
{
info = gtk_icon_theme_lookup_by_gicon (gtk_icon_theme_get_default (),
icon, (int) (size + 0.5),
GTK_ICON_LOOKUP_FORCE_SIZE);
}
if (info == NULL)
{
icon = g_themed_icon_new ("application-x-executable");
info = gtk_icon_theme_lookup_by_gicon (gtk_icon_theme_get_default (),
icon, (int) (size + 0.5),
GTK_ICON_LOOKUP_FORCE_SIZE);
g_object_unref (icon);
}
if (info == NULL)
return COGL_INVALID_HANDLE;
pixbuf = gtk_icon_info_load_icon (info, NULL);
gtk_icon_info_free (info);
if (pixbuf == NULL)
return COGL_INVALID_HANDLE;
width = gdk_pixbuf_get_width (pixbuf);
height = gdk_pixbuf_get_height (pixbuf);
rowstride = gdk_pixbuf_get_rowstride (pixbuf);
n_channels = gdk_pixbuf_get_n_channels (pixbuf);
orig_pixels = gdk_pixbuf_get_pixels (pixbuf);
have_alpha = gdk_pixbuf_get_has_alpha (pixbuf);
pixbuf_byte_size = (height - 1) * rowstride +
+ width * ((n_channels * gdk_pixbuf_get_bits_per_sample (pixbuf) + 7) / 8);
pixels = g_malloc0 (rowstride * height);
memcpy (pixels, orig_pixels, pixbuf_byte_size);
fade_start = width / 2;
fade_range = width - fade_start;
for (i = fade_start; i < width; i++)
{
for (j = 0; j < height; j++)
{
guchar *pixel = &pixels[j * rowstride + i * n_channels];
float fade = 1.0 - ((float) i - fade_start) / fade_range;
pixel[0] = 0.5 + pixel[0] * fade;
pixel[1] = 0.5 + pixel[1] * fade;
pixel[2] = 0.5 + pixel[2] * fade;
if (have_alpha)
pixel[3] = 0.5 + pixel[3] * fade;
}
}
texture = cogl_texture_new_from_data (width,
height,
COGL_TEXTURE_NONE,
have_alpha ? COGL_PIXEL_FORMAT_RGBA_8888 : COGL_PIXEL_FORMAT_RGB_888,
COGL_PIXEL_FORMAT_ANY,
rowstride,
pixels);
g_free (pixels);
g_object_unref (pixbuf);
return texture;
}
/**
* shell_app_get_faded_icon:
* @app: A #ShellApp
* @size: Size in pixels
*
* Return an actor with a horizontally faded look.
*
* Return value: (transfer none): A floating #ClutterActor, or %NULL if no icon
*/
ClutterActor *
shell_app_get_faded_icon (ShellApp *app, float size)
{
CoglHandle texture;
ClutterActor *result;
char *cache_key;
CreateFadedIconData data;
/* Don't fade for window backed apps for now...easier to reuse the
* property tracking bits, and this helps us visually distinguish
* app-tracked from not.
*/
if (!app->entry)
{
MetaWindow *window = window_backed_app_get_window (app);
return st_texture_cache_bind_pixbuf_property (st_texture_cache_get_default (),
G_OBJECT (window),
"icon");
}
cache_key = g_strdup_printf ("faded-icon:%s,size=%f", shell_app_get_id (app), size);
data.app = app;
data.size = (int) (0.5 + size);
texture = st_texture_cache_load (st_texture_cache_get_default (),
cache_key,
ST_TEXTURE_CACHE_POLICY_FOREVER,
shell_app_create_faded_icon_cpu,
&data,
NULL);
g_free (cache_key);
if (texture != COGL_INVALID_HANDLE)
{
result = clutter_texture_new ();
clutter_texture_set_cogl_texture (CLUTTER_TEXTURE (result), texture);
}
else
{
result = clutter_texture_new ();
g_object_set (result, "opacity", 0, "width", size, "height", size, NULL);
}
return result;
}
const char *
shell_app_get_name (ShellApp *app)
{
if (app->entry)
return g_app_info_get_name (G_APP_INFO (gmenu_tree_entry_get_app_info (app->entry)));
else
{
MetaWindow *window = window_backed_app_get_window (app);
const char *name;
name = meta_window_get_wm_class (window);
if (!name)
name = _("Unknown");
return name;
}
}
const char *
shell_app_get_description (ShellApp *app)
{
if (app->entry)
return g_app_info_get_description (G_APP_INFO (gmenu_tree_entry_get_app_info (app->entry)));
else
return NULL;
}
/**
* shell_app_is_window_backed:
*
* A window backed application is one which represents just an open
* window, i.e. there's no .desktop file assocation, so we don't know
* how to launch it again.
*/
gboolean
shell_app_is_window_backed (ShellApp *app)
{
return app->entry == NULL;
}
typedef struct {
MetaWorkspace *workspace;
GSList **transients;
} CollectTransientsData;
static gboolean
collect_transients_on_workspace (MetaWindow *window,
gpointer datap)
{
CollectTransientsData *data = datap;
if (data->workspace && meta_window_get_workspace (window) != data->workspace)
return TRUE;
*data->transients = g_slist_prepend (*data->transients, window);
return TRUE;
}
/* The basic idea here is that when we're targeting a window,
* if it has transients we want to pick the most recent one
* the user interacted with.
* This function makes raising GEdit with the file chooser
* open work correctly.
*/
static MetaWindow *
find_most_recent_transient_on_same_workspace (MetaDisplay *display,
MetaWindow *reference)
{
GSList *transients, *transients_sorted, *iter;
MetaWindow *result;
CollectTransientsData data;
transients = NULL;
data.workspace = meta_window_get_workspace (reference);
data.transients = &transients;
meta_window_foreach_transient (reference, collect_transients_on_workspace, &data);
transients_sorted = meta_display_sort_windows_by_stacking (display, transients);
/* Reverse this so we're top-to-bottom (yes, we should probably change the order
* returned from the sort_windows_by_stacking function)
*/
transients_sorted = g_slist_reverse (transients_sorted);
g_slist_free (transients);
transients = NULL;
result = NULL;
for (iter = transients_sorted; iter; iter = iter->next)
{
MetaWindow *window = iter->data;
MetaWindowType wintype = meta_window_get_window_type (window);
/* Don't want to focus UTILITY types, like the Gimp toolbars */
if (wintype == META_WINDOW_NORMAL ||
wintype == META_WINDOW_DIALOG)
{
result = window;
break;
}
}
g_slist_free (transients_sorted);
return result;
}
/**
* shell_app_activate_window:
* @app: a #ShellApp
* @window: (allow-none): Window to be focused
* @timestamp: Event timestamp
*
* Bring all windows for the given app to the foreground,
* but ensure that @window is on top. If @window is %NULL,
* the window with the most recent user time for the app
* will be used.
*
* This function has no effect if @app is not currently running.
*/
void
shell_app_activate_window (ShellApp *app,
MetaWindow *window,
guint32 timestamp)
{
GSList *windows;
if (shell_app_get_state (app) != SHELL_APP_STATE_RUNNING)
return;
windows = shell_app_get_windows (app);
if (window == NULL && windows)
window = windows->data;
if (!g_slist_find (windows, window))
return;
else
{
GSList *iter;
ShellGlobal *global = shell_global_get ();
MetaScreen *screen = shell_global_get_screen (global);
MetaDisplay *display = meta_screen_get_display (screen);
MetaWorkspace *active = meta_screen_get_active_workspace (screen);
MetaWorkspace *workspace = meta_window_get_workspace (window);
guint32 last_user_timestamp = meta_display_get_last_user_time (display);
MetaWindow *most_recent_transient;
if (meta_display_xserver_time_is_before (display, timestamp, last_user_timestamp))
{
meta_window_set_demands_attention (window);
return;
}
/* Now raise all the other windows for the app that are on
* the same workspace, in reverse order to preserve the stacking.
*/
for (iter = windows; iter; iter = iter->next)
{
MetaWindow *other_window = iter->data;
if (other_window != window)
meta_window_raise (other_window);
}
/* If we have a transient that the user's interacted with more recently than
* the window, pick that.
*/
most_recent_transient = find_most_recent_transient_on_same_workspace (display, window);
if (most_recent_transient
&& meta_display_xserver_time_is_before (display,
meta_window_get_user_time (window),
meta_window_get_user_time (most_recent_transient)))
window = most_recent_transient;
if (!shell_window_tracker_is_window_interesting (window))
{
/* We won't get notify::user-time signals for uninteresting windows,
* which means that an app's last_user_time won't get updated.
* Update it here instead.
*/
app->running_state->last_user_time = timestamp;
}
if (active != workspace)
meta_workspace_activate_with_focus (workspace, window, timestamp);
else
meta_window_activate (window, timestamp);
}
}
/**
* shell_app_activate:
* @app: a #ShellApp
* @workspace: launch on this workspace, or -1 for default. Ignored if
* activating an existing window
*
* Perform an appropriate default action for operating on this application,
* dependent on its current state. For example, if the application is not
* currently running, launch it. If it is running, activate the most
* recently used NORMAL window (or if that window has a transient, the most
* recently used transient for that window).
*/
void
shell_app_activate (ShellApp *app,
int workspace)
{
switch (app->state)
{
case SHELL_APP_STATE_STOPPED:
{
GError *error = NULL;
if (!shell_app_launch (app,
0,
NULL,
workspace,
NULL,
&error))
{
char *msg;
msg = g_strdup_printf (_("Failed to launch '%s'"), shell_app_get_name (app));
shell_global_notify_error (shell_global_get (),
msg,
error->message);
g_free (msg);
g_clear_error (&error);
}
}
break;
case SHELL_APP_STATE_STARTING:
break;
case SHELL_APP_STATE_RUNNING:
shell_app_activate_window (app, NULL, shell_global_get_current_time (shell_global_get ()));
break;
}
}
/**
* shell_app_open_new_window:
* @app: a #ShellApp
* @workspace: open on this workspace, or -1 for default
*
* Request that the application create a new window.
*/
void
shell_app_open_new_window (ShellApp *app,
int workspace)
{
g_return_if_fail (app->entry != NULL);
/* Here we just always launch the application again, even if we know
* it was already running. For most applications this
* should have the effect of creating a new window, whether that's
* a second process (in the case of Calculator) or IPC to existing
* instance (Firefox). There are a few less-sensical cases such
* as say Pidgin. Ideally, we have the application express to us
* that it supports an explicit new-window action.
*/
shell_app_launch (app,
0,
NULL,
workspace,
NULL,
NULL);
}
/**
* shell_app_get_state:
* @app: a #ShellApp
*
* Returns: State of the application
*/
ShellAppState
shell_app_get_state (ShellApp *app)
{
return app->state;
}
typedef struct {
ShellApp *app;
MetaWorkspace *active_workspace;
} CompareWindowsData;
static int
shell_app_compare_windows (gconstpointer a,
gconstpointer b,
gpointer datap)
{
MetaWindow *win_a = (gpointer)a;
MetaWindow *win_b = (gpointer)b;
CompareWindowsData *data = datap;
gboolean ws_a, ws_b;
gboolean vis_a, vis_b;
ws_a = meta_window_get_workspace (win_a) == data->active_workspace;
ws_b = meta_window_get_workspace (win_b) == data->active_workspace;
if (ws_a && !ws_b)
return -1;
else if (!ws_a && ws_b)
return 1;
vis_a = meta_window_showing_on_its_workspace (win_a);
vis_b = meta_window_showing_on_its_workspace (win_b);
if (vis_a && !vis_b)
return -1;
else if (!vis_a && vis_b)
return 1;
return meta_window_get_user_time (win_b) - meta_window_get_user_time (win_a);
}
/**
* shell_app_get_windows:
* @app:
*
* Get the toplevel, interesting windows which are associated with this
* application. The returned list will be sorted first by whether
* they're on the active workspace, then by whether they're visible,
* and finally by the time the user last interacted with them.
*
* Returns: (transfer none) (element-type MetaWindow): List of windows
*/
GSList *
shell_app_get_windows (ShellApp *app)
{
if (app->running_state == NULL)
return NULL;
if (app->running_state->window_sort_stale)
{
CompareWindowsData data;
data.app = app;
data.active_workspace = meta_screen_get_active_workspace (shell_global_get_screen (shell_global_get ()));
app->running_state->windows = g_slist_sort_with_data (app->running_state->windows, shell_app_compare_windows, &data);
app->running_state->window_sort_stale = FALSE;
}
return app->running_state->windows;
}
guint
shell_app_get_n_windows (ShellApp *app)
{
if (app->running_state == NULL)
return 0;
return g_slist_length (app->running_state->windows);
}
static gboolean
shell_app_has_visible_windows (ShellApp *app)
{
GSList *iter;
if (app->running_state == NULL)
return FALSE;
for (iter = app->running_state->windows; iter; iter = iter->next)
{
MetaWindow *window = iter->data;
if (meta_window_showing_on_its_workspace (window))
return TRUE;
}
return FALSE;
}
gboolean
shell_app_is_on_workspace (ShellApp *app,
MetaWorkspace *workspace)
{
GSList *iter;
if (shell_app_get_state (app) == SHELL_APP_STATE_STARTING)
{
if (app->started_on_workspace == -1 ||
meta_workspace_index (workspace) == app->started_on_workspace)
return TRUE;
else
return FALSE;
}
if (app->running_state == NULL)
return FALSE;
for (iter = app->running_state->windows; iter; iter = iter->next)
{
if (meta_window_get_workspace (iter->data) == workspace)
return TRUE;
}
return FALSE;
}
/**
* shell_app_compare:
* @app:
* @other: A #ShellApp
*
* Compare one #ShellApp instance to another, in the following way:
* - Running applications sort before not-running applications.
* - If one of them has visible windows and the other does not, the one
* with visible windows is first.
* - Finally, the application which the user interacted with most recently
* compares earlier.
*/
int
shell_app_compare (ShellApp *app,
ShellApp *other)
{
gboolean vis_app, vis_other;
if (app->state != other->state)
{
if (app->state == SHELL_APP_STATE_RUNNING)
return -1;
return 1;
}
vis_app = shell_app_has_visible_windows (app);
vis_other = shell_app_has_visible_windows (other);
if (vis_app && !vis_other)
return -1;
else if (!vis_app && vis_other)
return 1;
if (app->state == SHELL_APP_STATE_RUNNING)
{
if (app->running_state->windows && !other->running_state->windows)
return -1;
else if (!app->running_state->windows && other->running_state->windows)
return 1;
return other->running_state->last_user_time - app->running_state->last_user_time;
}
return 0;
}
ShellApp *
_shell_app_new_for_window (MetaWindow *window)
{
ShellApp *app;
app = g_object_new (SHELL_TYPE_APP, NULL);
/* For windows, its id is simply its pointer address as a string.
* There are various other alternatives, but the address is unique
* and unchanging, which is pretty much the best we can do.
*/
app->window_id_string = g_strdup_printf ("window:%p", window);
_shell_app_add_window (app, window);
return app;
}
ShellApp *
_shell_app_new (GMenuTreeEntry *info)
{
ShellApp *app;
app = g_object_new (SHELL_TYPE_APP, NULL);
app->entry = gmenu_tree_item_ref (info);
app->name_collation_key = g_utf8_collate_key (shell_app_get_name (app), -1);
return app;
}
static void
shell_app_state_transition (ShellApp *app,
ShellAppState state)
{
if (app->state == state)
return;
g_return_if_fail (!(app->state == SHELL_APP_STATE_RUNNING &&
state == SHELL_APP_STATE_STARTING));
app->state = state;
if (app->state == SHELL_APP_STATE_STOPPED && app->running_state)
{
unref_running_state (app->running_state);
app->running_state = NULL;
}
_shell_window_tracker_notify_app_state_changed (shell_window_tracker_get_default (), app);
g_object_notify (G_OBJECT (app), "state");
}
static void
shell_app_on_unmanaged (MetaWindow *window,
ShellApp *app)
{
_shell_app_remove_window (app, window);
}
static void
shell_app_on_user_time_changed (MetaWindow *window,
GParamSpec *pspec,
ShellApp *app)
{
g_assert (app->running_state != NULL);
app->running_state->last_user_time = meta_window_get_user_time (window);
/* Ideally we don't want to emit windows-changed if the sort order
* isn't actually changing. This check catches most of those.
*/
if (window != app->running_state->windows->data)
{
app->running_state->window_sort_stale = TRUE;
g_signal_emit (app, shell_app_signals[WINDOWS_CHANGED], 0);
}
}
static void
shell_app_on_ws_switch (MetaScreen *screen,
int from,
int to,
MetaMotionDirection direction,
gpointer data)
{
ShellApp *app = SHELL_APP (data);
g_assert (app->running_state != NULL);
app->running_state->window_sort_stale = TRUE;
g_signal_emit (app, shell_app_signals[WINDOWS_CHANGED], 0);
}
void
_shell_app_add_window (ShellApp *app,
MetaWindow *window)
{
guint32 user_time;
if (app->running_state && g_slist_find (app->running_state->windows, window))
return;
g_object_freeze_notify (G_OBJECT (app));
if (!app->running_state)
create_running_state (app);
app->running_state->window_sort_stale = TRUE;
app->running_state->windows = g_slist_prepend (app->running_state->windows, g_object_ref (window));
g_signal_connect (window, "unmanaged", G_CALLBACK(shell_app_on_unmanaged), app);
g_signal_connect (window, "notify::user-time", G_CALLBACK(shell_app_on_user_time_changed), app);
user_time = meta_window_get_user_time (window);
if (user_time > app->running_state->last_user_time)
app->running_state->last_user_time = user_time;
if (app->state != SHELL_APP_STATE_STARTING)
shell_app_state_transition (app, SHELL_APP_STATE_RUNNING);
g_object_thaw_notify (G_OBJECT (app));
g_signal_emit (app, shell_app_signals[WINDOWS_CHANGED], 0);
}
void
_shell_app_remove_window (ShellApp *app,
MetaWindow *window)
{
g_assert (app->running_state != NULL);
if (!g_slist_find (app->running_state->windows, window))
return;
g_signal_handlers_disconnect_by_func (window, G_CALLBACK(shell_app_on_unmanaged), app);
g_signal_handlers_disconnect_by_func (window, G_CALLBACK(shell_app_on_user_time_changed), app);
g_object_unref (window);
app->running_state->windows = g_slist_remove (app->running_state->windows, window);
if (app->running_state->windows == NULL)
shell_app_state_transition (app, SHELL_APP_STATE_STOPPED);
g_signal_emit (app, shell_app_signals[WINDOWS_CHANGED], 0);
}
/**
* shell_app_get_pids:
* @app: a #ShellApp
*
* Returns: (transfer container) (element-type int): An unordered list of process identifers associated with this application.
*/
GSList *
shell_app_get_pids (ShellApp *app)
{
GSList *result;
GSList *iter;
result = NULL;
for (iter = shell_app_get_windows (app); iter; iter = iter->next)
{
MetaWindow *window = iter->data;
int pid = meta_window_get_pid (window);
/* Note in the (by far) common case, app will only have one pid, so
* we'll hit the first element, so don't worry about O(N^2) here.
*/
if (!g_slist_find (result, GINT_TO_POINTER (pid)))
result = g_slist_prepend (result, GINT_TO_POINTER (pid));
}
return result;
}
void
_shell_app_handle_startup_sequence (ShellApp *app,
SnStartupSequence *sequence)
{
gboolean starting = !sn_startup_sequence_get_completed (sequence);
/* The Shell design calls for on application launch, the app title
* appears at top, and no X window is focused. So when we get
* a startup-notification for this app, transition it to STARTING
* if it's currently stopped, set it as our application focus,
* but focus the no_focus window.
*/
if (starting && shell_app_get_state (app) == SHELL_APP_STATE_STOPPED)
{
MetaScreen *screen = shell_global_get_screen (shell_global_get ());
MetaDisplay *display = meta_screen_get_display (screen);
shell_app_state_transition (app, SHELL_APP_STATE_STARTING);
meta_display_focus_the_no_focus_window (display, screen,
sn_startup_sequence_get_timestamp (sequence));
app->started_on_workspace = sn_startup_sequence_get_workspace (sequence);
}
if (!starting)
{
if (app->running_state && app->running_state->windows)
shell_app_state_transition (app, SHELL_APP_STATE_RUNNING);
else /* application have > 1 .desktop file */
shell_app_state_transition (app, SHELL_APP_STATE_STOPPED);
}
}
/**
* shell_app_request_quit:
* @app: A #ShellApp
*
* Initiate an asynchronous request to quit this application.
* The application may interact with the user, and the user
* might cancel the quit request from the application UI.
*
* This operation may not be supported for all applications.
*
* Returns: %TRUE if a quit request is supported for this application
*/
gboolean
shell_app_request_quit (ShellApp *app)
{
GSList *iter;
if (shell_app_get_state (app) != SHELL_APP_STATE_RUNNING)
return FALSE;
/* TODO - check for an XSMP connection; we could probably use that */
for (iter = app->running_state->windows; iter; iter = iter->next)
{
MetaWindow *win = iter->data;
if (!shell_window_tracker_is_window_interesting (win))
continue;
meta_window_delete (win, shell_global_get_current_time (shell_global_get ()));
}
return TRUE;
}
static void
_gather_pid_callback (GDesktopAppInfo *gapp,
GPid pid,
gpointer data)
{
ShellApp *app;
ShellWindowTracker *tracker;
g_return_if_fail (data != NULL);
app = SHELL_APP (data);
tracker = shell_window_tracker_get_default ();
_shell_window_tracker_add_child_process_app (tracker,
pid,
app);
}
/**
* shell_app_launch:
* @timestamp: Event timestamp, or 0 for current event timestamp
* @uris: List of uris to pass to application
* @workspace: Start on this workspace, or -1 for default
* @startup_id: (out): Returned startup notification ID, or %NULL if none
* @error: A #GError
*/
gboolean
shell_app_launch (ShellApp *app,
guint timestamp,
GList *uris,
int workspace,
char **startup_id,
GError **error)
{
GDesktopAppInfo *gapp;
GdkAppLaunchContext *context;
gboolean ret;
ShellGlobal *global;
MetaScreen *screen;
if (startup_id)
*startup_id = NULL;
if (app->entry == NULL)
{
MetaWindow *window = window_backed_app_get_window (app);
/* We can't pass URIs into a window; shouldn't hit this
* code path. If we do, fix the caller to disallow it.
*/
g_return_val_if_fail (uris == NULL, TRUE);
meta_window_activate (window, timestamp);
return TRUE;
}
global = shell_global_get ();
screen = shell_global_get_screen (global);
if (timestamp == 0)
timestamp = clutter_get_current_event_time ();
if (workspace < 0)
workspace = meta_screen_get_active_workspace_index (screen);
context = gdk_app_launch_context_new ();
gdk_app_launch_context_set_timestamp (context, timestamp);
gdk_app_launch_context_set_desktop (context, workspace);
gapp = gmenu_tree_entry_get_app_info (app->entry);
ret = g_desktop_app_info_launch_uris_as_manager (gapp, uris,
G_APP_LAUNCH_CONTEXT (context),
G_SPAWN_SEARCH_PATH | G_SPAWN_DO_NOT_REAP_CHILD,
NULL, NULL,
_gather_pid_callback, app,
error);
g_object_unref (context);
return ret;
}
/**
* shell_app_get_app_info:
* @app: a #ShellApp
*
* Returns: (transfer none): The #GDesktopAppInfo for this app, or %NULL if backed by a window
*/
GDesktopAppInfo *
shell_app_get_app_info (ShellApp *app)
{
if (app->entry)
return gmenu_tree_entry_get_app_info (app->entry);
return NULL;
}
/**
* shell_app_get_tree_entry:
* @app: a #ShellApp
*
* Returns: (transfer none): The #GMenuTreeEntry for this app, or %NULL if backed by a window
*/
GMenuTreeEntry *
shell_app_get_tree_entry (ShellApp *app)
{
return app->entry;
}
static void
create_running_state (ShellApp *app)
{
MetaScreen *screen;
g_assert (app->running_state == NULL);
screen = shell_global_get_screen (shell_global_get ());
app->running_state = g_slice_new0 (ShellAppRunningState);
app->running_state->refcount = 1;
app->running_state->workspace_switch_id =
g_signal_connect (screen, "workspace-switched", G_CALLBACK(shell_app_on_ws_switch), app);
}
static void
unref_running_state (ShellAppRunningState *state)
{
MetaScreen *screen;
state->refcount--;
if (state->refcount > 0)
return;
screen = shell_global_get_screen (shell_global_get ());
g_signal_handler_disconnect (screen, state->workspace_switch_id);
g_slice_free (ShellAppRunningState, state);
}
static char *
trim_exec_line (const char *str)
{
const char *start, *end, *pos;
end = strchr (str, ' ');
if (end == NULL)
end = str + strlen (str);
start = str;
while ((pos = strchr (start, '/')) && pos < end)
start = ++pos;
return g_strndup (start, end - start);
}
static void
shell_app_init_search_data (ShellApp *app)
{
const char *name;
const char *exec;
const char *comment;
char *normalized_exec;
GDesktopAppInfo *appinfo;
appinfo = gmenu_tree_entry_get_app_info (app->entry);
name = g_app_info_get_name (G_APP_INFO (appinfo));
app->casefolded_name = shell_util_normalize_and_casefold (name);
comment = g_app_info_get_description (G_APP_INFO (appinfo));
app->casefolded_description = shell_util_normalize_and_casefold (comment);
exec = g_app_info_get_executable (G_APP_INFO (appinfo));
normalized_exec = shell_util_normalize_and_casefold (exec);
app->casefolded_exec = trim_exec_line (normalized_exec);
g_free (normalized_exec);
}
/**
* shell_app_compare_by_name:
* @app:
* @other:
*
* Order two applications by name.
*
* Returns: -1, 0, or 1; suitable for use as a comparison function for e.g. g_slist_sort()
*/
int
shell_app_compare_by_name (ShellApp *app, ShellApp *other)
{
return strcmp (app->name_collation_key, other->name_collation_key);
}
static ShellAppSearchMatch
_shell_app_match_search_terms (ShellApp *app,
GSList *terms)
{
GSList *iter;
ShellAppSearchMatch match;
if (G_UNLIKELY (!app->casefolded_name))
shell_app_init_search_data (app);
match = MATCH_NONE;
for (iter = terms; iter; iter = iter->next)
{
ShellAppSearchMatch current_match;
const char *term = iter->data;
const char *p;
current_match = MATCH_NONE;
p = strstr (app->casefolded_name, term);
if (p == app->casefolded_name)
current_match = MATCH_PREFIX;
else if (p != NULL)
current_match = MATCH_SUBSTRING;
p = strstr (app->casefolded_exec, term);
if (p != NULL)
{
if (p == app->casefolded_exec)
current_match = (current_match == MATCH_NONE) ? MATCH_PREFIX
: MATCH_MULTIPLE_PREFIX;
else if (current_match < MATCH_PREFIX)
current_match = (current_match == MATCH_NONE) ? MATCH_SUBSTRING
: MATCH_MULTIPLE_SUBSTRING;
}
if (app->casefolded_description && current_match < MATCH_PREFIX)
{
/* Only do substring matches, as prefix matches are not meaningful
* enough for descriptions
*/
p = strstr (app->casefolded_description, term);
if (p != NULL)
current_match = (current_match == MATCH_NONE) ? MATCH_SUBSTRING
: MATCH_MULTIPLE_SUBSTRING;
}
if (current_match == MATCH_NONE)
return current_match;
if (current_match > match)
match = current_match;
}
return match;
}
void
_shell_app_do_match (ShellApp *app,
GSList *terms,
GSList **multiple_prefix_results,
GSList **prefix_results,
GSList **multiple_substring_results,
GSList **substring_results)
{
ShellAppSearchMatch match;
GAppInfo *appinfo;
g_assert (app != NULL);
/* Skip window-backed apps */
appinfo = (GAppInfo*)shell_app_get_app_info (app);
if (appinfo == NULL)
return;
/* Skip not-visible apps */
if (!g_app_info_should_show (appinfo))
return;
match = _shell_app_match_search_terms (app, terms);
switch (match)
{
case MATCH_NONE:
break;
case MATCH_MULTIPLE_PREFIX:
*multiple_prefix_results = g_slist_prepend (*multiple_prefix_results, app);
break;
case MATCH_PREFIX:
*prefix_results = g_slist_prepend (*prefix_results, app);
break;
case MATCH_MULTIPLE_SUBSTRING:
*multiple_substring_results = g_slist_prepend (*multiple_substring_results, app);
break;
case MATCH_SUBSTRING:
*substring_results = g_slist_prepend (*substring_results, app);
break;
}
}
static void
shell_app_init (ShellApp *self)
{
self->state = SHELL_APP_STATE_STOPPED;
}
static void
shell_app_dispose (GObject *object)
{
ShellApp *app = SHELL_APP (object);
if (app->entry)
{
gmenu_tree_item_unref (app->entry);
app->entry = NULL;
}
if (app->running_state)
{
while (app->running_state->windows)
_shell_app_remove_window (app, app->running_state->windows->data);
}
G_OBJECT_CLASS(shell_app_parent_class)->dispose (object);
}
static void
shell_app_finalize (GObject *object)
{
ShellApp *app = SHELL_APP (object);
g_free (app->window_id_string);
g_free (app->casefolded_name);
g_free (app->name_collation_key);
g_free (app->casefolded_description);
G_OBJECT_CLASS(shell_app_parent_class)->finalize (object);
}
static void
shell_app_class_init(ShellAppClass *klass)
{
GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
gobject_class->get_property = shell_app_get_property;
gobject_class->dispose = shell_app_dispose;
gobject_class->finalize = shell_app_finalize;
shell_app_signals[WINDOWS_CHANGED] = g_signal_new ("windows-changed",
SHELL_TYPE_APP,
G_SIGNAL_RUN_LAST,
0,
NULL, NULL,
g_cclosure_marshal_VOID__VOID,
G_TYPE_NONE, 0);
/**
* ShellApp:state:
*
* The high-level state of the application, effectively whether it's
* running or not, or transitioning between those states.
*/
g_object_class_install_property (gobject_class,
PROP_STATE,
g_param_spec_enum ("state",
"State",
"Application state",
SHELL_TYPE_APP_STATE,
SHELL_APP_STATE_STOPPED,
G_PARAM_READABLE));
}