diff options
Diffstat (limited to 'src/shell-app.c')
-rw-r--r-- | src/shell-app.c | 1756 |
1 files changed, 1756 insertions, 0 deletions
diff --git a/src/shell-app.c b/src/shell-app.c new file mode 100644 index 0000000..5c38a9c --- /dev/null +++ b/src/shell-app.c @@ -0,0 +1,1756 @@ +/* -*- 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-context.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; + + 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; + MetaWindow *fallback_icon_window; + + ShellAppRunningState *running_state; + + char *window_id_string; + char *name_collation_key; +}; + +enum { + PROP_0, + + PROP_STATE, + PROP_BUSY, + PROP_ID, + PROP_ACTION_GROUP, + PROP_ICON, + PROP_APP_INFO, + + N_PROPS +}; + +static GParamSpec *props[N_PROPS] = { NULL, }; + +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); + + if (!app->fallback_icon) + app->fallback_icon = g_themed_icon_new ("application-x-executable"); + + g_object_notify_by_pspec (G_OBJECT (app), props[PROP_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_window = window; + app->fallback_icon = x11_window_create_fallback_gicon (window); + 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) +{ + g_autoptr (GSList) windows = NULL; + + 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_by_pspec (G_OBJECT (app), props[PROP_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 via its desktop file, then we know + * for sure + */ + if (g_desktop_app_info_has_key (desktop_info, "SingleMainWindow")) + return !g_desktop_app_info_get_boolean (desktop_info, + "SingleMainWindow"); + + /* GNOME-specific key, for backwards compatibility with apps that haven't + * started using the XDG "SingleMainWindow" key yet + */ + 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 container) (element-type MetaWindow): List of windows + */ +GSList * +shell_app_get_windows (ShellApp *app) +{ + GSList *windows = NULL; + GSList *l; + + 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; + } + + for (l = app->running_state->windows; l; l = l->next) + if (!meta_window_is_override_redirect (META_WINDOW (l->data))) + windows = g_slist_prepend (windows, l->data); + + return g_slist_reverse (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_by_pspec (G_OBJECT (app), props[PROP_STATE]); +} + +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_by_pspec (G_OBJECT (app), props[PROP_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_by_pspec (G_OBJECT (app), props[PROP_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, "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); + + if (app->started_on_workspace >= 0 && !meta_window_is_on_all_workspaces (window)) + meta_window_change_workspace_by_index (window, app->started_on_workspace, FALSE); + app->started_on_workspace = -1; + + 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; + + app->running_state->windows = g_slist_remove (app->running_state->windows, window); + + if (!meta_window_is_skip_taskbar (window)) + app->running_state->interesting_windows--; + shell_app_sync_running_state (app); + + if (app->running_state->windows == NULL) + g_clear_pointer (&app->running_state, unref_running_state); + + 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); + if (window == app->fallback_icon_window) + { + g_signal_handlers_disconnect_by_func (window, G_CALLBACK(on_window_icon_changed), app); + app->fallback_icon_window = NULL; + + /* Select a new icon from a different window. */ + g_clear_object (&app->fallback_icon); + g_object_notify_by_pspec (G_OBJECT (app), props[PROP_ICON]); + } + + g_object_unref (window); + + 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; + g_autoptr (GSList) windows = NULL; + GSList *iter; + + result = NULL; + windows = shell_app_get_windows (app); + for (iter = windows; 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)); + } + + if (starting) + app->started_on_workspace = meta_startup_sequence_get_workspace (sequence); + else 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; +} + +static void +child_context_setup (gpointer user_data) +{ + ShellGlobal *shell_global = user_data; + MetaContext *meta_context; + + g_object_get (shell_global, "context", &meta_context, NULL); + meta_context_restore_rlimit_nofile (meta_context, NULL); +} + +#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); + ShellGlobal *shell_global = shell_global_get (); + + 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); + } + + child_context_setup (shell_global); +} +#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; + ShellGlobal *shell_global = shell_global_get (); + + 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, + child_context_setup, shell_global, + 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 + child_context_setup, shell_global, +#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_new0 (ShellAppRunningState, 1); + 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_free (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; + self->started_on_workspace = -1; +} + +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. + */ + props[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. + */ + props[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) + */ + props[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 + */ + props[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. + */ + props[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. + */ + props[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); + + g_object_class_install_properties (gobject_class, N_PROPS, props); +} |