auto-move: Overhaul preference dialog

auto-move uses the same outdated UI pattern as workspace-indicator did
until commit 90d3c5c51d, 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
This commit is contained in:
Florian Müllner
2020-05-03 19:51:24 +02:00
parent ff94c5a143
commit c87cfc822a
+249 -237
View File
@@ -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: '<b>%s</b>'.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();
}