gnome-shell-extensions/extra-extensions-0006-Add-window-grouper-extension.patch
2026-07-09 23:16:21 +02:00

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