summaryrefslogtreecommitdiffstats
path: root/js/ui/padOsd.js
diff options
context:
space:
mode:
Diffstat (limited to 'js/ui/padOsd.js')
-rw-r--r--js/ui/padOsd.js991
1 files changed, 991 insertions, 0 deletions
diff --git a/js/ui/padOsd.js b/js/ui/padOsd.js
new file mode 100644
index 0000000..e1e24f7
--- /dev/null
+++ b/js/ui/padOsd.js
@@ -0,0 +1,991 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+/* exported PadOsd, PadOsdService */
+
+const {
+ Atk, Clutter, GDesktopEnums, Gio,
+ GLib, GObject, Gtk, Meta, Pango, Rsvg, St,
+} = imports.gi;
+const Signals = imports.misc.signals;
+
+const Main = imports.ui.main;
+const PopupMenu = imports.ui.popupMenu;
+const Layout = imports.ui.layout;
+
+const { loadInterfaceXML } = imports.misc.fileUtils;
+
+const ACTIVE_COLOR = "#729fcf";
+
+const LTR = 0;
+const RTL = 1;
+
+const CW = 0;
+const CCW = 1;
+
+const UP = 0;
+const DOWN = 1;
+
+var PadChooser = GObject.registerClass({
+ Signals: { 'pad-selected': { param_types: [Clutter.InputDevice.$gtype] } },
+}, class PadChooser extends St.Button {
+ _init(device, groupDevices) {
+ super._init({
+ style_class: 'pad-chooser-button',
+ toggle_mode: true,
+ });
+ this.currentDevice = device;
+ this._padChooserMenu = null;
+
+ let arrow = new St.Icon({
+ style_class: 'popup-menu-arrow',
+ icon_name: 'pan-down-symbolic',
+ accessible_role: Atk.Role.ARROW,
+ x_align: Clutter.ActorAlign.CENTER,
+ y_align: Clutter.ActorAlign.CENTER,
+ });
+ this.set_child(arrow);
+ this._ensureMenu(groupDevices);
+
+ this.connect('destroy', this._onDestroy.bind(this));
+ }
+
+ vfunc_clicked() {
+ if (this.get_checked()) {
+ if (this._padChooserMenu != null)
+ this._padChooserMenu.open(true);
+ else
+ this.set_checked(false);
+ } else {
+ this._padChooserMenu.close(true);
+ }
+ }
+
+ _ensureMenu(devices) {
+ this._padChooserMenu = new PopupMenu.PopupMenu(this, 0.5, St.Side.TOP);
+ this._padChooserMenu.connect('menu-closed', () => {
+ this.set_checked(false);
+ });
+ this._padChooserMenu.actor.hide();
+ Main.uiGroup.add_actor(this._padChooserMenu.actor);
+
+ this._menuManager = new PopupMenu.PopupMenuManager(this);
+ this._menuManager.addMenu(this._padChooserMenu);
+
+ for (let i = 0; i < devices.length; i++) {
+ let device = devices[i];
+ if (device == this.currentDevice)
+ continue;
+
+ this._padChooserMenu.addAction(device.get_device_name(), () => {
+ this.emit('pad-selected', device);
+ });
+ }
+ }
+
+ _onDestroy() {
+ this._padChooserMenu.destroy();
+ }
+
+ update(devices) {
+ if (this._padChooserMenu)
+ this._padChooserMenu.actor.destroy();
+ this.set_checked(false);
+ this._ensureMenu(devices);
+ }
+});
+
+var KeybindingEntry = GObject.registerClass({
+ Signals: { 'keybinding-edited': { param_types: [GObject.TYPE_STRING] } },
+}, class KeybindingEntry extends St.Entry {
+ _init() {
+ super._init({ hint_text: _("New shortcut…"), style: 'width: 10em' });
+ }
+
+ vfunc_captured_event(event) {
+ if (event.type() != Clutter.EventType.KEY_PRESS)
+ return Clutter.EVENT_PROPAGATE;
+
+ let str = Gtk.accelerator_name_with_keycode(null,
+ event.get_key_symbol(),
+ event.get_key_code(),
+ event.get_state());
+ this.set_text(str);
+ this.emit('keybinding-edited', str);
+ return Clutter.EVENT_STOP;
+ }
+});
+
+var ActionComboBox = GObject.registerClass({
+ Signals: { 'action-selected': { param_types: [GObject.TYPE_INT] } },
+}, class ActionComboBox extends St.Button {
+ _init() {
+ super._init({ style_class: 'button' });
+ this.set_toggle_mode(true);
+
+ const boxLayout = new Clutter.BoxLayout({
+ orientation: Clutter.Orientation.HORIZONTAL,
+ spacing: 6,
+ });
+ let box = new St.Widget({ layout_manager: boxLayout });
+ this.set_child(box);
+
+ this._label = new St.Label({ style_class: 'combo-box-label' });
+ box.add_child(this._label);
+
+ const arrow = new St.Icon({
+ style_class: 'popup-menu-arrow',
+ icon_name: 'pan-down-symbolic',
+ accessible_role: Atk.Role.ARROW,
+ y_expand: true,
+ y_align: Clutter.ActorAlign.CENTER,
+ });
+ box.add_child(arrow);
+
+ this._editMenu = new PopupMenu.PopupMenu(this, 0, St.Side.TOP);
+ this._editMenu.connect('menu-closed', () => {
+ this.set_checked(false);
+ });
+ this._editMenu.actor.hide();
+ Main.uiGroup.add_actor(this._editMenu.actor);
+
+ this._editMenuManager = new PopupMenu.PopupMenuManager(this);
+ this._editMenuManager.addMenu(this._editMenu);
+
+ this._actionLabels = new Map();
+ this._actionLabels.set(GDesktopEnums.PadButtonAction.NONE, _("Application defined"));
+ this._actionLabels.set(GDesktopEnums.PadButtonAction.HELP, _("Show on-screen help"));
+ this._actionLabels.set(GDesktopEnums.PadButtonAction.SWITCH_MONITOR, _("Switch monitor"));
+ this._actionLabels.set(GDesktopEnums.PadButtonAction.KEYBINDING, _("Assign keystroke"));
+
+ this._buttonItems = [];
+
+ for (let [action, label] of this._actionLabels.entries()) {
+ let selectedAction = action;
+ let item = this._editMenu.addAction(label, () => {
+ this._onActionSelected(selectedAction);
+ });
+
+ /* These actions only apply to pad buttons */
+ if (selectedAction == GDesktopEnums.PadButtonAction.HELP ||
+ selectedAction == GDesktopEnums.PadButtonAction.SWITCH_MONITOR)
+ this._buttonItems.push(item);
+ }
+
+ this.setAction(GDesktopEnums.PadButtonAction.NONE);
+ }
+
+ _onActionSelected(action) {
+ this.setAction(action);
+ this.popdown();
+ this.emit('action-selected', action);
+ }
+
+ setAction(action) {
+ this._label.set_text(this._actionLabels.get(action));
+ }
+
+ popup() {
+ this._editMenu.open(true);
+ }
+
+ popdown() {
+ this._editMenu.close(true);
+ }
+
+ vfunc_clicked() {
+ if (this.get_checked())
+ this.popup();
+ else
+ this.popdown();
+ }
+
+ setButtonActionsActive(active) {
+ this._buttonItems.forEach(item => item.setSensitive(active));
+ }
+});
+
+var ActionEditor = GObject.registerClass({
+ Signals: { 'done': {} },
+}, class ActionEditor extends St.Widget {
+ _init() {
+ const boxLayout = new Clutter.BoxLayout({
+ orientation: Clutter.Orientation.HORIZONTAL,
+ spacing: 12,
+ });
+
+ super._init({ layout_manager: boxLayout });
+
+ this._actionComboBox = new ActionComboBox();
+ this._actionComboBox.connect('action-selected', this._onActionSelected.bind(this));
+ this.add_actor(this._actionComboBox);
+
+ this._keybindingEdit = new KeybindingEntry();
+ this._keybindingEdit.connect('keybinding-edited', this._onKeybindingEdited.bind(this));
+ this.add_actor(this._keybindingEdit);
+
+ this._doneButton = new St.Button({
+ label: _('Done'),
+ style_class: 'button',
+ x_expand: false,
+ });
+ this._doneButton.connect('clicked', this._onEditingDone.bind(this));
+ this.add_actor(this._doneButton);
+ }
+
+ _updateKeybindingEntryState() {
+ if (this._currentAction == GDesktopEnums.PadButtonAction.KEYBINDING) {
+ this._keybindingEdit.set_text(this._currentKeybinding);
+ this._keybindingEdit.show();
+ this._keybindingEdit.grab_key_focus();
+ } else {
+ this._keybindingEdit.hide();
+ }
+ }
+
+ setSettings(settings, action) {
+ this._buttonSettings = settings;
+
+ this._currentAction = this._buttonSettings.get_enum('action');
+ this._currentKeybinding = this._buttonSettings.get_string('keybinding');
+ this._actionComboBox.setAction(this._currentAction);
+ this._updateKeybindingEntryState();
+
+ let isButton = action == Meta.PadActionType.BUTTON;
+ this._actionComboBox.setButtonActionsActive(isButton);
+ }
+
+ close() {
+ this._actionComboBox.popdown();
+ this.hide();
+ }
+
+ _onKeybindingEdited(entry, keybinding) {
+ this._currentKeybinding = keybinding;
+ }
+
+ _onActionSelected(menu, action) {
+ this._currentAction = action;
+ this._updateKeybindingEntryState();
+ }
+
+ _storeSettings() {
+ if (!this._buttonSettings)
+ return;
+
+ let keybinding = null;
+
+ if (this._currentAction == GDesktopEnums.PadButtonAction.KEYBINDING)
+ keybinding = this._currentKeybinding;
+
+ this._buttonSettings.set_enum('action', this._currentAction);
+
+ if (keybinding)
+ this._buttonSettings.set_string('keybinding', keybinding);
+ else
+ this._buttonSettings.reset('keybinding');
+ }
+
+ _onEditingDone() {
+ this._storeSettings();
+ this.close();
+ this.emit('done');
+ }
+});
+
+var PadDiagram = GObject.registerClass({
+ Properties: {
+ 'left-handed': GObject.ParamSpec.boolean('left-handed',
+ 'left-handed', 'Left handed',
+ GObject.ParamFlags.READWRITE |
+ GObject.ParamFlags.CONSTRUCT_ONLY,
+ false),
+ 'image': GObject.ParamSpec.string('image', 'image', 'Image',
+ GObject.ParamFlags.READWRITE |
+ GObject.ParamFlags.CONSTRUCT_ONLY,
+ null),
+ 'editor-actor': GObject.ParamSpec.object('editor-actor',
+ 'editor-actor',
+ 'Editor actor',
+ GObject.ParamFlags.READWRITE |
+ GObject.ParamFlags.CONSTRUCT_ONLY,
+ Clutter.Actor.$gtype),
+ },
+}, class PadDiagram extends St.DrawingArea {
+ _init(params) {
+ let file = Gio.File.new_for_uri('resource:///org/gnome/shell/theme/pad-osd.css');
+ let [success_, css] = file.load_contents(null);
+ this._curEdited = null;
+ this._prevEdited = null;
+ this._css = new TextDecoder().decode(css);
+ this._labels = [];
+ this._activeButtons = [];
+ super._init(params);
+ }
+
+ get image() {
+ return this._imagePath;
+ }
+
+ set image(imagePath) {
+ let originalHandle = Rsvg.Handle.new_from_file(imagePath);
+ let dimensions = originalHandle.get_dimensions();
+ this._imageWidth = dimensions.width;
+ this._imageHeight = dimensions.height;
+
+ this._imagePath = imagePath;
+ this._handle = this._composeStyledDiagram();
+ this._initLabels();
+ }
+
+ get editorActor() {
+ return this._editorActor;
+ }
+
+ set editorActor(actor) {
+ actor.hide();
+ this._editorActor = actor;
+ this.add_actor(actor);
+ }
+
+ _initLabels() {
+ let i = 0;
+ for (i = 0; ; i++) {
+ if (!this._addLabel(Meta.PadActionType.BUTTON, i))
+ break;
+ }
+
+ for (i = 0; ; i++) {
+ if (!this._addLabel(Meta.PadActionType.RING, i, CW) ||
+ !this._addLabel(Meta.PadActionType.RING, i, CCW))
+ break;
+ }
+
+ for (i = 0; ; i++) {
+ if (!this._addLabel(Meta.PadActionType.STRIP, i, UP) ||
+ !this._addLabel(Meta.PadActionType.STRIP, i, DOWN))
+ break;
+ }
+ }
+
+ _wrappingSvgHeader() {
+ return '<?xml version="1.0" encoding="UTF-8" standalone="no"?>' +
+ '<svg version="1.1" xmlns="http://www.w3.org/2000/svg" ' +
+ 'xmlns:xi="http://www.w3.org/2001/XInclude" ' +
+ `width="${this._imageWidth}" height="${this._imageHeight}"> ` +
+ '<style type="text/css">';
+ }
+
+ _wrappingSvgFooter() {
+ return '%s%s%s'.format(
+ '</style>',
+ '<xi:include href="%s" />'.format(this._imagePath),
+ '</svg>');
+ }
+
+ _cssString() {
+ let css = this._css;
+
+ for (let i = 0; i < this._activeButtons.length; i++) {
+ let ch = String.fromCharCode('A'.charCodeAt() + this._activeButtons[i]);
+ css += `.${ch}.Leader { stroke: ${ACTIVE_COLOR} !important; }`;
+ css += `.${ch}.Button { stroke: ${ACTIVE_COLOR} !important; fill: ${ACTIVE_COLOR} !important; }`;
+ }
+
+ return css;
+ }
+
+ _composeStyledDiagram() {
+ let svgData = '';
+
+ if (!GLib.file_test(this._imagePath, GLib.FileTest.EXISTS))
+ return null;
+
+ svgData += this._wrappingSvgHeader();
+ svgData += this._cssString();
+ svgData += this._wrappingSvgFooter();
+
+ let istream = new Gio.MemoryInputStream();
+ istream.add_bytes(new GLib.Bytes(svgData));
+
+ return Rsvg.Handle.new_from_stream_sync(istream,
+ Gio.File.new_for_path(this._imagePath), 0, null);
+ }
+
+ _updateDiagramScale() {
+ [this._actorWidth, this._actorHeight] = this.get_size();
+ let dimensions = this._handle.get_dimensions();
+ let scaleX = this._actorWidth / dimensions.width;
+ let scaleY = this._actorHeight / dimensions.height;
+ this._scale = Math.min(scaleX, scaleY);
+ }
+
+ _allocateChild(child, x, y, direction) {
+ let [, natHeight] = child.get_preferred_height(-1);
+ let [, natWidth] = child.get_preferred_width(natHeight);
+ let childBox = new Clutter.ActorBox();
+
+ // I miss Cairo.Matrix
+ let dimensions = this._handle.get_dimensions();
+ x = x * this._scale + this._actorWidth / 2 - dimensions.width / 2 * this._scale;
+ y = y * this._scale + this._actorHeight / 2 - dimensions.height / 2 * this._scale;
+
+ if (direction == LTR) {
+ childBox.x1 = x;
+ childBox.x2 = x + natWidth;
+ } else {
+ childBox.x1 = x - natWidth;
+ childBox.x2 = x;
+ }
+
+ childBox.y1 = y - natHeight / 2;
+ childBox.y2 = y + natHeight / 2;
+ child.allocate(childBox);
+ }
+
+ vfunc_allocate(box) {
+ super.vfunc_allocate(box);
+ if (this._handle === null)
+ return;
+
+ this._updateDiagramScale();
+
+ for (let i = 0; i < this._labels.length; i++) {
+ const { label, x, y, arrangement } = this._labels[i];
+ this._allocateChild(label, x, y, arrangement);
+ }
+
+ if (this._editorActor && this._curEdited) {
+ const { x, y, arrangement } = this._curEdited;
+ this._allocateChild(this._editorActor, x, y, arrangement);
+ }
+ }
+
+ vfunc_repaint() {
+ if (this._handle == null)
+ return;
+
+ if (this._scale == null)
+ this._updateDiagramScale();
+
+ let [width, height] = this.get_surface_size();
+ let dimensions = this._handle.get_dimensions();
+ let cr = this.get_context();
+
+ cr.save();
+ cr.translate(width / 2, height / 2);
+ cr.scale(this._scale, this._scale);
+ if (this.leftHanded)
+ cr.rotate(Math.PI);
+ cr.translate(-dimensions.width / 2, -dimensions.height / 2);
+ this._handle.render_cairo(cr);
+ cr.restore();
+ cr.$dispose();
+ }
+
+ _getItemLabelCoords(labelName, leaderName) {
+ if (this._handle == null)
+ return [false];
+
+ const [labelFound, labelPos] = this._handle.get_position_sub(`#${labelName}`);
+ const [, labelSize] = this._handle.get_dimensions_sub(`#${labelName}`);
+ if (!labelFound)
+ return [false];
+
+ const [leaderFound, leaderPos] = this._handle.get_position_sub(`#${leaderName}`);
+ const [, leaderSize] = this._handle.get_dimensions_sub(`#${leaderName}`);
+ if (!leaderFound)
+ return [false];
+
+ let direction;
+ if (labelPos.x > leaderPos.x + leaderSize.width)
+ direction = LTR;
+ else
+ direction = RTL;
+
+ let pos = {x: labelPos.x, y: labelPos.y + labelSize.height};
+ if (this.leftHanded) {
+ direction = 1 - direction;
+ pos.x = this._imageWidth - pos.x;
+ pos.y = this._imageHeight - pos.y;
+ }
+
+ return [true, pos.x, pos.y, direction];
+ }
+
+ _getButtonLabels(button) {
+ let ch = String.fromCharCode('A'.charCodeAt() + button);
+ const labelName = `Label${ch}`;
+ const leaderName = `Leader${ch}`;
+ return [labelName, leaderName];
+ }
+
+ _getRingLabels(number, dir) {
+ let numStr = number > 0 ? (number + 1).toString() : '';
+ let dirStr = dir == CW ? 'CW' : 'CCW';
+ const labelName = `LabelRing${numStr}${dirStr}`;
+ const leaderName = `LeaderRing${numStr}${dirStr}`;
+ return [labelName, leaderName];
+ }
+
+ _getStripLabels(number, dir) {
+ let numStr = number > 0 ? (number + 1).toString() : '';
+ let dirStr = dir == UP ? 'Up' : 'Down';
+ const labelName = `LabelStrip${numStr}${dirStr}`;
+ const leaderName = `LeaderStrip${numStr}${dirStr}`;
+ return [labelName, leaderName];
+ }
+
+ _getLabelCoords(action, idx, dir) {
+ if (action == Meta.PadActionType.BUTTON)
+ return this._getItemLabelCoords(...this._getButtonLabels(idx));
+ else if (action == Meta.PadActionType.RING)
+ return this._getItemLabelCoords(...this._getRingLabels(idx, dir));
+ else if (action == Meta.PadActionType.STRIP)
+ return this._getItemLabelCoords(...this._getStripLabels(idx, dir));
+
+ return [false];
+ }
+
+ _invalidateSvg() {
+ if (this._handle == null)
+ return;
+ this._handle = this._composeStyledDiagram();
+ this.queue_repaint();
+ }
+
+ activateButton(button) {
+ this._activeButtons.push(button);
+ this._invalidateSvg();
+ }
+
+ deactivateButton(button) {
+ for (let i = 0; i < this._activeButtons.length; i++) {
+ if (this._activeButtons[i] == button)
+ this._activeButtons.splice(i, 1);
+ }
+ this._invalidateSvg();
+ }
+
+ _addLabel(action, idx, dir) {
+ let [found, x, y, arrangement] = this._getLabelCoords(action, idx, dir);
+ if (!found)
+ return false;
+
+ let label = new St.Label();
+ this._labels.push({ label, action, idx, dir, x, y, arrangement });
+ this.add_actor(label);
+ return true;
+ }
+
+ updateLabels(getText) {
+ for (let i = 0; i < this._labels.length; i++) {
+ const { label, action, idx, dir } = this._labels[i];
+ let str = getText(action, idx, dir);
+ label.set_text(str);
+ }
+
+ this.queue_relayout();
+ }
+
+ _applyLabel(label, action, idx, dir, str) {
+ if (str !== null)
+ label.set_text(str);
+ label.show();
+ }
+
+ stopEdition(continues, str) {
+ this._editorActor.hide();
+
+ if (this._prevEdited) {
+ const { label, action, idx, dir } = this._prevEdited;
+ this._applyLabel(label, action, idx, dir, str);
+ this._prevEdited = null;
+ }
+
+ if (this._curEdited) {
+ const { label, action, idx, dir } = this._curEdited;
+ this._applyLabel(label, action, idx, dir, str);
+ if (continues)
+ this._prevEdited = this._curEdited;
+ this._curEdited = null;
+ }
+
+ this.queue_relayout();
+ }
+
+ startEdition(action, idx, dir) {
+ let editedLabel;
+
+ if (this._curEdited)
+ return;
+
+ for (let i = 0; i < this._labels.length; i++) {
+ if (action == this._labels[i].action &&
+ idx == this._labels[i].idx && dir == this._labels[i].dir) {
+ this._curEdited = this._labels[i];
+ editedLabel = this._curEdited.label;
+ break;
+ }
+ }
+
+ if (this._curEdited == null)
+ return;
+ this._editorActor.show();
+ editedLabel.hide();
+ this.queue_relayout();
+ }
+});
+
+var PadOsd = GObject.registerClass({
+ Signals: {
+ 'pad-selected': { param_types: [Clutter.InputDevice.$gtype] },
+ 'closed': {},
+ },
+}, class PadOsd extends St.BoxLayout {
+ _init(padDevice, settings, imagePath, editionMode, monitorIndex) {
+ super._init({
+ style_class: 'pad-osd-window',
+ vertical: true,
+ x_expand: true,
+ y_expand: true,
+ reactive: true,
+ });
+
+ this.padDevice = padDevice;
+ this._groupPads = [padDevice];
+ this._settings = settings;
+ this._imagePath = imagePath;
+ this._editionMode = editionMode;
+ this._padChooser = null;
+
+ let seat = Clutter.get_default_backend().get_default_seat();
+ seat.connectObject(
+ 'device-added', (_seat, device) => {
+ if (device.get_device_type() === Clutter.InputDeviceType.PAD_DEVICE &&
+ this.padDevice.is_grouped(device)) {
+ this._groupPads.push(device);
+ this._updatePadChooser();
+ }
+ },
+ 'device-removed', (_seat, device) => {
+ // If the device is being removed, destroy the padOsd.
+ if (device === this.padDevice) {
+ this.destroy();
+ } else if (this._groupPads.includes(device)) {
+ // Or update the pad chooser if the device belongs to
+ // the same group.
+ this._groupPads.splice(this._groupPads.indexOf(device), 1);
+ this._updatePadChooser();
+ }
+ }, this);
+
+ seat.list_devices().forEach(device => {
+ if (device != this.padDevice &&
+ device.get_device_type() == Clutter.InputDeviceType.PAD_DEVICE &&
+ this.padDevice.is_grouped(device))
+ this._groupPads.push(device);
+ });
+
+ this.connect('destroy', this._onDestroy.bind(this));
+ Main.uiGroup.add_actor(this);
+
+ this._monitorIndex = monitorIndex;
+ let constraint = new Layout.MonitorConstraint({ index: monitorIndex });
+ this.add_constraint(constraint);
+
+ this._titleBox = new St.BoxLayout({
+ style_class: 'pad-osd-title-box',
+ vertical: false,
+ x_expand: false,
+ x_align: Clutter.ActorAlign.CENTER,
+ });
+ this.add_actor(this._titleBox);
+
+ const labelBox = new St.BoxLayout({
+ style_class: 'pad-osd-title-menu-box',
+ vertical: true,
+ });
+ this._titleBox.add_actor(labelBox);
+
+ this._titleLabel = new St.Label({
+ style: 'font-side: larger; font-weight: bold;',
+ x_align: Clutter.ActorAlign.CENTER,
+ });
+ this._titleLabel.clutter_text.set_ellipsize(Pango.EllipsizeMode.NONE);
+ this._titleLabel.clutter_text.set_text(padDevice.get_device_name());
+ labelBox.add_actor(this._titleLabel);
+
+ this._tipLabel = new St.Label({ x_align: Clutter.ActorAlign.CENTER });
+ labelBox.add_actor(this._tipLabel);
+
+ this._updatePadChooser();
+
+ this._actionEditor = new ActionEditor();
+ this._actionEditor.connect('done', this._endActionEdition.bind(this));
+
+ this._padDiagram = new PadDiagram({
+ image: this._imagePath,
+ left_handed: settings.get_boolean('left-handed'),
+ editor_actor: this._actionEditor,
+ x_expand: true,
+ y_expand: true,
+ });
+ this.add_actor(this._padDiagram);
+ this._updateActionLabels();
+
+ const buttonBox = new St.Widget({
+ layout_manager: new Clutter.BinLayout(),
+ x_expand: true,
+ x_align: Clutter.ActorAlign.CENTER,
+ y_align: Clutter.ActorAlign.CENTER,
+ });
+ this.add_actor(buttonBox);
+ this._editButton = new St.Button({
+ label: _('Edit…'),
+ style_class: 'button',
+ can_focus: true,
+ x_align: Clutter.ActorAlign.CENTER,
+ });
+ this._editButton.connect('clicked', () => {
+ this.setEditionMode(true);
+ });
+ buttonBox.add_actor(this._editButton);
+
+ this._syncEditionMode();
+ this._grab = Main.pushModal(this);
+ }
+
+ _updatePadChooser() {
+ if (this._groupPads.length > 1) {
+ if (this._padChooser == null) {
+ this._padChooser = new PadChooser(this.padDevice, this._groupPads);
+ this._padChooser.connect('pad-selected', (chooser, pad) => {
+ this._requestForOtherPad(pad);
+ });
+ this._titleBox.add_child(this._padChooser);
+ } else {
+ this._padChooser.update(this._groupPads);
+ }
+ } else if (this._padChooser != null) {
+ this._padChooser.destroy();
+ this._padChooser = null;
+ }
+ }
+
+ _requestForOtherPad(pad) {
+ if (pad == this.padDevice || !this._groupPads.includes(pad))
+ return;
+
+ let editionMode = this._editionMode;
+ this.destroy();
+ global.display.request_pad_osd(pad, editionMode);
+ }
+
+ _getActionText(type, number) {
+ let str = global.display.get_pad_action_label(this.padDevice, type, number);
+ return str ?? _('None');
+ }
+
+ _updateActionLabels() {
+ this._padDiagram.updateLabels(this._getActionText.bind(this));
+ }
+
+ vfunc_captured_event(event) {
+ let isModeSwitch =
+ (event.type() == Clutter.EventType.PAD_BUTTON_PRESS ||
+ event.type() == Clutter.EventType.PAD_BUTTON_RELEASE) &&
+ this.padDevice.get_mode_switch_button_group(event.get_button()) >= 0;
+
+ if (event.type() == Clutter.EventType.PAD_BUTTON_PRESS &&
+ event.get_source_device() == this.padDevice) {
+ this._padDiagram.activateButton(event.get_button());
+
+ /* Buttons that switch between modes cannot be edited */
+ if (this._editionMode && !isModeSwitch)
+ this._startButtonActionEdition(event.get_button());
+ return Clutter.EVENT_STOP;
+ } else if (event.type() == Clutter.EventType.PAD_BUTTON_RELEASE &&
+ event.get_source_device() == this.padDevice) {
+ this._padDiagram.deactivateButton(event.get_button());
+
+ if (isModeSwitch) {
+ this._endActionEdition();
+ this._updateActionLabels();
+ }
+ return Clutter.EVENT_STOP;
+ } else if (event.type() == Clutter.EventType.KEY_PRESS &&
+ (!this._editionMode || event.get_key_symbol() === Clutter.KEY_Escape)) {
+ if (this._editedAction != null)
+ this._endActionEdition();
+ else
+ this.destroy();
+ return Clutter.EVENT_STOP;
+ } else if (event.get_source_device() == this.padDevice &&
+ event.type() == Clutter.EventType.PAD_STRIP) {
+ if (this._editionMode) {
+ let [retval_, number, mode] = event.get_pad_event_details();
+ this._startStripActionEdition(number, UP, mode);
+ }
+ } else if (event.get_source_device() == this.padDevice &&
+ event.type() == Clutter.EventType.PAD_RING) {
+ if (this._editionMode) {
+ let [retval_, number, mode] = event.get_pad_event_details();
+ this._startRingActionEdition(number, CCW, mode);
+ }
+ }
+
+ // If the event comes from another pad in the same group,
+ // show the OSD for it.
+ if (this._groupPads.includes(event.get_source_device())) {
+ this._requestForOtherPad(event.get_source_device());
+ return Clutter.EVENT_STOP;
+ }
+
+ return Clutter.EVENT_PROPAGATE;
+ }
+
+ _syncEditionMode() {
+ this._editButton.set_reactive(!this._editionMode);
+ this._editButton.save_easing_state();
+ this._editButton.set_easing_duration(200);
+ this._editButton.set_opacity(this._editionMode ? 128 : 255);
+ this._editButton.restore_easing_state();
+
+ let title;
+
+ if (this._editionMode) {
+ title = _("Press a button to configure");
+ this._tipLabel.set_text(_("Press Esc to exit"));
+ } else {
+ title = this.padDevice.get_device_name();
+ this._tipLabel.set_text(_("Press any key to exit"));
+ }
+
+ this._titleLabel.set_text(title);
+ }
+
+ _isEditedAction(type, number, dir) {
+ if (!this._editedAction)
+ return false;
+
+ return this._editedAction.type == type &&
+ this._editedAction.number == number &&
+ this._editedAction.dir == dir;
+ }
+
+ _followUpActionEdition(str) {
+ let { type, dir, number, mode } = this._editedAction;
+ let hasNextAction = type == Meta.PadActionType.RING && dir == CCW ||
+ type == Meta.PadActionType.STRIP && dir == UP;
+ if (!hasNextAction)
+ return false;
+
+ this._padDiagram.stopEdition(true, str);
+ this._editedAction = null;
+ if (type == Meta.PadActionType.RING)
+ this._startRingActionEdition(number, CW, mode);
+ else
+ this._startStripActionEdition(number, DOWN, mode);
+
+ return true;
+ }
+
+ _endActionEdition() {
+ this._actionEditor.close();
+
+ if (this._editedAction != null) {
+ let str = global.display.get_pad_action_label(this.padDevice,
+ this._editedAction.type,
+ this._editedAction.number);
+ if (this._followUpActionEdition(str))
+ return;
+
+ this._padDiagram.stopEdition(false, str ?? _('None'));
+ this._editedAction = null;
+ }
+
+ this._editedActionSettings = null;
+ }
+
+ _startActionEdition(key, type, number, dir, mode) {
+ if (this._isEditedAction(type, number, dir))
+ return;
+
+ this._endActionEdition();
+ this._editedAction = { type, number, dir, mode };
+
+ const settingsPath = `${this._settings.path}${key}/`;
+ this._editedActionSettings = Gio.Settings.new_with_path('org.gnome.desktop.peripherals.tablet.pad-button',
+ settingsPath);
+ this._actionEditor.setSettings(this._editedActionSettings, type);
+ this._padDiagram.startEdition(type, number, dir);
+ }
+
+ _startButtonActionEdition(button) {
+ let ch = String.fromCharCode('A'.charCodeAt() + button);
+ let key = `button${ch}`;
+ this._startActionEdition(key, Meta.PadActionType.BUTTON, button);
+ }
+
+ _startRingActionEdition(ring, dir, mode) {
+ let ch = String.fromCharCode('A'.charCodeAt() + ring);
+ const key = `ring${ch}-${dir === CCW ? 'ccw' : 'cw'}-mode-${mode}`;
+ this._startActionEdition(key, Meta.PadActionType.RING, ring, dir, mode);
+ }
+
+ _startStripActionEdition(strip, dir, mode) {
+ let ch = String.fromCharCode('A'.charCodeAt() + strip);
+ const key = `strip${ch}-${dir === UP ? 'up' : 'down'}-mode-${mode}`;
+ this._startActionEdition(key, Meta.PadActionType.STRIP, strip, dir, mode);
+ }
+
+ setEditionMode(editionMode) {
+ if (this._editionMode == editionMode)
+ return;
+
+ this._editionMode = editionMode;
+ this._syncEditionMode();
+ }
+
+ _onDestroy() {
+ Main.popModal(this._grab);
+ this._grab = null;
+ this._actionEditor.close();
+
+ this.emit('closed');
+ }
+});
+
+const PadOsdIface = loadInterfaceXML('org.gnome.Shell.Wacom.PadOsd');
+
+var PadOsdService = class extends Signals.EventEmitter {
+ constructor() {
+ super();
+
+ this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(PadOsdIface, this);
+ this._dbusImpl.export(Gio.DBus.session, '/org/gnome/Shell/Wacom');
+ Gio.DBus.session.own_name('org.gnome.Shell.Wacom.PadOsd', Gio.BusNameOwnerFlags.REPLACE, null, null);
+ }
+
+ ShowAsync(params, invocation) {
+ let [deviceNode, editionMode] = params;
+ let seat = Clutter.get_default_backend().get_default_seat();
+ let devices = seat.list_devices();
+ let padDevice = null;
+
+ devices.forEach(device => {
+ if (deviceNode == device.get_device_node() &&
+ device.get_device_type() == Clutter.InputDeviceType.PAD_DEVICE)
+ padDevice = device;
+ });
+
+ if (padDevice == null) {
+ invocation.return_error_literal(Gio.IOErrorEnum,
+ Gio.IOErrorEnum.CANCELLED,
+ "Invalid params");
+ return;
+ }
+
+ global.display.request_pad_osd(padDevice, editionMode);
+ invocation.return_value(null);
+ }
+};