From c87cfc822a19fd75b444e733f0c9b51b3bc4c300 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=BCllner?= Date: Fri, 1 May 2020 16:33:41 +0200 Subject: [PATCH] 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 | 486 +++++++++++++------------- 1 file changed, 249 insertions(+), 237 deletions(-) diff --git a/extensions/auto-move-windows/prefs.js b/extensions/auto-move-windows/prefs.js index 84f98ae0..13244515 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 Widget = GObject.registerClass( -class Widget extends Gtk.Grid { - _init(params) { - super._init(params); - this.set_orientation(Gtk.Orientation.VERTICAL); + 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); + + box.add(new Gtk.Label({ + label: '%s'.format(_('Workspace Rules')), + use_markup: true, + halign: Gtk.Align.START, + })); + + 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); + + const context = this._list.get_style_context(); + const cssProvider = new Gtk.CssProvider(); + cssProvider.load_from_data( + 'list { min-width: 30em; }'); + + context.add_provider(cssProvider, + Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION); + context.add_class('frame'); + + this._list.add(new NewRuleRow()); + + this._actionGroup = new Gio.SimpleActionGroup(); + this._list.insert_action_group('rules', this._actionGroup); + + 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'), + }); + 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}`)); + }); + this._actionGroup.add_action(action); + this._updateAction = action; this._settings = ExtensionUtils.getSettings(); - this._settings.connect('changed', this._refresh.bind(this)); - this._changedPermitted = false; + this._changedId = this._settings.connect('changed', + this._sync.bind(this)); + this._sync(); - this._store = new Gtk.ListStore(); - this._store.set_column_types([ - Gio.AppInfo, - GObject.TYPE_STRING, - Gio.Icon, - GObject.TYPE_INT, - Gtk.Adjustment, - ]); + this.connect('destroy', () => this._settings.run_dispose()); - 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, - hexpand: true, - vexpand: true, - }); - this._treeView.get_selection().set_mode(Gtk.SelectionMode.SINGLE); - - 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 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, - }); - newButton.connect('clicked', this._createNew.bind(this)); - toolbar.add(newButton); - - let delButton = new Gtk.ToolButton({ icon_name: 'edit-delete-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(); + 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`, + ]); } - - 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; - - this._appendRow(appInfo, index); - 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); - } + _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; + })); } - _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; + _getRuleRows() { + return this._list.get_children().filter(row => !!row.id); } - _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, index] = currentItems[i].split(':'); - let appInfo = Gio.DesktopAppInfo.new(id); - if (!appInfo) - continue; - validItems.push(currentItems[i]); - - this._appendRow(appInfo, parseInt(index)); - } - - if (validItems.length !== currentItems.length) // some items were filtered out - this._settings.set_strv(SETTINGS_KEY, validItems); - } - - _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, + _sync() { + const oldRules = this._getRuleRows(); + const newRules = this._settings.get_strv(SETTINGS_KEY).map(entry => { + const [id, value] = entry.split(':'); + return { id, value }; }); - let { APPINFO, ICON, DISPLAY_NAME, WORKSPACE, ADJUSTMENT } = Columns; - this._store.set(iter, - [APPINFO, ICON, DISPLAY_NAME, WORKSPACE, ADJUSTMENT], - [appInfo, icon, displayName, workspace, adj]); + + this._settings.block_signal_handler(this._changedId); + this._updateAction.enabled = false; + + newRules.forEach(({ id, value }, index) => { + const row = oldRules.find(r => r.id === id); + const appInfo = row + ? null : Gio.DesktopAppInfo.new(id); + + 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()); + + this._settings.unblock_signal_handler(this._changedId); + this._updateAction.enabled = true; } - _checkId(id) { - let items = this._settings.get_strv(SETTINGS_KEY); - return !items.some(i => i.startsWith(`${id}:`)); - } - - _appendItem(id, workspace) { - let currentItems = this._settings.get_strv(SETTINGS_KEY); - currentItems.push(`${id}:${workspace}`); - this._settings.set_strv(SETTINGS_KEY, currentItems); - } - - _removeItem(id) { - let currentItems = this._settings.get_strv(SETTINGS_KEY); - let index = currentItems.map(el => el.split(':')[0]).indexOf(id); - - if (index < 0) + _updateHeader(row, before) { + if (!before || row.get_header()) return; - currentItems.splice(index, 1); - this._settings.set_strv(SETTINGS_KEY, currentItems); - } - - _changeItem(id, workspace) { - let currentItems = this._settings.get_strv(SETTINGS_KEY); - let index = currentItems.map(el => el.split(':')[0]).indexOf(id); - - if (index < 0) - currentItems.push(`${id}:${workspace}`); - else - currentItems[index] = `${id}:${workspace}`; - this._settings.set_strv(SETTINGS_KEY, currentItems); + row.set_header(new Gtk.Separator()); } }); +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, + }); + + const icon = new Gtk.Image({ + gicon: appInfo.get_icon(), + pixel_size: 32, + }); + icon.get_style_context().add_class('icon-dropshadow'); + box.add(icon); + + 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); + + this.add(box); + + this.connect('notify::value', () => { + const actionGroup = this.get_action_group('rules'); + actionGroup.activate_action('update', null); + }); + + this.show_all(); + } + + get id() { + return this._appInfo.get_id(); + } +}); + +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(); + } +}); + +const NewRuleDialog = GObject.registerClass( +class NewRuleDialog extends Gtk.AppChooserDialog { + _init(parent) { + super._init({ + transient_for: parent, + modal: true, + }); + + this._settings = ExtensionUtils.getSettings(); + + this.get_widget().set({ + show_all: true, + show_other: true, // hide more button + }); + + this.get_widget().connect('application-selected', + this._updateSensitivity.bind(this)); + this._updateSensitivity(); + + 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(); }