Add window-grouper extension
Resolves: https://redhat.atlassian.net/browse/RHEL-193195
This commit is contained in:
parent
ba5d99f3a9
commit
9025f29f55
422
extra-extensions-0006-Add-window-grouper-extension.patch
Normal file
422
extra-extensions-0006-Add-window-grouper-extension.patch
Normal file
@ -0,0 +1,422 @@
|
||||
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
|
||||
|
||||
@ -33,6 +33,7 @@ Patch: extra-extensions-0002-Add-classification-banner.patch
|
||||
Patch: extra-extensions-0003-Add-heads-up-display.patch
|
||||
Patch: extra-extensions-0004-Add-custom-menu-extension.patch
|
||||
Patch: extra-extensions-0005-Add-desktop-icons-extension.patch
|
||||
Patch: extra-extensions-0006-Add-window-grouper-extension.patch
|
||||
|
||||
Patch: 0001-Include-status-icons-in-classic-session.patch
|
||||
|
||||
@ -63,6 +64,7 @@ Enabled extensions:
|
||||
* status-icons
|
||||
* system-monitor
|
||||
* user-theme
|
||||
* window-grouper
|
||||
* window-list
|
||||
* windowsNavigator
|
||||
* workspace-indicator
|
||||
@ -269,6 +271,15 @@ This GNOME Shell extension enables loading a GNOME Shell theme from
|
||||
~/.themes/<name>/gnome-shell/.
|
||||
|
||||
|
||||
%package -n %{pkg_prefix}-window-grouper
|
||||
Summary: Keep windows that belong to the same process on the same workspace
|
||||
License: GPLv2+
|
||||
Requires: %{pkg_prefix}-common = %{version}-%{release}
|
||||
|
||||
%description -n %{pkg_prefix}-window-grouper
|
||||
This GNOME Shell extension keeps windows that belong to the same process on the same workspace.
|
||||
|
||||
|
||||
%package -n %{pkg_prefix}-window-list
|
||||
Summary: Display a window list at the bottom of the screen in GNOME Shell
|
||||
License: GPL-2.0-or-later
|
||||
@ -408,6 +419,11 @@ rm -rf %{buildroot}/%{_datadir}/xsessions
|
||||
%{_datadir}/gnome-shell/extensions/user-theme*/
|
||||
|
||||
|
||||
%files -n %{pkg_prefix}-window-grouper
|
||||
%{_datadir}/gnome-shell/extensions/window-grouper*/
|
||||
%{_datadir}/glib-2.0/schemas/org.gnome.shell.extensions.window-grouper.gschema.xml
|
||||
|
||||
|
||||
%files -n %{pkg_prefix}-window-list
|
||||
%{_datadir}/gnome-shell/extensions/window-list*/
|
||||
%{_datadir}/glib-2.0/schemas/org.gnome.shell.extensions.window-list.gschema.xml
|
||||
|
||||
Loading…
Reference in New Issue
Block a user