2023-07-10 09:53:00 +00:00
|
|
|
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';
|
2023-08-07 15:13:48 +00:00
|
|
|
import {loadInterfaceXML} from '../../misc/dbusUtils.js';
|
2023-02-02 18:58:43 +00:00
|
|
|
|
|
|
|
const DBUS_NAME = 'org.freedesktop.background.Monitor';
|
|
|
|
const DBUS_OBJECT_PATH = '/org/freedesktop/background/monitor';
|
|
|
|
|
2023-04-11 13:29:55 +00:00
|
|
|
const SPINNER_TIMEOUT = 5; // seconds
|
|
|
|
|
2023-02-02 18:58:43 +00:00
|
|
|
const BackgroundMonitorIface = loadInterfaceXML('org.freedesktop.background.Monitor');
|
|
|
|
const BackgroundMonitorProxy = Gio.DBusProxy.makeProxyWrapper(BackgroundMonitorIface);
|
|
|
|
|
|
|
|
Gio._promisify(Gio.DBusConnection.prototype, 'call');
|
|
|
|
|
2023-07-10 09:53:00 +00:00
|
|
|
const BackgroundAppMenuItem = GObject.registerClass({
|
2023-02-02 18:58:43 +00:00
|
|
|
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,
|
|
|
|
});
|
|
|
|
|
2023-05-19 14:36:39 +00:00
|
|
|
this.set({message});
|
2023-02-02 18:58:43 +00:00
|
|
|
|
|
|
|
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);
|
|
|
|
|
2023-05-16 23:33:19 +00:00
|
|
|
this.set_child_above_sibling(this._ornamentIcon, null);
|
2023-02-02 18:58:43 +00:00
|
|
|
|
2023-04-11 13:29:55 +00:00
|
|
|
this._spinner = new Spinner(16, {hideOnStop: true});
|
|
|
|
this._spinner.add_style_class_name('spinner');
|
|
|
|
this.add_child(this._spinner);
|
|
|
|
|
2023-02-02 18:58:43 +00:00
|
|
|
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);
|
|
|
|
|
2023-04-11 13:29:55 +00:00
|
|
|
this._spinner.bind_property('visible',
|
|
|
|
closeButton, 'visible',
|
|
|
|
GObject.BindingFlags.INVERT_BOOLEAN);
|
|
|
|
|
2023-02-02 18:58:43 +00:00
|
|
|
closeButton.connect('clicked', () => this._quitApp().catch(logError));
|
2023-04-11 13:29:55 +00:00
|
|
|
|
2023-05-19 17:02:08 +00:00
|
|
|
this.connect('activate', () => this.app.activate());
|
|
|
|
|
2023-04-11 13:29:55 +00:00
|
|
|
this.connect('destroy', () => this._onDestroy());
|
|
|
|
}
|
|
|
|
|
|
|
|
_onDestroy() {
|
|
|
|
if (this._spinnerTimeoutId)
|
|
|
|
GLib.source_remove(this._spinnerTimeoutId);
|
|
|
|
delete this._spinnerTimeoutId;
|
2023-02-02 18:58:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async _quitApp() {
|
|
|
|
const appId = this.app.get_id().replace(/\.desktop$/, '');
|
|
|
|
|
2023-04-11 13:29:55 +00:00
|
|
|
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;
|
|
|
|
});
|
|
|
|
|
2023-02-02 18:58:43 +00:00
|
|
|
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 {
|
2023-05-19 14:36:39 +00:00
|
|
|
Util.trySpawn(['flatpak', 'kill', appId]);
|
2023-02-02 18:58:43 +00:00
|
|
|
} 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);
|
|
|
|
|
2023-02-24 00:41:26 +00:00
|
|
|
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);
|
|
|
|
|
2023-02-02 18:58:43 +00:00
|
|
|
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();
|
|
|
|
|
2023-05-19 14:36:39 +00:00
|
|
|
const items = new Map();
|
|
|
|
(backgroundApps ?? [])
|
2023-02-02 18:58:43 +00:00
|
|
|
.map(backgroundApp => {
|
|
|
|
const appId = backgroundApp.app_id.deepUnpack();
|
|
|
|
const app = this._appSystem.lookup_app(`${appId}.desktop`);
|
|
|
|
const message = backgroundApp.message?.deepUnpack();
|
|
|
|
|
2023-05-19 14:36:39 +00:00
|
|
|
return {app, message};
|
2023-02-02 18:58:43 +00:00
|
|
|
})
|
|
|
|
.sort((a, b) => {
|
|
|
|
return a.app.get_name().localeCompare(b.app.get_name());
|
|
|
|
})
|
|
|
|
.forEach(backgroundApp => {
|
2023-05-19 14:36:39 +00:00
|
|
|
const {app, message} = backgroundApp;
|
|
|
|
|
|
|
|
let item = items.get(app);
|
|
|
|
if (!item) {
|
|
|
|
item = new BackgroundAppMenuItem(app);
|
|
|
|
items.set(app, item);
|
|
|
|
this._appsSection.addMenuItem(item);
|
|
|
|
}
|
2023-02-02 18:58:43 +00:00
|
|
|
|
2023-05-19 14:36:39 +00:00
|
|
|
if (message)
|
|
|
|
item.set({message});
|
2023-02-02 18:58:43 +00:00
|
|
|
});
|
2023-05-19 14:36:39 +00:00
|
|
|
|
|
|
|
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;
|
2023-02-02 18:58:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
vfunc_clicked() {
|
|
|
|
this.menu.open();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2023-07-10 09:53:00 +00:00
|
|
|
export const Indicator = GObject.registerClass(
|
2023-02-02 18:58:43 +00:00
|
|
|
class Indicator extends SystemIndicator {
|
|
|
|
_init() {
|
|
|
|
super._init();
|
|
|
|
|
|
|
|
this.quickSettingsItems.push(new BackgroundAppsToggle());
|
|
|
|
}
|
|
|
|
});
|