gnome-shell/js/ui/status/backgroundApps.js

265 lines
8.4 KiB
JavaScript
Raw Normal View History

import Clutter from 'gi://Clutter';
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Shell from 'gi://Shell';
import St from 'gi://St';
import * as Main from '../main.js';
import * as PopupMenu from '../popupMenu.js';
import * as Util from '../../misc/util.js';
import {Spinner} from '../animation.js';
import {QuickToggle, SystemIndicator} from '../quickSettings.js';
import {loadInterfaceXML} from '../../misc/dbusUtils.js';
const DBUS_NAME = 'org.freedesktop.background.Monitor';
const DBUS_OBJECT_PATH = '/org/freedesktop/background/monitor';
const SPINNER_TIMEOUT = 5; // seconds
const BackgroundMonitorIface = loadInterfaceXML('org.freedesktop.background.Monitor');
const BackgroundMonitorProxy = Gio.DBusProxy.makeProxyWrapper(BackgroundMonitorIface);
Gio._promisify(Gio.DBusConnection.prototype, 'call');
const BackgroundAppMenuItem = GObject.registerClass({
Properties: {
'app': GObject.ParamSpec.object('app', '', '',
GObject.ParamFlags.READWRITE,
Shell.App),
'message': GObject.ParamSpec.string('message', '', '',
GObject.ParamFlags.READWRITE,
null),
},
}, class BackgroundAppMenuItem extends PopupMenu.PopupImageMenuItem {
_init(app, params = {}) {
const message = params.message;
delete params.message;
super._init(app.get_name(), app.get_icon(), {
...params,
});
this.set({message});
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._ornamentIcon, null);
this._spinner = new Spinner(16, {hideOnStop: true});
this._spinner.add_style_class_name('spinner');
this.add_child(this._spinner);
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);
this._spinner.bind_property('visible',
closeButton, 'visible',
GObject.BindingFlags.INVERT_BOOLEAN);
closeButton.connect('clicked', () => this._quitApp().catch(logError));
this.connect('activate', () => this.app.activate());
this.connect('destroy', () => this._onDestroy());
}
_onDestroy() {
if (this._spinnerTimeoutId)
GLib.source_remove(this._spinnerTimeoutId);
delete this._spinnerTimeoutId;
}
async _quitApp() {
const appId = this.app.get_id().replace(/\.desktop$/, '');
this._spinner.play();
this._spinnerTimeoutId =
GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, SPINNER_TIMEOUT,
() => {
// Assume the quit request has failed, stop the spinner
this._spinner.stop();
delete this._spinnerTimeoutId;
return GLib.SOURCE_REMOVE;
});
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', appId]);
} 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._listTitle = new PopupMenu.PopupMenuItem(
_('Apps known to be running without a window'),
{reactive: false});
this._listTitle.label.clutter_text.set({
line_wrap: true,
});
this.menu.addMenuItem(this._listTitle);
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;
this._appsSection.removeAll();
const items = new Map();
(backgroundApps ?? [])
.map(backgroundApp => {
const appId = backgroundApp.app_id.deepUnpack();
const app = this._appSystem.lookup_app(`${appId}.desktop`);
const message = backgroundApp.message?.deepUnpack();
return {app, message};
})
.sort((a, b) => {
return a.app.get_name().localeCompare(b.app.get_name());
})
.forEach(backgroundApp => {
const {app, message} = backgroundApp;
let item = items.get(app);
if (!item) {
item = new BackgroundAppMenuItem(app);
items.set(app, item);
this._appsSection.addMenuItem(item);
}
if (message)
item.set({message});
});
const nBackgroundApps = items.size;
this.title = nBackgroundApps === 0
? _('No Background Apps')
: ngettext(
'%d Background App',
'%d Background Apps',
nBackgroundApps).format(nBackgroundApps);
this._listTitle.visible = nBackgroundApps > 0;
}
vfunc_clicked() {
this.menu.open();
}
});
export const Indicator = GObject.registerClass(
class Indicator extends SystemIndicator {
_init() {
super._init();
this.quickSettingsItems.push(new BackgroundAppsToggle());
}
});