[gnome-shell-extensions] workspace-indicator: Overhaul prefs widget



commit 90d3c5c51d2a621741c133bacd8b07a9264f123c
Author: Florian Müllner <fmuellner gnome org>
Date:   Fri Apr 17 04:56:50 2020 +0200

    workspace-indicator: Overhaul prefs widget
    
    Inline toolbars are an outdated UI pattern, and both the toolbar widget
    and the inline styling have been removed from GTK4. Making sure the
    extension doesn't get in the way of a future GTK4 port is a good excuse
    for modernizing the UI, so do just that :-)
    
    Replace treeview and toolbar with an editable list as outline in the HIG:
    https://developer.gnome.org/hig/stable/lists.html.en
    
    https://gitlab.gnome.org/GNOME/gnome-shell-extensions/-/merge_requests/120

 extensions/workspace-indicator/prefs.js | 323 +++++++++++++++++---------------
 1 file changed, 176 insertions(+), 147 deletions(-)
---
diff --git a/extensions/workspace-indicator/prefs.js b/extensions/workspace-indicator/prefs.js
index 80b9b5b..011953b 100644
--- a/extensions/workspace-indicator/prefs.js
+++ b/extensions/workspace-indicator/prefs.js
@@ -1,7 +1,7 @@
 // -*- mode: js2; indent-tabs-mode: nil; js2-basic-offset: 4 -*-
 /* exported init buildPrefsWidget */
 
-const { Gio, GObject, Gtk } = imports.gi;
+const { Gdk, Gio, GLib, GObject, Gtk, Pango } = imports.gi;
 
 const Gettext = imports.gettext.domain('gnome-shell-extensions');
 const _ = Gettext.gettext;
@@ -12,189 +12,221 @@ const ExtensionUtils = imports.misc.extensionUtils;
 const WORKSPACE_SCHEMA = 'org.gnome.desktop.wm.preferences';
 const WORKSPACE_KEY = 'workspace-names';
 
-const WorkspaceNameModel = GObject.registerClass(
-class WorkspaceNameModel extends Gtk.ListStore {
-    _init(params) {
-        super._init(params);
-        this.set_column_types([GObject.TYPE_STRING]);
-
-        this.Columns = {
-            LABEL: 0,
-        };
-
-        this._settings = new Gio.Settings({ schema_id: WORKSPACE_SCHEMA });
-        this._reloadFromSettings();
-
-        // overriding class closure doesn't work, because GtkTreeModel
-        // plays tricks with marshallers and class closures
-        this.connect('row-changed', this._onRowChanged.bind(this));
-        this.connect('row-inserted', this._onRowInserted.bind(this));
-        this.connect('row-deleted', this._onRowDeleted.bind(this));
-    }
+const WorkspaceSettingsWidget = GObject.registerClass(
+class WorkspaceSettingsWidget extends Gtk.ScrolledWindow {
+    _init() {
+        super._init({
+            hscrollbar_policy: Gtk.PolicyType.NEVER,
+        });
 
-    _reloadFromSettings() {
-        if (this._preventChanges)
-            return;
-        this._preventChanges = true;
+        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);
 
-        let newNames = this._settings.get_strv(WORKSPACE_KEY);
+        box.add(new Gtk.Label({
+            label: '<b>%s</b>'.format(_('Workspace Names')),
+            use_markup: true,
+            halign: Gtk.Align.START,
+        }));
 
-        let i = 0;
-        let [ok, iter] = this.get_iter_first();
-        while (ok && i < newNames.length) {
-            this.set(iter, [this.Columns.LABEL], [newNames[i]]);
+        this._list = new Gtk.ListBox({
+            selection_mode: Gtk.SelectionMode.NONE,
+            valign: Gtk.Align.START,
+        });
+        this._list.set_header_func(this._updateHeader.bind(this));
+        this._list.connect('row-activated', (l, row) => row.edit());
+        box.add(this._list);
+
+        const context = this._list.get_style_context();
+        const cssProvider = new Gtk.CssProvider();
+        cssProvider.load_from_data(
+            'list { min-width: 25em; }');
+
+        context.add_provider(cssProvider,
+            Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
+        context.add_class('frame');
+
+        this._list.add(new NewWorkspaceRow());
+
+        this._actionGroup = new Gio.SimpleActionGroup();
+        this._list.insert_action_group('workspaces', this._actionGroup);
+
+        let action;
+        action = new Gio.SimpleAction({ name: 'add' });
+        action.connect('activate', () => {
+            const names = this._settings.get_strv(WORKSPACE_KEY);
+            this._settings.set_strv(WORKSPACE_KEY, [
+                ...names,
+                _('Workspace %d').format(names.length + 1),
+            ]);
+        });
+        this._actionGroup.add_action(action);
 
-            ok = this.iter_next(iter);
-            i++;
-        }
+        action = new Gio.SimpleAction({
+            name: 'remove',
+            parameter_type: new GLib.VariantType('s'),
+        });
+        action.connect('activate', (a, param) => {
+            const removed = param.deepUnpack();
+            this._settings.set_strv(WORKSPACE_KEY,
+                this._settings.get_strv(WORKSPACE_KEY)
+                    .filter(name => name !== removed));
+        });
+        this._actionGroup.add_action(action);
 
-        while (ok)
-            ok = this.remove(iter);
+        action = new Gio.SimpleAction({ name: 'update' });
+        action.connect('activate', () => {
+            const names = this._getWorkspaceRows().map(row => row.name);
+            this._settings.set_strv(WORKSPACE_KEY, names);
+        });
+        this._actionGroup.add_action(action);
 
-        for (; i < newNames.length; i++) {
-            iter = this.append();
-            this.set(iter, [this.Columns.LABEL], [newNames[i]]);
-        }
+        this._settings = new Gio.Settings({
+            schema_id: WORKSPACE_SCHEMA,
+        });
+        this._settings.connect(`changed::${WORKSPACE_KEY}`,
+            this._sync.bind(this));
+        this._sync();
 
-        this._preventChanges = false;
+        this.show_all();
     }
 
-    _onRowChanged(self, path, iter) {
-        if (this._preventChanges)
-            return;
-        this._preventChanges = true;
-
-        let index = path.get_indices()[0];
-        let names = this._settings.get_strv(WORKSPACE_KEY);
-
-        if (index >= names.length) {
-            // fill with blanks
-            for (let i = names.length; i <= index; i++)
-                names[i] = '';
-        }
-
-        names[index] = this.get_value(iter, this.Columns.LABEL);
-
-        this._settings.set_strv(WORKSPACE_KEY, names);
-
-        this._preventChanges = false;
+    _getWorkspaceRows() {
+        return this._list.get_children().filter(row => row.name);
     }
 
-    _onRowInserted(self, path, iter) {
-        if (this._preventChanges)
-            return;
-        this._preventChanges = true;
+    _sync() {
+        const rows = this._getWorkspaceRows();
 
-        let index = path.get_indices()[0];
-        let names = this._settings.get_strv(WORKSPACE_KEY);
-        let label = this.get_value(iter, this.Columns.LABEL) || '';
-        names.splice(index, 0, label);
+        const oldNames = rows.map(row => row.name);
+        const newNames = this._settings.get_strv(WORKSPACE_KEY);
 
-        this._settings.set_strv(WORKSPACE_KEY, names);
+        const removed = oldNames.filter(n => !newNames.includes(n));
+        const added = newNames.filter(n => !oldNames.includes(n));
 
-        this._preventChanges = false;
+        removed.forEach(n => rows.find(r => r.name === n).destroy());
+        added.forEach(n => {
+            this._list.insert(new WorkspaceRow(n), newNames.indexOf(n));
+        });
     }
 
-    _onRowDeleted(self, path) {
-        if (this._preventChanges)
+    _updateHeader(row, before) {
+        if (!before || row.get_header())
             return;
-        this._preventChanges = true;
-
-        let index = path.get_indices()[0];
-        let names = this._settings.get_strv(WORKSPACE_KEY);
-
-        if (index >= names.length)
-            return;
-
-        names.splice(index, 1);
-
-        // compact the array
-        for (let i = names.length - 1; i >= 0 && !names[i]; i++)
-            names.pop();
-
-        this._settings.set_strv(WORKSPACE_KEY, names);
-
-        this._preventChanges = false;
+        row.set_header(new Gtk.Separator());
     }
 });
 
-const WorkspaceSettingsWidget = GObject.registerClass(
-class WorkspaceSettingsWidget extends Gtk.Grid {
-    _init(params) {
-        super._init(params);
-        this.margin = 12;
-        this.orientation = Gtk.Orientation.VERTICAL;
+const WorkspaceRow = GObject.registerClass(
+class WorkspaceRow extends Gtk.ListBoxRow {
+    _init(name) {
+        super._init({ name });
 
-        this.add(new Gtk.Label({
-            label: '<b>%s</b>'.format(_('Workspace Names')),
-            use_markup: true,
+        const box = new Gtk.Box({
+            spacing: 12,
+            margin_top: 6,
             margin_bottom: 6,
-            hexpand: true,
-            halign: Gtk.Align.START,
-        }));
-
-        let scrolled = new Gtk.ScrolledWindow({ shadow_type: Gtk.ShadowType.IN });
-        scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC);
-        this.add(scrolled);
+            margin_start: 6,
+            margin_end: 6,
+        });
 
-        this._store = new WorkspaceNameModel();
-        this._treeView = new Gtk.TreeView({
-            model: this._store,
-            headers_visible: false,
-            reorderable: true,
+        const label = new Gtk.Label({
             hexpand: true,
-            vexpand: true,
+            xalign: 0,
+            max_width_chars: 25,
+            ellipsize: Pango.EllipsizeMode.END,
         });
+        this.bind_property('name', label, 'label',
+            GObject.BindingFlags.SYNC_CREATE);
+        box.add(label);
 
-        let column = new Gtk.TreeViewColumn({ title: _('Name') });
-        let renderer = new Gtk.CellRendererText({ editable: true });
-        renderer.connect('edited', this._cellEdited.bind(this));
-        column.pack_start(renderer, true);
-        column.add_attribute(renderer, 'text', this._store.Columns.LABEL);
-        this._treeView.append_column(column);
+        const image = new Gtk.Image({
+            icon_name: 'edit-delete-symbolic',
+            pixel_size: 16,
+        });
+        const button = new Gtk.Button({
+            action_name: 'workspaces.remove',
+            action_target: new GLib.Variant('s', name),
+            image,
+        });
+        box.add(button);
 
-        scrolled.add(this._treeView);
+        this._entry = new Gtk.Entry({
+            max_width_chars: 25,
+        });
 
-        let toolbar = new Gtk.Toolbar({ icon_size: Gtk.IconSize.SMALL_TOOLBAR });
-        toolbar.get_style_context().add_class(Gtk.STYLE_CLASS_INLINE_TOOLBAR);
+        this._stack = new Gtk.Stack();
+        this._stack.add_named(box, 'display');
+        this._stack.add_named(this._entry, 'edit');
+        this.add(this._stack);
 
-        let newButton = new Gtk.ToolButton({ icon_name: 'list-add-symbolic' });
-        newButton.connect('clicked', this._newClicked.bind(this));
-        toolbar.add(newButton);
+        this._entry.connect('activate', () => {
+            this.name = this._entry.text;
+            this._stopEdit();
+        });
+        this._entry.connect('notify::has-focus', () => {
+            if (this._entry.has_focus)
+                return;
+            this._stopEdit();
+        });
+        this._entry.connect('key-press-event',
+            this._onEntryKeyPress.bind(this));
 
-        let delButton = new Gtk.ToolButton({ icon_name: 'list-remove-symbolic' });
-        delButton.connect('clicked', this._delClicked.bind(this));
-        toolbar.add(delButton);
+        this.connect('notify::name', () => {
+            button.action_target = new GLib.Variant('s', this.name);
 
-        let selection = this._treeView.get_selection();
-        selection.connect('changed', () => {
-            delButton.sensitive = selection.count_selected_rows() > 0;
+            const actionGroup = this.get_action_group('workspaces');
+            actionGroup.activate_action('update', null);
         });
-        delButton.sensitive = selection.count_selected_rows() > 0;
 
-        this.add(toolbar);
+        this.show_all();
     }
 
-    _cellEdited(renderer, path, newText) {
-        let [ok, iter] = this._store.get_iter_from_string(path);
-
-        if (ok)
-            this._store.set(iter, [this._store.Columns.LABEL], [newText]);
+    edit() {
+        this._entry.text = this.name;
+        this._entry.grab_focus();
+        this._stack.visible_child_name = 'edit';
     }
 
-    _newClicked() {
-        let iter = this._store.append();
-        let index = this._store.get_path(iter).get_indices()[0];
+    _stopEdit() {
+        this.grab_focus();
+        this._stack.visible_child_name = 'display';
+    }
 
-        let label = _('Workspace %d').format(index + 1);
-        this._store.set(iter, [this._store.Columns.LABEL], [label]);
+    _onEntryKeyPress(entry, event) {
+        const [, keyval] = event.get_keyval();
+        if (keyval !== Gdk.KEY_Escape)
+            return Gdk.EVENT_PROPAGATE;
+        this._stopEdit();
+        return Gdk.EVENT_STOP;
     }
+});
 
-    _delClicked() {
-        let [any, model_, iter] = this._treeView.get_selection().get_selected();
+const NewWorkspaceRow = GObject.registerClass(
+class NewWorkspaceRow extends Gtk.ListBoxRow {
+    _init() {
+        super._init({
+            action_name: 'workspaces.add',
+        });
+        this.get_accessible().set_name(_('Add Workspace'));
+
+        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,
+        }));
 
-        if (any)
-            this._store.remove(iter);
+        this.show_all();
     }
 });
 
@@ -203,8 +235,5 @@ function init() {
 }
 
 function buildPrefsWidget() {
-    let widget = new WorkspaceSettingsWidget();
-    widget.show_all();
-
-    return widget;
+    return new WorkspaceSettingsWidget();
 }


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