[gnome-shell-extensions/wip/rstrode/heads-up-display: 7/62] Add window-grouper extension
- From: Ray Strode <halfline src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gnome-shell-extensions/wip/rstrode/heads-up-display: 7/62] Add window-grouper extension
- Date: Thu, 26 Aug 2021 19:31:30 +0000 (UTC)
commit 5bc782c2f23c37380bd9adbd11668219a10c3431
Author: Florian Müllner <fmuellner gnome org>
Date: Tue Mar 26 19:44:43 2019 +0100
Add window-grouper extension
extensions/window-grouper/extension.js | 109 ++++++++++++
extensions/window-grouper/meson.build | 8 +
extensions/window-grouper/metadata.json.in | 11 ++
...ome.shell.extensions.window-grouper.gschema.xml | 9 +
extensions/window-grouper/prefs.js | 191 +++++++++++++++++++++
extensions/window-grouper/stylesheet.css | 1 +
meson.build | 3 +-
7 files changed, 331 insertions(+), 1 deletion(-)
---
diff --git a/extensions/window-grouper/extension.js b/extensions/window-grouper/extension.js
new file mode 100644
index 0000000..f66a764
--- /dev/null
+++ b/extensions/window-grouper/extension.js
@@ -0,0 +1,109 @@
+// -*- mode: js2; indent-tabs-mode: nil; js2-basic-offset: 4 -*-
+/* exported init */
+
+const { Shell } = imports.gi;
+
+const ExtensionUtils = imports.misc.extensionUtils;
+
+class WindowMover {
+ constructor() {
+ this._settings = ExtensionUtils.getSettings();
+ this._appSystem = Shell.AppSystem.get_default();
+ this._appConfigs = new Set();
+ this._appData = new Map();
+
+ this._appsChangedId = this._appSystem.connect(
+ 'installed-changed', this._updateAppData.bind(this));
+
+ this._settings.connect('changed', this._updateAppConfigs.bind(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() {
+ if (this._appsChangedId) {
+ this._appSystem.disconnect(this._appsChangedId);
+ this._appsChangedId = 0;
+ }
+
+ if (this._settings) {
+ this._settings.run_dispose();
+ 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;
+ }
+}
+
+class Extension {
+ constructor() {
+ this._winMover = null;
+ }
+
+ enable() {
+ this._winMover = new WindowMover();
+ }
+
+ disable() {
+ this._winMover.destroy();
+ this._winMover = null;
+ }
+}
+
+function init() {
+ ExtensionUtils.initTranslations();
+ return new Extension();
+}
diff --git a/extensions/window-grouper/meson.build b/extensions/window-grouper/meson.build
new file mode 100644
index 0000000..c55a783
--- /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 0000000..aa202c8
--- /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 0000000..ee052a6
--- /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 0000000..d7b748e
--- /dev/null
+++ b/extensions/window-grouper/prefs.js
@@ -0,0 +1,191 @@
+// -*- mode: js2; indent-tabs-mode: nil; js2-basic-offset: 4 -*-
+/* exported init buildPrefsWidget */
+
+const { Gio, GObject, Gtk } = imports.gi;
+
+const ExtensionUtils = imports.misc.extensionUtils;
+const Gettext = imports.gettext.domain('gnome-shell-extensions');
+const _ = Gettext.gettext;
+const N_ = e => e;
+
+const SETTINGS_KEY = 'application-list';
+
+const Columns = {
+ APPINFO: 0,
+ DISPLAY_NAME: 1,
+ ICON: 2
+};
+
+const Widget = GObject.registerClass({
+ GTypeName: 'WindowGrouperPrefsWidget',
+}, class Widget extends Gtk.Grid {
+ _init(params) {
+ super._init(params);
+ this.set_orientation(Gtk.Orientation.VERTICAL);
+
+ this._settings = ExtensionUtils.getSettings();
+ this._settings.connect('changed', this._refresh.bind(this));
+ this._changedPermitted = false;
+
+ this._store = new Gtk.ListStore();
+ this._store.set_column_types([Gio.AppInfo, GObject.TYPE_STRING, Gio.Icon]);
+
+ let scrolled = new Gtk.ScrolledWindow({ shadow_type: Gtk.ShadowType.IN });
+ scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC);
+ this.add(scrolled);
+
+
+ this._treeView = new Gtk.TreeView({
+ model: this._store,
+ headers_visible: false,
+ hexpand: true,
+ vexpand: true
+ });
+ this._treeView.get_selection().set_mode(Gtk.SelectionMode.SINGLE);
+
+ let appColumn = new Gtk.TreeViewColumn({
+ sort_column_id: Columns.DISPLAY_NAME,
+ spacing: 12
+ });
+ let iconRenderer = new Gtk.CellRendererPixbuf({
+ stock_size: Gtk.IconSize.DIALOG,
+ xpad: 12,
+ ypad: 12
+ });
+ appColumn.pack_start(iconRenderer, false);
+ appColumn.add_attribute(iconRenderer, 'gicon', Columns.ICON);
+ let nameRenderer = new Gtk.CellRendererText();
+ appColumn.pack_start(nameRenderer, true);
+ appColumn.add_attribute(nameRenderer, 'text', Columns.DISPLAY_NAME);
+ this._treeView.append_column(appColumn);
+
+ scrolled.add(this._treeView);
+
+ let toolbar = new Gtk.Toolbar({ icon_size: Gtk.IconSize.SMALL_TOOLBAR });
+ toolbar.get_style_context().add_class(Gtk.STYLE_CLASS_INLINE_TOOLBAR);
+ this.add(toolbar);
+
+ let newButton = new Gtk.ToolButton({
+ icon_name: 'list-add-symbolic'
+ });
+ newButton.connect('clicked', this._createNew.bind(this));
+ toolbar.add(newButton);
+
+ let delButton = new Gtk.ToolButton({
+ icon_name: 'list-remove-symbolic'
+ });
+ delButton.connect('clicked', this._deleteSelected.bind(this));
+ toolbar.add(delButton);
+
+ let selection = this._treeView.get_selection();
+ selection.connect('changed', () => {
+ delButton.sensitive = selection.count_selected_rows() > 0;
+ });
+ delButton.sensitive = selection.count_selected_rows() > 0;
+
+ this._changedPermitted = true;
+ this._refresh();
+ }
+
+ _createNew() {
+ let dialog = new Gtk.AppChooserDialog({
+ heading: _('Select an application for which grouping should apply'),
+ transient_for: this.get_toplevel(),
+ modal: true
+ });
+
+ dialog.get_widget().show_all = true;
+
+ dialog.connect('response', (dialog, id) => {
+ if (id != Gtk.ResponseType.OK) {
+ dialog.destroy();
+ return;
+ }
+
+ let appInfo = dialog.get_app_info();
+ if (!appInfo) {
+ dialog.destroy();
+ return;
+ }
+
+ this._changedPermitted = false;
+ this._appendItem(appInfo.get_id());
+ this._changedPermitted = true;
+
+ let iter = this._store.append();
+ this._store.set(iter,
+ [Columns.APPINFO, Columns.ICON, Columns.DISPLAY_NAME],
+ [appInfo, appInfo.get_icon(), appInfo.get_display_name()]);
+
+ dialog.destroy();
+ });
+ dialog.show_all();
+ }
+
+ _deleteSelected() {
+ let [any, model_, iter] = this._treeView.get_selection().get_selected();
+
+ if (any) {
+ let appInfo = this._store.get_value(iter, Columns.APPINFO);
+
+ this._changedPermitted = false;
+ this._removeItem(appInfo.get_id());
+ this._changedPermitted = true;
+ this._store.remove(iter);
+ }
+ }
+
+ _refresh() {
+ if (!this._changedPermitted)
+ // Ignore this notification, model is being modified outside
+ return;
+
+ this._store.clear();
+
+ let currentItems = this._settings.get_strv(SETTINGS_KEY);
+ let validItems = [];
+ for (let i = 0; i < currentItems.length; i++) {
+ let id = currentItems[i];
+ let appInfo = Gio.DesktopAppInfo.new(id);
+ if (!appInfo)
+ continue;
+ validItems.push(currentItems[i]);
+
+ let iter = this._store.append();
+ this._store.set(iter,
+ [Columns.APPINFO, Columns.ICON, Columns.DISPLAY_NAME],
+ [appInfo, appInfo.get_icon(), appInfo.get_display_name()]);
+ }
+
+ if (validItems.length != currentItems.length) // some items were filtered out
+ this._settings.set_strv(SETTINGS_KEY, validItems);
+ }
+
+ _appendItem(id) {
+ let currentItems = this._settings.get_strv(SETTINGS_KEY);
+ currentItems.push(id);
+ this._settings.set_strv(SETTINGS_KEY, currentItems);
+ }
+
+ _removeItem(id) {
+ let currentItems = this._settings.get_strv(SETTINGS_KEY);
+ let index = currentItems.indexOf(id);
+
+ if (index < 0)
+ return;
+ currentItems.splice(index, 1);
+ this._settings.set_strv(SETTINGS_KEY, currentItems);
+ }
+});
+
+
+function init() {
+ ExtensionUtils.initTranslations();
+}
+
+function buildPrefsWidget() {
+ let widget = new Widget({ margin: 12 });
+ widget.show_all();
+
+ return widget;
+}
diff --git a/extensions/window-grouper/stylesheet.css b/extensions/window-grouper/stylesheet.css
new file mode 100644
index 0000000..25134b6
--- /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 6f27f46..4b9d138 100644
--- a/meson.build
+++ b/meson.build
@@ -55,7 +55,8 @@ all_extensions += [
'panel-favorites',
'top-icons',
'updates-dialog',
- 'user-theme'
+ 'user-theme',
+ 'window-grouper'
]
enabled_extensions = get_option('enable_extensions')
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]