[gnome-shell-extensions] auto-move: Overhaul preference dialog



commit c87cfc822a19fd75b444e733f0c9b51b3bc4c300
Author: Florian Müllner <fmuellner gnome org>
Date:   Fri May 1 16:33:41 2020 +0200

    auto-move: Overhaul preference dialog
    
    auto-move uses the same outdated UI pattern as workspace-indicator did
    until commit 90d3c5c51d2, imposing the same problems for a future GTK4
    port.
    
    So replace treeview and toolbar with an editable list like we did for
    the other extension.
    
    https://gitlab.gnome.org/GNOME/gnome-shell-extensions/-/merge_requests/121

 extensions/auto-move-windows/prefs.js | 446 +++++++++++++++++-----------------
 1 file changed, 229 insertions(+), 217 deletions(-)
---
diff --git a/extensions/auto-move-windows/prefs.js b/extensions/auto-move-windows/prefs.js
index 84f98ae..1324451 100644
--- a/extensions/auto-move-windows/prefs.js
+++ b/extensions/auto-move-windows/prefs.js
@@ -2,11 +2,10 @@
 // Start apps on custom workspaces
 /* exported init buildPrefsWidget */
 
-const { Gio, GObject, Gtk } = imports.gi;
+const { Gio, GLib, GObject, Gtk, Pango } = imports.gi;
 
 const Gettext = imports.gettext.domain('gnome-shell-extensions');
 const _ = Gettext.gettext;
-const N_ = e => e;
 
 const ExtensionUtils = imports.misc.extensionUtils;
 
@@ -14,269 +13,282 @@ const SETTINGS_KEY = 'application-list';
 
 const WORKSPACE_MAX = 36; // compiled in limit of mutter
 
-const Columns = {
-    APPINFO: 0,
-    DISPLAY_NAME: 1,
-    ICON: 2,
-    WORKSPACE: 3,
-    ADJUSTMENT: 4,
-};
+const AutoMoveSettingsWidget = GObject.registerClass(
+class AutoMoveSettingsWidget extends Gtk.ScrolledWindow {
+    _init() {
+        super._init({
+            hscrollbar_policy: Gtk.PolicyType.NEVER,
+        });
+
+        const box = new Gtk.Box({
+            orientation: Gtk.Orientation.VERTICAL,
+            halign: Gtk.Align.CENTER,
+            spacing: 12,
+            margin_top: 36,
+            margin_bottom: 36,
+            margin_start: 36,
+            margin_end: 36,
+        });
+        this.add(box);
 
-const Widget = GObject.registerClass(
-class Widget extends Gtk.Grid {
-    _init(params) {
-        super._init(params);
-        this.set_orientation(Gtk.Orientation.VERTICAL);
+        box.add(new Gtk.Label({
+            label: '<b>%s</b>'.format(_('Workspace Rules')),
+            use_markup: true,
+            halign: Gtk.Align.START,
+        }));
 
-        this._settings = ExtensionUtils.getSettings();
-        this._settings.connect('changed', this._refresh.bind(this));
-        this._changedPermitted = false;
+        this._list = new Gtk.ListBox({
+            selection_mode: Gtk.SelectionMode.NONE,
+            valign: Gtk.Align.START,
+        });
+        this._list.set_header_func(this._updateHeader.bind(this));
+        box.add(this._list);
 
-        this._store = new Gtk.ListStore();
-        this._store.set_column_types([
-            Gio.AppInfo,
-            GObject.TYPE_STRING,
-            Gio.Icon,
-            GObject.TYPE_INT,
-            Gtk.Adjustment,
-        ]);
+        const context = this._list.get_style_context();
+        const cssProvider = new Gtk.CssProvider();
+        cssProvider.load_from_data(
+            'list { min-width: 30em; }');
 
-        let scrolled = new Gtk.ScrolledWindow({ shadow_type: Gtk.ShadowType.IN });
-        scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC);
-        this.add(scrolled);
+        context.add_provider(cssProvider,
+            Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
+        context.add_class('frame');
 
+        this._list.add(new NewRuleRow());
 
-        this._treeView = new Gtk.TreeView({
-            model: this._store,
-            hexpand: true,
-            vexpand: true,
-        });
-        this._treeView.get_selection().set_mode(Gtk.SelectionMode.SINGLE);
+        this._actionGroup = new Gio.SimpleActionGroup();
+        this._list.insert_action_group('rules', this._actionGroup);
 
-        let appColumn = new Gtk.TreeViewColumn({
-            expand: true,
-            sort_column_id: Columns.DISPLAY_NAME,
-            title: _('Application'),
-        });
-        let iconRenderer = new Gtk.CellRendererPixbuf();
-        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);
-
-        let workspaceColumn = new Gtk.TreeViewColumn({
-            title: _('Workspace'),
-            sort_column_id: Columns.WORKSPACE,
+        let action;
+        action = new Gio.SimpleAction({ name: 'add' });
+        action.connect('activate', this._onAddActivated.bind(this));
+        this._actionGroup.add_action(action);
+
+        action = new Gio.SimpleAction({
+            name: 'remove',
+            parameter_type: new GLib.VariantType('s'),
         });
-        let workspaceRenderer = new Gtk.CellRendererSpin({ editable: true });
-        workspaceRenderer.connect('edited', this._workspaceEdited.bind(this));
-        workspaceColumn.pack_start(workspaceRenderer, true);
-        workspaceColumn.add_attribute(workspaceRenderer, 'adjustment', Columns.ADJUSTMENT);
-        workspaceColumn.add_attribute(workspaceRenderer, 'text', Columns.WORKSPACE);
-        this._treeView.append_column(workspaceColumn);
-
-        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: 'bookmark-new-symbolic',
-            label: _('Add Rule'),
-            is_important: true,
+        action.connect('activate', this._onRemoveActivated.bind(this));
+        this._actionGroup.add_action(action);
+
+        action = new Gio.SimpleAction({ name: 'update' });
+        action.connect('activate', () => {
+            this._settings.set_strv(SETTINGS_KEY,
+                this._getRuleRows().map(row => `${row.id}:${row.value}`));
         });
-        newButton.connect('clicked', this._createNew.bind(this));
-        toolbar.add(newButton);
+        this._actionGroup.add_action(action);
+        this._updateAction = action;
 
-        let delButton = new Gtk.ToolButton({ icon_name: 'edit-delete-symbolic' });
-        delButton.connect('clicked', this._deleteSelected.bind(this));
-        toolbar.add(delButton);
+        this._settings = ExtensionUtils.getSettings();
+        this._changedId = this._settings.connect('changed',
+            this._sync.bind(this));
+        this._sync();
 
-        let selection = this._treeView.get_selection();
-        selection.connect('changed', () => {
-            delButton.sensitive = selection.count_selected_rows() > 0;
-        });
-        delButton.sensitive = selection.count_selected_rows() > 0;
+        this.connect('destroy', () => this._settings.run_dispose());
 
-        this._changedPermitted = true;
-        this._refresh();
+        this.show_all();
     }
 
-    _createNew() {
-        let dialog = new Gtk.Dialog({
-            title: _('Create new matching rule'),
-            transient_for: this.get_toplevel(),
-            use_header_bar: true,
-            modal: true,
-        });
-        dialog.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL);
-        let addButton = dialog.add_button(_('Add'), Gtk.ResponseType.OK);
-        dialog.set_default_response(Gtk.ResponseType.OK);
-
-        let grid = new Gtk.Grid({
-            column_spacing: 10,
-            row_spacing: 15,
-            margin: 10,
-        });
-        dialog._appChooser = new Gtk.AppChooserWidget({ show_all: true });
-        dialog._appChooser.connect('application-selected', (w, appInfo) => {
-            addButton.sensitive = appInfo && this._checkId(appInfo.get_id());
-        });
-        let appInfo = dialog._appChooser.get_app_info();
-        addButton.sensitive = appInfo && this._checkId(appInfo.get_id());
-
-        grid.attach(dialog._appChooser, 0, 0, 2, 1);
-        grid.attach(new Gtk.Label({
-            label: _('Workspace'),
-            halign: Gtk.Align.END,
-        }), 0, 1, 1, 1);
-        let adjustment = new Gtk.Adjustment({
-            lower: 1,
-            upper: WORKSPACE_MAX,
-            step_increment: 1,
-        });
-        dialog._spin = new Gtk.SpinButton({
-            adjustment,
-            snap_to_ticks: true,
-        });
-        dialog._spin.set_value(1);
-        grid.attach(dialog._spin, 1, 1, 1, 1);
-        dialog.get_content_area().add(grid);
-
+    _onAddActivated() {
+        const dialog = new NewRuleDialog(this.get_toplevel());
         dialog.connect('response', (dlg, id) => {
-            if (id !== Gtk.ResponseType.OK) {
-                dialog.destroy();
-                return;
+            const appInfo = id === Gtk.ResponseType.OK
+                ? dialog.get_widget().get_app_info() : null;
+            if (appInfo) {
+                this._settings.set_strv(SETTINGS_KEY, [
+                    ...this._settings.get_strv(SETTINGS_KEY),
+                    `${appInfo.get_id()}:1`,
+                ]);
             }
+            dialog.destroy();
+        });
+    }
 
-            appInfo = dialog._appChooser.get_app_info();
-            if (!appInfo)
-                return;
-            let index = Math.floor(dialog._spin.value);
-            if (isNaN(index) || index < 0)
-                index = 1;
-
-            this._changedPermitted = false;
-            this._appendItem(appInfo.get_id(), index);
-            this._changedPermitted = true;
+    _onRemoveActivated(action, param) {
+        const removed = param.deepUnpack();
+        this._settings.set_strv(SETTINGS_KEY,
+            this._settings.get_strv(SETTINGS_KEY).filter(entry => {
+                const [id] = entry.split(':');
+                return id !== removed;
+            }));
+    }
 
-            this._appendRow(appInfo, index);
+    _getRuleRows() {
+        return this._list.get_children().filter(row => !!row.id);
+    }
 
-            dialog.destroy();
+    _sync() {
+        const oldRules = this._getRuleRows();
+        const newRules = this._settings.get_strv(SETTINGS_KEY).map(entry => {
+            const [id, value] = entry.split(':');
+            return { id, value };
         });
-        dialog.show_all();
-    }
 
-    _deleteSelected() {
-        let [any, model_, iter] = this._treeView.get_selection().get_selected();
+        this._settings.block_signal_handler(this._changedId);
+        this._updateAction.enabled = false;
 
-        if (any) {
-            let appInfo = this._store.get_value(iter, Columns.APPINFO);
+        newRules.forEach(({ id, value }, index) => {
+            const row = oldRules.find(r => r.id === id);
+            const appInfo = row
+                ? null : Gio.DesktopAppInfo.new(id);
 
-            this._changedPermitted = false;
-            this._removeItem(appInfo.get_id());
-            this._changedPermitted = true;
-            this._store.remove(iter);
-        }
-    }
+            if (row)
+                row.set({ value });
+            else if (appInfo)
+                this._list.insert(new RuleRow(appInfo, value), index);
+        });
+
+        const removed = oldRules.filter(
+            ({ id }) => !newRules.find(r => r.id === id));
+        removed.forEach(r => r.destroy());
 
-    _workspaceEdited(renderer, pathString, text) {
-        let index = parseInt(text);
-        if (isNaN(index) || index < 0)
-            index = 1;
-        let path = Gtk.TreePath.new_from_string(pathString);
-        let [model_, iter] = this._store.get_iter(path);
-        let appInfo = this._store.get_value(iter, Columns.APPINFO);
-
-        this._changedPermitted = false;
-        this._changeItem(appInfo.get_id(), index);
-        this._store.set_value(iter, Columns.WORKSPACE, index);
-        this._changedPermitted = true;
+        this._settings.unblock_signal_handler(this._changedId);
+        this._updateAction.enabled = true;
     }
 
-    _refresh() {
-        if (!this._changedPermitted)
-            // Ignore this notification, model is being modified outside
+    _updateHeader(row, before) {
+        if (!before || row.get_header())
             return;
+        row.set_header(new Gtk.Separator());
+    }
+});
 
-        this._store.clear();
+const RuleRow = GObject.registerClass({
+    Properties: {
+        'id': GObject.ParamSpec.string(
+            'id', 'id', 'id',
+            GObject.ParamFlags.READABLE,
+            ''),
+        'value': GObject.ParamSpec.uint(
+            'value', 'value', 'value',
+            GObject.ParamFlags.READWRITE,
+            1, WORKSPACE_MAX, 1),
+    },
+}, class RuleRow extends Gtk.ListBoxRow {
+    _init(appInfo, value) {
+        super._init({
+            activatable: false,
+            value,
+        });
+        this._appInfo = appInfo;
+
+        const box = new Gtk.Box({
+            spacing: 6,
+            margin_top: 6,
+            margin_bottom: 6,
+            margin_start: 6,
+            margin_end: 6,
+        });
 
-        let currentItems = this._settings.get_strv(SETTINGS_KEY);
-        let validItems = [];
-        for (let i = 0; i < currentItems.length; i++) {
-            let [id, index] = currentItems[i].split(':');
-            let appInfo = Gio.DesktopAppInfo.new(id);
-            if (!appInfo)
-                continue;
-            validItems.push(currentItems[i]);
+        const icon = new Gtk.Image({
+            gicon: appInfo.get_icon(),
+            pixel_size: 32,
+        });
+        icon.get_style_context().add_class('icon-dropshadow');
+        box.add(icon);
 
-            this._appendRow(appInfo, parseInt(index));
-        }
+        const label = new Gtk.Label({
+            label: appInfo.get_display_name(),
+            halign: Gtk.Align.START,
+            hexpand: true,
+            max_width_chars: 20,
+            ellipsize: Pango.EllipsizeMode.END,
+        });
+        box.add(label);
+
+        const spinButton = new Gtk.SpinButton({
+            adjustment: new Gtk.Adjustment({
+                lower: 1,
+                upper: WORKSPACE_MAX,
+                step_increment: 1,
+            }),
+            snap_to_ticks: true,
+            margin_end: 6,
+        });
+        this.bind_property('value',
+            spinButton, 'value',
+            GObject.BindingFlags.SYNC_CREATE | GObject.BindingFlags.BIDIRECTIONAL);
+        box.add(spinButton);
+
+        const button = new Gtk.Button({
+            action_name: 'rules.remove',
+            action_target: new GLib.Variant('s', this.id),
+            image: new Gtk.Image({
+                icon_name: 'edit-delete-symbolic',
+                pixel_size: 16,
+            }),
+        });
+        box.add(button);
 
-        if (validItems.length !== currentItems.length) // some items were filtered out
-            this._settings.set_strv(SETTINGS_KEY, validItems);
-    }
+        this.add(box);
 
-    _appendRow(appInfo, workspace) {
-        let iter = this._store.append();
-        let icon = appInfo.get_icon();
-        let displayName = appInfo.get_display_name();
-        let adj = new Gtk.Adjustment({
-            lower: 1,
-            upper: WORKSPACE_MAX,
-            step_increment: 1,
-            value: workspace,
+        this.connect('notify::value', () => {
+            const actionGroup = this.get_action_group('rules');
+            actionGroup.activate_action('update', null);
         });
-        let { APPINFO, ICON, DISPLAY_NAME, WORKSPACE, ADJUSTMENT } = Columns;
-        this._store.set(iter,
-            [APPINFO, ICON, DISPLAY_NAME, WORKSPACE, ADJUSTMENT],
-            [appInfo, icon, displayName, workspace, adj]);
+
+        this.show_all();
     }
 
-    _checkId(id) {
-        let items = this._settings.get_strv(SETTINGS_KEY);
-        return !items.some(i => i.startsWith(`${id}:`));
+    get id() {
+        return this._appInfo.get_id();
     }
+});
 
-    _appendItem(id, workspace) {
-        let currentItems = this._settings.get_strv(SETTINGS_KEY);
-        currentItems.push(`${id}:${workspace}`);
-        this._settings.set_strv(SETTINGS_KEY, currentItems);
+const NewRuleRow = GObject.registerClass(
+class NewRuleRow extends Gtk.ListBoxRow {
+    _init() {
+        super._init({
+            action_name: 'rules.add',
+        });
+        this.get_accessible().set_name(_('Add Rule'));
+
+        this.add(new Gtk.Image({
+            icon_name: 'list-add-symbolic',
+            pixel_size: 16,
+            margin_top: 12,
+            margin_bottom: 12,
+            margin_start: 12,
+            margin_end: 12,
+        }));
+
+        this.show_all();
     }
+});
 
-    _removeItem(id) {
-        let currentItems = this._settings.get_strv(SETTINGS_KEY);
-        let index = currentItems.map(el => el.split(':')[0]).indexOf(id);
+const NewRuleDialog = GObject.registerClass(
+class NewRuleDialog extends Gtk.AppChooserDialog {
+    _init(parent) {
+        super._init({
+            transient_for: parent,
+            modal: true,
+        });
 
-        if (index < 0)
-            return;
-        currentItems.splice(index, 1);
-        this._settings.set_strv(SETTINGS_KEY, currentItems);
-    }
+        this._settings = ExtensionUtils.getSettings();
+
+        this.get_widget().set({
+            show_all: true,
+            show_other: true, // hide more button
+        });
 
-    _changeItem(id, workspace) {
-        let currentItems = this._settings.get_strv(SETTINGS_KEY);
-        let index = currentItems.map(el => el.split(':')[0]).indexOf(id);
+        this.get_widget().connect('application-selected',
+            this._updateSensitivity.bind(this));
+        this._updateSensitivity();
 
-        if (index < 0)
-            currentItems.push(`${id}:${workspace}`);
-        else
-            currentItems[index] = `${id}:${workspace}`;
-        this._settings.set_strv(SETTINGS_KEY, currentItems);
+        this.show();
     }
-});
 
+    _updateSensitivity() {
+        const rules = this._settings.get_strv(SETTINGS_KEY);
+        const appInfo = this.get_widget().get_app_info();
+        this.set_response_sensitive(Gtk.ResponseType.OK,
+            appInfo && !rules.some(i => i.startsWith(appInfo.get_id())));
+    }
+});
 
 function init() {
     ExtensionUtils.initTranslations();
 }
 
 function buildPrefsWidget() {
-    let widget = new Widget({ margin: 12 });
-    widget.show_all();
-
-    return widget;
+    return new AutoMoveSettingsWidget();
 }


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]