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