423 lines
13 KiB
Diff
423 lines
13 KiB
Diff
From 44302f6c5799172e0fa61b75cc101c68e2f57ece Mon Sep 17 00:00:00 2001
|
|
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
|
|
Date: Tue, 26 Mar 2019 19:44:43 +0100
|
|
Subject: [PATCH] Add window-grouper extension
|
|
|
|
---
|
|
extensions/window-grouper/extension.js | 99 ++++++++
|
|
extensions/window-grouper/meson.build | 8 +
|
|
extensions/window-grouper/metadata.json.in | 11 +
|
|
...hell.extensions.window-grouper.gschema.xml | 9 +
|
|
extensions/window-grouper/prefs.js | 222 ++++++++++++++++++
|
|
extensions/window-grouper/stylesheet.css | 1 +
|
|
meson.build | 1 +
|
|
7 files changed, 351 insertions(+)
|
|
create mode 100644 extensions/window-grouper/extension.js
|
|
create mode 100644 extensions/window-grouper/meson.build
|
|
create mode 100644 extensions/window-grouper/metadata.json.in
|
|
create mode 100644 extensions/window-grouper/org.gnome.shell.extensions.window-grouper.gschema.xml
|
|
create mode 100644 extensions/window-grouper/prefs.js
|
|
create mode 100644 extensions/window-grouper/stylesheet.css
|
|
|
|
diff --git a/extensions/window-grouper/extension.js b/extensions/window-grouper/extension.js
|
|
new file mode 100644
|
|
index 00000000..24bbc132
|
|
--- /dev/null
|
|
+++ b/extensions/window-grouper/extension.js
|
|
@@ -0,0 +1,99 @@
|
|
+// SPDX-FileCopyrightText: Florian Müllner <fmuellner@gnome.org>
|
|
+//
|
|
+// SPDX-License-Identifier: GPL-2.0-or-later
|
|
+
|
|
+import Shell from 'gi://Shell';
|
|
+
|
|
+import {Extension} from 'resource:///org/gnome/shell/extensions/extension.js';
|
|
+import * as Main from 'resource:///org/gnome/shell/ui/main.js';
|
|
+
|
|
+class WindowMover {
|
|
+ constructor(settings) {
|
|
+ this._settings = settings;
|
|
+ this._appSystem = Shell.AppSystem.get_default();
|
|
+ this._appConfigs = new Set();
|
|
+ this._appData = new Map();
|
|
+
|
|
+ this._appSystem.connectObject('installed-changed',
|
|
+ this._updateAppData.bind(this), this);
|
|
+
|
|
+ this._settings.connectObject('changed',
|
|
+ this._updateAppConfigs.bind(this), this);
|
|
+ this._updateAppConfigs();
|
|
+ }
|
|
+
|
|
+ _updateAppConfigs() {
|
|
+ this._appConfigs.clear();
|
|
+
|
|
+ this._settings.get_strv('application-list').forEach(appId => {
|
|
+ this._appConfigs.add(appId);
|
|
+ });
|
|
+
|
|
+ this._updateAppData();
|
|
+ }
|
|
+
|
|
+ _updateAppData() {
|
|
+ let ids = [...this._appConfigs.values()];
|
|
+ let removedApps = [...this._appData.keys()].filter(
|
|
+ a => !ids.includes(a.id)
|
|
+ );
|
|
+ removedApps.forEach(app => {
|
|
+ app.disconnect(this._appData.get(app).windowsChangedId);
|
|
+ this._appData.delete(app);
|
|
+ });
|
|
+
|
|
+ let addedApps = ids.map(id => this._appSystem.lookup_app(id)).filter(
|
|
+ app => app != null && !this._appData.has(app)
|
|
+ );
|
|
+ addedApps.forEach(app => {
|
|
+ let data = {
|
|
+ windows: app.get_windows(),
|
|
+ windowsChangedId: app.connect(
|
|
+ 'windows-changed', this._appWindowsChanged.bind(this))
|
|
+ };
|
|
+ this._appData.set(app, data);
|
|
+ });
|
|
+ }
|
|
+
|
|
+ destroy() {
|
|
+ this._appSystem.disconnectObject(this);
|
|
+ this._appSystem = null;
|
|
+
|
|
+ this._settings.disconnectObject(this);
|
|
+ this._settings = null;
|
|
+
|
|
+ this._appConfigs.clear();
|
|
+ this._updateAppData();
|
|
+ }
|
|
+
|
|
+ _appWindowsChanged(app) {
|
|
+ let data = this._appData.get(app);
|
|
+ let windows = app.get_windows();
|
|
+
|
|
+ // If get_compositor_private() returns non-NULL on a removed windows,
|
|
+ // the window still exists and is just moved to a different workspace
|
|
+ // or something; assume it'll be added back immediately, so keep it
|
|
+ // to avoid moving it again
|
|
+ windows.push(...data.windows.filter(
|
|
+ w => !windows.includes(w) && w.get_compositor_private() != null
|
|
+ ));
|
|
+
|
|
+ windows.filter(w => !data.windows.includes(w)).forEach(window => {
|
|
+ let leader = data.windows.find(w => w.get_pid() == window.get_pid());
|
|
+ if (leader)
|
|
+ window.change_workspace(leader.get_workspace());
|
|
+ });
|
|
+ data.windows = windows;
|
|
+ }
|
|
+}
|
|
+
|
|
+export default class WindowGrouperExtension extends Extension {
|
|
+ enable() {
|
|
+ this._winMover = new WindowMover(this.getSettings());
|
|
+ }
|
|
+
|
|
+ disable() {
|
|
+ this._winMover.destroy();
|
|
+ this._winMover = null;
|
|
+ }
|
|
+}
|
|
diff --git a/extensions/window-grouper/meson.build b/extensions/window-grouper/meson.build
|
|
new file mode 100644
|
|
index 00000000..c55a7830
|
|
--- /dev/null
|
|
+++ b/extensions/window-grouper/meson.build
|
|
@@ -0,0 +1,8 @@
|
|
+extension_data += configure_file(
|
|
+ input: metadata_name + '.in',
|
|
+ output: metadata_name,
|
|
+ configuration: metadata_conf
|
|
+)
|
|
+
|
|
+extension_sources += files('prefs.js')
|
|
+extension_schemas += files(metadata_conf.get('gschemaname') + '.gschema.xml')
|
|
diff --git a/extensions/window-grouper/metadata.json.in b/extensions/window-grouper/metadata.json.in
|
|
new file mode 100644
|
|
index 00000000..aa202c8f
|
|
--- /dev/null
|
|
+++ b/extensions/window-grouper/metadata.json.in
|
|
@@ -0,0 +1,11 @@
|
|
+{
|
|
+ "extension-id": "@extension_id@",
|
|
+ "uuid": "@uuid@",
|
|
+ "settings-schema": "@gschemaname@",
|
|
+ "gettext-domain": "@gettext_domain@",
|
|
+ "name": "Window grouper",
|
|
+ "description": "Keep windows that belong to the same process on the same workspace.",
|
|
+ "shell-version": [ "@shell_current@" ],
|
|
+ "original-authors": [ "fmuellner@redhat.com" ],
|
|
+ "url": "@url@"
|
|
+}
|
|
diff --git a/extensions/window-grouper/org.gnome.shell.extensions.window-grouper.gschema.xml b/extensions/window-grouper/org.gnome.shell.extensions.window-grouper.gschema.xml
|
|
new file mode 100644
|
|
index 00000000..ee052a6e
|
|
--- /dev/null
|
|
+++ b/extensions/window-grouper/org.gnome.shell.extensions.window-grouper.gschema.xml
|
|
@@ -0,0 +1,9 @@
|
|
+<schemalist gettext-domain="gnome-shell-extensions">
|
|
+ <schema id="org.gnome.shell.extensions.window-grouper" path="/org/gnome/shell/extensions/window-grouper/">
|
|
+ <key name="application-list" type="as">
|
|
+ <default>[ ]</default>
|
|
+ <summary>Application that should be grouped</summary>
|
|
+ <description>A list of application ids</description>
|
|
+ </key>
|
|
+ </schema>
|
|
+</schemalist>
|
|
diff --git a/extensions/window-grouper/prefs.js b/extensions/window-grouper/prefs.js
|
|
new file mode 100644
|
|
index 00000000..2eceeff2
|
|
--- /dev/null
|
|
+++ b/extensions/window-grouper/prefs.js
|
|
@@ -0,0 +1,222 @@
|
|
+// SPDX-FileCopyrightText: Florian Müllner <fmuellner@gnome.org>
|
|
+//
|
|
+// SPDX-License-Identifier: GPL-2.0-or-later
|
|
+
|
|
+import Adw from 'gi://Adw';
|
|
+import Gio from 'gi://Gio';
|
|
+import GLib from 'gi://GLib';
|
|
+import GObject from 'gi://GObject';
|
|
+import Gtk from 'gi://Gtk';
|
|
+
|
|
+import {ExtensionPreferences, gettext as _} from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js';
|
|
+
|
|
+const SETTINGS_KEY = 'application-list';
|
|
+
|
|
+class NewItem extends GObject.Object {}
|
|
+GObject.registerClass(NewItem);
|
|
+
|
|
+class NewItemModel extends GObject.Object {
|
|
+ static [GObject.interfaces] = [Gio.ListModel];
|
|
+ static {
|
|
+ GObject.registerClass(this);
|
|
+ }
|
|
+
|
|
+ #item = new NewItem();
|
|
+
|
|
+ vfunc_get_item_type() {
|
|
+ return NewItem;
|
|
+ }
|
|
+
|
|
+ vfunc_get_n_items() {
|
|
+ return 1;
|
|
+ }
|
|
+
|
|
+ vfunc_get_item(_pos) {
|
|
+ return this.#item;
|
|
+ }
|
|
+}
|
|
+
|
|
+class AppsList extends GObject.Object {
|
|
+ static [GObject.interfaces] = [Gio.ListModel];
|
|
+ static {
|
|
+ GObject.registerClass(this);
|
|
+ }
|
|
+
|
|
+ #settings;
|
|
+ #apps = [];
|
|
+ #changedId;
|
|
+
|
|
+ constructor(settings) {
|
|
+ super();
|
|
+
|
|
+ this.#settings = settings;
|
|
+ this.#changedId =
|
|
+ this.#settings.connect(`changed::${SETTINGS_KEY}`,
|
|
+ () => this.#sync());
|
|
+ this.#sync();
|
|
+ }
|
|
+
|
|
+ append(appInfo) {
|
|
+ const pos = this.#apps.length;
|
|
+
|
|
+ this.#apps.push(appInfo);
|
|
+ this.#saveApps();
|
|
+
|
|
+ this.items_changed(pos, 0, 1);
|
|
+ }
|
|
+
|
|
+ remove(id) {
|
|
+ const pos = this.#apps.findIndex(a => a.get_id() === id);
|
|
+ if (pos < 0)
|
|
+ return;
|
|
+
|
|
+ this.#apps.splice(pos, 1);
|
|
+ this.#saveApps();
|
|
+
|
|
+ this.items_changed(pos, 1, 0);
|
|
+ }
|
|
+
|
|
+ #saveApps() {
|
|
+ this.#settings.block_signal_handler(this.#changedId);
|
|
+ this.#settings.set_strv(SETTINGS_KEY,
|
|
+ this.#apps.map(a => a.get_id()));
|
|
+ this.#settings.unblock_signal_handler(this.#changedId);
|
|
+ }
|
|
+
|
|
+ #sync() {
|
|
+ const removed = this.#apps.length;
|
|
+
|
|
+ this.#apps = [];
|
|
+ for (const id of this.#settings.get_strv(SETTINGS_KEY)) {
|
|
+ const appInfo = Gio.DesktopAppInfo.new(id);
|
|
+ if (appInfo)
|
|
+ this.#apps.push(appInfo);
|
|
+ else
|
|
+ console.log(`Invalid ID ${id}`);
|
|
+ }
|
|
+ this.items_changed(0, removed, this.#apps.length);
|
|
+ }
|
|
+
|
|
+ vfunc_get_item_type() {
|
|
+ return Gio.AppInfo;
|
|
+ }
|
|
+
|
|
+ vfunc_get_n_items() {
|
|
+ return this.#apps.length;
|
|
+ }
|
|
+
|
|
+ vfunc_get_item(pos) {
|
|
+ return this.#apps[pos] ?? null;
|
|
+ }
|
|
+}
|
|
+
|
|
+class WindowGrouperSettingsWidget extends Adw.PreferencesGroup {
|
|
+ static {
|
|
+ GObject.registerClass(this);
|
|
+
|
|
+ this.install_action('apps.add', null, self => self._addNewApp());
|
|
+ this.install_action('apps.remove', 's',
|
|
+ (self, name, param) => self._apps.remove(param.unpack()));
|
|
+ }
|
|
+
|
|
+ constructor(settings) {
|
|
+ super({
|
|
+ title: _('Grouped Apps'),
|
|
+ });
|
|
+
|
|
+ this._settings = settings;
|
|
+ this._apps = new AppsList(this._settings);
|
|
+
|
|
+ const store = new Gio.ListStore({item_type: Gio.ListModel});
|
|
+ const listModel = new Gtk.FlattenListModel({model: store});
|
|
+ store.append(this._apps);
|
|
+ store.append(new NewItemModel());
|
|
+
|
|
+ this._list = new Gtk.ListBox({
|
|
+ selection_mode: Gtk.SelectionMode.NONE,
|
|
+ css_classes: ['boxed-list'],
|
|
+ });
|
|
+ this.add(this._list);
|
|
+
|
|
+ this._list.bind_model(listModel, item => {
|
|
+ return item instanceof NewItem
|
|
+ ? new NewAppRow()
|
|
+ : new AppRow(item);
|
|
+ });
|
|
+ }
|
|
+
|
|
+ _addNewApp() {
|
|
+ const dialog = new Gtk.AppChooserDialog({
|
|
+ heading: _('Select an application for which grouping should apply'),
|
|
+ transient_for: this.get_root(),
|
|
+ modal: true
|
|
+ });
|
|
+
|
|
+ dialog.get_widget().show_all = true;
|
|
+
|
|
+ dialog.connect('response', (dialog, id) => {
|
|
+ const appInfo = id === Gtk.ResponseType.OK
|
|
+ ? dialog.get_app_info() : null;
|
|
+
|
|
+ if (appInfo)
|
|
+ this._apps.append(appInfo);
|
|
+ dialog.destroy();
|
|
+ });
|
|
+ dialog.show();
|
|
+ }
|
|
+}
|
|
+
|
|
+class AppRow extends Adw.ActionRow {
|
|
+ static {
|
|
+ GObject.registerClass(this);
|
|
+ }
|
|
+
|
|
+ constructor(appInfo) {
|
|
+ super({
|
|
+ activatable: false,
|
|
+ title: appInfo.get_display_name(),
|
|
+ });
|
|
+
|
|
+ const icon = new Gtk.Image({
|
|
+ css_classes: ['icon-dropshadow'],
|
|
+ gicon: appInfo.get_icon(),
|
|
+ pixel_size: 32,
|
|
+ });
|
|
+ this.add_prefix(icon);
|
|
+
|
|
+ const button = new Gtk.Button({
|
|
+ action_name: 'apps.remove',
|
|
+ action_target: new GLib.Variant('s', appInfo.get_id()),
|
|
+ icon_name: 'edit-delete-symbolic',
|
|
+ has_frame: false,
|
|
+ valign: Gtk.Align.CENTER,
|
|
+ });
|
|
+ this.add_suffix(button);
|
|
+ }
|
|
+}
|
|
+
|
|
+const NewAppRow = GObject.registerClass(
|
|
+class NewAppRow extends Gtk.ListBoxRow {
|
|
+ _init() {
|
|
+ super._init({
|
|
+ action_name: 'apps.add',
|
|
+ child: new Gtk.Image({
|
|
+ icon_name: 'list-add-symbolic',
|
|
+ pixel_size: 16,
|
|
+ margin_top: 12,
|
|
+ margin_bottom: 12,
|
|
+ margin_start: 12,
|
|
+ margin_end: 12,
|
|
+ }),
|
|
+ });
|
|
+
|
|
+ this.update_property(
|
|
+ [Gtk.AccessibleProperty.LABEL], [_('Add App')]);
|
|
+ }
|
|
+});
|
|
+
|
|
+export default class WindowGrouperPrefs extends ExtensionPreferences {
|
|
+ getPreferencesWidget() {
|
|
+ return new WindowGrouperSettingsWidget(this.getSettings());
|
|
+ }
|
|
+}
|
|
diff --git a/extensions/window-grouper/stylesheet.css b/extensions/window-grouper/stylesheet.css
|
|
new file mode 100644
|
|
index 00000000..25134b65
|
|
--- /dev/null
|
|
+++ b/extensions/window-grouper/stylesheet.css
|
|
@@ -0,0 +1 @@
|
|
+/* This extensions requires no special styling */
|
|
diff --git a/meson.build b/meson.build
|
|
index 29ac83bb..d72a01ae 100644
|
|
--- a/meson.build
|
|
+++ b/meson.build
|
|
@@ -61,6 +61,7 @@ all_extensions += [
|
|
'gesture-inhibitor',
|
|
'native-window-placement',
|
|
'user-theme',
|
|
+ 'window-grouper',
|
|
]
|
|
|
|
enabled_extensions = get_option('enable_extensions')
|
|
--
|
|
2.55.0
|
|
|