[gnome-shell-extensions/wip/rstrode/heads-up-display: 7/62] Add window-grouper extension




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]