diff --git a/data/Makefile.am b/data/Makefile.am index 2eee47903..2c9bbce25 100644 --- a/data/Makefile.am +++ b/data/Makefile.am @@ -38,6 +38,7 @@ endif introspectiondir = $(datadir)/dbus-1/interfaces introspection_DATA = \ + org.gnome.Shell.PadOsd.xml \ org.gnome.Shell.Screencast.xml \ org.gnome.Shell.Screenshot.xml \ org.gnome.ShellSearchProvider.xml \ @@ -115,6 +116,7 @@ EXTRA_DIST = \ $(convert_DATA) \ $(keys_DATA) \ $(dist_theme_files) \ + pad-osd.css \ perf-background.xml.in \ org.gnome.Shell.PortalHelper.desktop.in.in \ org.gnome.Shell.PortalHelper.service.in \ diff --git a/data/gnome-shell-theme.gresource.xml b/data/gnome-shell-theme.gresource.xml index 5c8ca6e69..76aeaa579 100644 --- a/data/gnome-shell-theme.gresource.xml +++ b/data/gnome-shell-theme.gresource.xml @@ -22,6 +22,7 @@ <file>no-events.svg</file> <file>no-notifications.svg</file> <file>noise-texture.png</file> + <file>pad-osd.css</file> <file>page-indicator-active.svg</file> <file>page-indicator-inactive.svg</file> <file>page-indicator-checked.svg</file> diff --git a/data/org.gnome.Shell.PadOsd.xml b/data/org.gnome.Shell.PadOsd.xml new file mode 100644 index 000000000..765973c98 --- /dev/null +++ b/data/org.gnome.Shell.PadOsd.xml @@ -0,0 +1,28 @@ +<!DOCTYPE node PUBLIC +'-//freedesktop//DTD D-BUS Object Introspection 1.0//EN' +'http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd'> +<node> + + <!-- + org.gnome.Shell.PadOSD: + @short_description: Pad OSD interface + + The interface used to show button map OSD on pad devices. + --> + <interface name='org.gnome.Shell.Wacom.PadOsd'> + + <!-- + Show: + @device_node: device node file, usually in /dev/input/... + @edition_mode: whether toggling edition mode on when showing + + Shows the pad button map OSD for the requested device, the OSD + will be shown according the current device settings (output + mapping, left handed mode, ...) + --> + <method name='Show'> + <arg name='device_node' direction='in' type='o'/> + <arg name='edition_mode' direction='in' type='b'/> + </method> + </interface> +</node> diff --git a/data/theme/gnome-shell.css b/data/theme/gnome-shell.css index 690722372..d28a0a7cd 100644 --- a/data/theme/gnome-shell.css +++ b/data/theme/gnome-shell.css @@ -562,6 +562,16 @@ StScrollBar { background-color: #eeeeec; border-radius: 0.3em; } +/* Pad OSD */ +.pad-osd-window { + padding: 32px; + background-color: rgba(0, 0, 0, 0.8); +} + +.combo-box-label { + width: 15em; +} + /* App Switcher */ .switcher-popup { padding: 8px; diff --git a/data/theme/pad-osd.css b/data/theme/pad-osd.css new file mode 100644 index 000000000..31c237722 --- /dev/null +++ b/data/theme/pad-osd.css @@ -0,0 +1,30 @@ +.Leader { + stroke-width: .5 !important; + stroke: #535353; + fill: none !important; +} + +.Button { + stroke-width: .25; + stroke: #ededed; + fill: #ededed; +} + +.Ring { + stroke-width: .5 !important; + stroke: #535353 !important; + fill: none !important; +} + +.Label { + stroke: none !important; + stroke-width: .1 !important; + font-size: .1 !important; + fill: transparent !important; +} + +.TouchStrip, .TouchRing { + stroke-width: .1 !important; + stroke: #ededed !important; + fill: #535353 !important; +} diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml index 54f702b4a..58f433c3c 100644 --- a/js/js-resources.gresource.xml +++ b/js/js-resources.gresource.xml @@ -72,6 +72,7 @@ <file>ui/osdMonitorLabeler.js</file> <file>ui/overview.js</file> <file>ui/overviewControls.js</file> + <file>ui/padOsd.js</file> <file>ui/panel.js</file> <file>ui/panelMenu.js</file> <file>ui/pointerWatcher.js</file> diff --git a/js/ui/main.js b/js/ui/main.js index bd42960f3..b19c3b2d1 100644 --- a/js/ui/main.js +++ b/js/ui/main.js @@ -26,6 +26,7 @@ const ModalDialog = imports.ui.modalDialog; const OsdWindow = imports.ui.osdWindow; const OsdMonitorLabeler = imports.ui.osdMonitorLabeler; const Overview = imports.ui.overview; +const PadOsd = imports.ui.padOsd; const Panel = imports.ui.panel; const Params = imports.misc.params; const RunDialog = imports.ui.runDialog; @@ -61,6 +62,7 @@ let screenShield = null; let notificationDaemon = null; let windowAttentionHandler = null; let ctrlAltTabManager = null; +let padOsdService = null; let osdWindowManager = null; let osdMonitorLabeler = null; let sessionMode = null; @@ -155,6 +157,7 @@ function _initializeUI() { // working until it's updated. uiGroup = layoutManager.uiGroup; + padOsdService = new PadOsd.PadOsdService(); screencastService = new Screencast.ScreencastService(); xdndHandler = new XdndHandler.XdndHandler(); ctrlAltTabManager = new CtrlAltTab.CtrlAltTabManager(); diff --git a/js/ui/padOsd.js b/js/ui/padOsd.js new file mode 100644 index 000000000..e43b9a04f --- /dev/null +++ b/js/ui/padOsd.js @@ -0,0 +1,761 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- + +const Lang = imports.lang; +const Meta = imports.gi.Meta; +const Shell = imports.gi.Shell; +const Clutter = imports.gi.Clutter; +const St = imports.gi.St; +const Rsvg = imports.gi.Rsvg; +const GObject = imports.gi.GObject; +const GLib = imports.gi.GLib; +const Gtk = imports.gi.Gtk; +const Gio = imports.gi.Gio; +const GDesktopEnums = imports.gi.GDesktopEnums; +const Atk = imports.gi.Atk; +const Cairo = imports.cairo; +const Signals = imports.signals; + +const Main = imports.ui.main; +const PopupMenu = imports.ui.popupMenu; +const Layout = imports.ui.layout; + +const ACTIVE_COLOR = "#729fcf"; + +const LTR = 0; +const RTL = 1; + +const CW = 0; +const CCW = 1; + +const UP = 0; +const DOWN = 1; + +const KeybindingEntry = new Lang.Class({ + Name: 'KeybindingEntry', + + _init: function () { + this.actor = new St.Entry({ hint_text: _("New shortcut…"), + style: 'width: 10em' }); + this.actor.connect('captured-event', Lang.bind(this, this._onCapturedEvent)); + }, + + _onCapturedEvent: function (actor, 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.actor.set_text(str); + this.emit('keybinding-edited', str); + return Clutter.EVENT_STOP; + } +}); +Signals.addSignalMethods(KeybindingEntry.prototype); + +const ActionComboBox = new Lang.Class({ + Name: 'ActionComboBox', + + _init: function () { + this.actor = new St.Button({ style_class: 'button' }); + this.actor.connect('clicked', Lang.bind(this, this._onButtonClicked)); + this.actor.set_toggle_mode(true); + + let boxLayout = new Clutter.BoxLayout({ orientation: Clutter.Orientation.HORIZONTAL, + spacing: 6 }); + let box = new St.Widget({ layout_manager: boxLayout }); + this.actor.set_child(box); + + this._label = new St.Label({ style_class: 'combo-box-label' }); + box.add_child(this._label) + + let 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.actor, 0, St.Side.TOP); + this._editMenu.connect('menu-closed', Lang.bind(this, function() { this.actor.set_checked(false); })); + this._editMenu.actor.hide(); + Main.uiGroup.add_actor(this._editMenu.actor); + + 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")); + + for (let [action, label] of this._actionLabels.entries()) { + let selectedAction = action; + this._editMenu.addAction(label, Lang.bind(this, function() { this._onActionSelected(selectedAction) })); + } + + this.setAction(GDesktopEnums.PadButtonAction.NONE); + }, + + _onActionSelected: function (action) { + this.setAction(action); + this.popdown(); + this.emit('action-selected', action); + }, + + setAction: function (action) { + this._label.set_text(this._actionLabels.get(action)); + }, + + popup: function () { + this._editMenu.open(true); + }, + + popdown: function () { + this._editMenu.close(true); + }, + + _onButtonClicked: function () { + if (this.actor.get_checked()) + this.popup(); + else + this.popdown(); + } +}); +Signals.addSignalMethods(ActionComboBox.prototype); + +const ActionEditor = new Lang.Class({ + Name: 'ActionEditor', + + _init: function () { + let boxLayout = new Clutter.BoxLayout({ orientation: Clutter.Orientation.HORIZONTAL, + spacing: 12 }); + + this.actor = new St.Widget({ layout_manager: boxLayout }); + + this._actionComboBox = new ActionComboBox(); + this._actionComboBox.connect('action-selected', Lang.bind(this, this._onActionSelected)); + this.actor.add_actor(this._actionComboBox.actor); + + this._keybindingEdit = new KeybindingEntry(); + this._keybindingEdit.connect('keybinding-edited', Lang.bind(this, this._onKeybindingEdited)); + this.actor.add_actor(this._keybindingEdit.actor); + + this._doneButton = new St.Button({ label: _("Done"), + style_class: 'button', + x_expand: false}); + this._doneButton.connect('clicked', Lang.bind(this, this._onEditingDone)); + this.actor.add_actor(this._doneButton); + }, + + _updateKeybindingEntryState: function () { + if (this._currentAction == GDesktopEnums.PadButtonAction.KEYBINDING) { + this._keybindingEdit.actor.set_text(this._currentKeybinding); + this._keybindingEdit.actor.show(); + this._keybindingEdit.actor.grab_key_focus(); + } else { + this._keybindingEdit.actor.hide(); + } + }, + + setSettings: function (settings) { + this._buttonSettings = settings; + + this._currentAction = this._buttonSettings.get_enum('action'); + this._currentKeybinding = this._buttonSettings.get_string('keybinding'); + this._actionComboBox.setAction(this._currentAction); + this._updateKeybindingEntryState(); + }, + + close: function() { + this._actionComboBox.popdown(); + this.actor.hide(); + }, + + _onKeybindingEdited: function (entry, keybinding) { + this._currentKeybinding = keybinding; + }, + + _onActionSelected: function (menu, action) { + this._currentAction = action; + this._updateKeybindingEntryState(); + }, + + _storeSettings: function () { + 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: function () { + this._storeSettings(); + this.close(); + this.emit('done'); + } +}); +Signals.addSignalMethods(ActionEditor.prototype); + +const PadDiagram = new Lang.Class({ + Name: 'PadDiagram', + Extends: St.DrawingArea, + 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) }, + + _init: function (params) { + let file = Gio.File.new_for_uri('resource:///org/gnome/shell/theme/pad-osd.css'); + let [success, css, etag] = file.load_contents(null); + this._css = css; + this._labels = []; + this._activeButtons = []; + this.parent(params); + }, + + get left_handed() { + return this._leftHanded; + }, + + set left_handed(leftHanded) { + this._leftHanded = leftHanded; + }, + + 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(); + }, + + get editor_actor() { + return this._editorActor; + }, + + set editor_actor(actor) { + actor.hide(); + this._editorActor = actor; + this.add_actor(actor); + }, + + _wrappingSvgHeader: function () { + 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: function () { + return ('</style>' + + '<xi:include href="' + this._imagePath + '" />' + + '</svg>'); + }, + + _cssString: function () { + let css = this._css; + + for (let i = 0; i < this._activeButtons.length; i++) { + let ch = String.fromCharCode('A'.charCodeAt() + this._activeButtons[i]); + css += ('.' + ch + ' { ' + + ' stroke: ' + ACTIVE_COLOR + ' !important; ' + + ' fill: ' + ACTIVE_COLOR + ' !important; ' + + '} '); + } + + return css; + }, + + _composeStyledDiagram: function () { + let svgData = ''; + + if (!GLib.file_test(this._imagePath, GLib.FileTest.EXISTS)) + return null; + + svgData += this._wrappingSvgHeader(); + svgData += this._cssString(); + svgData += this._wrappingSvgFooter(); + + let handle = new Rsvg.Handle(); + handle.set_base_uri(GLib.path_get_dirname(this._imagePath)); + handle.write(svgData); + handle.close(); + + return handle; + }, + + _updateDiagramScale: function () { + if (this._handle == null) + return; + + [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: function (child, x, y, direction) { + let [prefHeight, natHeight] = child.get_preferred_height(-1); + let [prefWidth, natWidth] = child.get_preferred_width(natHeight); + let childBox = new Clutter.ActorBox(); + + 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, 0); + }, + + vfunc_allocate: function (box, flags) { + this.parent(box, flags); + this._updateDiagramScale(); + + for (let i = 0; i < this._labels.length; i++) { + let [label, action, idx, dir] = this._labels[i]; + let [found, x, y, arrangement] = this.getLabelCoords(action, idx, dir); + this._allocateChild(label, x, y, arrangement); + } + + if (this._editorActor && this._curEdited) { + let [label, action, idx, dir] = this._curEdited; + let [found, x, y, arrangement] = this.getLabelCoords(action, idx, dir); + this._allocateChild(this._editorActor, x, y, arrangement); + } + }, + + vfunc_repaint: function () { + 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(); + }, + + _transformPoint: function (x, y) { + if (this._handle == null || this._scale == null) + return [x, y]; + + // 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;; + return [Math.round(x), Math.round(y)]; + }, + + _getItemLabelCoords: function (labelName, leaderName) { + if (this._handle == null) + return [false]; + + let leaderPos, leaderSize, pos; + let found, direction; + + [found, pos] = this._handle.get_position_sub('#' + labelName); + if (!found) + return [false]; + + [found, leaderPos] = this._handle.get_position_sub('#' + leaderName); + [found, leaderSize] = this._handle.get_dimensions_sub('#' + leaderName); + if (!found) + return [false]; + + if (pos.x > leaderPos.x + leaderSize.width) + direction = LTR; + else + direction = RTL; + + if (this._leftHanded) { + direction = 1 - direction; + pos.x = this._imageWidth - pos.x; + pos.y = this._imageHeight - pos.y; + } + + let [x, y] = this._transformPoint(pos.x, pos.y) + + return [true, x, y, direction]; + }, + + getButtonLabelCoords: function (button) { + let ch = String.fromCharCode('A'.charCodeAt() + button); + let labelName = 'Label' + ch; + let leaderName = 'Leader' + ch; + + return this._getItemLabelCoords(labelName, leaderName); + }, + + getRingLabelCoords: function (number, dir) { + let numStr = number > 0 ? number.toString() : ''; + let dirStr = dir == CW ? 'CW' : 'CCW'; + let labelName = 'LabelRing' + numStr + dirStr; + let leaderName = 'LeaderRing' + numStr + dirStr; + + return this._getItemLabelCoords(labelName, leaderName); + }, + + getStripLabelCoords: function (number, dir) { + let numStr = number > 0 ? (number + 1).toString() : ''; + let dirStr = dir == UP ? 'Up' : 'Down'; + let labelName = 'LabelStrip' + numStr + dirStr; + let leaderName = 'LeaderStrip' + numStr + dirStr; + + return this._getItemLabelCoords(labelName, leaderName); + }, + + getLabelCoords: function (action, idx, dir) { + if (action == Meta.PadActionType.BUTTON) + return this.getButtonLabelCoords(idx); + else if (action == Meta.PadActionType.RING) + return this.getRingLabelCoords(idx, dir); + else if (action == Meta.PadActionType.STRIP) + return this.getStripLabelCoords(idx, dir); + + return [false]; + }, + + _invalidateSvg: function () { + if (this._handle == null) + return; + this._handle = this._composeStyledDiagram(); + this.queue_repaint(); + }, + + activateButton: function (button) { + this._activeButtons.push(button); + this._invalidateSvg(); + }, + + deactivateButton: function (button) { + for (let i = 0; i < this._activeButtons.length; i++) { + if (this._activeButtons[i] == button) + this._activeButtons.splice(i, 1); + } + this._invalidateSvg(); + }, + + addLabel: function (label, type, idx, dir) { + this._labels.push([label, type, idx, dir]); + this.add_actor(label); + }, + + stopEdition: function (str) { + this._editorActor.hide(); + + if (this._curEdited) { + let [label, action, idx, dir] = this._curEdited; + if (str != null) { + label.set_text(str); + + let [found, x, y, arrangement] = this.getLabelCoords(action, idx, dir); + this._allocateChild(label, x, y, arrangement); + } + label.show(); + this._curEdited = null; + } + }, + + startEdition: function(action, idx, dir) { + let editedLabel; + this.stopEdition(); + + for (let i = 0; i < this._labels.length; i++) { + let [label, itemAction, itemIdx, itemDir] = this._labels[i]; + if (action == itemAction && idx == itemIdx && dir == itemDir) { + this._curEdited = this._labels[i]; + editedLabel = label; + break; + } + } + + if (this._curEdited == null) + return; + let [found] = this.getLabelCoords(action, idx, dir); + if (!found) + return; + this._editorActor.show(); + editedLabel.hide(); + } +}); + +const PadOsd = new Lang.Class({ + Name: 'PadOsd', + + _init: function (padDevice, settings, imagePath, editionMode, monitorIndex) { + this.padDevice = padDevice; + this._settings = settings; + this._imagePath = imagePath; + this._editionMode = editionMode; + this._capturedEventId = global.stage.connect('captured-event', Lang.bind(this, this._onCapturedEvent)); + + let deviceManager = Clutter.DeviceManager.get_default(); + this._deviceRemovedId = deviceManager.connect('device-removed', Lang.bind(this, function (manager, device) { + // If the device is being removed, destroy the padOsd. + if (device == this.padDevice) + this.destroy(); + })); + + this.actor = new St.BoxLayout({ style_class: 'pad-osd-window', + x_expand: true, + y_expand: true, + vertical: true, + reactive: true }); + this.actor.connect('destroy', Lang.bind(this, this._onDestroy)); + Main.uiGroup.add_actor(this.actor); + + this._monitorIndex = monitorIndex; + let constraint = new Layout.MonitorConstraint({ index: monitorIndex }); + this.actor.add_constraint(constraint); + + this._titleLabel = new St.Label({ style: 'font-side: larger; font-weight: bold;', + x_align: Clutter.ActorAlign.CENTER }); + this._titleLabel.clutter_text.set_text(padDevice.get_device_name()); + this.actor.add_actor(this._titleLabel); + + this._tipLabel = new St.Label({ x_align: Clutter.ActorAlign.CENTER }); + this.actor.add_actor(this._tipLabel); + + this._actionEditor = new ActionEditor(); + this._actionEditor.connect('done', Lang.bind(this, this._endButtonActionEdition)); + + this._padDiagram = new PadDiagram({ image: this._imagePath, + left_handed: settings.get_boolean('left-handed'), + editor_actor: this._actionEditor.actor, + x_expand: true, + y_expand: true }); + this.actor.add_actor(this._padDiagram); + + // FIXME: Fix num buttons. + let i = 0; + for (i = 0; i < 50; i++) { + let [found] = this._padDiagram.getButtonLabelCoords(i); + if (!found) + break; + this._createLabel(Meta.PadActionType.BUTTON, i); + } + + for (i = 0; i < padDevice.get_n_rings(); i++) { + this._createLabel(Meta.PadActionType.RING, i, CW); + this._createLabel(Meta.PadActionType.RING, i, CCW); + } + + for (i = 0; i < padDevice.get_n_strips(); i++) { + this._createLabel(Meta.PadActionType.STRIP, i, UP); + this._createLabel(Meta.PadActionType.STRIP, i, DOWN); + } + + let buttonBox = new St.Widget({ layout_manager: new Clutter.BinLayout(), + x_expand: true, + x_align: Clutter.ActorAlign.CENTER, + y_align: Clutter.ActorAlign.CENTER }); + this.actor.add_actor(buttonBox); + this._editButton = new St.Button({ label: _("Edit…"), + style_class: 'button', + x_align: Clutter.ActorAlign.CENTER, + can_focus: true }); + this._editButton.connect('clicked', Lang.bind(this, function () { this.setEditionMode(true) })); + buttonBox.add_actor(this._editButton); + + this._syncEditionMode(); + Main.pushModal(this.actor); + }, + + _createLabel: function (type, number, dir) { + let str = global.display.get_pad_action_label(this.padDevice, type, number); + let label = new St.Label({ text: str ? str : _("None") }); + this._padDiagram.addLabel(label, type, number, dir); + }, + + _onCapturedEvent : function (actor, event) { + if (event.type() == Clutter.EventType.PAD_BUTTON_PRESS && + event.get_source_device() == this.padDevice) { + this._padDiagram.activateButton(event.get_button()); + + if (this._editionMode) + 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()); + return Clutter.EVENT_STOP; + } else if (event.type() == Clutter.EventType.KEY_PRESS && + (!this._editionMode || event.get_key_symbol() == Clutter.Escape)) { + if (this._editingButtonAction != null) + this._endButtonActionEdition(); + else + this.destroy(); + return Clutter.EVENT_STOP; + } + + return Clutter.EVENT_PROPAGATE; + }, + + _syncEditionMode: function () { + 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.clutter_text.set_markup('<span size="larger"><b>' + title + '</b></span>'); + }, + + _endButtonActionEdition: function () { + this._actionEditor.close(); + + if (this._editingButtonAction != null) { + let str = global.display.get_pad_action_label(this.padDevice, + Meta.PadActionType.BUTTON, + this._editingButtonAction); + this._padDiagram.stopEdition(str ? str : _("None")) + this._editingButtonAction = null; + } + + this._editedButtonSettings = null; + }, + + _startButtonActionEdition: function (button) { + if (this._editingButtonAction == button) + return; + + this._endButtonActionEdition(); + this._editingButtonAction = button; + + let ch = String.fromCharCode('A'.charCodeAt() + button); + let settingsPath = this._settings.path + "button" + ch + '/'; + this._editedButtonSettings = Gio.Settings.new_with_path('org.gnome.desktop.peripherals.tablet.pad-button', + settingsPath); + this._actionEditor.setSettings(this._editedButtonSettings); + this._padDiagram.startEdition(Meta.PadActionType.BUTTON, button); + }, + + setEditionMode: function (editionMode) { + if (this._editionMode == editionMode) + return; + + this._editionMode = editionMode; + this._syncEditionMode(); + }, + + destroy: function () { + this.actor.destroy(); + }, + + _onDestroy: function () { + Main.popModal(this.actor); + this._actionEditor.close(); + + if (this._deviceRemovedId != 0) { + let deviceManager = Clutter.DeviceManager.get_default(); + deviceManager.disconnect(this._deviceRemovedId); + this._deviceRemovedId = 0; + } + + if (this._capturedEventId != 0) { + global.stage.disconnect(this._capturedEventId); + this._capturedEventId = 0; + } + + this.actor = null; + this.emit('closed'); + } +}); +Signals.addSignalMethods(PadOsd.prototype); + +const PadOsdIface = '<node> \ +<interface name="org.gnome.Shell.Wacom.PadOsd"> \ +<method name="Show"> \ + <arg name="device_node" direction="in" type="o"/> \ + <arg name="edition_mode" direction="in" type="b"/> \ +</method> \ +</interface> \ +</node>'; + +const PadOsdService = new Lang.Class({ + Name: 'PadOsdService', + + _init: function() { + 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: function(params, invocation) { + let [deviceNode, editionMode] = params; + let deviceManager = Clutter.DeviceManager.get_default(); + let devices = deviceManager.list_devices(); + let padDevice = null; + + devices.forEach(Lang.bind(this, function(device) { + if (deviceNode == device.get_device_node()) + padDevice = device; + })); + + if (padDevice == null || + padDevice.get_device_type() != Clutter.InputDeviceType.PAD_DEVICE) { + invocation.return_error_literal(Gio.IOErrorEnum, + Gio.IOErrorEnum.CANCELLED, + "Invalid params"); + return; + } + + global.display.request_pad_osd(padDevice, editionMode); + invocation.return_value(null); + } +}); +Signals.addSignalMethods(PadOsdService.prototype); diff --git a/js/ui/windowManager.js b/js/ui/windowManager.js index a3872af4d..3fb59ec90 100644 --- a/js/ui/windowManager.js +++ b/js/ui/windowManager.js @@ -17,6 +17,7 @@ const Main = imports.ui.main; const ModalDialog = imports.ui.modalDialog; const Tweener = imports.ui.tweener; const WindowMenu = imports.ui.windowMenu; +const PadOsd = imports.ui.padOsd; const SHELL_KEYBINDINGS_SCHEMA = 'org.gnome.shell.keybindings'; const MINIMIZE_WINDOW_ANIMATION_TIME = 0.2; @@ -917,6 +918,7 @@ const WindowManager = new Lang.Class({ Lang.bind(this, this._toggleCalendar)); global.display.connect('show-resize-popup', Lang.bind(this, this._showResizePopup)); + global.display.connect('show-pad-osd', Lang.bind(this, this._showPadOsd)); Main.overview.connect('showing', Lang.bind(this, function() { for (let i = 0; i < this._dimmedWindows.length; i++) @@ -946,7 +948,13 @@ const WindowManager = new Lang.Class({ gesture = new AppSwitchAction(); gesture.connect('activated', Lang.bind(this, this._switchApp)); global.stage.add_action(gesture); + }, + _showPadOsd: function (display, device, settings, imagePath, editionMode, monitorIndex) { + this._currentPadOsd = new PadOsd.PadOsd(device, settings, imagePath, editionMode, monitorIndex); + this._currentPadOsd.connect('closed', Lang.bind(this, function() { this._currentPadOsd = null })); + + return this._currentPadOsd.actor; }, _actionSwitchWorkspace: function(action, direction) { diff --git a/po/POTFILES.in b/po/POTFILES.in index 3f24e7d68..122f79f1e 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -39,6 +39,7 @@ js/ui/mpris.js js/ui/notificationDaemon.js js/ui/overviewControls.js js/ui/overview.js +js/ui/padOsd.js js/ui/panel.js js/ui/popupMenu.js js/ui/runDialog.js