summaryrefslogtreecommitdiffstats
path: root/src/shell-app.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/shell-app.c')
-rw-r--r--src/shell-app.c1718
1 files changed, 1718 insertions, 0 deletions
diff --git a/src/shell-app.c b/src/shell-app.c
new file mode 100644
index 0000000..b526e7d
--- /dev/null
+++ b/src/shell-app.c
@@ -0,0 +1,1718 @@
+/* -*- 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 <meta/meta-workspace-manager.h>
+#include <meta/meta-x11-display.h>
+
+#include "shell-app-private.h"
+#include "shell-enum-types.h"
+#include "shell-global.h"
+#include "shell-util.h"
+#include "shell-app-system-private.h"
+#include "shell-window-tracker-private.h"
+#include "st.h"
+#include "gtkactionmuxer.h"
+#include "org-gtk-application.h"
+#include "switcheroo-control.h"
+
+#ifdef HAVE_SYSTEMD
+#include <systemd/sd-journal.h>
+#include <errno.h>
+#include <unistd.h>
+#endif
+
+/* 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;
+
+ /* Signal connection to dirty window sort list on workspace changes */
+ gulong workspace_switch_id;
+
+ gulong icon_changed_id;
+
+ GSList *windows;
+
+ guint interesting_windows;
+
+ /* Whether or not we need to resort the windows; this is done on demand */
+ guint window_sort_stale : 1;
+
+ /* See GApplication documentation */
+ GtkActionMuxer *muxer;
+ char *unique_bus_name;
+ GDBusConnection *session;
+
+ /* GDBus Proxy for getting application busy state */
+ ShellOrgGtkApplication *application_proxy;
+ GCancellable *cancellable;
+
+} ShellAppRunningState;
+
+/**
+ * SECTION:shell-app
+ * @short_description: Object representing an application
+ *
+ * This object wraps a #GDesktopAppInfo, providing methods and signals
+ * primarily useful for running applications.
+ */
+struct _ShellApp
+{
+ GObject parent;
+
+ int started_on_workspace;
+
+ ShellAppState state;
+
+ GDesktopAppInfo *info; /* If NULL, this app is backed by one or more
+ * MetaWindow. For purposes of app title
+ * etc., we use the first window added,
+ * because it's most likely to be what we
+ * want (e.g. it will be of TYPE_NORMAL from
+ * the way shell-window-tracker.c works).
+ */
+ GIcon *fallback_icon;
+
+ ShellAppRunningState *running_state;
+
+ char *window_id_string;
+ char *name_collation_key;
+};
+
+enum {
+ PROP_0,
+ PROP_STATE,
+ PROP_BUSY,
+ PROP_ID,
+ PROP_DBUS_ID,
+ PROP_ACTION_GROUP,
+ PROP_ICON,
+ PROP_APP_INFO
+};
+
+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);
+
+G_DEFINE_TYPE (ShellApp, shell_app, G_TYPE_OBJECT)
+
+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;
+ case PROP_BUSY:
+ g_value_set_boolean (value, shell_app_get_busy (app));
+ break;
+ case PROP_ID:
+ g_value_set_string (value, shell_app_get_id (app));
+ break;
+ case PROP_ICON:
+ g_value_set_object (value, shell_app_get_icon (app));
+ break;
+ case PROP_ACTION_GROUP:
+ if (app->running_state)
+ g_value_set_object (value, app->running_state->muxer);
+ break;
+ case PROP_APP_INFO:
+ if (app->info)
+ g_value_set_object (value, app->info);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+shell_app_set_property (GObject *gobject,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ ShellApp *app = SHELL_APP (gobject);
+
+ switch (prop_id)
+ {
+ case PROP_APP_INFO:
+ _shell_app_set_app_info (app, g_value_get_object (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec);
+ break;
+ }
+}
+
+const char *
+shell_app_get_id (ShellApp *app)
+{
+ if (app->info)
+ return g_app_info_get_id (G_APP_INFO (app->info));
+ return app->window_id_string;
+}
+
+static MetaWindow *
+window_backed_app_get_window (ShellApp *app)
+{
+ g_assert (app->info == NULL);
+ if (app->running_state)
+ {
+ g_assert (app->running_state->windows);
+ return app->running_state->windows->data;
+ }
+ else
+ return NULL;
+}
+
+static GIcon *
+x11_window_create_fallback_gicon (MetaWindow *window)
+{
+ StTextureCache *texture_cache;
+ cairo_surface_t *surface;
+
+ g_object_get (window, "icon", &surface, NULL);
+
+ texture_cache = st_texture_cache_get_default ();
+ return st_texture_cache_load_cairo_surface_to_gicon (texture_cache, surface);
+}
+
+static void
+on_window_icon_changed (GObject *object,
+ const GParamSpec *pspec,
+ gpointer user_data)
+{
+ MetaWindow *window = META_WINDOW (object);
+ ShellApp *app = user_data;
+
+ g_clear_object (&app->fallback_icon);
+ app->fallback_icon = x11_window_create_fallback_gicon (window);
+
+ g_object_notify (G_OBJECT (app), "icon");
+}
+
+/**
+ * shell_app_get_icon:
+ *
+ * Look up the icon for this application
+ *
+ * Return value: (transfer none): A #GIcon
+ */
+GIcon *
+shell_app_get_icon (ShellApp *app)
+{
+ MetaWindow *window = NULL;
+
+ g_return_val_if_fail (SHELL_IS_APP (app), NULL);
+
+ if (app->info)
+ return g_app_info_get_icon (G_APP_INFO (app->info));
+
+ if (app->fallback_icon)
+ return app->fallback_icon;
+
+ /* During a state transition from running to not-running for
+ * window-backend apps, it's possible we get a request for the icon.
+ * Avoid asserting here and just return a fallback icon
+ */
+ if (app->running_state != NULL)
+ window = window_backed_app_get_window (app);
+
+ if (window &&
+ meta_window_get_client_type (window) == META_WINDOW_CLIENT_TYPE_X11)
+ {
+ app->fallback_icon = x11_window_create_fallback_gicon (window);
+ app->running_state->icon_changed_id =
+ g_signal_connect (G_OBJECT (window),
+ "notify::icon", G_CALLBACK (on_window_icon_changed), app);
+ }
+ else
+ {
+ app->fallback_icon = g_themed_icon_new ("application-x-executable");
+ }
+
+ return app->fallback_icon;
+}
+
+/**
+ * shell_app_create_icon_texture:
+ *
+ * Look up the icon for this application, and create a #ClutterActor
+ * for it at the given size.
+ *
+ * Return value: (transfer none): A floating #ClutterActor
+ */
+ClutterActor *
+shell_app_create_icon_texture (ShellApp *app,
+ int size)
+{
+ ClutterActor *ret;
+
+ ret = st_icon_new ();
+ st_icon_set_icon_size (ST_ICON (ret), size);
+ st_icon_set_fallback_icon_name (ST_ICON (ret), "application-x-executable");
+
+ g_object_bind_property (app, "icon", ret, "gicon", G_BINDING_SYNC_CREATE);
+
+ if (shell_app_is_window_backed (app))
+ st_widget_add_style_class_name (ST_WIDGET (ret), "fallback-app-icon");
+
+ return ret;
+}
+
+const char *
+shell_app_get_name (ShellApp *app)
+{
+ if (app->info)
+ return g_app_info_get_name (G_APP_INFO (app->info));
+ else
+ {
+ MetaWindow *window = window_backed_app_get_window (app);
+ const char *name = NULL;
+
+ if (window)
+ name = meta_window_get_wm_class (window);
+ if (!name)
+ name = C_("program", "Unknown");
+ return name;
+ }
+}
+
+const char *
+shell_app_get_description (ShellApp *app)
+{
+ if (app->info)
+ return g_app_info_get_description (G_APP_INFO (app->info));
+ 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 association, so we don't know
+ * how to launch it again.
+ */
+gboolean
+shell_app_is_window_backed (ShellApp *app)
+{
+ return app->info == 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;
+}
+
+static MetaWorkspace *
+get_active_workspace (void)
+{
+ ShellGlobal *global = shell_global_get ();
+ MetaDisplay *display = shell_global_get_display (global);
+ MetaWorkspaceManager *workspace_manager =
+ meta_display_get_workspace_manager (display);
+
+ return meta_workspace_manager_get_active_workspace (workspace_manager);
+}
+
+/**
+ * shell_app_activate_window:
+ * @app: a #ShellApp
+ * @window: (nullable): 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 *windows_reversed, *iter;
+ ShellGlobal *global = shell_global_get ();
+ MetaDisplay *display = shell_global_get_display (global);
+ MetaWorkspace *active = get_active_workspace ();
+ 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.
+ */
+ windows_reversed = g_slist_copy (windows);
+ windows_reversed = g_slist_reverse (windows_reversed);
+ for (iter = windows_reversed; iter; iter = iter->next)
+ {
+ MetaWindow *other_window = iter->data;
+
+ if (other_window != window && meta_window_get_workspace (other_window) == workspace)
+ meta_window_raise (other_window);
+ }
+ g_slist_free (windows_reversed);
+
+ /* 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 (active != workspace)
+ meta_workspace_activate_with_focus (workspace, window, timestamp);
+ else
+ meta_window_activate (window, timestamp);
+ }
+}
+
+
+void
+shell_app_update_window_actions (ShellApp *app, MetaWindow *window)
+{
+ const char *object_path;
+
+ object_path = meta_window_get_gtk_window_object_path (window);
+ if (object_path != NULL)
+ {
+ GActionGroup *actions;
+
+ actions = g_object_get_data (G_OBJECT (window), "actions");
+ if (actions == NULL)
+ {
+ actions = G_ACTION_GROUP (g_dbus_action_group_get (app->running_state->session,
+ meta_window_get_gtk_unique_bus_name (window),
+ object_path));
+ g_object_set_data_full (G_OBJECT (window), "actions", actions, g_object_unref);
+ }
+
+ g_assert (app->running_state->muxer);
+ gtk_action_muxer_insert (app->running_state->muxer, "win", actions);
+ g_object_notify (G_OBJECT (app), "action-group");
+ }
+}
+
+/**
+ * shell_app_activate:
+ * @app: a #ShellApp
+ *
+ * Like shell_app_activate_full(), but using the default workspace and
+ * event timestamp.
+ */
+void
+shell_app_activate (ShellApp *app)
+{
+ return shell_app_activate_full (app, -1, 0);
+}
+
+/**
+ * shell_app_activate_full:
+ * @app: a #ShellApp
+ * @workspace: launch on this workspace, or -1 for default. Ignored if
+ * activating an existing window
+ * @timestamp: Event timestamp
+ *
+ * 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_full (ShellApp *app,
+ int workspace,
+ guint32 timestamp)
+{
+ ShellGlobal *global;
+
+ global = shell_global_get ();
+
+ if (timestamp == 0)
+ timestamp = shell_global_get_current_time (global);
+
+ switch (app->state)
+ {
+ case SHELL_APP_STATE_STOPPED:
+ {
+ GError *error = NULL;
+ if (!shell_app_launch (app, timestamp, workspace, SHELL_APP_LAUNCH_GPU_APP_PREF, &error))
+ {
+ char *msg;
+ msg = g_strdup_printf (_("Failed to launch ā€œ%sā€"), shell_app_get_name (app));
+ shell_global_notify_error (global,
+ 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, timestamp);
+ break;
+ default:
+ g_assert_not_reached();
+ 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)
+{
+ GActionGroup *group = NULL;
+ const char * const *actions;
+
+ g_return_if_fail (app->info != NULL);
+
+ /* First check whether the application provides a "new-window" desktop
+ * action - it is a safe bet that it will open a new window, and activating
+ * it will trigger startup notification if necessary
+ */
+ actions = g_desktop_app_info_list_actions (G_DESKTOP_APP_INFO (app->info));
+
+ if (g_strv_contains (actions, "new-window"))
+ {
+ shell_app_launch_action (app, "new-window", 0, workspace);
+ return;
+ }
+
+ /* Next, check whether the app exports an explicit "new-window" action
+ * that we can activate on the bus - the muxer will add startup notification
+ * information to the platform data, so this should work just as well as
+ * desktop actions.
+ */
+ group = app->running_state ? G_ACTION_GROUP (app->running_state->muxer)
+ : NULL;
+
+ if (group &&
+ g_action_group_has_action (group, "app.new-window") &&
+ g_action_group_get_action_parameter_type (group, "app.new-window") == NULL)
+ {
+ g_action_group_activate_action (group, "app.new-window", NULL);
+
+ return;
+ }
+
+ /* Lastly, 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.
+ */
+ shell_app_launch (app, 0, workspace, SHELL_APP_LAUNCH_GPU_APP_PREF, NULL);
+}
+
+/**
+ * shell_app_can_open_new_window:
+ * @app: a #ShellApp
+ *
+ * Returns %TRUE if the app supports opening a new window through
+ * shell_app_open_new_window() (ie, if calling that function will
+ * result in actually opening a new window and not something else,
+ * like presenting the most recently active one)
+ */
+gboolean
+shell_app_can_open_new_window (ShellApp *app)
+{
+ ShellAppRunningState *state;
+ MetaWindow *window;
+ GDesktopAppInfo *desktop_info;
+ const char * const *desktop_actions;
+
+ /* Apps that are stopped can always open new windows, because
+ * activating them would open the first one; if they are starting,
+ * we cannot tell whether they can open additional windows until
+ * they are running */
+ if (app->state != SHELL_APP_STATE_RUNNING)
+ return app->state == SHELL_APP_STATE_STOPPED;
+
+ state = app->running_state;
+
+ /* If the app has an explicit new-window action, then it can
+ (or it should be able to) ...
+ */
+ if (g_action_group_has_action (G_ACTION_GROUP (state->muxer), "app.new-window"))
+ return TRUE;
+
+ /* If the app doesn't have a desktop file, then nothing is possible */
+ if (!app->info)
+ return FALSE;
+
+ desktop_info = G_DESKTOP_APP_INFO (app->info);
+
+ /* If the app is explicitly telling us, then we know for sure */
+ if (g_desktop_app_info_has_key (desktop_info, "X-GNOME-SingleWindow"))
+ return !g_desktop_app_info_get_boolean (desktop_info,
+ "X-GNOME-SingleWindow");
+
+ /* If it has a new-window desktop action, it should be able to */
+ desktop_actions = g_desktop_app_info_list_actions (desktop_info);
+ if (desktop_actions && g_strv_contains (desktop_actions, "new-window"))
+ return TRUE;
+
+ /* If this is a unique GtkApplication, and we don't have a new-window, then
+ probably we can't
+
+ We don't consider non-unique GtkApplications here to handle cases like
+ evince, which don't export a new-window action because each window is in
+ a different process. In any case, in a non-unique GtkApplication each
+ Activate() knows nothing about the other instances, so it will show a
+ new window.
+ */
+
+ window = state->windows->data;
+
+ if (state->unique_bus_name != NULL &&
+ meta_window_get_gtk_application_object_path (window) != NULL)
+ {
+ if (meta_window_get_gtk_application_id (window) != NULL)
+ return FALSE;
+ else
+ return TRUE;
+ }
+
+ /* In all other cases, we don't have a reliable source of information
+ or a decent heuristic, so we err on the compatibility side and say
+ yes.
+ */
+ return TRUE;
+}
+
+/**
+ * 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 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 = get_active_workspace ();
+ 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);
+}
+
+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;
+}
+
+static int
+shell_app_get_last_user_time (ShellApp *app)
+{
+ GSList *iter;
+ guint32 last_user_time;
+
+ last_user_time = 0;
+
+ if (app->running_state != NULL)
+ {
+ for (iter = app->running_state->windows; iter; iter = iter->next)
+ last_user_time = MAX (last_user_time, meta_window_get_user_time (iter->data));
+ }
+
+ return (int)last_user_time;
+}
+
+static gboolean
+shell_app_is_minimized (ShellApp *app)
+{
+ GSList *iter;
+
+ if (app->running_state == NULL)
+ return FALSE;
+
+ for (iter = app->running_state->windows; iter; iter = iter->next)
+ {
+ if (meta_window_showing_on_its_workspace (iter->data))
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+/**
+ * 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 non-minimized 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 min_app, min_other;
+
+ if (app->state != other->state)
+ {
+ if (app->state == SHELL_APP_STATE_RUNNING)
+ return -1;
+ return 1;
+ }
+
+ min_app = shell_app_is_minimized (app);
+ min_other = shell_app_is_minimized (other);
+
+ if (min_app != min_other)
+ {
+ if (min_other)
+ return -1;
+ 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 shell_app_get_last_user_time (other) - shell_app_get_last_user_time (app);
+ }
+
+ return 0;
+}
+
+ShellApp *
+_shell_app_new_for_window (MetaWindow *window)
+{
+ ShellApp *app;
+
+ app = g_object_new (SHELL_TYPE_APP, NULL);
+
+ app->window_id_string = g_strdup_printf ("window:%d", meta_window_get_stable_sequence (window));
+
+ _shell_app_add_window (app, window);
+
+ return app;
+}
+
+ShellApp *
+_shell_app_new (GDesktopAppInfo *info)
+{
+ ShellApp *app;
+
+ app = g_object_new (SHELL_TYPE_APP,
+ "app-info", info,
+ NULL);
+
+ return app;
+}
+
+void
+_shell_app_set_app_info (ShellApp *app,
+ GDesktopAppInfo *info)
+{
+ g_set_object (&app->info, info);
+
+ g_clear_pointer (&app->name_collation_key, g_free);
+ if (app->info)
+ app->name_collation_key = g_utf8_collate_key (shell_app_get_name (app), -1);
+}
+
+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;
+
+ _shell_app_system_notify_app_state_changed (shell_app_system_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);
+
+ /* 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_sync_running_state (ShellApp *app)
+{
+ g_return_if_fail (app->running_state != NULL);
+
+ if (app->state != SHELL_APP_STATE_STARTING)
+ {
+ if (app->running_state->interesting_windows == 0)
+ shell_app_state_transition (app, SHELL_APP_STATE_STOPPED);
+ else
+ shell_app_state_transition (app, SHELL_APP_STATE_RUNNING);
+ }
+}
+
+
+static void
+shell_app_on_skip_taskbar_changed (MetaWindow *window,
+ GParamSpec *pspec,
+ ShellApp *app)
+{
+ g_assert (app->running_state != NULL);
+
+ /* we rely on MetaWindow:skip-taskbar only being notified
+ * when it actually changes; when that assumption breaks,
+ * we'll have to track the "interesting" windows themselves
+ */
+ if (meta_window_is_skip_taskbar (window))
+ app->running_state->interesting_windows--;
+ else
+ app->running_state->interesting_windows++;
+
+ shell_app_sync_running_state (app);
+}
+
+static void
+shell_app_on_ws_switch (MetaWorkspaceManager *workspace_manager,
+ 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);
+}
+
+gboolean
+shell_app_get_busy (ShellApp *app)
+{
+ if (app->running_state != NULL &&
+ app->running_state->application_proxy != NULL &&
+ shell_org_gtk_application_get_busy (app->running_state->application_proxy))
+ return TRUE;
+
+ return FALSE;
+}
+
+static void
+busy_changed_cb (GObject *object,
+ GParamSpec *pspec,
+ gpointer user_data)
+{
+ ShellApp *app = user_data;
+
+ g_assert (SHELL_IS_APP (app));
+
+ g_object_notify (G_OBJECT (app), "busy");
+}
+
+static void
+get_application_proxy (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ ShellApp *app = user_data;
+ ShellOrgGtkApplication *proxy;
+ g_autoptr (GError) error = NULL;
+
+ g_assert (SHELL_IS_APP (app));
+
+ proxy = shell_org_gtk_application_proxy_new_finish (result, &error);
+ if (proxy != NULL)
+ {
+ app->running_state->application_proxy = proxy;
+ g_signal_connect (proxy,
+ "notify::busy",
+ G_CALLBACK (busy_changed_cb),
+ app);
+ if (shell_org_gtk_application_get_busy (proxy))
+ g_object_notify (G_OBJECT (app), "busy");
+ }
+
+ if (app->running_state != NULL &&
+ !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ g_clear_object (&app->running_state->cancellable);
+
+ g_object_unref (app);
+}
+
+static void
+shell_app_ensure_busy_watch (ShellApp *app)
+{
+ ShellAppRunningState *running_state = app->running_state;
+ MetaWindow *window;
+ const gchar *object_path;
+
+ if (running_state->application_proxy != NULL ||
+ running_state->cancellable != NULL)
+ return;
+
+ if (running_state->unique_bus_name == NULL)
+ return;
+
+ window = g_slist_nth_data (running_state->windows, 0);
+ object_path = meta_window_get_gtk_application_object_path (window);
+
+ if (object_path == NULL)
+ return;
+
+ running_state->cancellable = g_cancellable_new();
+ /* Take a reference to app to make sure it isn't finalized before
+ get_application_proxy runs */
+ shell_org_gtk_application_proxy_new (running_state->session,
+ G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START,
+ running_state->unique_bus_name,
+ object_path,
+ running_state->cancellable,
+ get_application_proxy,
+ g_object_ref (app));
+}
+
+void
+_shell_app_add_window (ShellApp *app,
+ MetaWindow *window)
+{
+ 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_object (window, "unmanaged", G_CALLBACK(shell_app_on_unmanaged), app, 0);
+ g_signal_connect_object (window, "notify::user-time", G_CALLBACK(shell_app_on_user_time_changed), app, 0);
+ g_signal_connect_object (window, "notify::skip-taskbar", G_CALLBACK(shell_app_on_skip_taskbar_changed), app, 0);
+
+ shell_app_update_app_actions (app, window);
+ shell_app_ensure_busy_watch (app);
+
+ if (!meta_window_is_skip_taskbar (window))
+ app->running_state->interesting_windows++;
+ shell_app_sync_running_state (app);
+
+ 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_signal_handlers_disconnect_by_func (window, G_CALLBACK(shell_app_on_skip_taskbar_changed), app);
+ app->running_state->windows = g_slist_remove (app->running_state->windows, window);
+
+ g_clear_signal_handler (&app->running_state->icon_changed_id, window);
+
+ if (!meta_window_is_skip_taskbar (window))
+ app->running_state->interesting_windows--;
+ shell_app_sync_running_state (app);
+
+ g_object_unref (window);
+
+ if (app->running_state->windows == NULL)
+ g_clear_pointer (&app->running_state, unref_running_state);
+
+ 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 identifiers 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;
+ pid_t pid = meta_window_get_pid (window);
+
+ if (pid < 1)
+ continue;
+
+ /* 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,
+ MetaStartupSequence *sequence)
+{
+ gboolean starting = !meta_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)
+ {
+ MetaDisplay *display = shell_global_get_display (shell_global_get ());
+
+ shell_app_state_transition (app, SHELL_APP_STATE_STARTING);
+ meta_display_unset_input_focus (display,
+ meta_startup_sequence_get_timestamp (sequence));
+ app->started_on_workspace = meta_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)
+{
+ GActionGroup *group = NULL;
+ GSList *iter;
+
+ if (shell_app_get_state (app) != SHELL_APP_STATE_RUNNING)
+ return FALSE;
+
+ /* First, check whether the app exports an explicit "quit" action
+ * that we can activate on the bus
+ */
+ group = G_ACTION_GROUP (app->running_state->muxer);
+
+ if (g_action_group_has_action (group, "app.quit") &&
+ g_action_group_get_action_parameter_type (group, "app.quit") == NULL)
+ {
+ g_action_group_activate_action (group, "app.quit", NULL);
+
+ return TRUE;
+ }
+
+ /* Otherwise, fall back to closing all the app's windows */
+ for (iter = app->running_state->windows; iter; iter = iter->next)
+ {
+ MetaWindow *win = iter->data;
+
+ if (!meta_window_can_close (win))
+ continue;
+
+ meta_window_delete (win, shell_global_get_current_time (shell_global_get ()));
+ }
+ return TRUE;
+}
+
+#if !defined(HAVE_GIO_DESKTOP_LAUNCH_URIS_WITH_FDS) && defined(HAVE_SYSTEMD)
+/* This sets up the launched application to log to the journal
+ * using its own identifier, instead of just "gnome-session".
+ */
+static void
+app_child_setup (gpointer user_data)
+{
+ const char *appid = user_data;
+ int res;
+ int journalfd = sd_journal_stream_fd (appid, LOG_INFO, FALSE);
+ if (journalfd >= 0)
+ {
+ do
+ res = dup2 (journalfd, 1);
+ while (G_UNLIKELY (res == -1 && errno == EINTR));
+ do
+ res = dup2 (journalfd, 2);
+ while (G_UNLIKELY (res == -1 && errno == EINTR));
+ (void) close (journalfd);
+ }
+}
+#endif
+
+static void
+wait_pid (GDesktopAppInfo *appinfo,
+ GPid pid,
+ gpointer user_data)
+{
+ g_child_watch_add (pid, (GChildWatchFunc) g_spawn_close_pid, NULL);
+}
+
+static void
+apply_discrete_gpu_env (GAppLaunchContext *context,
+ ShellGlobal *global)
+{
+ GDBusProxy *proxy;
+ GVariant* variant;
+ guint num_children, i;
+
+ proxy = shell_global_get_switcheroo_control (global);
+ if (!proxy)
+ {
+ g_warning ("Could not apply discrete GPU environment, switcheroo-control not available");
+ return;
+ }
+
+ variant = shell_net_hadess_switcheroo_control_get_gpus (SHELL_NET_HADESS_SWITCHEROO_CONTROL (proxy));
+ if (!variant)
+ {
+ g_warning ("Could not apply discrete GPU environment, no GPUs in list");
+ return;
+ }
+
+ num_children = g_variant_n_children (variant);
+ for (i = 0; i < num_children; i++)
+ {
+ g_autoptr(GVariant) gpu = NULL;
+ g_autoptr(GVariant) env = NULL;
+ g_autoptr(GVariant) default_variant = NULL;
+ g_autofree const char **env_s = NULL;
+ guint j;
+
+ gpu = g_variant_get_child_value (variant, i);
+ if (!gpu ||
+ !g_variant_is_of_type (gpu, G_VARIANT_TYPE ("a{s*}")))
+ continue;
+
+ /* Skip over the default GPU */
+ default_variant = g_variant_lookup_value (gpu, "Default", NULL);
+ if (!default_variant || g_variant_get_boolean (default_variant))
+ continue;
+
+ env = g_variant_lookup_value (gpu, "Environment", NULL);
+ if (!env)
+ continue;
+
+ env_s = g_variant_get_strv (env, NULL);
+ for (j = 0; env_s[j] != NULL; j = j + 2)
+ g_app_launch_context_setenv (context, env_s[j], env_s[j+1]);
+ return;
+ }
+
+ g_debug ("Could not find discrete GPU in switcheroo-control, not applying environment");
+}
+
+/**
+ * shell_app_launch:
+ * @timestamp: Event timestamp, or 0 for current event timestamp
+ * @workspace: Start on this workspace, or -1 for default
+ * @gpu_pref: the GPU to prefer launching on
+ * @error: A #GError
+ */
+gboolean
+shell_app_launch (ShellApp *app,
+ guint timestamp,
+ int workspace,
+ ShellAppLaunchGpu gpu_pref,
+ GError **error)
+{
+ ShellGlobal *global;
+ GAppLaunchContext *context;
+ gboolean ret;
+ GSpawnFlags flags;
+ gboolean discrete_gpu = FALSE;
+
+ if (app->info == NULL)
+ {
+ MetaWindow *window = window_backed_app_get_window (app);
+ /* We don't use an error return if there no longer any windows, because the
+ * user attempting to activate a stale window backed app isn't something
+ * we would expect the caller to meaningfully handle or display an error
+ * message to the user.
+ */
+ if (window)
+ meta_window_activate (window, timestamp);
+ return TRUE;
+ }
+
+ global = shell_global_get ();
+ context = shell_global_create_app_launch_context (global, timestamp, workspace);
+ if (gpu_pref == SHELL_APP_LAUNCH_GPU_APP_PREF)
+ discrete_gpu = g_desktop_app_info_get_boolean (app->info, "PrefersNonDefaultGPU");
+ else
+ discrete_gpu = (gpu_pref == SHELL_APP_LAUNCH_GPU_DISCRETE);
+
+ if (discrete_gpu)
+ apply_discrete_gpu_env (context, global);
+
+ /* Set LEAVE_DESCRIPTORS_OPEN in order to use an optimized gspawn
+ * codepath. The shell's open file descriptors should be marked CLOEXEC
+ * so that they are automatically closed even with this flag set.
+ */
+ flags = G_SPAWN_SEARCH_PATH | G_SPAWN_DO_NOT_REAP_CHILD |
+ G_SPAWN_LEAVE_DESCRIPTORS_OPEN;
+
+#ifdef HAVE_GIO_DESKTOP_LAUNCH_URIS_WITH_FDS
+ /* Optimized spawn path, avoiding a child_setup function */
+ {
+ int journalfd = -1;
+
+#ifdef HAVE_SYSTEMD
+ journalfd = sd_journal_stream_fd (shell_app_get_id (app), LOG_INFO, FALSE);
+#endif /* HAVE_SYSTEMD */
+
+ ret = g_desktop_app_info_launch_uris_as_manager_with_fds (app->info, NULL,
+ context,
+ flags,
+ NULL, NULL,
+ wait_pid, NULL,
+ -1,
+ journalfd,
+ journalfd,
+ error);
+
+ if (journalfd >= 0)
+ (void) close (journalfd);
+ }
+#else /* !HAVE_GIO_DESKTOP_LAUNCH_URIS_WITH_FDS */
+ ret = g_desktop_app_info_launch_uris_as_manager (app->info, NULL,
+ context,
+ flags,
+#ifdef HAVE_SYSTEMD
+ app_child_setup, (gpointer)shell_app_get_id (app),
+#else
+ NULL, NULL,
+#endif
+ wait_pid, NULL,
+ error);
+#endif /* HAVE_GIO_DESKTOP_LAUNCH_URIS_WITH_FDS */
+ g_object_unref (context);
+
+ return ret;
+}
+
+/**
+ * shell_app_launch_action:
+ * @app: the #ShellApp
+ * @action_name: the name of the action to launch (as obtained by
+ * g_desktop_app_info_list_actions())
+ * @timestamp: Event timestamp, or 0 for current event timestamp
+ * @workspace: Start on this workspace, or -1 for default
+ */
+void
+shell_app_launch_action (ShellApp *app,
+ const char *action_name,
+ guint timestamp,
+ int workspace)
+{
+ ShellGlobal *global;
+ GAppLaunchContext *context;
+
+ global = shell_global_get ();
+ context = shell_global_create_app_launch_context (global, timestamp, workspace);
+
+ g_desktop_app_info_launch_action (G_DESKTOP_APP_INFO (app->info),
+ action_name, context);
+
+ g_object_unref (context);
+}
+
+/**
+ * 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)
+{
+ return app->info;
+}
+
+static void
+create_running_state (ShellApp *app)
+{
+ MetaDisplay *display = shell_global_get_display (shell_global_get ());
+ MetaWorkspaceManager *workspace_manager =
+ meta_display_get_workspace_manager (display);
+
+ g_assert (app->running_state == NULL);
+
+ app->running_state = g_slice_new0 (ShellAppRunningState);
+ app->running_state->refcount = 1;
+ app->running_state->workspace_switch_id =
+ g_signal_connect (workspace_manager, "workspace-switched",
+ G_CALLBACK (shell_app_on_ws_switch), app);
+
+ app->running_state->session = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, NULL);
+ g_assert (app->running_state->session != NULL);
+ app->running_state->muxer = gtk_action_muxer_new ();
+}
+
+void
+shell_app_update_app_actions (ShellApp *app,
+ MetaWindow *window)
+{
+ const gchar *unique_bus_name;
+
+ /* We assume that 'gtk-application-object-path' and
+ * 'gtk-app-menu-object-path' are the same for all windows which
+ * have it set.
+ *
+ * It could be possible, however, that the first window we see
+ * belonging to the app didn't have them set. For this reason, we
+ * take the values from the first window that has them set and ignore
+ * all the rest (until the app is stopped and restarted).
+ */
+
+ unique_bus_name = meta_window_get_gtk_unique_bus_name (window);
+
+ if (g_strcmp0 (app->running_state->unique_bus_name, unique_bus_name) != 0)
+ {
+ const gchar *application_object_path;
+ GDBusActionGroup *actions;
+
+ application_object_path = meta_window_get_gtk_application_object_path (window);
+
+ if (application_object_path == NULL || unique_bus_name == NULL)
+ return;
+
+ g_clear_pointer (&app->running_state->unique_bus_name, g_free);
+ app->running_state->unique_bus_name = g_strdup (unique_bus_name);
+ actions = g_dbus_action_group_get (app->running_state->session, unique_bus_name, application_object_path);
+ gtk_action_muxer_insert (app->running_state->muxer, "app", G_ACTION_GROUP (actions));
+ g_object_unref (actions);
+ }
+}
+
+static void
+unref_running_state (ShellAppRunningState *state)
+{
+ MetaDisplay *display = shell_global_get_display (shell_global_get ());
+ MetaWorkspaceManager *workspace_manager =
+ meta_display_get_workspace_manager (display);
+
+ g_assert (state->refcount > 0);
+
+ state->refcount--;
+ if (state->refcount > 0)
+ return;
+
+ g_clear_signal_handler (&state->workspace_switch_id, workspace_manager);
+
+ g_clear_object (&state->application_proxy);
+
+ if (state->cancellable != NULL)
+ {
+ g_cancellable_cancel (state->cancellable);
+ g_clear_object (&state->cancellable);
+ }
+
+ g_clear_object (&state->muxer);
+ g_clear_object (&state->session);
+ g_clear_pointer (&state->unique_bus_name, g_free);
+
+ g_slice_free (ShellAppRunningState, state);
+}
+
+/**
+ * shell_app_compare_by_name:
+ * @app: One app
+ * @other: The other app
+ *
+ * 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 void
+shell_app_init (ShellApp *self)
+{
+ self->state = SHELL_APP_STATE_STOPPED;
+}
+
+static void
+shell_app_dispose (GObject *object)
+{
+ ShellApp *app = SHELL_APP (object);
+
+ g_clear_object (&app->info);
+ g_clear_object (&app->fallback_icon);
+
+ while (app->running_state)
+ _shell_app_remove_window (app, app->running_state->windows->data);
+
+ /* We should have been transitioned when we removed all of our windows */
+ g_assert (app->state == SHELL_APP_STATE_STOPPED);
+ g_assert (app->running_state == NULL);
+
+ 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->name_collation_key);
+
+ 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->set_property = shell_app_set_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, NULL,
+ 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 | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * ShellApp:busy:
+ *
+ * Whether the application has marked itself as busy.
+ */
+ g_object_class_install_property (gobject_class,
+ PROP_BUSY,
+ g_param_spec_boolean ("busy",
+ "Busy",
+ "Busy state",
+ FALSE,
+ G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * ShellApp:id:
+ *
+ * The id of this application (a desktop filename, or a special string
+ * like window:0xabcd1234)
+ */
+ g_object_class_install_property (gobject_class,
+ PROP_ID,
+ g_param_spec_string ("id",
+ "Application id",
+ "The desktop file id of this ShellApp",
+ NULL,
+ G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * ShellApp:icon:
+ *
+ * The #GIcon representing this ShellApp
+ */
+ g_object_class_install_property (gobject_class,
+ PROP_ICON,
+ g_param_spec_object ("icon",
+ "GIcon",
+ "The GIcon representing this app",
+ G_TYPE_ICON,
+ G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * ShellApp:action-group:
+ *
+ * The #GDBusActionGroup associated with this ShellApp, if any. See the
+ * documentation of #GApplication and #GActionGroup for details.
+ */
+ g_object_class_install_property (gobject_class,
+ PROP_ACTION_GROUP,
+ g_param_spec_object ("action-group",
+ "Application Action Group",
+ "The action group exported by the remote application",
+ G_TYPE_ACTION_GROUP,
+ G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+ /**
+ * ShellApp:app-info:
+ *
+ * The #GDesktopAppInfo associated with this ShellApp, if any.
+ */
+ g_object_class_install_property (gobject_class,
+ PROP_APP_INFO,
+ g_param_spec_object ("app-info",
+ "DesktopAppInfo",
+ "The DesktopAppInfo associated with this app",
+ G_TYPE_DESKTOP_APP_INFO,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+}