From 7604dd1103718fec1ce3b95364ec4279034466be Mon Sep 17 00:00:00 2001 From: Georges Basile Stavracas Neto Date: Thu, 2 Feb 2023 15:58:43 -0300 Subject: [PATCH] quickSettings: Add background apps menu Sandboxed apps that run without a window are detected by the new background monitoring service, introduced by xdg-desktop-portal. We have an opportunity to improve the predictability of the desktop and ensure that application state in transparently reported to users by showing these apps, and allowing them to closed. Add a new background apps menu to the quick settings, that is always added at the bottom of the popover, and has a slightly custom, flat style applied to it. Show background-running apps in this menu, and allow closing them by first attempting to execute the 'quit' action through D-Bus, and if that fails, sending SIGKILL to the process. See https://gitlab.gnome.org/Teams/Design/os-mockups/-/issues/191 Part-of: --- .../org.freedesktop.background.Monitor.xml | 64 +++++ .../gnome-shell-dbus-interfaces.gresource.xml | 1 + data/gnome-shell-icons.gresource.xml | 1 + .../status/background-app-ghost-symbolic.svg | 4 + .../widgets/_quick-settings.scss | 24 ++ js/js-resources.gresource.xml | 1 + js/ui/panel.js | 3 + js/ui/status/backgroundApps.js | 225 ++++++++++++++++++ po/POTFILES.in | 1 + 9 files changed, 324 insertions(+) create mode 100644 data/dbus-interfaces/org.freedesktop.background.Monitor.xml create mode 100644 data/icons/scalable/status/background-app-ghost-symbolic.svg create mode 100644 js/ui/status/backgroundApps.js diff --git a/data/dbus-interfaces/org.freedesktop.background.Monitor.xml b/data/dbus-interfaces/org.freedesktop.background.Monitor.xml new file mode 100644 index 000000000..1a2028a4e --- /dev/null +++ b/data/dbus-interfaces/org.freedesktop.background.Monitor.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + diff --git a/data/gnome-shell-dbus-interfaces.gresource.xml b/data/gnome-shell-dbus-interfaces.gresource.xml index 6682c462d..31d1f1266 100644 --- a/data/gnome-shell-dbus-interfaces.gresource.xml +++ b/data/gnome-shell-dbus-interfaces.gresource.xml @@ -6,6 +6,7 @@ net.reactivated.Fprint.Device.xml net.reactivated.Fprint.Manager.xml org.freedesktop.Application.xml + org.freedesktop.background.Monitor.xml org.freedesktop.bolt1.Device.xml org.freedesktop.bolt1.Manager.xml org.freedesktop.DBus.xml diff --git a/data/gnome-shell-icons.gresource.xml b/data/gnome-shell-icons.gresource.xml index d808c9be6..fb33b22dc 100644 --- a/data/gnome-shell-icons.gresource.xml +++ b/data/gnome-shell-icons.gresource.xml @@ -18,6 +18,7 @@ scalable/actions/screenshot-ui-show-pointer-symbolic.svg scalable/actions/screenshot-ui-window-symbolic.svg scalable/actions/screenshot-recorded-symbolic.svg + scalable/status/background-app-ghost-symbolic.svg scalable/status/keyboard-caps-lock-symbolic.svg scalable/status/keyboard-enter-symbolic.svg scalable/status/keyboard-hide-symbolic.svg diff --git a/data/icons/scalable/status/background-app-ghost-symbolic.svg b/data/icons/scalable/status/background-app-ghost-symbolic.svg new file mode 100644 index 000000000..213b6010f --- /dev/null +++ b/data/icons/scalable/status/background-app-ghost-symbolic.svg @@ -0,0 +1,4 @@ + + + + diff --git a/data/theme/gnome-shell-sass/widgets/_quick-settings.scss b/data/theme/gnome-shell-sass/widgets/_quick-settings.scss index 0064aeed3..6a4c42d9a 100644 --- a/data/theme/gnome-shell-sass/widgets/_quick-settings.scss +++ b/data/theme/gnome-shell-sass/widgets/_quick-settings.scss @@ -149,3 +149,27 @@ } .device-subtitle { color: transparentize($fg_color, 0.5); } + +// background apps + +.background-apps-quick-toggle { + min-height: 40px; + background-color: transparent; + + & StIcon { icon-size: $base_icon_size !important; } +} + +.background-app-item { + & .title { @extend %heading; } + & .subtitle { @extend %caption; } + & .popup-menu-icon { + icon-size: $large_icon_size !important; + -st-icon-style: regular !important; + } + & .close-button { + @extend .icon-button; + padding: $base_padding; + } + + &.popup-inactive-menu-item { color: $fg_color; } +} diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml index 4213081d2..ba481aafa 100644 --- a/js/js-resources.gresource.xml +++ b/js/js-resources.gresource.xml @@ -135,6 +135,7 @@ ui/status/accessibility.js ui/status/autoRotate.js + ui/status/backgroundApps.js ui/status/brightness.js ui/status/darkMode.js ui/status/dwellClick.js diff --git a/js/ui/panel.js b/js/ui/panel.js index 0dd62561f..19a28dedc 100644 --- a/js/ui/panel.js +++ b/js/ui/panel.js @@ -366,6 +366,7 @@ class QuickSettings extends PanelMenu.Button { this._rfkill = new imports.ui.status.rfkill.Indicator(); this._autoRotate = new imports.ui.status.autoRotate.Indicator(); this._unsafeMode = new UnsafeModeIndicator(); + this._backgroundApps = new imports.ui.status.backgroundApps.Indicator(); this._indicators.add_child(this._brightness); this._indicators.add_child(this._remoteAccess); @@ -401,6 +402,8 @@ class QuickSettings extends PanelMenu.Button { this._addItems(this._rfkill.quickSettingsItems); this._addItems(this._autoRotate.quickSettingsItems); this._addItems(this._unsafeMode.quickSettingsItems); + + this._addItems(this._backgroundApps.quickSettingsItems, N_QUICK_SETTINGS_COLUMNS); } _addItems(items, colSpan = 1) { diff --git a/js/ui/status/backgroundApps.js b/js/ui/status/backgroundApps.js new file mode 100644 index 000000000..606fa3348 --- /dev/null +++ b/js/ui/status/backgroundApps.js @@ -0,0 +1,225 @@ +/* exported Indicator */ +const {Clutter, Gio, GLib, GObject, Shell, St} = imports.gi; + +const Main = imports.ui.main; +const PopupMenu = imports.ui.popupMenu; +const Util = imports.misc.util; + +const {QuickToggle, SystemIndicator} = imports.ui.quickSettings; +const {loadInterfaceXML} = imports.misc.dbusUtils; + +const DBUS_NAME = 'org.freedesktop.background.Monitor'; +const DBUS_OBJECT_PATH = '/org/freedesktop/background/monitor'; + +const BackgroundMonitorIface = loadInterfaceXML('org.freedesktop.background.Monitor'); +const BackgroundMonitorProxy = Gio.DBusProxy.makeProxyWrapper(BackgroundMonitorIface); + +Gio._promisify(Gio.DBusConnection.prototype, 'call'); + +var BackgroundAppMenuItem = GObject.registerClass({ + Properties: { + 'app': GObject.ParamSpec.object('app', '', '', + GObject.ParamFlags.READWRITE, + Shell.App), + 'instance': GObject.ParamSpec.int64('instance', '', '', + GObject.ParamFlags.READWRITE, + -1, GLib.MAXINT64_BIGINT, -1), + 'message': GObject.ParamSpec.string('message', '', '', + GObject.ParamFlags.READWRITE, + null), + }, +}, class BackgroundAppMenuItem extends PopupMenu.PopupImageMenuItem { + _init(app, params = {}) { + const message = params.message; + delete params.message; + + const instance = params.instance; + delete params.instance; + + super._init(app.get_name(), app.get_icon(), { + activate: false, + reactive: false, + ...params, + }); + + this.set({message, instance}); + + this.add_style_class_name('background-app-item'); + this.label.add_style_class_name('title'); + + this.app = app; + + const box = new St.BoxLayout({ + vertical: true, + x_expand: true, + x_align: Clutter.ActorAlign.START, + y_align: Clutter.ActorAlign.CENTER, + }); + this.add_child(box); + + this.remove_child(this.label); + box.add_child(this.label); + + const messageLabel = new St.Label({ + y_expand: true, + y_align: Clutter.ActorAlign.CENTER, + style_class: 'subtitle', + }); + box.add_child(messageLabel); + + this.bind_property('message', + messageLabel, 'text', GObject.BindingFlags.SYNC_CREATE); + this.bind_property_full('message', + messageLabel, 'visible', GObject.BindingFlags.SYNC_CREATE, + (bind, source) => [true, source !== null], + null); + + this.set_child_above_sibling(this._ornamentLabel, null); + + const closeButton = new St.Button({ + iconName: 'window-close-symbolic', + styleClass: 'close-button', + x_expand: true, + y_expand: false, + x_align: Clutter.ActorAlign.END, + y_align: Clutter.ActorAlign.CENTER, + }); + this.add_child(closeButton); + + closeButton.connect('clicked', () => this._quitApp().catch(logError)); + } + + async _quitApp() { + const appId = this.app.get_id().replace(/\.desktop$/, ''); + + try { + await Gio.DBus.session.call( + appId, + `/${appId.replaceAll('.', '/')}`, + 'org.freedesktop.Application', + 'ActivateAction', + new GLib.Variant('(sava{sv})', ['quit', [], {}]), + null, + Gio.DBusCallFlags.NONE, + -1, + null); + } catch (_error) { + try { + Util.trySpawn(['flatpak', 'kill', this.instance]); + } catch (pidError) { + logError(pidError, 'Failed to kill application'); + } + } + } +}); + +const BackgroundAppsToggle = GObject.registerClass( +class BackgroundAppsToggle extends QuickToggle { + _init() { + super._init({ + visible: false, + hasMenu: true, + // The background apps toggle looks like a flat menu, but doesn't + // have a separate menu button. Fake it with an arrow icon. + iconName: 'go-next-symbolic', + }); + + this.add_style_class_name('background-apps-quick-toggle'); + + this._box.set_child_above_sibling(this._icon, null); + + this._appSystem = Shell.AppSystem.get_default(); + + this.menu.setHeader( + 'background-app-ghost-symbolic', + C_('title', 'Background Apps')); + + new BackgroundMonitorProxy( + Gio.DBus.session, + DBUS_NAME, + DBUS_OBJECT_PATH, + proxy => { + this._proxy = proxy; + proxy?.connect('g-properties-changed', () => this._sync()); + this._sync(); + }, + null, + Gio.DBusProxyFlags.DO_NOT_AUTO_START); + + this._appsSection = new PopupMenu.PopupMenuSection(); + this.menu.addMenuItem(this._appsSection); + + this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); + this.menu.addSettingsAction(_('App Settings'), + 'gnome-applications-panel.desktop'); + + this.connect('popup-menu', () => this.menu.open()); + + this.menu.connect('open-state-changed', () => this._syncVisibility()); + Main.sessionMode.connect('updated', () => this._syncVisibility()); + } + + _syncVisibility() { + const {isLocked} = Main.sessionMode; + const nBackgroundApps = this._proxy?.BackgroundApps?.length; + // We cannot hide the quick toggle while the menu is open, otherwise + // the menu position goes bogus. We can't show it in locked sessions + // either + this.visible = !isLocked && (this.menu.isOpen || nBackgroundApps > 0); + } + + _sync() { + this._syncVisibility(); + + if (!this._proxy) + return; + + const {BackgroundApps: backgroundApps} = this._proxy; + + const nBackgroundApps = backgroundApps?.length ?? 0; + this.title = nBackgroundApps === 0 + ? _('No Background Apps') + : ngettext( + '%d Background App', + '%d Background Apps', + nBackgroundApps).format(nBackgroundApps); + + this._appsSection.removeAll(); + + if (!backgroundApps) + return; + + backgroundApps + .map(backgroundApp => { + const appId = backgroundApp.app_id.deepUnpack(); + const app = this._appSystem.lookup_app(`${appId}.desktop`); + const message = backgroundApp.message?.deepUnpack(); + const instance = backgroundApp.instance.deepUnpack(); + + return {app, message, instance}; + }) + .sort((a, b) => { + return a.app.get_name().localeCompare(b.app.get_name()); + }) + .forEach(backgroundApp => { + const {app, message, instance} = backgroundApp; + + const item = new BackgroundAppMenuItem(app, + {instance, message}); + this._appsSection.addMenuItem(item); + }); + } + + vfunc_clicked() { + this.menu.open(); + } +}); + +var Indicator = GObject.registerClass( +class Indicator extends SystemIndicator { + _init() { + super._init(); + + this.quickSettingsItems.push(new BackgroundAppsToggle()); + } +}); diff --git a/po/POTFILES.in b/po/POTFILES.in index 5ceecebcb..88409eb95 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -58,6 +58,7 @@ js/ui/shellEntry.js js/ui/shellMountOperation.js js/ui/status/accessibility.js js/ui/status/autoRotate.js +js/ui/status/backgroundApps.js js/ui/status/bluetooth.js js/ui/status/brightness.js js/ui/status/darkMode.js