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: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/2624>
This commit is contained in:
parent
055694de9d
commit
7604dd1103
64
data/dbus-interfaces/org.freedesktop.background.Monitor.xml
Normal file
64
data/dbus-interfaces/org.freedesktop.background.Monitor.xml
Normal file
@ -0,0 +1,64 @@
|
||||
<?xml version="1.0"?>
|
||||
<!--
|
||||
Copyright (C) 2022 Endless OS Foundation, LLC
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 2 of the License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Author: Georges Basile Stavracas Neto <georges@endlessos.org>
|
||||
-->
|
||||
|
||||
<node name="/" xmlns:doc="http://www.freedesktop.org/dbus/1.0/doc.dtd">
|
||||
<!--
|
||||
org.freedesktop.background.Monitor:
|
||||
@short_description: Background applications monitor
|
||||
|
||||
This interface provides APIs related to applications
|
||||
that are running in the background.
|
||||
|
||||
This documentation describes version 1 of this interface.
|
||||
-->
|
||||
<interface name="org.freedesktop.background.Monitor">
|
||||
|
||||
<!--
|
||||
BackgroundApps:
|
||||
|
||||
The list of applications that are considered to be running in
|
||||
background. The following keys are supported:
|
||||
|
||||
<variablelist>
|
||||
<varlistentry>
|
||||
<term>app_id s</term>
|
||||
<listitem><para>
|
||||
App id of the application.
|
||||
</para></listitem>
|
||||
</varlistentry>
|
||||
<varlistentry>
|
||||
<term>instance s</term>
|
||||
<listitem><para>
|
||||
The Flatpak instance of the application.
|
||||
</para></listitem>
|
||||
</varlistentry>
|
||||
<varlistentry>
|
||||
<term>message s</term>
|
||||
<listitem><para>
|
||||
Status message reported by the application. Optional.
|
||||
</para></listitem>
|
||||
</varlistentry>
|
||||
</variablelist>
|
||||
-->
|
||||
<property name="BackgroundApps" type="aa{sv}" access="read"/>
|
||||
|
||||
<property name="version" type="u" access="read"/>
|
||||
</interface>
|
||||
</node>
|
@ -6,6 +6,7 @@
|
||||
<file preprocess="xml-stripblanks">net.reactivated.Fprint.Device.xml</file>
|
||||
<file preprocess="xml-stripblanks">net.reactivated.Fprint.Manager.xml</file>
|
||||
<file preprocess="xml-stripblanks">org.freedesktop.Application.xml</file>
|
||||
<file preprocess="xml-stripblanks">org.freedesktop.background.Monitor.xml</file>
|
||||
<file preprocess="xml-stripblanks">org.freedesktop.bolt1.Device.xml</file>
|
||||
<file preprocess="xml-stripblanks">org.freedesktop.bolt1.Manager.xml</file>
|
||||
<file preprocess="xml-stripblanks">org.freedesktop.DBus.xml</file>
|
||||
|
@ -18,6 +18,7 @@
|
||||
<file>scalable/actions/screenshot-ui-show-pointer-symbolic.svg</file>
|
||||
<file>scalable/actions/screenshot-ui-window-symbolic.svg</file>
|
||||
<file>scalable/actions/screenshot-recorded-symbolic.svg</file>
|
||||
<file>scalable/status/background-app-ghost-symbolic.svg</file>
|
||||
<file>scalable/status/keyboard-caps-lock-symbolic.svg</file>
|
||||
<file>scalable/status/keyboard-enter-symbolic.svg</file>
|
||||
<file>scalable/status/keyboard-hide-symbolic.svg</file>
|
||||
|
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m 7.992188 1 c -3.867188 0.003906 -6.996094 3.140625 -6.992188 7.007812 v 5.988282 c 0 0.554687 0.449219 1.003906 1.003906 1.003906 c 0.550782 -0.003906 1 -0.453125 0.996094 -1.003906 v -0.992188 c 0 -0.554687 0.449219 -1.003906 1.003906 -1.003906 c 0.550782 0.003906 1 0.453125 0.996094 1.003906 v 0.992188 c 0 0.554687 0.449219 1.003906 1.003906 1.003906 c 0.550782 -0.003906 1 -0.453125 0.996094 -1.003906 v -0.992188 c 0 -0.554687 0.449219 -1.003906 1.003906 -1.003906 c 0.550782 0.003906 1 0.453125 0.996094 1.003906 v 0.992188 c 0 0.554687 0.449219 1.003906 1.003906 1.003906 c 0.550782 -0.003906 1 -0.453125 0.996094 -1.003906 v -0.992188 c 0 -0.554687 0.449219 -1.003906 1.003906 -1.003906 c 0.550782 0.003906 1 0.453125 0.996094 1.003906 v 0.992188 c 0 0.554687 0.449219 1.003906 1.003906 1.003906 c 0.550782 -0.003906 1 -0.453125 0.996094 -1.003906 v -5.988282 c 0 -3.871093 -3.136719 -7.007812 -7.007812 -7.007812 z m -1.992188 4 c 0.554688 0 1 0.445312 1 1 v 1 c 0 0.554688 -0.445312 1 -1 1 s -1 -0.445312 -1 -1 v -1 c 0 -0.554688 0.445312 -1 1 -1 z m 4 0 c 0.554688 0 1 0.445312 1 1 v 1 c 0 0.554688 -0.445312 1 -1 1 s -1 -0.445312 -1 -1 v -1 c 0 -0.554688 0.445312 -1 1 -1 z m 0 0" fill="#222222"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
@ -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; }
|
||||
}
|
||||
|
@ -135,6 +135,7 @@
|
||||
|
||||
<file>ui/status/accessibility.js</file>
|
||||
<file>ui/status/autoRotate.js</file>
|
||||
<file>ui/status/backgroundApps.js</file>
|
||||
<file>ui/status/brightness.js</file>
|
||||
<file>ui/status/darkMode.js</file>
|
||||
<file>ui/status/dwellClick.js</file>
|
||||
|
@ -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) {
|
||||
|
225
js/ui/status/backgroundApps.js
Normal file
225
js/ui/status/backgroundApps.js
Normal file
@ -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());
|
||||
}
|
||||
});
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user