/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ #include "config.h" #include #include #include #include #include #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 #include #include #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; 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_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_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; } /** * 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 = st_texture_cache_bind_cairo_surface_property (st_texture_cache_get_default (), G_OBJECT (window), "icon"); } 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) { GIcon *icon; 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"); icon = shell_app_get_icon (app); st_icon_set_gicon (ST_ICON (ret), icon); 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_assert (SHELL_IS_APP (app)); proxy = shell_org_gtk_application_proxy_new_finish (result, NULL); 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_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); if (app->started_on_workspace >= 0) 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; 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); 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)); } 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; } #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_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. */ 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: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)); }