summaryrefslogtreecommitdiffstats
path: root/panels/wacom
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--panels/wacom/button-mapping.ui149
-rw-r--r--panels/wacom/calibrator/COPYING27
-rw-r--r--panels/wacom/calibrator/calibrator-gui.c461
-rw-r--r--panels/wacom/calibrator/calibrator-gui.h65
-rw-r--r--panels/wacom/calibrator/calibrator.c183
-rw-r--r--panels/wacom/calibrator/calibrator.css47
-rw-r--r--panels/wacom/calibrator/calibrator.h99
-rw-r--r--panels/wacom/calibrator/calibrator.ui203
-rw-r--r--panels/wacom/calibrator/cc-clock.c300
-rw-r--r--panels/wacom/calibrator/cc-clock.h41
-rw-r--r--panels/wacom/calibrator/main.c418
-rw-r--r--panels/wacom/calibrator/meson.build35
-rw-r--r--panels/wacom/calibrator/target.svg93
-rw-r--r--panels/wacom/cc-drawing-area.c214
-rw-r--r--panels/wacom/cc-drawing-area.h31
-rw-r--r--panels/wacom/cc-tablet-tool-map.c398
-rw-r--r--panels/wacom/cc-tablet-tool-map.h44
-rw-r--r--panels/wacom/cc-wacom-button-row.c280
-rw-r--r--panels/wacom/cc-wacom-button-row.h43
-rw-r--r--panels/wacom/cc-wacom-device.c406
-rw-r--r--panels/wacom/cc-wacom-device.h63
-rw-r--r--panels/wacom/cc-wacom-mapping-panel.c338
-rw-r--r--panels/wacom/cc-wacom-mapping-panel.h36
-rw-r--r--panels/wacom/cc-wacom-nav-button.c220
-rw-r--r--panels/wacom/cc-wacom-nav-button.h31
-rw-r--r--panels/wacom/cc-wacom-page.c1030
-rw-r--r--panels/wacom/cc-wacom-page.h43
-rw-r--r--panels/wacom/cc-wacom-panel.c767
-rw-r--r--panels/wacom/cc-wacom-panel.h40
-rw-r--r--panels/wacom/cc-wacom-stylus-page.c503
-rw-r--r--panels/wacom/cc-wacom-stylus-page.h38
-rw-r--r--panels/wacom/cc-wacom-tool.c309
-rw-r--r--panels/wacom/cc-wacom-tool.h44
-rw-r--r--panels/wacom/gnome-wacom-panel.desktop.in.in18
-rw-r--r--panels/wacom/gnome-wacom-properties.ui465
-rw-r--r--panels/wacom/gsd-enums.h7
-rw-r--r--panels/wacom/gsd-wacom-key-shortcut-button.c571
-rw-r--r--panels/wacom/gsd-wacom-key-shortcut-button.h40
-rw-r--r--panels/wacom/meson.build108
-rw-r--r--panels/wacom/test-wacom.c160
-rw-r--r--panels/wacom/wacom-stylus-3btn-no-eraser.svg132
-rw-r--r--panels/wacom/wacom-stylus-3btn.svg138
-rw-r--r--panels/wacom/wacom-stylus-airbrush.svg94
-rw-r--r--panels/wacom/wacom-stylus-art-pen.svg127
-rw-r--r--panels/wacom/wacom-stylus-classic.svg103
-rw-r--r--panels/wacom/wacom-stylus-inking.svg87
-rw-r--r--panels/wacom/wacom-stylus-no-eraser.svg118
-rw-r--r--panels/wacom/wacom-stylus-page.ui417
-rw-r--r--panels/wacom/wacom-stylus.svg124
-rw-r--r--panels/wacom/wacom-tablet-cintiq.svg83
-rw-r--r--panels/wacom/wacom-tablet-pc.svg77
-rw-r--r--panels/wacom/wacom-tablet.svg81
-rw-r--r--panels/wacom/wacom.gresource.xml22
53 files changed, 9971 insertions, 0 deletions
diff --git a/panels/wacom/button-mapping.ui b/panels/wacom/button-mapping.ui
new file mode 100644
index 0000000..23b2226
--- /dev/null
+++ b/panels/wacom/button-mapping.ui
@@ -0,0 +1,149 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <!-- interface-requires gtk+ 3.0 -->
+ <object class="GtkDialog" id="button-mapping-dialog">
+ <property name="width_request">600</property>
+ <property name="height_request">450</property>
+ <property name="can_focus">False</property>
+ <property name="border_width">5</property>
+ <property name="title" translatable="yes">Map Buttons</property>
+ <property name="resizable">False</property>
+ <property name="modal">True</property>
+ <property name="default_width">600</property>
+ <property name="default_height">450</property>
+ <property name="type_hint">dialog</property>
+ <child internal-child="vbox">
+ <object class="GtkBox" id="top_vbox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">2</property>
+ <child internal-child="action_area">
+ <object class="GtkButtonBox" id="dialog-action_area1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="layout_style">end</property>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <object class="GtkButton" id="close_button">
+ <property name="label" translatable="yes">_Close</property>
+ <property name="use_action_appearance">False</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="can_default">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_action_appearance">False</property>
+ <property name="use_underline">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">3</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="pack_type">end</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkVBox" id="shortcuts_vbox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">6</property>
+ <child>
+ <object class="GtkLabel" id="label1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="ypad">12</property>
+ <property name="label" translatable="yes">Map buttons to functions</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkTable" id="table11">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="column_spacing">5</property>
+ <child>
+ <object class="GtkScrolledWindow" id="actions_swindow">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="hscrollbar_policy">never</property>
+ <property name="shadow_type">in</property>
+ <child>
+ <object class="GtkListBox" id="shortcuts_list">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkHBox" id="hbox1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="border_width">5</property>
+ <property name="spacing">12</property>
+ <child>
+ <object class="GtkLabel" id="label12">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">To edit a shortcut, choose the “Send Keystroke” action, press the keyboard shortcut button and hold down the new keys or press Backspace to clear.</property>
+ <property name="justify">fill</property>
+ <property name="wrap">True</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <action-widgets>
+ <action-widget response="0">close_button</action-widget>
+ </action-widgets>
+ </object>
+</interface>
diff --git a/panels/wacom/calibrator/COPYING b/panels/wacom/calibrator/COPYING
new file mode 100644
index 0000000..e0a1dc9
--- /dev/null
+++ b/panels/wacom/calibrator/COPYING
@@ -0,0 +1,27 @@
+Copyright (c) 2010 Tias Guns <tias@ulyssis.org> and others
+See the respective files for detailed copyright information.
+
+
+Source code: MIT/X11 License
+------------
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+
diff --git a/panels/wacom/calibrator/calibrator-gui.c b/panels/wacom/calibrator/calibrator-gui.c
new file mode 100644
index 0000000..012291f
--- /dev/null
+++ b/panels/wacom/calibrator/calibrator-gui.c
@@ -0,0 +1,461 @@
+/*
+ * Copyright © 2013 Red Hat, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * Author: Carlos Garnacho <carlosg@gnome.org>
+ * (based on previous work by Joaquim Rocha, Tias Guns and Soren Hauberg)
+ */
+
+#include "config.h"
+
+#include <math.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <glib/gi18n.h>
+#include <gdk/gdkx.h>
+#include <gtk/gtk.h>
+
+#include "calibrator.h"
+#include "calibrator-gui.h"
+#include "cc-clock.h"
+
+struct CalibArea
+{
+ struct Calib calibrator;
+ XYinfo axis;
+ gboolean swap;
+ gboolean success;
+ GdkDevice *device;
+
+ double X[4], Y[4];
+ int display_width, display_height;
+
+ GtkWidget *window;
+ GtkBuilder *builder;
+ GtkWidget *error_revealer;
+ GtkWidget *clock;
+ GtkCssProvider *style_provider;
+
+ FinishCallback callback;
+ gpointer user_data;
+};
+
+/* Timeout parameters */
+#define MAX_TIME 15000 /* 15000 = 15 sec */
+#define END_TIME 750 /* 750 = 0.75 sec */
+
+static void
+set_display_size (CalibArea *calib_area,
+ int width,
+ int height)
+{
+ int delta_x;
+ int delta_y;
+
+ calib_area->display_width = width;
+ calib_area->display_height = height;
+
+ /* Compute absolute circle centers */
+ delta_x = calib_area->display_width/NUM_BLOCKS;
+ delta_y = calib_area->display_height/NUM_BLOCKS;
+
+ calib_area->X[UL] = delta_x;
+ calib_area->Y[UL] = delta_y;
+
+ calib_area->X[UR] = calib_area->display_width - delta_x - 1;
+ calib_area->Y[UR] = delta_y;
+
+ calib_area->X[LL] = delta_x;
+ calib_area->Y[LL] = calib_area->display_height - delta_y - 1;
+
+ calib_area->X[LR] = calib_area->display_width - delta_x - 1;
+ calib_area->Y[LR] = calib_area->display_height - delta_y - 1;
+
+ /* reset calibration if already started */
+ reset (&calib_area->calibrator);
+}
+
+static void
+calib_area_notify_finish (CalibArea *area)
+{
+ gtk_widget_hide (area->window);
+
+ (*area->callback) (area, area->user_data);
+}
+
+static gboolean
+on_delete_event (GtkWidget *widget,
+ GdkEvent *event,
+ CalibArea *area)
+{
+ calib_area_notify_finish (area);
+ return TRUE;
+}
+
+static gboolean
+calib_area_finish_idle_cb (CalibArea *area)
+{
+ calib_area_notify_finish (area);
+ return FALSE;
+}
+
+static void
+set_success (CalibArea *area)
+{
+ GtkWidget *stack;
+
+ stack = GTK_WIDGET (gtk_builder_get_object (area->builder, "stack"));
+ gtk_stack_set_visible_child_name (GTK_STACK (stack), "page1");
+}
+
+static void
+set_calibration_status (CalibArea *area)
+{
+ area->success = finish (&area->calibrator, &area->axis, &area->swap);
+
+ if (area->success)
+ {
+ set_success (area);
+ g_timeout_add (END_TIME,
+ (GSourceFunc) calib_area_finish_idle_cb,
+ area);
+ }
+ else
+ {
+ g_idle_add ((GSourceFunc) calib_area_finish_idle_cb, area);
+ }
+}
+
+static void
+show_error_message (CalibArea *area)
+{
+ gtk_revealer_set_reveal_child (GTK_REVEALER (area->error_revealer), TRUE);
+}
+
+static void
+hide_error_message (CalibArea *area)
+{
+ gtk_revealer_set_reveal_child (GTK_REVEALER (area->error_revealer), FALSE);
+}
+
+static void
+set_active_target (CalibArea *area,
+ int n_target)
+{
+ GtkWidget *targets[] = {
+ GTK_WIDGET (gtk_builder_get_object (area->builder, "target1")),
+ GTK_WIDGET (gtk_builder_get_object (area->builder, "target2")),
+ GTK_WIDGET (gtk_builder_get_object (area->builder, "target3")),
+ GTK_WIDGET (gtk_builder_get_object (area->builder, "target4")),
+ };
+ int i;
+
+ for (i = 0; i < G_N_ELEMENTS (targets); i++)
+ gtk_widget_set_sensitive (targets[i], i == n_target);
+}
+
+static void
+on_gesture_press (GtkGestureMultiPress *gesture,
+ guint n_press,
+ gdouble x,
+ gdouble y,
+ CalibArea *area)
+{
+ gint num_clicks;
+ gboolean success;
+ GdkDevice *source;
+ GdkEvent *event;
+
+ if (area->success)
+ return;
+
+ event = gtk_get_current_event ();
+ source = gdk_event_get_source_device ((GdkEvent *) event);
+ gdk_event_free (event);
+
+ /* Check matching device if a device was provided */
+ if (area->device && area->device != source)
+ {
+ g_debug ("Ignoring input from device %s",
+ gdk_device_get_name (source));
+ return;
+ }
+
+ /* Handle click */
+ /* FIXME: reset clock */
+ success = add_click(&area->calibrator,
+ (int) x,
+ (int) y);
+
+ num_clicks = area->calibrator.num_clicks;
+
+ if (!success && num_clicks == 0)
+ show_error_message (area);
+ else
+ hide_error_message (area);
+
+ /* Are we done yet? */
+ if (num_clicks >= 4)
+ {
+ set_calibration_status (area);
+ return;
+ }
+
+ set_active_target (area, num_clicks);
+}
+
+static gboolean
+on_key_release_event (GtkWidget *widget,
+ GdkEventKey *event,
+ CalibArea *area)
+{
+ if (area->success ||
+ event->keyval != GDK_KEY_Escape)
+ return GDK_EVENT_PROPAGATE;
+
+ calib_area_notify_finish (area);
+ return GDK_EVENT_STOP;
+}
+
+static gboolean
+on_focus_out_event (GtkWidget *widget,
+ GdkEvent *event,
+ CalibArea *area)
+{
+ if (area->success)
+ return FALSE;
+
+ /* If the calibrator window loses focus, simply bail out... */
+ calib_area_notify_finish (area);
+
+ return FALSE;
+}
+
+static void
+on_clock_finished (CcClock *clock,
+ CalibArea *area)
+{
+ set_calibration_status (area);
+}
+
+static void
+on_title_revealed (CalibArea *area)
+{
+ GtkWidget *revealer;
+
+ revealer = GTK_WIDGET (gtk_builder_get_object (area->builder, "subtitle_revealer"));
+ gtk_revealer_set_reveal_child (GTK_REVEALER (revealer), TRUE);
+}
+
+static gboolean
+on_fullscreen (GtkWindow *window,
+ GdkEventWindowState *event,
+ CalibArea *area)
+{
+ GtkWidget *revealer;
+
+ if ((event->changed_mask & GDK_WINDOW_STATE_FULLSCREEN) == 0)
+ return FALSE;
+
+ revealer = GTK_WIDGET (gtk_builder_get_object (area->builder, "title_revealer"));
+ g_signal_connect_swapped (revealer, "notify::child-revealed",
+ G_CALLBACK (on_title_revealed),
+ area);
+ gtk_revealer_set_reveal_child (GTK_REVEALER (revealer), TRUE);
+
+ set_active_target (area, 0);
+
+ return FALSE;
+}
+
+static void
+on_size_allocate (GtkWidget *widget,
+ GtkAllocation *allocation,
+ CalibArea *area)
+{
+ set_display_size (area, allocation->width, allocation->height);
+}
+
+/**
+ * Creates the windows and other objects required to do calibration
+ * under GTK. When the window is closed (timed out, calibration finished
+ * or user cancellation), callback will be called, where you should call
+ * calib_area_finish().
+ */
+CalibArea *
+calib_area_new (GdkScreen *screen,
+ int n_monitor,
+ GdkDevice *device,
+ FinishCallback callback,
+ gpointer user_data,
+ int threshold_doubleclick,
+ int threshold_misclick)
+{
+ CalibArea *calib_area;
+ GdkRectangle rect;
+ GdkVisual *visual;
+ GdkMonitor *monitor;
+#ifndef FAKE_AREA
+ GdkWindow *window;
+ g_autoptr(GdkCursor) cursor = NULL;
+#endif /* FAKE_AREA */
+ GtkGesture *press;
+
+ g_return_val_if_fail (callback, NULL);
+
+ g_type_ensure (CC_TYPE_CLOCK);
+
+ calib_area = g_new0 (CalibArea, 1);
+ calib_area->callback = callback;
+ calib_area->user_data = user_data;
+ calib_area->device = device;
+ calib_area->calibrator.threshold_doubleclick = threshold_doubleclick;
+ calib_area->calibrator.threshold_misclick = threshold_misclick;
+
+ calib_area->builder = gtk_builder_new_from_resource ("/org/gnome/control-center/wacom/calibrator/calibrator.ui");
+ calib_area->window = GTK_WIDGET (gtk_builder_get_object (calib_area->builder, "window"));
+ calib_area->error_revealer = GTK_WIDGET (gtk_builder_get_object (calib_area->builder, "error_revealer"));
+ calib_area->clock = GTK_WIDGET (gtk_builder_get_object (calib_area->builder, "clock"));
+ calib_area->style_provider = gtk_css_provider_new ();
+ gtk_css_provider_load_from_resource (calib_area->style_provider, "/org/gnome/control-center/wacom/calibrator/calibrator.css");
+ gtk_style_context_add_provider_for_screen (gtk_widget_get_screen (calib_area->window),
+ GTK_STYLE_PROVIDER (calib_area->style_provider),
+ GTK_STYLE_PROVIDER_PRIORITY_USER);
+
+ cc_clock_set_duration (CC_CLOCK (calib_area->clock), MAX_TIME);
+ g_signal_connect (calib_area->clock, "finished",
+ G_CALLBACK (on_clock_finished), calib_area);
+
+#ifndef FAKE_AREA
+ /* No cursor */
+ gtk_widget_realize (calib_area->window);
+ window = gtk_widget_get_window (calib_area->window);
+ cursor = gdk_cursor_new_for_display (gdk_display_get_default (), GDK_BLANK_CURSOR);
+ gdk_window_set_cursor (window, cursor);
+
+ gtk_widget_set_can_focus (calib_area->window, TRUE);
+ gtk_window_set_keep_above (GTK_WINDOW (calib_area->window), TRUE);
+#endif /* FAKE_AREA */
+
+ /* Move to correct screen */
+ if (screen == NULL)
+ screen = gdk_screen_get_default ();
+ monitor = gdk_display_get_monitor (gdk_screen_get_display (screen), n_monitor);
+ gdk_monitor_get_geometry (monitor, &rect);
+
+ calib_area->calibrator.geometry = rect;
+
+ g_signal_connect (calib_area->window,
+ "key-release-event",
+ G_CALLBACK (on_key_release_event),
+ calib_area);
+ g_signal_connect (calib_area->window,
+ "delete-event",
+ G_CALLBACK (on_delete_event),
+ calib_area);
+ g_signal_connect (calib_area->window,
+ "focus-out-event",
+ G_CALLBACK(on_focus_out_event),
+ calib_area);
+ g_signal_connect (calib_area->window,
+ "window-state-event",
+ G_CALLBACK (on_fullscreen),
+ calib_area);
+ g_signal_connect (calib_area->window,
+ "size-allocate",
+ G_CALLBACK (on_size_allocate),
+ calib_area);
+
+ press = gtk_gesture_multi_press_new (calib_area->window);
+ gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (press), GDK_BUTTON_PRIMARY);
+ g_signal_connect (press, "pressed",
+ G_CALLBACK (on_gesture_press), calib_area);
+
+ gtk_window_fullscreen_on_monitor (GTK_WINDOW (calib_area->window), screen, n_monitor);
+
+ visual = gdk_screen_get_rgba_visual (screen);
+ if (visual != NULL)
+ gtk_widget_set_visual (GTK_WIDGET (calib_area->window), visual);
+
+ gtk_widget_show (calib_area->window);
+
+ return calib_area;
+}
+
+/* Finishes the calibration. Note that CalibArea
+ * needs to be destroyed with calib_area_free() afterwards */
+gboolean
+calib_area_finish (CalibArea *area)
+{
+ g_return_val_if_fail (area != NULL, FALSE);
+
+ if (area->success)
+ g_debug ("Final calibration: %f, %f, %f, %f\n",
+ area->axis.x_min,
+ area->axis.y_min,
+ area->axis.x_max,
+ area->axis.y_max);
+ else
+ g_debug ("Calibration was aborted or timed out");
+
+ return area->success;
+}
+
+void
+calib_area_free (CalibArea *area)
+{
+ g_return_if_fail (area != NULL);
+
+ gtk_style_context_remove_provider_for_screen (gtk_widget_get_screen (area->window),
+ GTK_STYLE_PROVIDER (area->style_provider));
+ gtk_widget_destroy (area->window);
+ g_free (area);
+}
+
+void
+calib_area_get_display_size (CalibArea *area, gint *width, gint *height)
+{
+ g_return_if_fail (area != NULL);
+
+ *width = area->display_width;
+ *height = area->display_height;
+}
+
+void
+calib_area_get_axis (CalibArea *area,
+ XYinfo *new_axis,
+ gboolean *swap_xy)
+{
+ g_return_if_fail (area != NULL);
+
+ *new_axis = area->axis;
+ *swap_xy = area->swap;
+}
+
+void
+calib_area_get_padding (CalibArea *area,
+ XYinfo *padding)
+{
+ g_return_if_fail (area != NULL);
+
+ /* min/max values are monitor coordinates scaled to be between
+ * 0 and 1, padding starts at 0 on "the edge", and positive
+ * values grow towards the center of the rectangle.
+ */
+ padding->x_min = area->axis.x_min;
+ padding->y_min = area->axis.y_min;
+ padding->x_max = 1 - area->axis.x_max;
+ padding->y_max = 1 - area->axis.y_max;
+}
diff --git a/panels/wacom/calibrator/calibrator-gui.h b/panels/wacom/calibrator/calibrator-gui.h
new file mode 100644
index 0000000..d3301c1
--- /dev/null
+++ b/panels/wacom/calibrator/calibrator-gui.h
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2009 Tias Guns
+ * Copyright (c) 2009 Soren Hauberg
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+/* struct to hold min/max info of the X and Y axis */
+typedef struct
+{
+ gdouble x_min;
+ gdouble x_max;
+ gdouble y_min;
+ gdouble y_max;
+} XYinfo;
+
+typedef struct CalibArea CalibArea;
+typedef void (*FinishCallback) (CalibArea *area, gpointer user_data);
+
+CalibArea * calib_area_new (GdkScreen *screen,
+ int monitor,
+ GdkDevice *device,
+ FinishCallback callback,
+ gpointer user_data,
+ int threshold_doubleclick,
+ int threshold_misclick);
+
+gboolean calib_area_finish (CalibArea *area);
+
+void calib_area_free (CalibArea *area);
+
+void calib_area_get_display_size (CalibArea *area,
+ gint *width,
+ gint *height);
+
+void calib_area_get_axis (CalibArea *area,
+ XYinfo *new_axis,
+ gboolean *swap_xy);
+
+void calib_area_get_padding (CalibArea *area,
+ XYinfo *padding);
+
+G_END_DECLS
diff --git a/panels/wacom/calibrator/calibrator.c b/panels/wacom/calibrator/calibrator.c
new file mode 100644
index 0000000..4ac316e
--- /dev/null
+++ b/panels/wacom/calibrator/calibrator.c
@@ -0,0 +1,183 @@
+/*
+ * Copyright (c) 2009 Tias Guns
+ * Copyright (c) 2009 Soren Hauberg
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#include <stdlib.h>
+
+#include "calibrator.h"
+
+#define SWAP(valtype,x,y) \
+ G_STMT_START { \
+ valtype t; t = (x); x = (y); y = t; \
+ } G_STMT_END
+
+/* reset clicks */
+void
+reset (struct Calib *c)
+{
+ c->num_clicks = 0;
+}
+
+/* check whether the coordinates are along the respective axis */
+static gboolean
+along_axis (struct Calib *c,
+ int xy,
+ int x0,
+ int y0)
+{
+ return ((abs(xy - x0) <= c->threshold_misclick) ||
+ (abs(xy - y0) <= c->threshold_misclick));
+}
+
+/* add a click with the given coordinates */
+gboolean
+add_click (struct Calib *c,
+ int x,
+ int y)
+{
+ g_debug ("Trying to add click (%d, %d)", x, y);
+
+ /* Double-click detection */
+ if (c->threshold_doubleclick > 0 && c->num_clicks > 0)
+ {
+ int i = c->num_clicks-1;
+ while (i >= 0)
+ {
+ if (abs(x - c->clicked_x[i]) <= c->threshold_doubleclick &&
+ abs(y - c->clicked_y[i]) <= c->threshold_doubleclick)
+ {
+ g_debug ("Detected double-click, ignoring");
+ return FALSE;
+ }
+ i--;
+ }
+ }
+
+ /* Mis-click detection */
+ if (c->threshold_misclick > 0 && c->num_clicks > 0)
+ {
+ gboolean misclick = TRUE;
+
+ if (c->num_clicks == 1)
+ {
+ /* check that along one axis of first point */
+ if (along_axis(c, x,c->clicked_x[0],c->clicked_y[0]) ||
+ along_axis(c, y,c->clicked_x[0],c->clicked_y[0]))
+ {
+ misclick = FALSE;
+ }
+ }
+ else if (c->num_clicks == 2)
+ {
+ /* check that along other axis of first point than second point */
+ if ((along_axis(c, y,c->clicked_x[0],c->clicked_y[0]) &&
+ along_axis(c, c->clicked_x[1],c->clicked_x[0],c->clicked_y[0])) ||
+ (along_axis(c, x,c->clicked_x[0],c->clicked_y[0]) &&
+ along_axis(c, c->clicked_y[1],c->clicked_x[0],c->clicked_y[0])))
+ {
+ misclick = FALSE;
+ }
+ }
+ else if (c->num_clicks == 3)
+ {
+ /* check that along both axis of second and third point */
+ if ((along_axis(c, x,c->clicked_x[1],c->clicked_y[1]) &&
+ along_axis(c, y,c->clicked_x[2],c->clicked_y[2])) ||
+ (along_axis(c, y,c->clicked_x[1],c->clicked_y[1]) &&
+ along_axis(c, x,c->clicked_x[2],c->clicked_y[2])))
+ {
+ misclick = FALSE;
+ }
+ }
+
+ if (misclick)
+ {
+ g_debug ("Detected misclick, resetting");
+ reset(c);
+ return FALSE;
+ }
+ }
+
+ g_debug ("Click (%d, %d) added", x, y);
+ c->clicked_x[c->num_clicks] = x;
+ c->clicked_y[c->num_clicks] = y;
+ c->num_clicks++;
+
+ return TRUE;
+}
+
+/* calculate and apply the calibration */
+gboolean
+finish (struct Calib *c,
+ XYinfo *new_axis,
+ gboolean *swap)
+{
+ gboolean swap_xy;
+ float scale_x;
+ float scale_y;
+ float delta_x;
+ float delta_y;
+ XYinfo axis = {-1, -1, -1, -1};
+
+ if (c->num_clicks != 4)
+ return FALSE;
+
+ /* Should x and y be swapped? If the device and output are wider
+ * towards different axes, swapping must be performed
+ *
+ * FIXME: Would be even better to know the actual output orientation,
+ * not just the direction.
+ */
+ swap_xy = (c->geometry.width < c->geometry.height);
+
+ /* Compute the scale to transform from pixel positions to [0..1]. */
+ scale_x = 1 / (float)c->geometry.width;
+ scale_y = 1 / (float)c->geometry.height;
+
+ axis.x_min = ((((c->clicked_x[UL] + c->clicked_x[LL]) / 2)) * scale_x);
+ axis.x_max = ((((c->clicked_x[UR] + c->clicked_x[LR]) / 2)) * scale_x);
+ axis.y_min = ((((c->clicked_y[UL] + c->clicked_y[UR]) / 2)) * scale_y);
+ axis.y_max = ((((c->clicked_y[LL] + c->clicked_y[LR]) / 2)) * scale_y);
+
+ /* Add/subtract the offset that comes from not having the points in the
+ * corners (using the same coordinate system they are currently in)
+ */
+ delta_x = (axis.x_max - axis.x_min) / (float)(NUM_BLOCKS - 2);
+ axis.x_min -= delta_x;
+ axis.x_max += delta_x;
+ delta_y = (axis.y_max - axis.y_min) / (float)(NUM_BLOCKS - 2);
+ axis.y_min -= delta_y;
+ axis.y_max += delta_y;
+
+ /* If x and y has to be swapped we also have to swap the parameters */
+ if (swap_xy)
+ {
+ SWAP (gdouble, axis.x_min, axis.y_min);
+ SWAP (gdouble, axis.x_max, axis.y_max);
+ }
+
+ *new_axis = axis;
+ *swap = swap_xy;
+
+ return TRUE;
+}
+
diff --git a/panels/wacom/calibrator/calibrator.css b/panels/wacom/calibrator/calibrator.css
new file mode 100644
index 0000000..cf5fced
--- /dev/null
+++ b/panels/wacom/calibrator/calibrator.css
@@ -0,0 +1,47 @@
+#calibrator {
+ background-color: #000;
+}
+
+#calibrator * {
+ color: #fff;
+}
+
+#calibrator label {
+ font-size: larger;
+}
+
+#calibrator #title {
+ font-weight: bold;
+ color: #888;
+}
+
+#calibrator #error {
+ font-weight: bold;
+}
+
+#calibrator #target {
+ background-image: url('target.svg');
+ background-repeat: no-repeat;
+ background-position: 50% 50%;
+}
+
+@keyframes target-enabled-animation {
+ 0% { background-size: 0px }
+ 90% { background-size: 120px }
+ 100% { background-size: 100px }
+}
+
+@keyframes target-disabled-animation {
+ 0% { background-size: 100px }
+ 100% { background-size: 0px }
+}
+
+#calibrator #target:not(disabled) {
+ animation: target-enabled-animation 1 ease 0.5s;
+ background-size: 100px;
+}
+
+#calibrator #target:disabled {
+ animation: target-disabled-animation 1 ease 0.2s;
+ background-size: 0px;
+}
diff --git a/panels/wacom/calibrator/calibrator.h b/panels/wacom/calibrator/calibrator.h
new file mode 100644
index 0000000..dab7a2f
--- /dev/null
+++ b/panels/wacom/calibrator/calibrator.h
@@ -0,0 +1,99 @@
+/*
+ * Copyright (c) 2009 Tias Guns
+ * Copyright (c) 2009 Soren Hauberg
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#pragma once
+
+#include <glib.h>
+#include "calibrator-gui.h"
+
+G_BEGIN_DECLS
+
+/*
+ * Number of blocks. We partition the screen into 'num_blocks' x 'num_blocks'
+ * rectangles of equal size. We then ask the user to press points that are
+ * located at the corner closes to the center of the four blocks in the corners
+ * of the screen. The following ascii art illustrates the situation. We partition
+ * the screen into 8 blocks in each direction. We then let the user press the
+ * points marked with 'O'.
+ *
+ * +--+--+--+--+--+--+--+--+
+ * | | | | | | | | |
+ * +--O--+--+--+--+--+--O--+
+ * | | | | | | | | |
+ * +--+--+--+--+--+--+--+--+
+ * | | | | | | | | |
+ * +--+--+--+--+--+--+--+--+
+ * | | | | | | | | |
+ * +--+--+--+--+--+--+--+--+
+ * | | | | | | | | |
+ * +--+--+--+--+--+--+--+--+
+ * | | | | | | | | |
+ * +--+--+--+--+--+--+--+--+
+ * | | | | | | | | |
+ * +--O--+--+--+--+--+--O--+
+ * | | | | | | | | |
+ * +--+--+--+--+--+--+--+--+
+ */
+#define NUM_BLOCKS 8
+
+/* Names of the points */
+enum
+{
+ UL = 0, /* Upper-left */
+ UR = 1, /* Upper-right */
+ LL = 2, /* Lower-left */
+ LR = 3 /* Lower-right */
+};
+
+struct Calib
+{
+ /* Geometry of the calibration window */
+ GdkRectangle geometry;
+
+ /* nr of clicks registered */
+ int num_clicks;
+
+ /* click coordinates */
+ int clicked_x[4], clicked_y[4];
+
+ /* Threshold to keep the same point from being clicked twice.
+ * Set to zero if you don't want this check
+ */
+ int threshold_doubleclick;
+
+ /* Threshold to detect mis-clicks (clicks not along axes)
+ * A lower value forces more precise calibration
+ * Set to zero if you don't want this check
+ */
+ int threshold_misclick;
+};
+
+void reset (struct Calib *c);
+gboolean add_click (struct Calib *c,
+ int x,
+ int y);
+gboolean finish (struct Calib *c,
+ XYinfo *new_axis,
+ gboolean *swap);
+
+G_END_DECLS
diff --git a/panels/wacom/calibrator/calibrator.ui b/panels/wacom/calibrator/calibrator.ui
new file mode 100644
index 0000000..6734b76
--- /dev/null
+++ b/panels/wacom/calibrator/calibrator.ui
@@ -0,0 +1,203 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.20"/>
+ <object class="GtkWindow" id="window">
+ <property name="name">calibrator</property>
+ <child>
+ <object class="GtkStack" id="stack">
+ <property name="visible">True</property>
+ <property name="transition_duration">0</property>
+ <child>
+ <object class="GtkGrid">
+ <property name="visible">True</property>
+ <property name="row_homogeneous">True</property>
+ <property name="column_homogeneous">True</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkBox" id="box1">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ </packing>
+ </child>
+ <child>
+ <object class="CcClock" id="clock">
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="box2">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkRevealer" id="title_revealer">
+ <property name="visible">True</property>
+ <property name="transition_duration">300</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="name">title</property>
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">Screen Calibration</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkRevealer" id="subtitle_revealer">
+ <property name="visible">True</property>
+ <property name="transition_duration">300</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="name">subtitle</property>
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">Please tap the target markers as they appear on screen to calibrate the tablet.</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkRevealer" id="error_revealer">
+ <property name="visible">True</property>
+ <property name="transition_type">crossfade</property>
+ <property name="transition_duration">500</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="name">error</property>
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">Mis-click detected, restarting…</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ <property name="width">8</property>
+ <property name="height">8</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEventBox" id="target1">
+ <property name="name">target</property>
+ <property name="width_request">100</property>
+ <property name="height_request">100</property>
+ <property name="visible">True</property>
+ <property name="visible_window">True</property>
+ <property name="sensitive">False</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ <property name="width">2</property>
+ <property name="height">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEventBox" id="target2">
+ <property name="name">target</property>
+ <property name="width_request">100</property>
+ <property name="height_request">100</property>
+ <property name="visible">True</property>
+ <property name="visible_window">True</property>
+ <property name="sensitive">False</property>
+ </object>
+ <packing>
+ <property name="left_attach">6</property>
+ <property name="top_attach">0</property>
+ <property name="width">2</property>
+ <property name="height">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEventBox" id="target3">
+ <property name="name">target</property>
+ <property name="width_request">100</property>
+ <property name="height_request">100</property>
+ <property name="visible">True</property>
+ <property name="visible_window">True</property>
+ <property name="sensitive">False</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">6</property>
+ <property name="width">2</property>
+ <property name="height">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEventBox" id="target4">
+ <property name="name">target</property>
+ <property name="width_request">100</property>
+ <property name="height_request">100</property>
+ <property name="visible">True</property>
+ <property name="visible_window">True</property>
+ <property name="sensitive">False</property>
+ </object>
+ <packing>
+ <property name="left_attach">6</property>
+ <property name="top_attach">6</property>
+ <property name="width">2</property>
+ <property name="height">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkRevealer">
+ <property name="visible">True</property>
+ <property name="transition_type">none</property>
+ <property name="transition_duration">0</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ <property name="width">8</property>
+ <property name="height">8</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="name">page0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="pixel_size">300</property>
+ <property name="icon_name">emblem-ok-symbolic</property>
+ </object>
+ <packing>
+ <property name="name">page1</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ <object class="GtkSizeGroup">
+ <property name="mode">vertical</property>
+ <widgets>
+ <widget name="box1"/>
+ <widget name="box2"/>
+ </widgets>
+ </object>
+</interface>
diff --git a/panels/wacom/calibrator/cc-clock.c b/panels/wacom/calibrator/cc-clock.c
new file mode 100644
index 0000000..b39ddac
--- /dev/null
+++ b/panels/wacom/calibrator/cc-clock.c
@@ -0,0 +1,300 @@
+/*
+ * Copyright © 2018 Red Hat, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Joaquim Rocha <jrocha@redhat.com>
+ * Carlos Garnacho <carlosg@gnome.org>
+ */
+#include "config.h"
+#include "cc-clock.h"
+
+#include <math.h>
+
+#define CLOCK_RADIUS 50
+#define CLOCK_LINE_WIDTH 10
+#define CLOCK_LINE_PADDING 10
+#define EXTRA_SPACE 2
+
+typedef struct _CcClock CcClock;
+
+struct _CcClock
+{
+ GtkWidget parent_instance;
+ guint duration;
+ gint64 start_time;
+ gboolean running;
+};
+
+enum
+{
+ PROP_DURATION = 1,
+ N_PROPS
+};
+
+static GParamSpec *props[N_PROPS] = { 0, };
+
+enum {
+ FINISHED,
+ N_SIGNALS
+};
+
+static guint signals[N_SIGNALS] = { 0, };
+
+G_DEFINE_TYPE (CcClock, cc_clock, GTK_TYPE_WIDGET)
+
+static gint64
+cc_clock_get_time_diff (CcClock *clock)
+{
+ GdkFrameClock *frame_clock;
+ gint64 current_time;
+
+ frame_clock = gtk_widget_get_frame_clock (GTK_WIDGET (clock));
+ current_time = gdk_frame_clock_get_frame_time (frame_clock);
+
+ return current_time - clock->start_time;
+}
+
+static gdouble
+cc_clock_get_angle (CcClock *clock)
+{
+ gint64 time_diff;
+
+ time_diff = cc_clock_get_time_diff (clock);
+
+ if (time_diff > clock->duration * 1000)
+ return 360;
+
+ return ((gdouble) time_diff / (clock->duration * 1000)) * 360;
+}
+
+static gboolean
+cc_clock_draw (GtkWidget *widget,
+ cairo_t *cr)
+{
+ GtkAllocation allocation;
+ gdouble angle;
+
+ gtk_widget_get_allocation (widget, &allocation);
+ angle = cc_clock_get_angle (CC_CLOCK (widget));
+
+ cairo_set_operator (cr, CAIRO_OPERATOR_CLEAR);
+ cairo_paint (cr);
+ cairo_set_operator (cr, CAIRO_OPERATOR_OVER);
+
+ /* Draw the clock background */
+ cairo_arc (cr, allocation.width / 2, allocation.height / 2, CLOCK_RADIUS / 2, 0.0, 2.0 * M_PI);
+ cairo_set_source_rgb (cr, 0.5, 0.5, 0.5);
+ cairo_fill_preserve (cr);
+ cairo_stroke (cr);
+
+ cairo_set_line_width (cr, CLOCK_LINE_WIDTH);
+
+ cairo_arc (cr,
+ allocation.width / 2,
+ allocation.height / 2,
+ (CLOCK_RADIUS - CLOCK_LINE_WIDTH - CLOCK_LINE_PADDING) / 2,
+ 3 * M_PI_2,
+ 3 * M_PI_2 + angle * M_PI / 180.0);
+ cairo_set_source_rgb (cr, 1.0, 1.0, 1.0);
+ cairo_stroke (cr);
+
+ return TRUE;
+}
+
+static void
+cc_clock_stop (CcClock *clock)
+{
+ GdkFrameClock *frame_clock;
+
+ if (!clock->running)
+ return;
+
+ frame_clock = gtk_widget_get_frame_clock (GTK_WIDGET (clock));
+
+ gdk_frame_clock_end_updating (frame_clock);
+ clock->running = FALSE;
+}
+
+static void
+on_frame_clock_update (CcClock *clock)
+{
+ gint64 time_diff;
+
+ if (!clock->running)
+ return;
+
+ time_diff = cc_clock_get_time_diff (clock);
+
+ if (time_diff > clock->duration * 1000)
+ {
+ g_signal_emit (clock, signals[FINISHED], 0);
+ cc_clock_stop (clock);
+ }
+
+ gtk_widget_queue_draw (GTK_WIDGET (clock));
+}
+
+static void
+cc_clock_map (GtkWidget *widget)
+{
+ GdkFrameClock *frame_clock;
+
+ GTK_WIDGET_CLASS (cc_clock_parent_class)->map (widget);
+
+ frame_clock = gtk_widget_get_frame_clock (widget);
+ g_signal_connect_object (frame_clock, "update",
+ G_CALLBACK (on_frame_clock_update),
+ widget, G_CONNECT_SWAPPED);
+ cc_clock_reset (CC_CLOCK (widget));
+}
+
+static void
+cc_clock_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ CcClock *clock = CC_CLOCK (object);
+
+ switch (prop_id)
+ {
+ case PROP_DURATION:
+ clock->duration = g_value_get_uint (value);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+cc_clock_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ CcClock *clock = CC_CLOCK (object);
+
+ switch (prop_id)
+ {
+ case PROP_DURATION:
+ g_value_set_uint (value, clock->duration);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+cc_clock_get_preferred_width (GtkWidget *widget,
+ gint *minimum,
+ gint *natural)
+{
+ if (minimum)
+ *minimum = CLOCK_RADIUS + EXTRA_SPACE;
+ if (natural)
+ *natural = CLOCK_RADIUS + EXTRA_SPACE;
+}
+
+static void
+cc_clock_get_preferred_height (GtkWidget *widget,
+ gint *minimum,
+ gint *natural)
+{
+ if (minimum)
+ *minimum = CLOCK_RADIUS + EXTRA_SPACE;
+ if (natural)
+ *natural = CLOCK_RADIUS + EXTRA_SPACE;
+}
+
+static void
+cc_clock_class_init (CcClockClass *klass)
+{
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->set_property = cc_clock_set_property;
+ object_class->get_property = cc_clock_get_property;
+
+ widget_class->map = cc_clock_map;
+ widget_class->draw = cc_clock_draw;
+ widget_class->get_preferred_width = cc_clock_get_preferred_width;
+ widget_class->get_preferred_height = cc_clock_get_preferred_height;
+
+ signals[FINISHED] =
+ g_signal_new ("finished",
+ CC_TYPE_CLOCK,
+ G_SIGNAL_RUN_LAST,
+ 0, NULL, NULL, NULL,
+ G_TYPE_NONE, 0);
+
+ props[PROP_DURATION] =
+ g_param_spec_uint ("duration",
+ "Duration",
+ "Duration",
+ 0, G_MAXUINT, 0,
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS |
+ G_PARAM_CONSTRUCT_ONLY);
+
+ g_object_class_install_properties (object_class, N_PROPS, props);
+}
+
+static void
+cc_clock_init (CcClock *clock)
+{
+ gtk_widget_set_has_window (GTK_WIDGET (clock), FALSE);
+}
+
+GtkWidget *
+cc_clock_new (guint duration)
+{
+ return g_object_new (CC_TYPE_CLOCK,
+ "duration", duration,
+ NULL);
+}
+
+void
+cc_clock_reset (CcClock *clock)
+{
+ GdkFrameClock *frame_clock;
+
+ if (!gtk_widget_get_mapped (GTK_WIDGET (clock)))
+ return;
+
+ frame_clock = gtk_widget_get_frame_clock (GTK_WIDGET (clock));
+
+ cc_clock_stop (clock);
+
+ clock->running = TRUE;
+ clock->start_time = g_get_monotonic_time ();
+ gdk_frame_clock_begin_updating (frame_clock);
+}
+
+void
+cc_clock_set_duration (CcClock *clock,
+ guint duration)
+{
+ clock->duration = duration;
+ g_object_notify (G_OBJECT (clock), "duration");
+ cc_clock_reset (clock);
+}
+
+guint
+cc_clock_get_duration (CcClock *clock)
+{
+ return clock->duration;
+}
diff --git a/panels/wacom/calibrator/cc-clock.h b/panels/wacom/calibrator/cc-clock.h
new file mode 100644
index 0000000..9ebf024
--- /dev/null
+++ b/panels/wacom/calibrator/cc-clock.h
@@ -0,0 +1,41 @@
+/*
+ * Copyright © 2018 Red Hat, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * Author: Carlos Garnacho <carlosg@gnome.org>
+ */
+
+#pragma once
+
+#include <glib.h>
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define CC_TYPE_CLOCK (cc_clock_get_type ())
+
+G_DECLARE_FINAL_TYPE (CcClock, cc_clock, CC, CLOCK, GtkWidget)
+
+GtkWidget * cc_clock_new (guint duration);
+
+void cc_clock_reset (CcClock *clock);
+
+void cc_clock_set_duration (CcClock *clock,
+ guint duration);
+guint cc_clock_get_duration (CcClock *clock);
+
+GType cc_clock_get_type (void);
+
+G_END_DECLS
diff --git a/panels/wacom/calibrator/main.c b/panels/wacom/calibrator/main.c
new file mode 100644
index 0000000..1a82e87
--- /dev/null
+++ b/panels/wacom/calibrator/main.c
@@ -0,0 +1,418 @@
+/*
+ * Copyright (c) 2009 Tias Guns
+ * Copyright (c) 2009 Soren Hauberg
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <stdlib.h>
+#include <ctype.h>
+#include <string.h>
+#include <dirent.h>
+#include <glib/gi18n.h>
+
+#include <X11/extensions/XInput.h>
+
+#include "calibrator-gui.h"
+#include "calibrator.h"
+
+/**
+ * find a calibratable touchscreen device (using XInput)
+ *
+ * if pre_device is NULL, the last calibratable device is selected.
+ * retuns number of devices found,
+ * the data of the device is returned in the last 3 function parameters
+ */
+static int find_device(const char* pre_device, gboolean verbose, gboolean list_devices,
+ XID* device_id, const char** device_name, XYinfo* device_axis)
+{
+ gboolean pre_device_is_id = TRUE;
+ int found = 0;
+
+ Display* display = XOpenDisplay(NULL);
+ if (display == NULL) {
+ fprintf(stderr, "Unable to connect to X server\n");
+ exit(1);
+ }
+
+ int xi_opcode, event, error;
+ if (!XQueryExtension(display, "XInputExtension", &xi_opcode, &event, &error)) {
+ fprintf(stderr, "X Input extension not available.\n");
+ exit(1);
+ }
+
+ /* verbose, get Xi version */
+ if (verbose) {
+ XExtensionVersion *version = XGetExtensionVersion(display, INAME);
+
+ if (version && (version != (XExtensionVersion*) NoSuchExtension)) {
+ printf("DEBUG: %s version is %i.%i\n",
+ INAME, version->major_version, version->minor_version);
+ XFree(version);
+ }
+ }
+
+ if (pre_device != NULL) {
+ /* check whether the pre_device is an ID (only digits) */
+ int len = strlen(pre_device);
+ int loop;
+ for (loop=0; loop<len; loop++) {
+ if (!isdigit(pre_device[loop])) {
+ pre_device_is_id = FALSE;
+ break;
+ }
+ }
+ }
+
+
+ if (verbose)
+ printf("DEBUG: Skipping virtual master devices and devices without axis valuators.\n");
+ int ndevices;
+ XDeviceInfoPtr list, slist;
+ slist=list=(XDeviceInfoPtr) XListInputDevices (display, &ndevices);
+ int i;
+ for (i=0; i<ndevices; i++, list++)
+ {
+ if (list->use == IsXKeyboard || list->use == IsXPointer) /* virtual master device */
+ continue;
+
+ /* if we are looking for a specific device */
+ if (pre_device != NULL) {
+ if ((pre_device_is_id && list->id == (XID) atoi(pre_device)) ||
+ (!pre_device_is_id && strcmp(list->name, pre_device) == 0)) {
+ /* OK, fall through */
+ } else {
+ /* skip, not this device */
+ continue;
+ }
+ }
+
+ XAnyClassPtr any = (XAnyClassPtr) (list->inputclassinfo);
+ int j;
+ for (j=0; j<list->num_classes; j++)
+ {
+
+ if (any->class == ValuatorClass)
+ {
+ XValuatorInfoPtr V = (XValuatorInfoPtr) any;
+ XAxisInfoPtr ax = (XAxisInfoPtr) V->axes;
+
+ if (V->mode != Absolute) {
+ if (verbose)
+ printf("DEBUG: Skipping device '%s' id=%i, does not report Absolute events.\n",
+ list->name, (int)list->id);
+ } else if (V->num_axes < 2 ||
+ (ax[0].min_value == -1 && ax[0].max_value == -1) ||
+ (ax[1].min_value == -1 && ax[1].max_value == -1)) {
+ if (verbose)
+ printf("DEBUG: Skipping device '%s' id=%i, does not have two calibratable axes.\n",
+ list->name, (int)list->id);
+ } else {
+ /* a calibratable device (has 2 axis valuators) */
+ found++;
+ *device_id = list->id;
+ *device_name = g_strdup(list->name);
+ device_axis->x_min = ax[0].min_value;
+ device_axis->x_max = ax[0].max_value;
+ device_axis->y_min = ax[1].min_value;
+ device_axis->y_max = ax[1].max_value;
+
+ if (list_devices)
+ printf("Device \"%s\" id=%i\n", *device_name, (int)*device_id);
+ }
+
+ }
+
+ /*
+ * Increment 'any' to point to the next item in the linked
+ * list. The length is in bytes, so 'any' must be cast to
+ * a character pointer before being incremented.
+ */
+ any = (XAnyClassPtr) ((char *) any + any->length);
+ }
+
+ }
+ XFreeDeviceList(slist);
+ XCloseDisplay(display);
+
+ return found;
+}
+
+static void usage(char* cmd, unsigned thr_misclick)
+{
+ fprintf(stderr, "Usage: %s [-h|--help] [-v|--verbose] [--list] [--device <device name or id>] [--precalib <minx> <maxx> <miny> <maxy>] [--misclick <nr of pixels>] [--output-type <auto|xorg.conf.d|hal|xinput>] [--fake]\n", cmd);
+ fprintf(stderr, "\t-h, --help: print this help message\n");
+ fprintf(stderr, "\t-v, --verbose: print debug messages during the process\n");
+ fprintf(stderr, "\t--list: list calibratable input devices and quit\n");
+ fprintf(stderr, "\t--device <device name or id>: select a specific device to calibrate\n");
+ fprintf(stderr, "\t--precalib: manually provide the current calibration setting (eg. the values in xorg.conf)\n");
+ fprintf(stderr, "\t--misclick: set the misclick threshold (0=off, default: %i pixels)\n",
+ thr_misclick);
+ fprintf(stderr, "\t--fake: emulate a fake device (for testing purposes)\n");
+}
+
+static struct Calib* CalibratorXorgPrint(const char* const device_name0, const XYinfo *axis0, const gboolean verbose0, const int thr_misclick, const int thr_doubleclick)
+{
+ struct Calib* c = (struct Calib*)calloc(1, sizeof(struct Calib));
+ c->threshold_misclick = thr_misclick;
+ c->threshold_doubleclick = thr_doubleclick;
+
+ printf("Calibrating standard Xorg driver \"%s\"\n", device_name0);
+ printf("\tcurrent calibration values: min_x=%lf, max_x=%lf and min_y=%lf, max_y=%lf\n",
+ axis0->x_min, axis0->x_max, axis0->y_min, axis0->y_max);
+ printf("\tIf these values are estimated wrong, either supply it manually with the --precalib option, or run the 'get_precalib.sh' script to automatically get it (through HAL).\n");
+
+ return c;
+}
+
+static struct Calib* main_common(int argc, char** argv)
+{
+ gboolean verbose = FALSE;
+ gboolean list_devices = FALSE;
+ gboolean fake = FALSE;
+ gboolean precalib = FALSE;
+ XYinfo pre_axis = {-1, -1, -1, -1};
+ const char* pre_device = NULL;
+ unsigned thr_misclick = 15;
+ unsigned thr_doubleclick = 7;
+
+ /* parse input */
+ if (argc > 1) {
+ int i;
+ for (i=1; i!=argc; i++) {
+ /* Display help ? */
+ if (strcmp("-h", argv[i]) == 0 ||
+ strcmp("--help", argv[i]) == 0) {
+ fprintf(stderr, "xinput_calibrator, v%s\n\n", "0.0.0");
+ usage(argv[0], thr_misclick);
+ exit(0);
+ } else
+
+ /* Verbose output ? */
+ if (strcmp("-v", argv[i]) == 0 ||
+ strcmp("--verbose", argv[i]) == 0) {
+ verbose = TRUE;
+ } else
+
+ /* Just list devices ? */
+ if (strcmp("--list", argv[i]) == 0) {
+ list_devices = TRUE;
+ } else
+
+ /* Select specific device ? */
+ if (strcmp("--device", argv[i]) == 0) {
+ if (argc > i+1)
+ pre_device = argv[++i];
+ else {
+ fprintf(stderr, "Error: --device needs a device name or id as argument; use --list to list the calibratable input devices.\n\n");
+ usage(argv[0], thr_misclick);
+ exit(1);
+ }
+ } else
+
+ /* Get pre-calibration ? */
+ if (strcmp("--precalib", argv[i]) == 0) {
+ precalib = TRUE;
+ if (argc > i+1)
+ pre_axis.x_min = atoi(argv[++i]);
+ if (argc > i+1)
+ pre_axis.x_max = atoi(argv[++i]);
+ if (argc > i+1)
+ pre_axis.y_min = atoi(argv[++i]);
+ if (argc > i+1)
+ pre_axis.y_max = atoi(argv[++i]);
+ } else
+
+ /* Get mis-click threshold ? */
+ if (strcmp("--misclick", argv[i]) == 0) {
+ if (argc > i+1)
+ thr_misclick = atoi(argv[++i]);
+ else {
+ fprintf(stderr, "Error: --misclick needs a number (the pixel threshold) as argument. Set to 0 to disable mis-click detection.\n\n");
+ usage(argv[0], thr_misclick);
+ exit(1);
+ }
+ } else
+
+ /* Fake calibratable device ? */
+ if (strcmp("--fake", argv[i]) == 0) {
+ fake = TRUE;
+ }
+
+ /* unknown option */
+ else {
+ fprintf(stderr, "Unknown option: %s\n\n", argv[i]);
+ usage(argv[0], thr_misclick);
+ exit(0);
+ }
+ }
+ }
+
+
+ /* Choose the device to calibrate */
+ XID device_id = (XID) -1;
+ const char* device_name = NULL;
+ XYinfo device_axis = {-1, -1, -1, -1};
+ if (fake) {
+ /* Fake a calibratable device */
+ device_name = "Fake_device";
+ device_axis.x_min=0;
+ device_axis.x_max=1000;
+ device_axis.y_min=0;
+ device_axis.y_max=1000;
+
+ if (verbose) {
+ printf("DEBUG: Faking device: %s\n", device_name);
+ }
+ } else {
+ /* Find the right device */
+ int nr_found = find_device(pre_device, verbose, list_devices, &device_id, &device_name, &device_axis);
+
+ if (list_devices) {
+ /* printed the list in find_device */
+ if (nr_found == 0)
+ printf("No calibratable devices found.\n");
+ exit(0);
+ }
+
+ if (nr_found == 0) {
+ if (pre_device == NULL)
+ fprintf (stderr, "Error: No calibratable devices found.\n");
+ else
+ fprintf (stderr, "Error: Device \"%s\" not found; use --list to list the calibratable input devices.\n", pre_device);
+ exit(1);
+
+ } else if (nr_found > 1) {
+ printf ("Warning: multiple calibratable devices found, calibrating last one (%s)\n\tuse --device to select another one.\n", device_name);
+ }
+
+ if (verbose) {
+ printf("DEBUG: Selected device: %s\n", device_name);
+ }
+ }
+
+ /* override min/max XY from command line ? */
+ if (precalib) {
+ if (pre_axis.x_min != -1)
+ device_axis.x_min = pre_axis.x_min;
+ if (pre_axis.x_max != -1)
+ device_axis.x_max = pre_axis.x_max;
+ if (pre_axis.y_min != -1)
+ device_axis.y_min = pre_axis.y_min;
+ if (pre_axis.y_max != -1)
+ device_axis.y_max = pre_axis.y_max;
+
+ if (verbose) {
+ printf("DEBUG: Setting precalibration: %lf, %lf, %lf, %lf\n",
+ device_axis.x_min, device_axis.x_max,
+ device_axis.y_min, device_axis.y_max);
+ }
+ }
+
+ /* lastly, presume a standard Xorg driver (evtouch, mutouch, ...) */
+ return CalibratorXorgPrint(device_name, &device_axis,
+ verbose, thr_misclick, thr_doubleclick);
+}
+
+static gboolean output_xorgconfd(const XYinfo new_axis, int swap_xy, int new_swap_xy)
+{
+ const char* sysfs_name = "!!Name_Of_TouchScreen!!";
+
+ /* xorg.conf.d snippet */
+ printf(" copy the snippet below into '/etc/X11/xorg.conf.d/99-calibration.conf'\n");
+ printf("Section \"InputClass\"\n");
+ printf(" Identifier \"calibration\"\n");
+ printf(" MatchProduct \"%s\"\n", sysfs_name);
+ printf(" Option \"MinX\" \"%lf\"\n", new_axis.x_min);
+ printf(" Option \"MaxX\" \"%lf\"\n", new_axis.x_max);
+ printf(" Option \"MinY\" \"%lf\"\n", new_axis.y_min);
+ printf(" Option \"MaxY\" \"%lf\"\n", new_axis.y_max);
+ if (swap_xy != 0)
+ printf(" Option \"SwapXY\" \"%d\" # unless it was already set to 1\n", new_swap_xy);
+ printf("EndSection\n");
+
+ return TRUE;
+}
+
+static gboolean finish_data(const XYinfo new_axis, int swap_xy)
+{
+ gboolean success = TRUE;
+
+ /* we suppose the previous 'swap_xy' value was 0 */
+ /* (unfortunately there is no way to verify this (yet)) */
+ int new_swap_xy = swap_xy;
+
+ printf("\n\n--> Making the calibration permanent <--\n");
+ success &= output_xorgconfd(new_axis, swap_xy, new_swap_xy);
+
+ return success;
+}
+
+static void
+calibration_finished_cb (CalibArea *area,
+ gpointer user_data)
+{
+ gboolean success;
+ XYinfo axis;
+ gboolean swap_xy;
+
+ success = calib_area_finish (area);
+ if (success)
+ {
+ calib_area_get_axis (area, &axis, &swap_xy);
+ success = finish_data (axis, swap_xy);
+ }
+ else
+ fprintf(stderr, "Error: unable to apply or save configuration values\n");
+
+ gtk_main_quit ();
+}
+
+int main(int argc, char** argv)
+{
+
+ struct Calib* calibrator = main_common(argc, argv);
+ CalibArea *calib_area;
+
+ bindtextdomain (GETTEXT_PACKAGE, GNOMELOCALEDIR);
+ bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
+ textdomain (GETTEXT_PACKAGE);
+
+ gtk_init (&argc, &argv);
+
+ g_setenv ("G_MESSAGES_DEBUG", "all", TRUE);
+
+ calib_area = calib_area_new (NULL,
+ 0, /* monitor */
+ NULL, /* NULL to accept input from any device */
+ calibration_finished_cb,
+ NULL,
+ calibrator->threshold_doubleclick,
+ calibrator->threshold_misclick);
+
+ gtk_main ();
+
+ calib_area_free (calib_area);
+
+ free(calibrator);
+
+ return 0;
+}
diff --git a/panels/wacom/calibrator/meson.build b/panels/wacom/calibrator/meson.build
new file mode 100644
index 0000000..f894e60
--- /dev/null
+++ b/panels/wacom/calibrator/meson.build
@@ -0,0 +1,35 @@
+calibrator_inc = include_directories('.')
+
+common_sources = files(
+ 'calibrator.c',
+ 'calibrator-gui.c',
+ 'cc-clock.c',
+)
+
+calibrator_deps = deps + [m_dep]
+
+libwacom_calibrator = static_library(
+ cappletname + '-calibrator',
+ sources: common_sources,
+ include_directories: top_inc,
+ dependencies: calibrator_deps,
+ c_args: cflags
+)
+
+libwacom_calibrator_test = static_library(
+ cappletname + '-calibrator-test',
+ sources: common_sources,
+ include_directories: top_inc,
+ dependencies: calibrator_deps,
+ c_args: test_cflags
+)
+
+sources = common_sources + wacom_gresource + files('main.c')
+
+executable(
+ 'test-calibrator',
+ sources,
+ include_directories: top_inc,
+ dependencies: calibrator_deps,
+ c_args: cflags
+)
diff --git a/panels/wacom/calibrator/target.svg b/panels/wacom/calibrator/target.svg
new file mode 100644
index 0000000..60b4cbb
--- /dev/null
+++ b/panels/wacom/calibrator/target.svg
@@ -0,0 +1,93 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="200"
+ height="200"
+ viewBox="0 0 52.916666 52.916668"
+ version="1.1"
+ id="svg8"
+ inkscape:version="0.92.3 (2405546, 2018-03-11)"
+ sodipodi:docname="target.svg">
+ <defs
+ id="defs2" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="2.8"
+ inkscape:cx="26.277089"
+ inkscape:cy="74.824155"
+ inkscape:document-units="mm"
+ inkscape:current-layer="layer1"
+ showgrid="true"
+ units="px"
+ showguides="true"
+ inkscape:guide-bbox="true"
+ inkscape:window-width="2160"
+ inkscape:window-height="1311"
+ inkscape:window-x="0"
+ inkscape:window-y="55"
+ inkscape:window-maximized="1">
+ <inkscape:grid
+ type="xygrid"
+ id="grid4518" />
+ <sodipodi:guide
+ position="-7.9374999,13.229167"
+ orientation="1,0"
+ id="guide4542"
+ inkscape:locked="false" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata5">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(0,-244.08332)">
+ <path
+ style="fill:none;stroke:#ffffff;stroke-width:0.5291667px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 0,270.54165 h 52.916667 v 0"
+ id="path4520"
+ inkscape:connector-curvature="0" />
+ <path
+ style="fill:none;stroke:#ffffff;stroke-width:0.5291667px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 26.458334,244.08331 v 52.91667"
+ id="path4522"
+ inkscape:connector-curvature="0" />
+ <ellipse
+ style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:0.5291667;stroke-opacity:1"
+ id="path4530"
+ cx="26.458332"
+ cy="270.54163"
+ rx="5.2916665"
+ ry="5.2916679" />
+ <ellipse
+ style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:1.00000012;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="path4534"
+ cx="26.458332"
+ cy="270.54163"
+ rx="15.875"
+ ry="15.875004" />
+ </g>
+</svg>
diff --git a/panels/wacom/cc-drawing-area.c b/panels/wacom/cc-drawing-area.c
new file mode 100644
index 0000000..6c63597
--- /dev/null
+++ b/panels/wacom/cc-drawing-area.c
@@ -0,0 +1,214 @@
+/*
+ * Copyright © 2016 Red Hat, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * Author: Carlos Garnacho <carlosg@gnome.org>
+ */
+
+#include "config.h"
+#include <cairo/cairo.h>
+#include "cc-drawing-area.h"
+
+typedef struct _CcDrawingArea CcDrawingArea;
+
+struct _CcDrawingArea {
+ GtkEventBox parent;
+ GdkDevice *current_device;
+ cairo_surface_t *surface;
+ cairo_t *cr;
+};
+
+G_DEFINE_TYPE (CcDrawingArea, cc_drawing_area, GTK_TYPE_EVENT_BOX)
+
+static void
+ensure_drawing_surface (CcDrawingArea *area,
+ gint width,
+ gint height)
+{
+ if (!area->surface ||
+ cairo_image_surface_get_width (area->surface) != width ||
+ cairo_image_surface_get_height (area->surface) != height) {
+ cairo_surface_t *surface;
+
+ surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32,
+ width, height);
+ if (area->surface) {
+ cairo_t *cr;
+
+ cr = cairo_create (surface);
+ cairo_set_source_surface (cr, area->surface, 0, 0);
+ cairo_paint (cr);
+
+ cairo_surface_destroy (area->surface);
+ cairo_destroy (area->cr);
+ cairo_destroy (cr);
+ }
+
+ area->surface = surface;
+ area->cr = cairo_create (surface);
+ }
+}
+
+static void
+cc_drawing_area_size_allocate (GtkWidget *widget,
+ GtkAllocation *allocation)
+{
+ CcDrawingArea *area = CC_DRAWING_AREA (widget);
+
+ ensure_drawing_surface (area, allocation->width, allocation->height);
+
+ GTK_WIDGET_CLASS (cc_drawing_area_parent_class)->size_allocate (widget,
+ allocation);
+}
+
+static void
+cc_drawing_area_map (GtkWidget *widget)
+{
+ GtkAllocation allocation;
+
+ GTK_WIDGET_CLASS (cc_drawing_area_parent_class)->map (widget);
+
+ gtk_widget_get_allocation (widget, &allocation);
+ ensure_drawing_surface (CC_DRAWING_AREA (widget),
+ allocation.width, allocation.height);
+}
+
+static void
+cc_drawing_area_unmap (GtkWidget *widget)
+{
+ CcDrawingArea *area = CC_DRAWING_AREA (widget);
+
+ if (area->cr) {
+ cairo_destroy (area->cr);
+ area->cr = NULL;
+ }
+
+ if (area->surface) {
+ cairo_surface_destroy (area->surface);
+ area->surface = NULL;
+ }
+
+ GTK_WIDGET_CLASS (cc_drawing_area_parent_class)->unmap (widget);
+}
+
+static gboolean
+cc_drawing_area_draw (GtkWidget *widget,
+ cairo_t *cr)
+{
+ CcDrawingArea *area = CC_DRAWING_AREA (widget);
+ GtkAllocation allocation;
+
+ GTK_WIDGET_CLASS (cc_drawing_area_parent_class)->draw (widget, cr);
+
+ gtk_widget_get_allocation (widget, &allocation);
+ cairo_set_source_rgb (cr, 1, 1, 1);
+ cairo_paint (cr);
+
+ cairo_set_source_surface (cr, area->surface, 0, 0);
+ cairo_paint (cr);
+
+ cairo_set_source_rgb (cr, 0.6, 0.6, 0.6);
+ cairo_rectangle (cr, 0, 0, allocation.width, allocation.height);
+ cairo_stroke (cr);
+
+ return FALSE;
+}
+
+static gboolean
+cc_drawing_area_event (GtkWidget *widget,
+ GdkEvent *event)
+{
+ CcDrawingArea *area = CC_DRAWING_AREA (widget);
+ GdkInputSource source;
+ GdkDeviceTool *tool;
+ GdkDevice *device;
+
+ device = gdk_event_get_source_device (event);
+
+ if (!device)
+ return GDK_EVENT_PROPAGATE;
+
+ source = gdk_device_get_source (device);
+ tool = gdk_event_get_device_tool (event);
+
+ if (source != GDK_SOURCE_PEN && source != GDK_SOURCE_ERASER)
+ return GDK_EVENT_PROPAGATE;
+
+ if (area->current_device && area->current_device != device)
+ return GDK_EVENT_PROPAGATE;
+
+ if (event->type == GDK_BUTTON_PRESS &&
+ event->button.button == 1 && !area->current_device) {
+ area->current_device = device;
+ } else if (event->type == GDK_BUTTON_RELEASE &&
+ event->button.button == 1 && area->current_device) {
+ cairo_new_path (area->cr);
+ area->current_device = NULL;
+ } else if (event->type == GDK_MOTION_NOTIFY &&
+ event->motion.state & GDK_BUTTON1_MASK) {
+ gdouble x, y, pressure;
+
+ gdk_event_get_coords (event, &x, &y);
+ gdk_event_get_axis (event, GDK_AXIS_PRESSURE, &pressure);
+
+ if (gdk_device_tool_get_tool_type (tool) == GDK_DEVICE_TOOL_TYPE_ERASER) {
+ cairo_set_line_width (area->cr, 10 * pressure);
+ cairo_set_operator (area->cr, CAIRO_OPERATOR_DEST_OUT);
+ } else {
+ cairo_set_line_width (area->cr, 4 * pressure);
+ cairo_set_operator (area->cr, CAIRO_OPERATOR_SATURATE);
+ }
+
+ cairo_set_source_rgba (area->cr, 0, 0, 0, pressure);
+ cairo_line_to (area->cr, x, y);
+ cairo_stroke (area->cr);
+
+ cairo_move_to (area->cr, x, y);
+
+ gtk_widget_queue_draw (widget);
+
+ return GDK_EVENT_STOP;
+ }
+
+ return GDK_EVENT_PROPAGATE;
+}
+
+static void
+cc_drawing_area_class_init (CcDrawingAreaClass *klass)
+{
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ widget_class->size_allocate = cc_drawing_area_size_allocate;
+ widget_class->draw = cc_drawing_area_draw;
+ widget_class->event = cc_drawing_area_event;
+ widget_class->map = cc_drawing_area_map;
+ widget_class->unmap = cc_drawing_area_unmap;
+}
+
+static void
+cc_drawing_area_init (CcDrawingArea *area)
+{
+ gtk_event_box_set_above_child (GTK_EVENT_BOX (area), TRUE);
+ gtk_widget_add_events (GTK_WIDGET (area),
+ GDK_BUTTON_PRESS_MASK |
+ GDK_BUTTON_RELEASE_MASK |
+ GDK_POINTER_MOTION_MASK);
+}
+
+GtkWidget *
+cc_drawing_area_new (void)
+{
+ return g_object_new (CC_TYPE_DRAWING_AREA, NULL);
+}
diff --git a/panels/wacom/cc-drawing-area.h b/panels/wacom/cc-drawing-area.h
new file mode 100644
index 0000000..9b22a42
--- /dev/null
+++ b/panels/wacom/cc-drawing-area.h
@@ -0,0 +1,31 @@
+/*
+ * Copyright © 2016 Red Hat, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * Author: Carlos Garnacho <carlosg@gnome.org>
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define CC_TYPE_DRAWING_AREA (cc_drawing_area_get_type ())
+G_DECLARE_FINAL_TYPE (CcDrawingArea, cc_drawing_area, CC, DRAWING_AREA, GtkEventBox)
+
+GtkWidget *cc_drawing_area_new (void);
+
+G_END_DECLS
diff --git a/panels/wacom/cc-tablet-tool-map.c b/panels/wacom/cc-tablet-tool-map.c
new file mode 100644
index 0000000..bdc51b9
--- /dev/null
+++ b/panels/wacom/cc-tablet-tool-map.c
@@ -0,0 +1,398 @@
+/*
+ * Copyright © 2016 Red Hat, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Carlos Garnacho <carlosg@gnome.org>
+ *
+ */
+
+#include "config.h"
+#include "cc-tablet-tool-map.h"
+
+#define KEY_TOOL_ID "ID"
+#define KEY_DEVICE_STYLI "Styli"
+#define GENERIC_STYLUS "generic"
+
+typedef struct _CcTabletToolMap CcTabletToolMap;
+
+struct _CcTabletToolMap {
+ GObject parent_instance;
+ GKeyFile *tablets;
+ GKeyFile *tools;
+ GHashTable *tool_map;
+ GHashTable *tablet_map;
+ GHashTable *no_serial_tool_map;
+
+ gchar *tablet_path;
+ gchar *tool_path;
+};
+
+G_DEFINE_TYPE (CcTabletToolMap, cc_tablet_tool_map, G_TYPE_OBJECT)
+
+static void
+load_keyfiles (CcTabletToolMap *map)
+{
+ g_autoptr(GError) devices_error = NULL;
+ g_autoptr(GError) tools_error = NULL;
+ g_autofree gchar *dir = NULL;
+
+ dir = g_build_filename (g_get_user_cache_dir (), "gnome-control-center", "wacom", NULL);
+
+ if (g_mkdir_with_parents (dir, 0700) < 0) {
+ g_warning ("Could not create directory '%s', expect stylus mapping oddities: %m", dir);
+ return;
+ }
+
+ map->tablet_path = g_build_filename (dir, "devices", NULL);
+ g_key_file_load_from_file (map->tablets, map->tablet_path,
+ G_KEY_FILE_NONE, &devices_error);
+
+ if (devices_error && !g_error_matches (devices_error, G_FILE_ERROR, G_FILE_ERROR_NOENT)) {
+ g_warning ("Could not load tablets keyfile '%s': %s",
+ map->tablet_path, devices_error->message);
+ }
+
+ map->tool_path = g_build_filename (dir, "tools", NULL);
+ g_key_file_load_from_file (map->tools, map->tool_path,
+ G_KEY_FILE_NONE, &tools_error);
+
+ if (tools_error && !g_error_matches (tools_error, G_FILE_ERROR, G_FILE_ERROR_NOENT)) {
+ g_warning ("Could not load tools keyfile '%s': %s",
+ map->tool_path, tools_error->message);
+ }
+}
+
+static void
+cache_tools (CcTabletToolMap *map)
+{
+ g_auto(GStrv) serials = NULL;
+ gsize n_serials, i;
+
+ serials = g_key_file_get_groups (map->tools, &n_serials);
+
+ for (i = 0; i < n_serials; i++) {
+ g_autofree gchar *str = NULL;
+ gchar *end;
+ guint64 serial, id;
+ g_autoptr(GError) error = NULL;
+ CcWacomTool *tool;
+
+ serial = g_ascii_strtoull (serials[i], &end, 16);
+
+ if (*end != '\0') {
+ g_warning ("Invalid tool serial %s", serials[i]);
+ continue;
+ }
+
+ str = g_key_file_get_string (map->tools, serials[i], KEY_TOOL_ID, &error);
+ if (str == NULL) {
+ g_warning ("Could not get cached ID for tool with serial %s: %s",
+ serials[i], error->message);
+ continue;
+ }
+
+ id = g_ascii_strtoull (str, &end, 16);
+ if (*end != '\0') {
+ g_warning ("Invalid tool ID %s", str);
+ continue;
+ }
+
+ tool = cc_wacom_tool_new (serial, id, NULL);
+ g_hash_table_insert (map->tool_map, g_strdup (serials[i]), tool);
+ }
+}
+
+static void
+cache_devices (CcTabletToolMap *map)
+{
+ gchar **ids;
+ gsize n_ids, i;
+
+ ids = g_key_file_get_groups (map->tablets, &n_ids);
+
+ for (i = 0; i < n_ids; i++) {
+ gchar **styli;
+ gsize n_styli, j;
+ g_autoptr(GError) error = NULL;
+ GList *tools = NULL;
+
+ styli = g_key_file_get_string_list (map->tablets, ids[i], KEY_DEVICE_STYLI, &n_styli, &error);
+ if (styli == NULL) {
+ g_warning ("Could not get cached styli for with ID %s: %s",
+ ids[i], error->message);
+ continue;
+ }
+
+ for (j = 0; j < n_styli; j++) {
+ CcWacomTool *tool;
+
+ if (g_str_equal (styli[j], GENERIC_STYLUS)) {
+ /* We don't have a GsdDevice yet to create the
+ * serial=0 CcWacomTool, insert a NULL and defer
+ * to device lookups.
+ */
+ g_hash_table_insert (map->no_serial_tool_map,
+ g_strdup (ids[i]), NULL);
+ }
+
+ tool = g_hash_table_lookup (map->tool_map, styli[j]);
+
+ if (tool)
+ tools = g_list_prepend (tools, tool);
+ }
+
+ if (tools) {
+ g_hash_table_insert (map->tablet_map, g_strdup (ids[i]), tools);
+ }
+
+ g_strfreev (styli);
+ }
+
+ g_strfreev (ids);
+}
+
+static void
+cc_tablet_tool_map_finalize (GObject *object)
+{
+ CcTabletToolMap *map = CC_TABLET_TOOL_MAP (object);
+
+ g_key_file_unref (map->tools);
+ g_key_file_unref (map->tablets);
+ g_hash_table_destroy (map->tool_map);
+ g_hash_table_destroy (map->tablet_map);
+ g_hash_table_destroy (map->no_serial_tool_map);
+ g_free (map->tablet_path);
+ g_free (map->tool_path);
+
+ G_OBJECT_CLASS (cc_tablet_tool_map_parent_class)->finalize (object);
+}
+
+static void
+null_safe_unref (gpointer data)
+{
+ if (data != NULL)
+ g_object_unref (data);
+}
+
+static void
+cc_tablet_tool_map_init (CcTabletToolMap *map)
+{
+ map->tablets = g_key_file_new ();
+ map->tools = g_key_file_new ();
+ map->tool_map = g_hash_table_new_full (g_str_hash, g_str_equal,
+ (GDestroyNotify) g_free,
+ (GDestroyNotify) g_object_unref);
+ map->tablet_map = g_hash_table_new_full (g_str_hash, g_str_equal,
+ (GDestroyNotify) g_free,
+ (GDestroyNotify) g_list_free);
+ map->no_serial_tool_map = g_hash_table_new_full (g_str_hash, g_str_equal,
+ (GDestroyNotify) g_free,
+ (GDestroyNotify) null_safe_unref);
+ load_keyfiles (map);
+ cache_tools (map);
+ cache_devices (map);
+}
+
+static void
+cc_tablet_tool_map_class_init (CcTabletToolMapClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->finalize = cc_tablet_tool_map_finalize;
+}
+
+CcTabletToolMap *
+cc_tablet_tool_map_new (void)
+{
+ return g_object_new (CC_TYPE_TABLET_TOOL_MAP, NULL);
+}
+
+static gchar *
+get_device_key (CcWacomDevice *device)
+{
+ const gchar *vendor, *product;
+ GsdDevice *gsd_device;
+
+ gsd_device = cc_wacom_device_get_device (device);
+ gsd_device_get_device_ids (gsd_device, &vendor, &product);
+
+ return g_strdup_printf ("%s:%s", vendor, product);
+}
+
+static gchar *
+get_tool_key (guint64 serial)
+{
+ return g_strdup_printf ("%lx", serial);
+}
+
+GList *
+cc_tablet_tool_map_list_tools (CcTabletToolMap *map,
+ CcWacomDevice *device)
+{
+ CcWacomTool *no_serial_tool;
+ GList *styli;
+ g_autofree gchar *key = NULL;
+
+ g_return_val_if_fail (CC_IS_TABLET_TOOL_MAP (map), NULL);
+ g_return_val_if_fail (CC_IS_WACOM_DEVICE (device), NULL);
+
+ key = get_device_key (device);
+ styli = g_list_copy (g_hash_table_lookup (map->tablet_map, key));
+
+ if (g_hash_table_lookup_extended (map->no_serial_tool_map, key,
+ NULL, (gpointer) &no_serial_tool)) {
+ if (!no_serial_tool) {
+ no_serial_tool = cc_wacom_tool_new (0, 0, device);
+ g_hash_table_replace (map->no_serial_tool_map,
+ g_strdup (key),
+ no_serial_tool);
+ }
+
+ styli = g_list_prepend (styli, no_serial_tool);
+ }
+
+ return styli;
+}
+
+CcWacomTool *
+cc_tablet_tool_map_lookup_tool (CcTabletToolMap *map,
+ CcWacomDevice *device,
+ guint64 serial)
+{
+ CcWacomTool *tool = NULL;
+ g_autofree gchar *key = NULL;
+
+ g_return_val_if_fail (CC_IS_TABLET_TOOL_MAP (map), FALSE);
+ g_return_val_if_fail (CC_IS_WACOM_DEVICE (device), FALSE);
+
+ if (serial == 0) {
+ key = get_device_key (device);
+ tool = g_hash_table_lookup (map->no_serial_tool_map, key);
+ } else {
+ key = get_tool_key (serial);
+ tool = g_hash_table_lookup (map->tool_map, key);
+ }
+
+ return tool;
+}
+
+static void
+keyfile_add_device_stylus (CcTabletToolMap *map,
+ const gchar *device_key,
+ const gchar *tool_key)
+{
+ g_autoptr(GArray) array = NULL;
+ g_auto(GStrv) styli = NULL;
+ gsize n_styli;
+
+ array = g_array_new (FALSE, FALSE, sizeof (gchar *));
+ styli = g_key_file_get_string_list (map->tablets, device_key,
+ KEY_DEVICE_STYLI, &n_styli,
+ NULL);
+
+ if (styli) {
+ g_array_append_vals (array, styli, n_styli);
+ }
+
+ g_array_append_val (array, tool_key);
+ g_key_file_set_string_list (map->tablets, device_key, KEY_DEVICE_STYLI,
+ (const gchar **) array->data, array->len);
+}
+
+static void
+keyfile_add_stylus (CcTabletToolMap *map,
+ const gchar *tool_key,
+ guint64 id)
+{
+ g_autofree gchar *str = NULL;
+
+ /* Also works for IDs */
+ str = get_tool_key (id);
+ g_key_file_set_string (map->tools, tool_key, KEY_TOOL_ID, str);
+}
+
+void
+cc_tablet_tool_map_add_relation (CcTabletToolMap *map,
+ CcWacomDevice *device,
+ CcWacomTool *tool)
+{
+ gboolean tablets_changed = FALSE, tools_changed = FALSE;
+ gboolean new_tool_without_serial = FALSE;
+ g_autofree gchar *tool_key = NULL;
+ g_autofree gchar *device_key = NULL;
+ guint64 serial, id;
+ GList *styli;
+
+ g_return_if_fail (CC_IS_TABLET_TOOL_MAP (map));
+ g_return_if_fail (CC_IS_WACOM_DEVICE (device));
+ g_return_if_fail (CC_IS_WACOM_TOOL (tool));
+
+ serial = cc_wacom_tool_get_serial (tool);
+ id = cc_wacom_tool_get_id (tool);
+ device_key = get_device_key (device);
+
+ if (serial == 0) {
+ tool_key = g_strdup (GENERIC_STYLUS);
+
+ if (!g_hash_table_contains (map->no_serial_tool_map, device_key)) {
+ g_hash_table_insert (map->no_serial_tool_map,
+ g_strdup (device_key),
+ g_object_ref (tool));
+ new_tool_without_serial = TRUE;
+ }
+ } else {
+ tool_key = get_tool_key (serial);
+
+ if (!g_hash_table_contains (map->tool_map, tool_key)) {
+ keyfile_add_stylus (map, tool_key, id);
+ tools_changed = TRUE;
+ g_hash_table_insert (map->tool_map,
+ g_strdup (tool_key),
+ g_object_ref (tool));
+ }
+ }
+
+ styli = g_hash_table_lookup (map->tablet_map, device_key);
+
+ if (!g_list_find (styli, tool)) {
+ styli = g_list_prepend (styli, tool);
+ g_hash_table_replace (map->tablet_map,
+ g_strdup (device_key),
+ g_list_copy (styli));
+
+ if (serial || new_tool_without_serial) {
+ tablets_changed = TRUE;
+ keyfile_add_device_stylus (map, device_key, tool_key);
+ }
+ }
+
+ if (tools_changed) {
+ g_autoptr(GError) error = NULL;
+
+ if (!g_key_file_save_to_file (map->tools, map->tool_path, &error)) {
+ g_warning ("Error saving tools keyfile: %s",
+ error->message);
+ }
+ }
+
+ if (tablets_changed) {
+ g_autoptr(GError) error = NULL;
+
+ if (!g_key_file_save_to_file (map->tablets, map->tablet_path, &error)) {
+ g_warning ("Error saving tablets keyfile: %s",
+ error->message);
+ }
+ }
+}
diff --git a/panels/wacom/cc-tablet-tool-map.h b/panels/wacom/cc-tablet-tool-map.h
new file mode 100644
index 0000000..a65eb2a
--- /dev/null
+++ b/panels/wacom/cc-tablet-tool-map.h
@@ -0,0 +1,44 @@
+/*
+ * Copyright © 2016 Red Hat, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Carlos Garnacho <carlosg@gnome.org>
+ *
+ */
+
+#pragma once
+
+#include "config.h"
+#include <gtk/gtk.h>
+#include "cc-wacom-device.h"
+#include "cc-wacom-tool.h"
+
+G_BEGIN_DECLS
+
+#define CC_TYPE_TABLET_TOOL_MAP (cc_tablet_tool_map_get_type ())
+G_DECLARE_FINAL_TYPE (CcTabletToolMap, cc_tablet_tool_map, CC, TABLET_TOOL_MAP, GObject)
+
+CcTabletToolMap * cc_tablet_tool_map_new (void);
+
+GList * cc_tablet_tool_map_list_tools (CcTabletToolMap *map,
+ CcWacomDevice *device);
+CcWacomTool * cc_tablet_tool_map_lookup_tool (CcTabletToolMap *map,
+ CcWacomDevice *device,
+ guint64 serial);
+void cc_tablet_tool_map_add_relation (CcTabletToolMap *map,
+ CcWacomDevice *device,
+ CcWacomTool *tool);
+
+G_END_DECLS
diff --git a/panels/wacom/cc-wacom-button-row.c b/panels/wacom/cc-wacom-button-row.c
new file mode 100644
index 0000000..b3ae07e
--- /dev/null
+++ b/panels/wacom/cc-wacom-button-row.c
@@ -0,0 +1,280 @@
+/*
+ * Copyright © 2013 Red Hat, Inc.
+ *
+ * Authors: Joaquim Rocha <jrocha@redhat.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <config.h>
+#include <glib/gi18n-lib.h>
+
+#include "gsd-wacom-key-shortcut-button.h"
+#include "cc-wacom-button-row.h"
+
+#define ACTION_KEY "action"
+#define KEYBINDING_KEY "keybinding"
+
+#define WACOM_C(x) g_dpgettext2(NULL, "Wacom action-type", x)
+
+enum {
+ ACTION_NAME_COLUMN,
+ ACTION_TYPE_COLUMN,
+ ACTION_N_COLUMNS
+};
+
+struct _CcWacomButtonRow {
+ GtkListBoxRow parent_instance;
+ guint button;
+ GSettings *settings;
+ GtkDirectionType direction;
+ GtkComboBox *action_combo;
+ GsdWacomKeyShortcutButton *key_shortcut_button;
+};
+
+G_DEFINE_TYPE (CcWacomButtonRow, cc_wacom_button_row, GTK_TYPE_LIST_BOX_ROW)
+
+static GtkWidget *
+create_actions_combo (void)
+{
+ GtkListStore *model;
+ GtkTreeIter iter;
+ GtkWidget *combo;
+ GtkCellRenderer *renderer;
+ gint i;
+
+ model = gtk_list_store_new (ACTION_N_COLUMNS, G_TYPE_STRING, G_TYPE_INT);
+
+ for (i = 0; i < G_N_ELEMENTS (action_table); i++)
+ {
+ gtk_list_store_append (model, &iter);
+ gtk_list_store_set (model, &iter,
+ ACTION_NAME_COLUMN, WACOM_C(action_table[i].action_name),
+ ACTION_TYPE_COLUMN, action_table[i].action_type, -1);
+ }
+
+ combo = gtk_combo_box_new_with_model (GTK_TREE_MODEL (model));
+
+ renderer = gtk_cell_renderer_text_new ();
+ gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (combo), renderer, TRUE);
+ gtk_cell_layout_set_attributes (GTK_CELL_LAYOUT (combo), renderer,
+ "text", ACTION_NAME_COLUMN, NULL);
+
+
+ return combo;
+}
+
+static void
+cc_wacom_button_row_update_shortcut (CcWacomButtonRow *row,
+ GDesktopPadButtonAction action_type)
+{
+ guint keyval;
+ GdkModifierType mask;
+ g_autofree gchar *shortcut = NULL;
+
+ if (action_type != G_DESKTOP_PAD_BUTTON_ACTION_KEYBINDING)
+ return;
+
+ shortcut = g_settings_get_string (row->settings, KEYBINDING_KEY);
+
+ if (shortcut != NULL)
+ {
+ gtk_accelerator_parse (shortcut, &keyval, &mask);
+
+ g_object_set (row->key_shortcut_button,
+ "key-value", keyval,
+ "key-mods", mask,
+ NULL);
+ }
+}
+
+static void
+cc_wacom_button_row_update_action (CcWacomButtonRow *row,
+ GDesktopPadButtonAction action_type)
+{
+ GtkTreeIter iter;
+ gboolean iter_valid;
+ GDesktopPadButtonAction current_action_type, real_action_type;
+ GtkTreeModel *model;
+
+ model = gtk_combo_box_get_model (row->action_combo);
+ real_action_type = action_type;
+
+ for (iter_valid = gtk_tree_model_get_iter_first (model, &iter); iter_valid;
+ iter_valid = gtk_tree_model_iter_next (model, &iter))
+ {
+ gtk_tree_model_get (model, &iter,
+ ACTION_TYPE_COLUMN, &current_action_type,
+ -1);
+
+ if (current_action_type == real_action_type)
+ {
+ gtk_combo_box_set_active_iter (row->action_combo, &iter);
+ break;
+ }
+ }
+}
+
+static void
+cc_wacom_button_row_update (CcWacomButtonRow *row)
+{
+ GDesktopPadButtonAction current_action_type;
+
+ current_action_type = g_settings_get_enum (row->settings, ACTION_KEY);
+
+ cc_wacom_button_row_update_shortcut (row, current_action_type);
+
+ cc_wacom_button_row_update_action (row, current_action_type);
+
+ gtk_widget_set_sensitive (GTK_WIDGET (row->key_shortcut_button),
+ current_action_type == G_DESKTOP_PAD_BUTTON_ACTION_KEYBINDING);
+}
+
+static void
+change_button_action_type (CcWacomButtonRow *row,
+ GDesktopPadButtonAction type)
+{
+ g_settings_set_enum (row->settings, ACTION_KEY, type);
+ gtk_widget_set_sensitive (GTK_WIDGET (row->key_shortcut_button),
+ type == G_DESKTOP_PAD_BUTTON_ACTION_KEYBINDING);
+}
+
+static void
+on_key_shortcut_edited (CcWacomButtonRow *row)
+{
+ g_autofree gchar *custom_key = NULL;
+ guint keyval;
+ GdkModifierType mask;
+
+ change_button_action_type (row, G_DESKTOP_PAD_BUTTON_ACTION_KEYBINDING);
+
+ g_object_get (row->key_shortcut_button,
+ "key-value", &keyval,
+ "key-mods", &mask,
+ NULL);
+
+ mask &= ~GDK_LOCK_MASK;
+
+ custom_key = gtk_accelerator_name (keyval, mask);
+
+ g_settings_set_string (row->settings, KEYBINDING_KEY, custom_key);
+}
+
+static void
+on_key_shortcut_cleared (CcWacomButtonRow *row)
+{
+ change_button_action_type (row, G_DESKTOP_PAD_BUTTON_ACTION_NONE);
+ cc_wacom_button_row_update_action (row, G_DESKTOP_PAD_BUTTON_ACTION_NONE);
+}
+
+static void
+on_row_action_combo_box_changed (CcWacomButtonRow *row)
+{
+ GDesktopPadButtonAction type;
+ GtkTreeModel *model;
+ GtkListBox *list_box;
+ GtkTreeIter iter;
+
+ if (!gtk_combo_box_get_active_iter (row->action_combo, &iter))
+ return;
+
+ /* Select the row where we changed the combo box (if not yet selected) */
+ list_box = GTK_LIST_BOX (gtk_widget_get_parent (GTK_WIDGET (row)));
+ if (list_box && gtk_list_box_get_selected_row (list_box) != GTK_LIST_BOX_ROW (row))
+ gtk_list_box_select_row (list_box, GTK_LIST_BOX_ROW (row));
+
+ model = gtk_combo_box_get_model (row->action_combo);
+ gtk_tree_model_get (model, &iter, ACTION_TYPE_COLUMN, &type, -1);
+
+ change_button_action_type (row, type);
+}
+
+static gboolean
+on_key_shortcut_button_press_event (CcWacomButtonRow *row)
+{
+ GtkListBox *list_box;
+
+ /* Select the row where we pressed the button (if not yet selected) */
+ list_box = GTK_LIST_BOX (gtk_widget_get_parent (GTK_WIDGET (row)));
+ if (list_box && gtk_list_box_get_selected_row (list_box) != GTK_LIST_BOX_ROW (row))
+ gtk_list_box_select_row (list_box, GTK_LIST_BOX_ROW (row));
+
+ return FALSE;
+}
+
+static void
+cc_wacom_button_row_class_init (CcWacomButtonRowClass *button_row_class)
+{
+}
+
+static void
+cc_wacom_button_row_init (CcWacomButtonRow *button_row)
+{
+}
+
+GtkWidget *
+cc_wacom_button_row_new (guint button,
+ GSettings *settings)
+{
+ CcWacomButtonRow *row;
+ GtkWidget *grid, *combo, *label, *shortcut_button;
+ g_autofree gchar *name = NULL;
+
+ row = CC_WACOM_BUTTON_ROW (g_object_new (CC_WACOM_TYPE_BUTTON_ROW, NULL));
+
+ row->button = button;
+ row->settings = g_object_ref (settings);
+
+ grid = gtk_grid_new ();
+ gtk_widget_show (grid);
+ gtk_grid_set_row_homogeneous (GTK_GRID (grid), TRUE);
+ gtk_grid_set_column_homogeneous (GTK_GRID (grid), TRUE);
+
+ name = g_strdup_printf (_("Button %d"), button);
+ label = gtk_label_new (name);
+ g_object_set (label, "halign", GTK_ALIGN_START, NULL);
+ gtk_grid_attach (GTK_GRID (grid), label, 0, 0, 1, 1);
+ gtk_widget_show (label);
+
+ combo = create_actions_combo ();
+ gtk_grid_attach (GTK_GRID (grid), combo, 1, 0, 1, 1);
+ gtk_widget_show (combo);
+ row->action_combo = GTK_COMBO_BOX (combo);
+ g_signal_connect_object (combo, "changed",
+ G_CALLBACK (on_row_action_combo_box_changed), row, G_CONNECT_SWAPPED);
+
+ shortcut_button = gsd_wacom_key_shortcut_button_new ();
+ g_object_set (shortcut_button, "mode", GSD_WACOM_KEY_SHORTCUT_BUTTON_MODE_ALL, NULL);
+ gtk_grid_attach (GTK_GRID (grid), shortcut_button, 2, 0, 1, 1);
+ gtk_widget_show (shortcut_button);
+ row->key_shortcut_button = GSD_WACOM_KEY_SHORTCUT_BUTTON (shortcut_button);
+ g_signal_connect_object (shortcut_button, "key-shortcut-cleared",
+ G_CALLBACK (on_key_shortcut_cleared),
+ row,
+ G_CONNECT_SWAPPED);
+ g_signal_connect_object (shortcut_button, "key-shortcut-edited",
+ G_CALLBACK (on_key_shortcut_edited),
+ row,
+ G_CONNECT_SWAPPED);
+ g_signal_connect_object (shortcut_button, "button-press-event",
+ G_CALLBACK (on_key_shortcut_button_press_event),
+ row,
+ G_CONNECT_SWAPPED);
+
+ gtk_container_add (GTK_CONTAINER (row), grid);
+
+ cc_wacom_button_row_update (CC_WACOM_BUTTON_ROW (row));
+
+ return GTK_WIDGET (row);
+}
diff --git a/panels/wacom/cc-wacom-button-row.h b/panels/wacom/cc-wacom-button-row.h
new file mode 100644
index 0000000..7a30d11
--- /dev/null
+++ b/panels/wacom/cc-wacom-button-row.h
@@ -0,0 +1,43 @@
+/*
+ * Copyright © 2013 Red Hat, Inc.
+ *
+ * Authors: Joaquim Rocha <jrocha@redhat.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+#include <gdesktop-enums.h>
+
+G_BEGIN_DECLS
+
+#define CC_WACOM_TYPE_BUTTON_ROW (cc_wacom_button_row_get_type ())
+G_DECLARE_FINAL_TYPE (CcWacomButtonRow, cc_wacom_button_row, CC, WACOM_BUTTON_ROW, GtkListBoxRow)
+
+static struct {
+ GDesktopPadButtonAction action_type;
+ const gchar *action_name;
+} action_table[] = {
+ { G_DESKTOP_PAD_BUTTON_ACTION_NONE, NC_("Wacom action-type", "Application defined") },
+ { G_DESKTOP_PAD_BUTTON_ACTION_KEYBINDING, NC_("Wacom action-type", "Send Keystroke") },
+ { G_DESKTOP_PAD_BUTTON_ACTION_SWITCH_MONITOR, NC_("Wacom action-type", "Switch Monitor") },
+ { G_DESKTOP_PAD_BUTTON_ACTION_HELP, NC_("Wacom action-type", "Show On-Screen Help") }
+};
+
+GtkWidget * cc_wacom_button_row_new (guint button,
+ GSettings *settings);
+
+G_END_DECLS
diff --git a/panels/wacom/cc-wacom-device.c b/panels/wacom/cc-wacom-device.c
new file mode 100644
index 0000000..4a3f980
--- /dev/null
+++ b/panels/wacom/cc-wacom-device.c
@@ -0,0 +1,406 @@
+/*
+ * Copyright © 2016 Red Hat, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Carlos Garnacho <carlosg@gnome.org>
+ *
+ */
+
+#include "config.h"
+
+#include <string.h>
+#include "cc-wacom-device.h"
+
+enum {
+ PROP_0,
+ PROP_DEVICE,
+ N_PROPS
+};
+
+GParamSpec *props[N_PROPS] = { 0 };
+
+typedef struct _CcWacomDevice CcWacomDevice;
+
+struct _CcWacomDevice {
+ GObject parent_instance;
+
+ GsdDevice *device;
+ WacomDevice *wdevice;
+};
+
+static void cc_wacom_device_initable_iface_init (GInitableIface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (CcWacomDevice, cc_wacom_device, G_TYPE_OBJECT,
+ G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE,
+ cc_wacom_device_initable_iface_init))
+
+WacomDeviceDatabase *
+cc_wacom_device_database_get (void)
+{
+ static WacomDeviceDatabase *db = NULL;
+
+ if (g_once_init_enter (&db)) {
+ gpointer p = libwacom_database_new ();
+ g_once_init_leave (&db, p);
+ }
+
+ return db;
+}
+
+static void
+cc_wacom_device_init (CcWacomDevice *device)
+{
+}
+
+static void
+cc_wacom_device_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ CcWacomDevice *device = CC_WACOM_DEVICE (object);
+
+ switch (prop_id) {
+ case PROP_DEVICE:
+ device->device = g_value_get_object (value);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+cc_wacom_device_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ CcWacomDevice *device = CC_WACOM_DEVICE (object);
+
+ switch (prop_id) {
+ case PROP_DEVICE:
+ g_value_set_object (value, device->device);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+cc_wacom_device_finalize (GObject *object)
+{
+ CcWacomDevice *device = CC_WACOM_DEVICE (object);
+
+ g_clear_pointer (&device->wdevice, libwacom_destroy);
+
+ G_OBJECT_CLASS (cc_wacom_device_parent_class)->finalize (object);
+}
+
+static void
+cc_wacom_device_class_init (CcWacomDeviceClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->set_property = cc_wacom_device_set_property;
+ object_class->get_property = cc_wacom_device_get_property;
+ object_class->finalize = cc_wacom_device_finalize;
+
+ props[PROP_DEVICE] =
+ g_param_spec_object ("device",
+ "device",
+ "device",
+ GSD_TYPE_DEVICE,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
+
+ g_object_class_install_properties (object_class, N_PROPS, props);
+}
+
+static gboolean
+cc_wacom_device_initable_init (GInitable *initable,
+ GCancellable *cancellable,
+ GError **error)
+{
+ CcWacomDevice *device = CC_WACOM_DEVICE (initable);
+ WacomDeviceDatabase *wacom_db;
+ const gchar *node_path;
+
+ wacom_db = cc_wacom_device_database_get ();
+ node_path = gsd_device_get_device_file (device->device);
+ device->wdevice = libwacom_new_from_path (wacom_db, node_path, FALSE, NULL);
+
+ if (!device->wdevice) {
+ g_set_error (error, 0, 0, "Tablet description not found");
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static void
+cc_wacom_device_initable_iface_init (GInitableIface *iface)
+{
+ iface->init = cc_wacom_device_initable_init;
+}
+
+CcWacomDevice *
+cc_wacom_device_new (GsdDevice *device)
+{
+ return g_initable_new (CC_TYPE_WACOM_DEVICE,
+ NULL, NULL,
+ "device", device,
+ NULL);
+}
+
+CcWacomDevice *
+cc_wacom_device_new_fake (const gchar *name)
+{
+ CcWacomDevice *device;
+ WacomDevice *wacom_device;
+
+ device = g_object_new (CC_TYPE_WACOM_DEVICE,
+ NULL);
+
+ wacom_device = libwacom_new_from_name (cc_wacom_device_database_get(),
+ name, NULL);
+ if (wacom_device == NULL)
+ return NULL;
+
+ device->wdevice = wacom_device;
+
+ return device;
+}
+
+const gchar *
+cc_wacom_device_get_name (CcWacomDevice *device)
+{
+ g_return_val_if_fail (CC_IS_WACOM_DEVICE (device), NULL);
+
+ return libwacom_get_name (device->wdevice);
+}
+
+const gchar *
+cc_wacom_device_get_icon_name (CcWacomDevice *device)
+{
+ WacomIntegrationFlags integration_flags;
+
+ g_return_val_if_fail (CC_IS_WACOM_DEVICE (device), NULL);
+
+ integration_flags = libwacom_get_integration_flags (device->wdevice);
+
+ if (integration_flags & WACOM_DEVICE_INTEGRATED_SYSTEM) {
+ return "wacom-tablet-pc";
+ } else if (integration_flags & WACOM_DEVICE_INTEGRATED_DISPLAY) {
+ return "wacom-tablet-cintiq";
+ } else {
+ return "wacom-tablet";
+ }
+}
+
+gboolean
+cc_wacom_device_is_reversible (CcWacomDevice *device)
+{
+ g_return_val_if_fail (CC_IS_WACOM_DEVICE (device), FALSE);
+
+ return libwacom_is_reversible (device->wdevice);
+}
+
+WacomIntegrationFlags
+cc_wacom_device_get_integration_flags (CcWacomDevice *device)
+{
+ g_return_val_if_fail (CC_IS_WACOM_DEVICE (device), 0);
+
+ return libwacom_get_integration_flags (device->wdevice);
+}
+
+GsdDevice *
+cc_wacom_device_get_device (CcWacomDevice *device)
+{
+ g_return_val_if_fail (CC_IS_WACOM_DEVICE (device), NULL);
+
+ return device->device;
+}
+
+GSettings *
+cc_wacom_device_get_settings (CcWacomDevice *device)
+{
+ g_return_val_if_fail (CC_IS_WACOM_DEVICE (device), NULL);
+
+ return gsd_device_get_settings (device->device);
+}
+
+const gint *
+cc_wacom_device_get_supported_tools (CcWacomDevice *device,
+ gint *n_tools)
+{
+ *n_tools = 0;
+
+ g_return_val_if_fail (CC_IS_WACOM_DEVICE (device), NULL);
+
+ return libwacom_get_supported_styli (device->wdevice, n_tools);
+}
+
+static GnomeRROutput *
+find_output_by_edid (GnomeRRScreen *rr_screen,
+ const gchar *vendor,
+ const gchar *product,
+ const gchar *serial)
+{
+ GnomeRROutput **rr_outputs;
+ GnomeRROutput *retval = NULL;
+ guint i;
+
+ rr_outputs = gnome_rr_screen_list_outputs (rr_screen);
+
+ for (i = 0; rr_outputs[i] != NULL; i++) {
+ g_autofree gchar *o_vendor = NULL;
+ g_autofree gchar *o_product = NULL;
+ g_autofree gchar *o_serial = NULL;
+ gboolean match;
+
+ gnome_rr_output_get_ids_from_edid (rr_outputs[i],
+ &o_vendor,
+ &o_product,
+ &o_serial);
+
+ g_debug ("Checking for match between '%s','%s','%s' and '%s','%s','%s'", \
+ vendor, product, serial, o_vendor, o_product, o_serial);
+
+ match = (g_strcmp0 (vendor, o_vendor) == 0) && \
+ (g_strcmp0 (product, o_product) == 0) && \
+ (g_strcmp0 (serial, o_serial) == 0);
+
+ if (match) {
+ retval = rr_outputs[i];
+ break;
+ }
+ }
+
+ if (retval == NULL)
+ g_debug ("Did not find a matching output for EDID '%s,%s,%s'",
+ vendor, product, serial);
+
+ return retval;
+}
+
+static GnomeRROutput *
+find_output (GnomeRRScreen *rr_screen,
+ CcWacomDevice *device)
+{
+ g_autoptr(GSettings) settings = NULL;
+ g_autoptr(GVariant) variant = NULL;
+ g_autofree const gchar **edid = NULL;
+ gsize n;
+
+ settings = cc_wacom_device_get_settings (device);
+ variant = g_settings_get_value (settings, "output");
+ edid = g_variant_get_strv (variant, &n);
+
+ if (n != 3) {
+ g_critical ("Expected 'output' key to store %d values; got %"G_GSIZE_FORMAT".", 3, n);
+ return NULL;
+ }
+
+ if (strlen (edid[0]) == 0 || strlen (edid[1]) == 0 || strlen (edid[2]) == 0)
+ return NULL;
+
+ return find_output_by_edid (rr_screen, edid[0], edid[1], edid[2]);
+}
+
+GnomeRROutput *
+cc_wacom_device_get_output (CcWacomDevice *device,
+ GnomeRRScreen *rr_screen)
+{
+ GnomeRROutput *rr_output;
+ GnomeRRCrtc *crtc;
+
+ g_return_val_if_fail (CC_IS_WACOM_DEVICE (device), NULL);
+ g_return_val_if_fail (GNOME_IS_RR_SCREEN (rr_screen), NULL);
+
+ rr_output = find_output (rr_screen, device);
+ if (rr_output == NULL) {
+ return NULL;
+ }
+
+ crtc = gnome_rr_output_get_crtc (rr_output);
+
+ if (!crtc || gnome_rr_crtc_get_current_mode (crtc) == NULL) {
+ g_debug ("Output is not active.");
+ return NULL;
+ }
+
+ return rr_output;
+}
+
+void
+cc_wacom_device_set_output (CcWacomDevice *device,
+ GnomeRROutput *output)
+{
+ g_autoptr(GSettings) settings = NULL;
+ g_autofree gchar *vendor = NULL;
+ g_autofree gchar *product = NULL;
+ g_autofree gchar *serial = NULL;
+ const gchar *values[] = { "", "", "", NULL };
+
+ g_return_if_fail (CC_IS_WACOM_DEVICE (device));
+
+ vendor = product = serial = NULL;
+ settings = cc_wacom_device_get_settings (device);
+
+ if (output != NULL) {
+ gnome_rr_output_get_ids_from_edid (output,
+ &vendor,
+ &product,
+ &serial);
+ values[0] = vendor;
+ values[1] = product;
+ values[2] = serial;
+ }
+
+ g_settings_set_strv (settings, "output", values);
+}
+
+guint
+cc_wacom_device_get_num_buttons (CcWacomDevice *device)
+{
+ g_return_val_if_fail (CC_IS_WACOM_DEVICE (device), 0);
+
+ return libwacom_get_num_buttons (device->wdevice);
+}
+
+GSettings *
+cc_wacom_device_get_button_settings (CcWacomDevice *device,
+ guint button)
+{
+ g_autoptr(GSettings) tablet_settings = NULL;
+ GSettings *settings;
+ g_autofree gchar *path = NULL;
+ g_autofree gchar *button_path = NULL;
+
+ g_return_val_if_fail (CC_IS_WACOM_DEVICE (device), NULL);
+
+ if (button > cc_wacom_device_get_num_buttons (device))
+ return NULL;
+
+ tablet_settings = cc_wacom_device_get_settings (device);
+ g_object_get (tablet_settings, "path", &path, NULL);
+
+ button_path = g_strdup_printf ("%sbutton%c/", path, 'A' + button);
+ settings = g_settings_new_with_path ("org.gnome.desktop.peripherals.tablet.pad-button",
+ button_path);
+
+ return settings;
+}
diff --git a/panels/wacom/cc-wacom-device.h b/panels/wacom/cc-wacom-device.h
new file mode 100644
index 0000000..fae504a
--- /dev/null
+++ b/panels/wacom/cc-wacom-device.h
@@ -0,0 +1,63 @@
+/*
+ * Copyright © 2016 Red Hat, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Carlos Garnacho <carlosg@gnome.org>
+ *
+ */
+
+#pragma once
+
+#include "config.h"
+#include <glib-object.h>
+#include <libwacom/libwacom.h>
+
+#include "gsd-device-manager.h"
+
+#define GNOME_DESKTOP_USE_UNSTABLE_API
+#include <libgnome-desktop/gnome-rr.h>
+
+#define CC_TYPE_WACOM_DEVICE (cc_wacom_device_get_type ())
+G_DECLARE_FINAL_TYPE (CcWacomDevice, cc_wacom_device, CC, WACOM_DEVICE, GObject)
+
+WacomDeviceDatabase *
+ cc_wacom_device_database_get (void);
+
+CcWacomDevice * cc_wacom_device_new (GsdDevice *device);
+CcWacomDevice * cc_wacom_device_new_fake (const gchar *name);
+
+const gchar * cc_wacom_device_get_name (CcWacomDevice *device);
+const gchar * cc_wacom_device_get_icon_name (CcWacomDevice *device);
+
+gboolean cc_wacom_device_is_reversible (CcWacomDevice *device);
+
+WacomIntegrationFlags
+ cc_wacom_device_get_integration_flags (CcWacomDevice *device);
+
+GsdDevice * cc_wacom_device_get_device (CcWacomDevice *device);
+GSettings * cc_wacom_device_get_settings (CcWacomDevice *device);
+
+const gint * cc_wacom_device_get_supported_tools (CcWacomDevice *device,
+ gint *n_tools);
+
+GnomeRROutput * cc_wacom_device_get_output (CcWacomDevice *device,
+ GnomeRRScreen *screen);
+void cc_wacom_device_set_output (CcWacomDevice *wacom_device,
+ GnomeRROutput *monitor);
+
+guint cc_wacom_device_get_num_buttons (CcWacomDevice *wacom_device);
+
+GSettings * cc_wacom_device_get_button_settings (CcWacomDevice *device,
+ guint button);
diff --git a/panels/wacom/cc-wacom-mapping-panel.c b/panels/wacom/cc-wacom-mapping-panel.c
new file mode 100644
index 0000000..2b8b012
--- /dev/null
+++ b/panels/wacom/cc-wacom-mapping-panel.c
@@ -0,0 +1,338 @@
+/*
+ * Copyright © 2012 Wacom.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Jason Gerecke <killertofu@gmail.com>
+ *
+ */
+
+#include <config.h>
+
+#include <gtk/gtk.h>
+#include <glib/gi18n.h>
+#define GNOME_DESKTOP_USE_UNSTABLE_API
+#include <libgnome-desktop/gnome-rr.h>
+#include <libgnome-desktop/gnome-rr-config.h>
+
+#include <string.h>
+
+#include "cc-wacom-device.h"
+#include "cc-wacom-mapping-panel.h"
+
+struct _CcWacomMappingPanel
+{
+ GtkBox parent_instance;
+
+ CcWacomDevice *device;
+ GtkWidget *label;
+ GtkWidget *combobox;
+ GtkWidget *checkbutton;
+ GtkWidget *aspectlabel;
+ GtkWidget *aspectswitch;
+
+ GnomeRRScreen *rr_screen;
+};
+
+G_DEFINE_TYPE (CcWacomMappingPanel, cc_wacom_mapping_panel, GTK_TYPE_BOX)
+
+enum {
+ MONITOR_NAME_COLUMN,
+ MONITOR_PTR_COLUMN,
+ MONITOR_NUM_COLUMNS
+};
+
+static void combobox_changed_cb (CcWacomMappingPanel *self);
+static void checkbutton_toggled_cb (CcWacomMappingPanel *self);
+static void aspectswitch_toggled_cb (CcWacomMappingPanel *self);
+
+static void
+set_combobox_sensitive (CcWacomMappingPanel *self,
+ gboolean sensitive)
+{
+ gtk_widget_set_sensitive (GTK_WIDGET(self->combobox), sensitive);
+ gtk_widget_set_sensitive (GTK_WIDGET(self->label), sensitive);
+ gtk_widget_set_sensitive (GTK_WIDGET(self->aspectswitch), sensitive);
+ gtk_widget_set_sensitive (GTK_WIDGET(self->aspectlabel), sensitive);
+}
+
+/* Update the display of available monitors based on the latest
+ * information from RandR. At the moment the chooser is just a
+ * a combobox crudely listing available outputs. The UI mockup
+ * has something more akin to the Display panel, with the ability
+ * to do rubber-band selection of multiple outputs (note: the
+ * g-s-d backend can only handle a single output at the moment)
+ */
+static void
+update_monitor_chooser (CcWacomMappingPanel *self)
+{
+ g_autoptr(GtkListStore) store = NULL;
+ GnomeRROutput **outputs;
+ GSettings *settings;
+ GnomeRROutput *cur_output;
+ guint i;
+
+ store = gtk_list_store_new (MONITOR_NUM_COLUMNS, G_TYPE_STRING, G_TYPE_POINTER);
+ gtk_combo_box_set_model (GTK_COMBO_BOX(self->combobox), GTK_TREE_MODEL(store));
+
+ if (self->device == NULL) {
+ set_combobox_sensitive (self, FALSE);
+ return;
+ }
+
+ settings = cc_wacom_device_get_settings (self->device);
+ cur_output = cc_wacom_device_get_output (self->device,
+ self->rr_screen);
+
+ g_signal_handlers_block_by_func (G_OBJECT (self->checkbutton), checkbutton_toggled_cb, self);
+ gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON(self->checkbutton), cur_output != NULL);
+ g_signal_handlers_unblock_by_func (G_OBJECT (self->checkbutton), checkbutton_toggled_cb, self);
+
+ g_signal_handlers_block_by_func (G_OBJECT (self->aspectswitch), aspectswitch_toggled_cb, self);
+ gtk_switch_set_active (GTK_SWITCH(self->aspectswitch), g_settings_get_boolean (settings, "keep-aspect"));
+ g_signal_handlers_unblock_by_func (G_OBJECT (self->aspectswitch), aspectswitch_toggled_cb, self);
+
+ if (!self->rr_screen) {
+ set_combobox_sensitive (self, FALSE);
+ return;
+ }
+
+ outputs = gnome_rr_screen_list_outputs (self->rr_screen);
+
+ for (i = 0; outputs[i] != NULL; i++) {
+ GnomeRROutput *output = outputs[i];
+ GnomeRRCrtc *crtc = gnome_rr_output_get_crtc (output);
+
+ /* Output is turned on? */
+ if (crtc && gnome_rr_crtc_get_current_mode (crtc) != NULL) {
+ GtkTreeIter iter;
+ const gchar *name, *disp_name;
+ g_autofree gchar *text = NULL;
+
+ name = gnome_rr_output_get_name (output);
+ disp_name = gnome_rr_output_get_display_name (output);
+ text = g_strdup_printf ("%s (%s)", name, disp_name);
+
+ gtk_list_store_append (store, &iter);
+ gtk_list_store_set (store, &iter, MONITOR_NAME_COLUMN, text, MONITOR_PTR_COLUMN, output, -1);
+
+ if (i == 0 || output == cur_output) {
+ g_signal_handlers_block_by_func (G_OBJECT (self->combobox), combobox_changed_cb, self);
+ gtk_combo_box_set_active_iter (GTK_COMBO_BOX(self->combobox), &iter);
+ g_signal_handlers_unblock_by_func (G_OBJECT (self->combobox), combobox_changed_cb, self);
+ }
+ }
+ }
+
+ set_combobox_sensitive (self, cur_output != NULL);
+}
+
+static void
+update_ui (CcWacomMappingPanel *self)
+{
+ if (self->device == NULL) {
+ gtk_widget_set_sensitive (GTK_WIDGET(self->checkbutton), FALSE);
+ gtk_toggle_button_set_inconsistent (GTK_TOGGLE_BUTTON(self->checkbutton), TRUE);
+ } else {
+ gboolean is_screen_tablet;
+
+ is_screen_tablet =
+ cc_wacom_device_get_integration_flags (self->device) &
+ WACOM_DEVICE_INTEGRATED_DISPLAY;
+
+ gtk_widget_set_sensitive (GTK_WIDGET(self->checkbutton), !is_screen_tablet);
+ gtk_toggle_button_set_inconsistent (GTK_TOGGLE_BUTTON(self->checkbutton), FALSE);
+ }
+
+ update_monitor_chooser (self);
+}
+
+static void
+update_mapping (CcWacomMappingPanel *self)
+{
+ GnomeRROutput *output = NULL;
+
+ if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (self->checkbutton))) {
+ GtkTreeIter iter;
+ GtkTreeModel *model;
+ char *name;
+
+ model = gtk_combo_box_get_model (GTK_COMBO_BOX (self->combobox));
+ if (!gtk_combo_box_get_active_iter (GTK_COMBO_BOX (self->combobox), &iter)) {
+ g_warning ("Map to single monitor checked, but no screen selected.");
+ return;
+ }
+
+ gtk_tree_model_get (model, &iter, MONITOR_NAME_COLUMN, &name, MONITOR_PTR_COLUMN, &output, -1);
+ }
+
+ cc_wacom_device_set_output (self->device, output);
+}
+
+void
+cc_wacom_mapping_panel_set_device (CcWacomMappingPanel *self,
+ CcWacomDevice *device)
+{
+ self->device = device;
+ update_ui (self);
+}
+
+static void
+checkbutton_toggled_cb (CcWacomMappingPanel *self)
+{
+ gboolean active;
+
+ active = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (self->checkbutton));
+ set_combobox_sensitive (self, active);
+ if (!active)
+ gtk_switch_set_active (GTK_SWITCH(self->aspectswitch), FALSE);
+ update_mapping (self);
+}
+
+static void
+aspectswitch_toggled_cb (CcWacomMappingPanel *self)
+{
+ GSettings *settings;
+
+ settings = cc_wacom_device_get_settings (self->device);
+ g_settings_set_boolean (settings,
+ "keep-aspect",
+ gtk_switch_get_active (GTK_SWITCH (self->aspectswitch)));
+}
+
+static void
+combobox_changed_cb (CcWacomMappingPanel *self)
+{
+ update_mapping (self);
+}
+
+static void
+cc_wacom_mapping_panel_init (CcWacomMappingPanel *self)
+{
+ GtkWidget *vbox, *grid;
+ GtkCellRenderer *renderer;
+ g_autoptr(GError) error = NULL;
+
+ self->rr_screen = gnome_rr_screen_new (gdk_screen_get_default (), &error);
+
+ if (error)
+ g_warning ("Could not get RR screen: %s", error->message);
+
+ g_signal_connect_object (self->rr_screen, "changed",
+ G_CALLBACK (update_monitor_chooser), self, G_CONNECT_SWAPPED);
+
+ vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
+ gtk_container_add (GTK_CONTAINER (self), vbox);
+ gtk_container_set_border_width (GTK_CONTAINER (self), 12);
+ gtk_widget_set_vexpand (GTK_WIDGET (vbox), TRUE);
+ gtk_widget_set_hexpand (GTK_WIDGET (vbox), TRUE);
+
+ /* Output Combobox */
+ grid = gtk_grid_new();
+ gtk_grid_set_row_spacing (GTK_GRID (grid), 10);
+ gtk_grid_set_column_spacing (GTK_GRID (grid), 10);
+ self->label = gtk_label_new (_("Output:"));
+ gtk_widget_set_halign (self->label, GTK_ALIGN_END);
+ self->combobox = gtk_combo_box_new ();
+ g_signal_connect_object (self->combobox, "changed",
+ G_CALLBACK (combobox_changed_cb), self, G_CONNECT_SWAPPED);
+ renderer = gtk_cell_renderer_text_new ();
+ gtk_cell_layout_pack_start (GTK_CELL_LAYOUT(self->combobox), renderer, TRUE);
+ gtk_cell_layout_add_attribute (GTK_CELL_LAYOUT(self->combobox), renderer, "text", 0);
+ gtk_grid_attach (GTK_GRID(grid), GTK_WIDGET(self->label), 0, 0, 1, 1);
+ gtk_grid_attach (GTK_GRID(grid), GTK_WIDGET(self->combobox), 1, 0, 1, 1);
+
+ /* Keep ratio switch */
+ self->aspectlabel = gtk_label_new (_("Keep aspect ratio (letterbox):"));
+ gtk_widget_set_halign (self->aspectlabel, GTK_ALIGN_END);
+ self->aspectswitch = gtk_switch_new ();
+ gtk_widget_set_halign (self->aspectswitch, GTK_ALIGN_START);
+ gtk_switch_set_active (GTK_SWITCH (self->aspectswitch), FALSE);
+ g_signal_connect_object (self->aspectswitch, "notify::active",
+ G_CALLBACK (aspectswitch_toggled_cb), self, G_CONNECT_SWAPPED);
+ gtk_grid_attach (GTK_GRID(grid), GTK_WIDGET(self->aspectlabel), 0, 1, 1, 1);
+ gtk_grid_attach (GTK_GRID(grid), GTK_WIDGET(self->aspectswitch), 1, 1, 1, 1);
+
+ /* Whole-desktop checkbox */
+ self->checkbutton = gtk_check_button_new_with_label (_("Map to single monitor"));
+ gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (self->checkbutton), FALSE);
+ g_signal_connect_object (self->checkbutton, "toggled",
+ G_CALLBACK (checkbutton_toggled_cb), self, G_CONNECT_SWAPPED);
+
+ gtk_box_pack_start (GTK_BOX(vbox), GTK_WIDGET(self->checkbutton),
+ FALSE, FALSE, 0);
+ gtk_box_pack_start (GTK_BOX(vbox), GTK_WIDGET(grid),
+ FALSE, FALSE, 8);
+
+ /* Update display */
+ cc_wacom_mapping_panel_set_device (self, NULL);
+ gtk_widget_show_all(GTK_WIDGET(self));
+}
+
+GtkWidget *
+cc_wacom_mapping_panel_new (void)
+{
+ CcWacomMappingPanel *panel;
+
+ panel = CC_WACOM_MAPPING_PANEL(g_object_new (CC_TYPE_WACOM_MAPPING_PANEL, NULL));
+ panel->device = NULL;
+
+ return GTK_WIDGET(panel);
+}
+
+static void
+cc_wacom_mapping_panel_get_property (GObject *object,
+ guint property_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ switch (property_id)
+ {
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ }
+}
+
+static void
+cc_wacom_mapping_panel_set_property (GObject *object,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ switch (property_id)
+ {
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ }
+}
+
+static void
+cc_wacom_mapping_panel_dispose (GObject *object)
+{
+ CcWacomMappingPanel *self = CC_WACOM_MAPPING_PANEL (object);
+
+ g_clear_object (&self->rr_screen);
+
+ G_OBJECT_CLASS (cc_wacom_mapping_panel_parent_class)->dispose (object);
+}
+
+static void
+cc_wacom_mapping_panel_class_init (CcWacomMappingPanelClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->get_property = cc_wacom_mapping_panel_get_property;
+ object_class->set_property = cc_wacom_mapping_panel_set_property;
+ object_class->dispose = cc_wacom_mapping_panel_dispose;
+}
diff --git a/panels/wacom/cc-wacom-mapping-panel.h b/panels/wacom/cc-wacom-mapping-panel.h
new file mode 100644
index 0000000..ac9501c
--- /dev/null
+++ b/panels/wacom/cc-wacom-mapping-panel.h
@@ -0,0 +1,36 @@
+/*
+ * Copyright © 2012 Wacom.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Jason Gerecke <killertofu@gmail.com>
+ *
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+#include "cc-wacom-device.h"
+
+G_BEGIN_DECLS
+
+#define CC_TYPE_WACOM_MAPPING_PANEL (cc_wacom_mapping_panel_get_type ())
+G_DECLARE_FINAL_TYPE (CcWacomMappingPanel, cc_wacom_mapping_panel, CC, WACOM_MAPPING_PANEL, GtkBox)
+
+GtkWidget * cc_wacom_mapping_panel_new (void);
+
+void cc_wacom_mapping_panel_set_device (CcWacomMappingPanel *self,
+ CcWacomDevice *device);
+
+G_END_DECLS
diff --git a/panels/wacom/cc-wacom-nav-button.c b/panels/wacom/cc-wacom-nav-button.c
new file mode 100644
index 0000000..3239f5f
--- /dev/null
+++ b/panels/wacom/cc-wacom-nav-button.c
@@ -0,0 +1,220 @@
+/*
+ * Copyright © 2011 Red Hat, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Bastien Nocera <hadess@hadess.net>
+ *
+ */
+
+#include <config.h>
+
+#include <glib/gi18n.h>
+#include <gtk/gtk.h>
+
+#include "cc-wacom-nav-button.h"
+
+struct _CcWacomNavButton
+{
+ GtkBox parent_instance;
+
+ GtkNotebook *notebook;
+ GtkWidget *label;
+ GtkWidget *prev;
+ GtkWidget *next;
+ guint page_added_id;
+ guint page_removed_id;
+ guint page_switched_id;
+ gboolean ignore_first_page;
+};
+
+G_DEFINE_TYPE (CcWacomNavButton, cc_wacom_nav_button, GTK_TYPE_BOX)
+
+enum {
+ PROP_0,
+ PROP_NOTEBOOK,
+ PROP_IGNORE_FIRST
+};
+
+static void
+cc_wacom_nav_button_update (CcWacomNavButton *nav)
+{
+ int num_pages;
+ int current_page;
+ char *text;
+
+ if (nav->notebook == NULL) {
+ gtk_widget_hide (GTK_WIDGET (nav));
+ return;
+ }
+
+ num_pages = gtk_notebook_get_n_pages (GTK_NOTEBOOK (nav->notebook));
+ if (num_pages == 0)
+ return;
+ if (nav->ignore_first_page && num_pages == 1)
+ return;
+
+ if (nav->ignore_first_page)
+ num_pages--;
+
+ g_assert (num_pages >= 1);
+
+ gtk_revealer_set_reveal_child (GTK_REVEALER (gtk_widget_get_parent (GTK_WIDGET (nav))),
+ num_pages > 1);
+
+ current_page = gtk_notebook_get_current_page (GTK_NOTEBOOK (nav->notebook));
+ if (current_page < 0)
+ return;
+ if (nav->ignore_first_page)
+ current_page--;
+ gtk_widget_set_sensitive (nav->prev, current_page == 0 ? FALSE : TRUE);
+ gtk_widget_set_sensitive (nav->next, current_page + 1 == num_pages ? FALSE : TRUE);
+
+ text = g_strdup_printf (_("%d of %d"),
+ current_page + 1,
+ num_pages);
+ gtk_label_set_text (GTK_LABEL (nav->label), text);
+}
+
+static void
+pages_changed (CcWacomNavButton *nav)
+{
+ cc_wacom_nav_button_update (nav);
+}
+
+static void
+page_switched (CcWacomNavButton *nav)
+{
+ cc_wacom_nav_button_update (nav);
+}
+
+static void
+next_clicked (CcWacomNavButton *nav)
+{
+ int current_page;
+
+ current_page = gtk_notebook_get_current_page (GTK_NOTEBOOK (nav->notebook));
+ current_page++;
+ gtk_notebook_set_current_page (GTK_NOTEBOOK (nav->notebook), current_page);
+}
+
+static void
+prev_clicked (CcWacomNavButton *nav)
+{
+ int current_page;
+
+ current_page = gtk_notebook_get_current_page (GTK_NOTEBOOK (nav->notebook));
+ current_page--;
+ gtk_notebook_set_current_page (GTK_NOTEBOOK (nav->notebook), current_page--);
+}
+
+static void
+cc_wacom_nav_button_set_property (GObject *object,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ CcWacomNavButton *nav = CC_WACOM_NAV_BUTTON (object);
+
+ switch (property_id) {
+ case PROP_NOTEBOOK:
+ if (nav->notebook) {
+ g_signal_handler_disconnect (nav->notebook, nav->page_added_id);
+ g_signal_handler_disconnect (nav->notebook, nav->page_removed_id);
+ g_signal_handler_disconnect (nav->notebook, nav->page_switched_id);
+ }
+ g_clear_object (&nav->notebook);
+ nav->notebook = g_value_dup_object (value);
+ nav->page_added_id = g_signal_connect_object (nav->notebook, "page-added",
+ G_CALLBACK (pages_changed), nav, G_CONNECT_SWAPPED);
+ nav->page_removed_id = g_signal_connect_object (nav->notebook, "page-removed",
+ G_CALLBACK (pages_changed), nav, G_CONNECT_SWAPPED);
+ nav->page_switched_id = g_signal_connect_object (nav->notebook, "notify::page",
+ G_CALLBACK (page_switched), nav, G_CONNECT_SWAPPED);
+ cc_wacom_nav_button_update (nav);
+ break;
+ case PROP_IGNORE_FIRST:
+ nav->ignore_first_page = g_value_get_boolean (value);
+ cc_wacom_nav_button_update (nav);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ }
+}
+
+static void
+cc_wacom_nav_button_class_init (CcWacomNavButtonClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->set_property = cc_wacom_nav_button_set_property;
+
+ g_object_class_install_property (object_class, PROP_NOTEBOOK,
+ g_param_spec_object ("notebook", "notebook", "notebook",
+ GTK_TYPE_NOTEBOOK,
+ G_PARAM_WRITABLE));
+ g_object_class_install_property (object_class, PROP_IGNORE_FIRST,
+ g_param_spec_boolean ("ignore-first", "ignore-first", "ignore-first",
+ FALSE,
+ G_PARAM_WRITABLE));
+}
+
+static void
+cc_wacom_nav_button_init (CcWacomNavButton *self)
+{
+ GtkStyleContext *context;
+ GtkWidget *image, *box;
+
+ /* Label */
+ self->label = gtk_label_new (NULL);
+ gtk_style_context_add_class (gtk_widget_get_style_context (self->label), "dim-label");
+ gtk_box_pack_start (GTK_BOX (self), self->label,
+ FALSE, FALSE, 8);
+
+ box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
+ context = gtk_widget_get_style_context (GTK_WIDGET (box));
+ gtk_style_context_add_class (context, GTK_STYLE_CLASS_LINKED);
+ gtk_box_pack_start (GTK_BOX (self), box,
+ FALSE, FALSE, 0);
+
+ /* Prev button */
+ self->prev = gtk_button_new ();
+ image = gtk_image_new_from_icon_name ("go-previous-symbolic", GTK_ICON_SIZE_MENU);
+ gtk_container_add (GTK_CONTAINER (self->prev), image);
+ g_signal_connect_object (G_OBJECT (self->prev), "clicked",
+ G_CALLBACK (prev_clicked), self, G_CONNECT_SWAPPED);
+ gtk_widget_set_valign (self->prev, GTK_ALIGN_CENTER);
+
+ /* Next button */
+ self->next = gtk_button_new ();
+ image = gtk_image_new_from_icon_name ("go-next-symbolic", GTK_ICON_SIZE_MENU);
+ gtk_container_add (GTK_CONTAINER (self->next), image);
+ g_signal_connect_object (G_OBJECT (self->next), "clicked",
+ G_CALLBACK (next_clicked), self, G_CONNECT_SWAPPED);
+ gtk_widget_set_valign (self->next, GTK_ALIGN_CENTER);
+
+ gtk_box_pack_start (GTK_BOX (box), self->prev,
+ FALSE, FALSE, 0);
+ gtk_box_pack_start (GTK_BOX (box), self->next,
+ FALSE, FALSE, 0);
+
+ gtk_widget_show (self->label);
+ gtk_widget_show_all (box);
+}
+
+GtkWidget *
+cc_wacom_nav_button_new (void)
+{
+ return GTK_WIDGET (g_object_new (CC_TYPE_WACOM_NAV_BUTTON, NULL));
+}
diff --git a/panels/wacom/cc-wacom-nav-button.h b/panels/wacom/cc-wacom-nav-button.h
new file mode 100644
index 0000000..6b60105
--- /dev/null
+++ b/panels/wacom/cc-wacom-nav-button.h
@@ -0,0 +1,31 @@
+/*
+ * Copyright © 2011 Red Hat, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Bastien Nocera <hadess@hadess.net>
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define CC_TYPE_WACOM_NAV_BUTTON (cc_wacom_nav_button_get_type ())
+G_DECLARE_FINAL_TYPE (CcWacomNavButton, cc_wacom_nav_button, CC, WACOM_NAV_BUTTON, GtkBox)
+
+GtkWidget * cc_wacom_nav_button_new (void);
+
+G_END_DECLS
diff --git a/panels/wacom/cc-wacom-page.c b/panels/wacom/cc-wacom-page.c
new file mode 100644
index 0000000..b85bce7
--- /dev/null
+++ b/panels/wacom/cc-wacom-page.c
@@ -0,0 +1,1030 @@
+/*
+ * Copyright © 2011 Red Hat, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Peter Hutterer <peter.hutterer@redhat.com>
+ * Bastien Nocera <hadess@hadess.net>
+ *
+ */
+
+#include <config.h>
+
+#ifdef FAKE_AREA
+#include <gdk/gdk.h>
+#endif /* FAKE_AREA */
+
+#include <glib/gi18n-lib.h>
+#include <gtk/gtk.h>
+#include <gdesktop-enums.h>
+#ifdef GDK_WINDOWING_X11
+#include <gdk/gdkx.h>
+#endif
+#ifdef GDK_WINDOWING_WAYLAND
+#include <gdk/gdkwayland.h>
+#endif
+
+#include "cc-wacom-device.h"
+#include "cc-wacom-button-row.h"
+#include "cc-wacom-page.h"
+#include "cc-wacom-nav-button.h"
+#include "cc-wacom-mapping-panel.h"
+#include "cc-wacom-stylus-page.h"
+#include "gsd-enums.h"
+#include "calibrator-gui.h"
+#include "gsd-input-helper.h"
+
+#include <string.h>
+
+#define WID(x) (GtkWidget *) gtk_builder_get_object (page->builder, x)
+#define CWID(x) (GtkContainer *) gtk_builder_get_object (page->builder, x)
+#define MWID(x) (GtkWidget *) gtk_builder_get_object (page->mapping_builder, x)
+
+#define THRESHOLD_MISCLICK 15
+#define THRESHOLD_DOUBLECLICK 7
+
+enum {
+ MAPPING_DESCRIPTION_COLUMN,
+ MAPPING_TYPE_COLUMN,
+ MAPPING_BUTTON_COLUMN,
+ MAPPING_BUTTON_DIRECTION,
+ MAPPING_N_COLUMNS
+};
+
+struct _CcWacomPage
+{
+ GtkBox parent_instance;
+
+ CcWacomPanel *panel;
+ CcWacomDevice *stylus;
+ GList *pads;
+ GtkBuilder *builder;
+ GtkWidget *nav;
+ GtkWidget *notebook;
+ CalibArea *area;
+ GSettings *wacom_settings;
+
+ GtkSizeGroup *header_group;
+
+ /* Button mapping */
+ GtkBuilder *mapping_builder;
+ GtkWidget *button_map;
+ GtkListStore *action_store;
+
+ /* Display mapping */
+ GtkWidget *mapping;
+ GtkWidget *dialog;
+
+ GCancellable *cancellable;
+
+ /* To reach other grouped devices */
+ GsdDeviceManager *manager;
+};
+
+G_DEFINE_TYPE (CcWacomPage, cc_wacom_page, GTK_TYPE_BOX)
+
+/* Button combo box storage columns */
+enum {
+ BUTTONNUMBER_COLUMN,
+ BUTTONNAME_COLUMN,
+ N_BUTTONCOLUMNS
+};
+
+/* Tablet mode combo box storage columns */
+enum {
+ MODENUMBER_COLUMN,
+ MODELABEL_COLUMN,
+ N_MODECOLUMNS
+};
+
+/* Tablet mode options - keep in sync with .ui */
+enum {
+ MODE_ABSOLUTE, /* stylus + eraser absolute */
+ MODE_RELATIVE, /* stylus + eraser relative */
+};
+
+/* Different types of layout for the tablet config */
+enum {
+ LAYOUT_NORMAL, /* tracking mode, button mapping */
+ LAYOUT_REVERSIBLE, /* tracking mode, button mapping, left-hand orientation */
+ LAYOUT_SCREEN /* button mapping, calibration, display resolution */
+};
+
+static void
+set_page_layout (CcWacomPage *page,
+ int layout);
+
+static int
+get_layout_type (CcWacomDevice *device)
+{
+ int layout;
+
+ if (cc_wacom_device_get_integration_flags (device) &
+ (WACOM_DEVICE_INTEGRATED_DISPLAY | WACOM_DEVICE_INTEGRATED_SYSTEM))
+ layout = LAYOUT_SCREEN;
+ else if (cc_wacom_device_is_reversible (device))
+ layout = LAYOUT_REVERSIBLE;
+ else
+ layout = LAYOUT_NORMAL;
+
+ return layout;
+}
+
+static void
+set_calibration (CcWacomDevice *device,
+ const gint display_width,
+ const gint display_height,
+ gdouble *cal,
+ gsize ncal,
+ GSettings *settings)
+{
+ GVariant *current; /* current calibration */
+ GVariant *array; /* new calibration */
+ g_autofree GVariant **tmp = NULL;
+ gsize nvalues;
+ gint i;
+
+ current = g_settings_get_value (settings, "area");
+ g_variant_get_fixed_array (current, &nvalues, sizeof (gdouble));
+ if ((ncal != 4) || (nvalues != 4)) {
+ g_warning("Unable to set device calibration property. Got %"G_GSIZE_FORMAT" items to put in %"G_GSIZE_FORMAT" slots; expected %d items.\n", ncal, nvalues, 4);
+ return;
+ }
+
+ tmp = g_malloc (nvalues * sizeof (GVariant*));
+ for (i = 0; i < ncal; i++)
+ tmp[i] = g_variant_new_double (cal[i]);
+
+ array = g_variant_new_array (G_VARIANT_TYPE_DOUBLE, tmp, nvalues);
+ g_settings_set_value (settings, "area", array);
+
+ g_debug ("Setting area to %f, %f, %f, %f (left/right/top/bottom) (last used resolution: %d x %d)",
+ cal[0], cal[1], cal[2], cal[3],
+ display_width, display_height);
+}
+
+static void
+finish_calibration (CalibArea *area,
+ gpointer user_data)
+{
+ CcWacomPage *page = (CcWacomPage *) user_data;
+ XYinfo axis;
+ gdouble cal[4];
+ gint display_width, display_height;
+
+ if (calib_area_finish (area)) {
+ calib_area_get_padding (area, &axis);
+ cal[0] = axis.x_min;
+ cal[1] = axis.x_max;
+ cal[2] = axis.y_min;
+ cal[3] = axis.y_max;
+
+ calib_area_get_display_size (area, &display_width, &display_height);
+
+ set_calibration (page->stylus,
+ display_width,
+ display_height,
+ cal, 4, page->wacom_settings);
+ } else {
+ /* Reset the old values */
+ GVariant *old_calibration;
+
+ old_calibration = g_object_get_data (G_OBJECT (page), "old-calibration");
+ g_settings_set_value (page->wacom_settings, "area", old_calibration);
+ g_object_set_data (G_OBJECT (page), "old-calibration", NULL);
+ }
+
+ calib_area_free (area);
+ page->area = NULL;
+ gtk_widget_set_sensitive (WID ("button-calibrate"), TRUE);
+}
+
+static GdkDevice *
+cc_wacom_page_get_gdk_device (CcWacomPage *page)
+{
+ GsdDevice *gsd_device;
+ GdkDevice *gdk_device = NULL;
+ GdkDisplay *display;
+ GdkSeat *seat;
+ g_autoptr(GList) slaves = NULL;
+ GList *l;
+
+ gsd_device = cc_wacom_device_get_device (page->stylus);
+ g_return_val_if_fail (GSD_IS_DEVICE (gsd_device), NULL);
+
+ display = gtk_widget_get_display (GTK_WIDGET (page));
+ seat = gdk_display_get_default_seat (display);
+ slaves = gdk_seat_get_slaves (seat, GDK_SEAT_CAPABILITY_TABLET_STYLUS);
+
+ for (l = slaves; l && !gdk_device; l = l->next) {
+ g_autofree gchar *device_node = NULL;
+
+ if (gdk_device_get_source (l->data) != GDK_SOURCE_PEN)
+ continue;
+
+#ifdef GDK_WINDOWING_X11
+ if (GDK_IS_X11_DISPLAY (display))
+ device_node = xdevice_get_device_node (gdk_x11_device_get_id (l->data));
+#endif
+#ifdef GDK_WINDOWING_WAYLAND
+ if (GDK_IS_WAYLAND_DISPLAY (display))
+ device_node = g_strdup (gdk_wayland_device_get_node_path (l->data));
+#endif
+
+ if (g_strcmp0 (device_node, gsd_device_get_device_file (gsd_device)) == 0)
+ gdk_device = l->data;
+ }
+
+ return gdk_device;
+}
+
+static gboolean
+run_calibration (CcWacomPage *page,
+ GVariant *old_calibration,
+ gdouble *cal,
+ GdkMonitor *monitor)
+{
+ GdkDisplay *display = gdk_monitor_get_display (monitor);
+ gint i, n_monitor = 0;
+
+ g_assert (page->area == NULL);
+
+ for (i = 0; i < gdk_display_get_n_monitors (display); i++) {
+ if (monitor == gdk_display_get_monitor (display, i)) {
+ n_monitor = i;
+ break;
+ }
+ }
+
+ page->area = calib_area_new (NULL,
+ n_monitor,
+ cc_wacom_page_get_gdk_device (page),
+ finish_calibration,
+ page,
+ THRESHOLD_MISCLICK,
+ THRESHOLD_DOUBLECLICK);
+
+ g_object_set_data_full (G_OBJECT (page),
+ "old-calibration",
+ old_calibration,
+ (GDestroyNotify) g_variant_unref);
+
+ return FALSE;
+}
+
+static void
+calibrate (CcWacomPage *page)
+{
+ int i;
+ GVariant *old_calibration, *array;
+ g_autofree GVariant **tmp = NULL;
+ g_autofree gdouble *calibration = NULL;
+ gsize ncal;
+ GdkMonitor *monitor;
+ GdkScreen *screen;
+ g_autoptr(GnomeRRScreen) rr_screen = NULL;
+ GnomeRROutput *output;
+ g_autoptr(GError) error = NULL;
+ gint x, y;
+
+ screen = gdk_screen_get_default ();
+ rr_screen = gnome_rr_screen_new (screen, &error);
+ if (error) {
+ g_warning ("Could not connect to display manager: %s", error->message);
+ return;
+ }
+
+ output = cc_wacom_device_get_output (page->stylus, rr_screen);
+ gnome_rr_output_get_position (output, &x, &y);
+ monitor = gdk_display_get_monitor_at_point (gdk_screen_get_display (screen), x, y);
+
+ if (!monitor) {
+ /* The display the tablet should be mapped to could not be located.
+ * This shouldn't happen if the EDID data is good...
+ */
+ g_critical("Output associated with the tablet is not connected. Unable to calibrate.");
+ return;
+ }
+
+ old_calibration = g_settings_get_value (page->wacom_settings, "area");
+ g_variant_get_fixed_array (old_calibration, &ncal, sizeof (gdouble));
+
+ if (ncal != 4) {
+ g_warning("Device calibration property has wrong length. Got %"G_GSIZE_FORMAT" items; expected %d.\n", ncal, 4);
+ return;
+ }
+
+ calibration = g_new0 (gdouble, ncal);
+
+ /* Reset the current values, to avoid old calibrations
+ * from interfering with the calibration */
+ tmp = g_malloc (ncal * sizeof (GVariant*));
+ for (i = 0; i < ncal; i++) {
+ calibration[i] = 0.0;
+ tmp[i] = g_variant_new_double (calibration[i]);
+ }
+
+ array = g_variant_new_array (G_VARIANT_TYPE_DOUBLE, tmp, ncal);
+ g_settings_set_value (page->wacom_settings, "area", array);
+
+ run_calibration (page, old_calibration, calibration, monitor);
+ gtk_widget_set_sensitive (WID ("button-calibrate"), FALSE);
+}
+
+static void
+calibrate_button_clicked_cb (CcWacomPage *page)
+{
+ calibrate (page);
+}
+
+/* This avoids us crashing when a newer version of
+ * gnome-control-center has been used, and we load up an
+ * old one, as the action type if unknown to the old g-c-c */
+static gboolean
+action_type_is_valid (GDesktopPadButtonAction action)
+{
+ if (action >= G_N_ELEMENTS (action_table))
+ return FALSE;
+ return TRUE;
+}
+
+static void
+create_row_from_button (GtkWidget *list_box,
+ guint button,
+ GSettings *settings)
+{
+ GtkWidget *row;
+
+ row = cc_wacom_button_row_new (button, settings);
+ gtk_container_add (GTK_CONTAINER (list_box), row);
+ gtk_widget_show (row);
+}
+
+static void
+setup_button_mapping (CcWacomPage *page)
+{
+ GDesktopPadButtonAction action;
+ CcWacomDevice *pad;
+ GtkWidget *list_box;
+ guint i, n_buttons;
+ GSettings *settings;
+
+ list_box = MWID ("shortcuts_list");
+ pad = page->pads->data;
+ n_buttons = cc_wacom_device_get_num_buttons (pad);
+
+ for (i = 0; i < n_buttons; i++) {
+ settings = cc_wacom_device_get_button_settings (pad, i);
+ if (!settings)
+ continue;
+
+ action = g_settings_get_enum (settings, "action");
+ if (!action_type_is_valid (action))
+ continue;
+
+ create_row_from_button (list_box, i, settings);
+ }
+}
+
+static void
+button_mapping_dialog_closed (CcWacomPage *page)
+{
+ gtk_widget_destroy (MWID ("button-mapping-dialog"));
+ g_clear_object (&page->mapping_builder);
+}
+
+static void
+show_button_mapping_dialog (CcWacomPage *page)
+{
+ GtkWidget *toplevel;
+ g_autoptr(GError) error = NULL;
+ GtkWidget *dialog;
+
+ g_assert (page->mapping_builder == NULL);
+ page->mapping_builder = gtk_builder_new ();
+ gtk_builder_add_from_resource (page->mapping_builder,
+ "/org/gnome/control-center/wacom/button-mapping.ui",
+ &error);
+
+ if (error != NULL) {
+ g_warning ("Error loading UI file: %s", error->message);
+ g_clear_object (&page->mapping_builder);
+ return;
+ }
+
+ setup_button_mapping (page);
+
+ dialog = MWID ("button-mapping-dialog");
+ toplevel = gtk_widget_get_toplevel (GTK_WIDGET (page));
+ gtk_window_set_transient_for (GTK_WINDOW (dialog), GTK_WINDOW (toplevel));
+ gtk_window_set_modal (GTK_WINDOW (dialog), TRUE);
+ g_signal_connect_object (dialog, "response",
+ G_CALLBACK (button_mapping_dialog_closed), page, G_CONNECT_SWAPPED);
+
+ gtk_widget_show (dialog);
+
+ page->button_map = dialog;
+ g_object_add_weak_pointer (G_OBJECT (dialog), (gpointer *) &page->button_map);
+}
+
+static void
+set_osd_visibility_cb (GObject *source_object,
+ GAsyncResult *res,
+ gpointer data)
+{
+ g_autoptr(GError) error = NULL;
+ GVariant *result;
+ CcWacomPage *page;
+
+ page = CC_WACOM_PAGE (data);
+
+ result = g_dbus_proxy_call_finish (G_DBUS_PROXY (source_object), res, &error);
+
+ if (result == NULL) {
+ if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+ g_printerr ("Error setting OSD's visibility: %s\n", error->message);
+ show_button_mapping_dialog (page);
+ } else {
+ return;
+ }
+ }
+}
+
+static void
+set_osd_visibility (CcWacomPage *page)
+{
+ GDBusProxy *proxy;
+ GsdDevice *gsd_device;
+ const gchar *device_path;
+
+ proxy = cc_wacom_panel_get_gsd_wacom_bus_proxy (page->panel);
+
+ /* Pick the first device, the OSD may change later between them */
+ gsd_device = cc_wacom_device_get_device (page->pads->data);
+
+ device_path = gsd_device_get_device_file (gsd_device);
+
+ if (proxy == NULL) {
+ show_button_mapping_dialog (page);
+ return;
+ }
+
+ g_dbus_proxy_call (proxy,
+ "Show",
+ g_variant_new ("(ob)", device_path, TRUE),
+ G_DBUS_CALL_FLAGS_NONE,
+ -1,
+ page->cancellable,
+ set_osd_visibility_cb,
+ page);
+}
+
+static void
+map_buttons_button_clicked_cb (CcWacomPage *page)
+{
+ set_osd_visibility (page);
+}
+
+static void
+display_mapping_dialog_closed (CcWacomPage *page)
+{
+ int layout;
+
+ gtk_widget_destroy (page->dialog);
+ page->dialog = NULL;
+ page->mapping = NULL;
+ layout = get_layout_type (page->stylus);
+ set_page_layout (page, layout);
+}
+
+static void
+display_mapping_button_clicked_cb (CcWacomPage *page)
+{
+ g_assert (page->mapping == NULL);
+
+ page->dialog = gtk_dialog_new_with_buttons (_("Display Mapping"),
+ GTK_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (page))),
+ GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT,
+ _("_Close"),
+ GTK_RESPONSE_ACCEPT,
+ NULL);
+ page->mapping = cc_wacom_mapping_panel_new ();
+ cc_wacom_mapping_panel_set_device (CC_WACOM_MAPPING_PANEL (page->mapping),
+ page->stylus);
+ gtk_container_add (GTK_CONTAINER (gtk_dialog_get_content_area (GTK_DIALOG (page->dialog))),
+ page->mapping);
+ g_signal_connect_object (page->dialog, "response",
+ G_CALLBACK (display_mapping_dialog_closed), page, G_CONNECT_SWAPPED);
+ gtk_widget_show_all (page->dialog);
+
+ g_object_add_weak_pointer (G_OBJECT (page->mapping), (gpointer *) &page->dialog);
+}
+
+static void
+tabletmode_changed_cb (CcWacomPage *page)
+{
+ GtkListStore *liststore;
+ GtkTreeIter iter;
+ gint mode;
+
+ if (!gtk_combo_box_get_active_iter (GTK_COMBO_BOX (WID ("combo-tabletmode")), &iter))
+ return;
+
+ liststore = GTK_LIST_STORE (WID ("liststore-tabletmode"));
+ gtk_tree_model_get (GTK_TREE_MODEL (liststore), &iter,
+ MODENUMBER_COLUMN, &mode,
+ -1);
+
+ g_settings_set_enum (page->wacom_settings, "mapping", mode);
+}
+
+static void
+left_handed_toggled_cb (CcWacomPage *page)
+{
+ gboolean left_handed;
+
+ left_handed = gtk_switch_get_active (GTK_SWITCH (WID ("switch-left-handed")));
+ g_settings_set_boolean (page->wacom_settings, "left-handed", left_handed);
+}
+
+static void
+set_left_handed_from_gsettings (CcWacomPage *page)
+{
+ gboolean left_handed;
+
+ left_handed = g_settings_get_boolean (page->wacom_settings, "left-handed");
+ gtk_switch_set_active (GTK_SWITCH (WID ("switch-left-handed")), left_handed);
+}
+
+static void
+set_mode_from_gsettings (GtkComboBox *combo,
+ CcWacomPage *page)
+{
+ GDesktopTabletMapping mapping;
+
+ mapping = g_settings_get_enum (page->wacom_settings, "mapping");
+
+ /* this must be kept in sync with the .ui file */
+ gtk_combo_box_set_active (combo, mapping);
+}
+
+static void
+update_display_decoupled_sensitivity (CcWacomPage *page,
+ gboolean active)
+{
+ if (get_layout_type (page->stylus) != LAYOUT_SCREEN)
+ return;
+
+ gtk_widget_set_sensitive (WID ("label-trackingmode"), active);
+ gtk_widget_set_sensitive (WID ("combo-tabletmode"), active);
+ gtk_widget_set_sensitive (WID ("display-mapping-button-2"), active);
+
+ gtk_widget_set_sensitive (WID ("button-calibrate"), !active);
+}
+
+static void
+set_display_decoupled_from_gsettings (GtkSwitch *sw,
+ CcWacomPage *page)
+{
+ g_auto(GStrv) output = g_settings_get_strv (page->wacom_settings, "output");
+ gboolean active = g_strcmp0 (output[0], "") != 0;
+
+ gtk_switch_set_active (sw, active);
+ update_display_decoupled_sensitivity (page, active);
+}
+
+static void
+combobox_text_cellrenderer (GtkComboBox *combo, int name_column)
+{
+ GtkCellRenderer *renderer;
+
+ renderer = gtk_cell_renderer_text_new ();
+ gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (combo), renderer, TRUE);
+ gtk_cell_layout_set_attributes (GTK_CELL_LAYOUT (combo), renderer,
+ "text", BUTTONNAME_COLUMN, NULL);
+}
+
+static gboolean
+display_clicked_cb (CcWacomPage *page)
+{
+ cc_wacom_panel_switch_to_panel (page->panel, "display");
+ return TRUE;
+}
+
+static gboolean
+mouse_clicked_cb (CcWacomPage *page)
+{
+ cc_wacom_panel_switch_to_panel (page->panel, "mouse");
+ return TRUE;
+}
+
+static void
+decouple_display_toggled_cb (CcWacomPage *page)
+{
+ gboolean active = gtk_switch_get_active (GTK_SWITCH (WID ("switch-decouple-display")));
+
+ update_display_decoupled_sensitivity (page, active);
+
+ if (!active) {
+ cc_wacom_device_set_output (page->stylus, NULL);
+ } else {
+ GdkScreen *screen;
+ GnomeRRScreen *rr_screen;
+ GnomeRROutput **outputs, *picked = NULL;
+ g_autoptr(GError) error = NULL;
+ int i;
+
+ screen = gtk_widget_get_screen (GTK_WIDGET (WID ("switch-decouple-display")));
+ rr_screen = gnome_rr_screen_new (screen, &error);
+ if (rr_screen == NULL) {
+ g_warning ("Could not connect to display manager: %s", error->message);
+ return;
+ }
+
+ outputs = gnome_rr_screen_list_outputs (rr_screen);
+
+ /* Pick *some* output here. decoupled mode can only jump across
+ * monitors, not map to the full span of those. We prefer the
+ * builtin display, falling back to the first output found if
+ * there's none.
+ */
+ for (i = 0; outputs[i] != NULL; i++) {
+ if (gnome_rr_output_is_builtin_display (outputs[i]))
+ picked = outputs[i];
+ }
+
+ if (!picked)
+ picked = outputs[0];
+
+ cc_wacom_device_set_output (page->stylus, picked);
+ }
+}
+
+/* Boilerplate code goes below */
+
+static void
+cc_wacom_page_get_property (GObject *object,
+ guint property_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ switch (property_id)
+ {
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ }
+}
+
+static void
+cc_wacom_page_set_property (GObject *object,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ switch (property_id)
+ {
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ }
+}
+
+static void
+cc_wacom_page_dispose (GObject *object)
+{
+ CcWacomPage *self = CC_WACOM_PAGE (object);
+
+ g_cancellable_cancel (self->cancellable);
+ g_clear_object (&self->cancellable);
+ g_clear_pointer (&self->area, calib_area_free);
+ g_clear_pointer (&self->button_map, gtk_widget_destroy);
+ g_clear_pointer (&self->dialog, gtk_widget_destroy);
+ g_clear_object (&self->builder);
+ g_clear_object (&self->header_group);
+ g_list_free_full (self->pads, g_object_unref);
+ self->pads = NULL;
+
+ self->panel = NULL;
+
+ G_OBJECT_CLASS (cc_wacom_page_parent_class)->dispose (object);
+}
+
+static void
+cc_wacom_page_class_init (CcWacomPageClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->get_property = cc_wacom_page_get_property;
+ object_class->set_property = cc_wacom_page_set_property;
+ object_class->dispose = cc_wacom_page_dispose;
+}
+
+static void
+remove_link_padding (GtkWidget *widget)
+{
+ g_autoptr(GtkCssProvider) provider = NULL;
+
+ provider = gtk_css_provider_new ();
+ gtk_css_provider_load_from_data (GTK_CSS_PROVIDER (provider),
+ ".link { padding: 0px; }", -1, NULL);
+ gtk_style_context_add_provider (gtk_widget_get_style_context (widget),
+ GTK_STYLE_PROVIDER (provider),
+ GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
+}
+
+static void
+cc_wacom_page_init (CcWacomPage *page)
+{
+ g_autoptr(GError) error = NULL;
+ GtkComboBox *combo;
+ GtkWidget *box;
+ char *objects[] = {
+ "main-grid",
+ "liststore-tabletmode",
+ "liststore-buttons",
+ "adjustment-tip-feel",
+ "adjustment-eraser-feel",
+ NULL
+ };
+
+ page->builder = gtk_builder_new ();
+
+ gtk_builder_add_objects_from_resource (page->builder,
+ "/org/gnome/control-center/wacom/gnome-wacom-properties.ui",
+ objects,
+ &error);
+ if (error != NULL) {
+ g_warning ("Error loading UI file: %s", error->message);
+ return;
+ }
+
+ box = WID ("main-grid");
+ gtk_container_add (GTK_CONTAINER (page), box);
+ gtk_widget_set_vexpand (GTK_WIDGET (box), TRUE);
+
+ g_signal_connect_object (WID ("button-calibrate"), "clicked",
+ G_CALLBACK (calibrate_button_clicked_cb), page, G_CONNECT_SWAPPED);
+ g_signal_connect_object (WID ("map-buttons-button"), "clicked",
+ G_CALLBACK (map_buttons_button_clicked_cb), page, G_CONNECT_SWAPPED);
+
+ combo = GTK_COMBO_BOX (WID ("combo-tabletmode"));
+ combobox_text_cellrenderer (combo, MODELABEL_COLUMN);
+ g_signal_connect_object (combo, "changed",
+ G_CALLBACK (tabletmode_changed_cb), page, G_CONNECT_SWAPPED);
+
+ g_signal_connect_object (WID ("switch-left-handed"), "notify::active",
+ G_CALLBACK (left_handed_toggled_cb), page, G_CONNECT_SWAPPED);
+
+ g_signal_connect_object (WID ("display-link"), "activate-link",
+ G_CALLBACK (display_clicked_cb), page, G_CONNECT_SWAPPED);
+ remove_link_padding (WID ("display-link"));
+
+ g_signal_connect_object (WID ("mouse-link"), "activate-link",
+ G_CALLBACK (mouse_clicked_cb), page, G_CONNECT_SWAPPED);
+ remove_link_padding (WID ("mouse-link"));
+
+ g_signal_connect_object (WID ("display-mapping-button"), "clicked",
+ G_CALLBACK (display_mapping_button_clicked_cb), page, G_CONNECT_SWAPPED);
+ g_signal_connect_object (WID ("display-mapping-button-2"), "clicked",
+ G_CALLBACK (display_mapping_button_clicked_cb), page, G_CONNECT_SWAPPED);
+ g_signal_connect_object (WID ("switch-decouple-display"), "notify::active",
+ G_CALLBACK (decouple_display_toggled_cb), page, G_CONNECT_SWAPPED);
+
+ page->nav = cc_wacom_nav_button_new ();
+ gtk_widget_set_halign (page->nav, GTK_ALIGN_END);
+ gtk_widget_set_margin_start (page->nav, 10);
+ gtk_widget_show (page->nav);
+ gtk_container_add (CWID ("navigation-placeholder"), page->nav);
+
+ page->cancellable = g_cancellable_new ();
+}
+
+static void
+set_icon_name (CcWacomPage *page,
+ const char *widget_name,
+ const char *icon_name)
+{
+ g_autofree gchar *resource = NULL;
+
+ resource = g_strdup_printf ("/org/gnome/control-center/wacom/%s.svg", icon_name);
+ gtk_image_set_from_resource (GTK_IMAGE (WID (widget_name)), resource);
+}
+
+static void
+remove_left_handed (CcWacomPage *page)
+{
+ gtk_widget_destroy (WID ("label-left-handed"));
+ gtk_widget_destroy (WID ("switch-left-handed"));
+}
+
+static void
+remove_display_link (CcWacomPage *page)
+{
+ gtk_widget_destroy (WID ("display-link"));
+}
+
+static void
+remove_mouse_link (CcWacomPage *page)
+{
+ gtk_widget_destroy (WID ("mouse-link"));
+}
+
+static void
+remove_decouple_options (CcWacomPage *page)
+{
+ gtk_widget_destroy (WID ("label-decouple-display"));
+ gtk_widget_destroy (WID ("switch-decouple-display"));
+ gtk_widget_destroy (WID ("display-mapping-button-2"));
+}
+
+static gboolean
+has_monitor (CcWacomPage *page)
+{
+ WacomIntegrationFlags integration_flags;
+
+ integration_flags = cc_wacom_device_get_integration_flags (page->stylus);
+
+ return ((integration_flags &
+ (WACOM_DEVICE_INTEGRATED_DISPLAY | WACOM_DEVICE_INTEGRATED_SYSTEM)) != 0);
+}
+
+static void
+set_page_layout (CcWacomPage *page,
+ int layout)
+{
+ WacomIntegrationFlags integration_flags;
+
+ integration_flags = cc_wacom_device_get_integration_flags (page->stylus);
+
+ if ((integration_flags &
+ (WACOM_DEVICE_INTEGRATED_DISPLAY | WACOM_DEVICE_INTEGRATED_SYSTEM)) != 0) {
+ /* FIXME: Check we've got a puck, or a corresponding touchpad device */
+ remove_mouse_link (page);
+ }
+
+ switch (layout) {
+ case LAYOUT_NORMAL:
+ remove_left_handed (page);
+ remove_display_link (page);
+ remove_decouple_options (page);
+ break;
+ case LAYOUT_REVERSIBLE:
+ remove_display_link (page);
+ remove_decouple_options (page);
+ break;
+ case LAYOUT_SCREEN:
+ remove_left_handed (page);
+
+ gtk_widget_destroy (WID ("display-mapping-button"));
+
+ gtk_widget_show (WID ("button-calibrate"));
+ gtk_widget_set_sensitive (WID ("button-calibrate"),
+ has_monitor (page));
+
+ gtk_container_child_set (CWID ("main-controls-grid"),
+ WID ("label-trackingmode"),
+ "top_attach", 5, NULL);
+ gtk_container_child_set (CWID ("main-controls-grid"),
+ WID ("combo-tabletmode"),
+ "top_attach", 5, NULL);
+ break;
+ default:
+ g_assert_not_reached ();
+ }
+}
+
+static void
+update_pad_availability (CcWacomPage *page)
+{
+ gtk_widget_set_visible (WID ("map-buttons-button"), page->pads != NULL);
+}
+
+static void
+check_add_pad (CcWacomPage *page,
+ GsdDevice *gsd_device)
+{
+ g_autoptr(CcWacomDevice) wacom_device = NULL;
+
+ if ((gsd_device_get_device_type (gsd_device) & GSD_DEVICE_TYPE_PAD) == 0)
+ return;
+
+ if (!gsd_device_shares_group (cc_wacom_device_get_device (page->stylus),
+ gsd_device))
+ return;
+
+ wacom_device = cc_wacom_device_new (gsd_device);
+ if (!wacom_device)
+ return;
+
+ page->pads = g_list_prepend (page->pads, g_steal_pointer (&wacom_device));
+ update_pad_availability (page);
+}
+
+static void
+check_remove_pad (CcWacomPage *page,
+ GsdDevice *gsd_device)
+{
+ GList *l;
+
+ if ((gsd_device_get_device_type (gsd_device) & GSD_DEVICE_TYPE_PAD) == 0)
+ return;
+
+ for (l = page->pads; l; l = l->next) {
+ CcWacomDevice *wacom_device = l->data;
+ if (cc_wacom_device_get_device (wacom_device) == gsd_device) {
+ page->pads = g_list_delete_link (page->pads, l);
+ g_object_unref (wacom_device);
+ }
+ }
+
+ update_pad_availability (page);
+}
+
+GtkWidget *
+cc_wacom_page_new (CcWacomPanel *panel,
+ CcWacomDevice *stylus)
+{
+ g_autoptr (GList) pads = NULL;
+ CcWacomPage *page;
+ GList *l;
+
+ g_return_val_if_fail (CC_IS_WACOM_DEVICE (stylus), NULL);
+
+ page = g_object_new (CC_TYPE_WACOM_PAGE, NULL);
+
+ page->panel = panel;
+ page->stylus = stylus;
+
+ set_page_layout (page, get_layout_type (stylus));
+
+ /* FIXME move this to construct */
+ page->wacom_settings = cc_wacom_device_get_settings (stylus);
+ set_mode_from_gsettings (GTK_COMBO_BOX (WID ("combo-tabletmode")), page);
+ if (get_layout_type (page->stylus) == LAYOUT_SCREEN)
+ set_display_decoupled_from_gsettings (GTK_SWITCH (WID ("switch-decouple-display")), page);
+
+ /* Tablet name */
+ gtk_label_set_text (GTK_LABEL (WID ("label-tabletmodel")), cc_wacom_device_get_name (stylus));
+
+ /* Left-handedness */
+ if (cc_wacom_device_is_reversible (stylus))
+ set_left_handed_from_gsettings (page);
+
+ /* Tablet icon */
+ set_icon_name (page, "image-tablet", cc_wacom_device_get_icon_name (stylus));
+
+ /* Listen to changes in related/paired pads */
+ page->manager = gsd_device_manager_get ();
+ g_signal_connect_object (G_OBJECT (page->manager), "device-added",
+ G_CALLBACK (check_add_pad), page,
+ G_CONNECT_SWAPPED);
+ g_signal_connect_object (G_OBJECT (page->manager), "device-removed",
+ G_CALLBACK (check_remove_pad), page,
+ G_CONNECT_SWAPPED);
+
+ pads = gsd_device_manager_list_devices (page->manager, GSD_DEVICE_TYPE_PAD);
+ for (l = pads; l ; l = l->next)
+ check_add_pad (page, l->data);
+
+ return GTK_WIDGET (page);
+}
+
+void
+cc_wacom_page_set_navigation (CcWacomPage *page,
+ GtkNotebook *notebook,
+ gboolean ignore_first_page)
+{
+ g_return_if_fail (CC_IS_WACOM_PAGE (page));
+
+ g_object_set (G_OBJECT (page->nav),
+ "notebook", notebook,
+ "ignore-first", ignore_first_page,
+ NULL);
+}
+
+void
+cc_wacom_page_calibrate (CcWacomPage *page)
+{
+ g_return_if_fail (CC_IS_WACOM_PAGE (page));
+
+ calibrate (page);
+}
+
+gboolean
+cc_wacom_page_can_calibrate (CcWacomPage *page)
+{
+ g_return_val_if_fail (CC_IS_WACOM_PAGE (page),
+ FALSE);
+
+ return has_monitor (page);
+}
diff --git a/panels/wacom/cc-wacom-page.h b/panels/wacom/cc-wacom-page.h
new file mode 100644
index 0000000..56b32ac
--- /dev/null
+++ b/panels/wacom/cc-wacom-page.h
@@ -0,0 +1,43 @@
+/*
+ * Copyright © 2011 Red Hat, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Peter Hutterer <peter.hutterer@redhat.com>
+ * Bastien Nocera <hadess@hadess.net>
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+#include "cc-wacom-panel.h"
+#include "cc-wacom-device.h"
+
+G_BEGIN_DECLS
+
+#define CC_TYPE_WACOM_PAGE (cc_wacom_page_get_type ())
+G_DECLARE_FINAL_TYPE (CcWacomPage, cc_wacom_page, CC, WACOM_PAGE, GtkBox)
+
+GtkWidget * cc_wacom_page_new (CcWacomPanel *panel,
+ CcWacomDevice *stylus);
+
+void cc_wacom_page_set_navigation (CcWacomPage *page,
+ GtkNotebook *notebook,
+ gboolean ignore_first_page);
+
+void cc_wacom_page_calibrate (CcWacomPage *page);
+
+gboolean cc_wacom_page_can_calibrate (CcWacomPage *page);
+
+G_END_DECLS
diff --git a/panels/wacom/cc-wacom-panel.c b/panels/wacom/cc-wacom-panel.c
new file mode 100644
index 0000000..17404c1
--- /dev/null
+++ b/panels/wacom/cc-wacom-panel.c
@@ -0,0 +1,767 @@
+/*
+ * Copyright © 2011 Red Hat, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Peter Hutterer <peter.hutterer@redhat.com>
+ * Bastien Nocera <hadess@hadess.net>
+ *
+ */
+
+#include <config.h>
+
+#include <string.h>
+#include <gtk/gtk.h>
+#include <glib/gi18n-lib.h>
+
+#include "shell/cc-application.h"
+#include "shell/cc-debug.h"
+#include "cc-wacom-panel.h"
+#include "cc-wacom-page.h"
+#include "cc-wacom-stylus-page.h"
+#include "cc-wacom-resources.h"
+#include "cc-drawing-area.h"
+#include "cc-tablet-tool-map.h"
+#include "gsd-device-manager.h"
+
+#ifdef GDK_WINDOWING_WAYLAND
+#include <gdk/gdkwayland.h>
+#endif
+
+#define WID(x) (GtkWidget *) gtk_builder_get_object (self->builder, x)
+
+struct _CcWacomPanel
+{
+ CcPanel parent_instance;
+
+ GtkBuilder *builder;
+ GtkWidget *stack;
+ GtkWidget *switcher;
+ GtkWidget *tablet_notebook;
+ GtkWidget *stylus_notebook;
+ GtkWidget *test_popover;
+ GtkWidget *test_draw_area;
+ GtkWidget *test_button;
+ GHashTable *devices; /* key=GsdDevice, value=CcWacomDevice */
+ GHashTable *pages; /* key=CcWacomDevice, value=GtkWidget */
+ GHashTable *stylus_pages; /* key=CcWacomTool, value=GtkWidget */
+
+ CcTabletToolMap *tablet_tool_map;
+
+ /* DBus */
+ GDBusProxy *proxy;
+};
+
+CC_PANEL_REGISTER (CcWacomPanel, cc_wacom_panel)
+
+typedef struct {
+ const char *name;
+ CcWacomDevice *stylus;
+ CcWacomDevice *pad;
+} Tablet;
+
+enum {
+ WACOM_PAGE = -1,
+ PLUG_IN_PAGE = 0,
+};
+
+enum {
+ PROP_0,
+ PROP_PARAMETERS
+};
+
+/* Static init function */
+static void
+update_visibility (GsdDeviceManager *manager,
+ GsdDevice *device,
+ gpointer user_data)
+{
+ CcApplication *application;
+ g_autoptr(GList) devices = NULL;
+ guint i;
+
+ devices = gsd_device_manager_list_devices (manager, GSD_DEVICE_TYPE_TABLET);
+ i = g_list_length (devices);
+
+ /* Set the new visibility */
+ application = CC_APPLICATION (g_application_get_default ());
+ cc_shell_model_set_panel_visibility (cc_application_get_model (application),
+ "wacom",
+ i > 0 ? CC_PANEL_VISIBLE : CC_PANEL_VISIBLE_IN_SEARCH);
+
+ g_debug ("Wacom panel visible: %s", i > 0 ? "yes" : "no");
+}
+
+void
+cc_wacom_panel_static_init_func (void)
+{
+ GsdDeviceManager *manager;
+
+ manager = gsd_device_manager_get ();
+ g_signal_connect (G_OBJECT (manager), "device-added",
+ G_CALLBACK (update_visibility), NULL);
+ g_signal_connect (G_OBJECT (manager), "device-removed",
+ G_CALLBACK (update_visibility), NULL);
+ update_visibility (manager, NULL, NULL);
+}
+
+static CcWacomDevice *
+lookup_wacom_device (CcWacomPanel *self,
+ const gchar *name)
+{
+ GHashTableIter iter;
+ CcWacomDevice *wacom_device;
+
+ g_hash_table_iter_init (&iter, self->devices);
+ while (g_hash_table_iter_next (&iter, NULL, (gpointer *) &wacom_device)) {
+ if (g_strcmp0 (cc_wacom_device_get_name (wacom_device), name) == 0)
+ return wacom_device;
+ }
+
+ return NULL;
+}
+
+static CcWacomPage *
+set_device_page (CcWacomPanel *self, const gchar *device_name)
+{
+ CcWacomPage *page;
+ CcWacomDevice *wacom_device;
+ gint current;
+
+ if (device_name == NULL)
+ return NULL;
+
+ wacom_device = lookup_wacom_device (self, device_name);
+ if (!wacom_device) {
+ g_warning ("Failed to find device '%s', supplied in the command line.", device_name);
+ return NULL;
+ }
+
+ page = g_hash_table_lookup (self->pages, wacom_device);
+ current = gtk_notebook_page_num (GTK_NOTEBOOK (self->tablet_notebook), GTK_WIDGET (page));
+ gtk_notebook_set_current_page (GTK_NOTEBOOK (self->tablet_notebook), current);
+
+ return page;
+}
+
+static void
+run_operation_from_params (CcWacomPanel *self, GVariant *parameters)
+{
+ g_autoptr(GVariant) v = NULL;
+ g_autoptr(GVariant) v2 = NULL;
+ CcWacomPage *page;
+ const gchar *operation = NULL;
+ const gchar *device_name = NULL;
+ gint n_params;
+
+ n_params = g_variant_n_children (parameters);
+
+ g_variant_get_child (parameters, n_params - 1, "v", &v);
+ device_name = g_variant_get_string (v, NULL);
+
+ if (!g_variant_is_of_type (v, G_VARIANT_TYPE_STRING)) {
+ g_warning ("Wrong type for the second argument GVariant, expected 's' but got '%s'",
+ g_variant_get_type_string (v));
+ return;
+ }
+
+ switch (n_params) {
+ case 3:
+ page = set_device_page (self, device_name);
+ if (page == NULL)
+ return;
+
+ g_variant_get_child (parameters, 1, "v", &v2);
+
+ if (!g_variant_is_of_type (v2, G_VARIANT_TYPE_STRING)) {
+ g_warning ("Wrong type for the operation name argument. A string is expected.");
+ break;
+ }
+
+ operation = g_variant_get_string (v2, NULL);
+ if (g_strcmp0 (operation, "run-calibration") == 0) {
+ if (cc_wacom_page_can_calibrate (page))
+ cc_wacom_page_calibrate (page);
+ else
+ g_warning ("The device %s cannot be calibrated.", device_name);
+ } else {
+ g_warning ("Ignoring unrecognized operation '%s'", operation);
+ }
+ case 2:
+ set_device_page (self, device_name);
+ break;
+ case 1:
+ g_assert_not_reached ();
+ default:
+ g_warning ("Unexpected number of parameters found: %d. Request ignored.", n_params);
+ }
+}
+
+/* Boilerplate code goes below */
+
+static void
+cc_wacom_panel_get_property (GObject *object,
+ guint property_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ switch (property_id)
+ {
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ }
+}
+
+static void
+cc_wacom_panel_set_property (GObject *object,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ CcWacomPanel *self;
+ self = CC_WACOM_PANEL (object);
+
+ switch (property_id)
+ {
+ case PROP_PARAMETERS: {
+ GVariant *parameters;
+
+ parameters = g_value_get_variant (value);
+ if (parameters == NULL || g_variant_n_children (parameters) <= 1)
+ return;
+
+ run_operation_from_params (self, parameters);
+
+ break;
+ }
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ }
+}
+
+static void
+cc_wacom_panel_dispose (GObject *object)
+{
+ CcWacomPanel *self = CC_WACOM_PANEL (object);
+
+ g_clear_object (&self->builder);
+
+ g_clear_pointer (&self->devices, g_hash_table_unref);
+ g_clear_object (&self->proxy);
+ g_clear_pointer (&self->pages, g_hash_table_unref);
+ g_clear_pointer (&self->stylus_pages, g_hash_table_unref);
+
+ G_OBJECT_CLASS (cc_wacom_panel_parent_class)->dispose (object);
+}
+
+static void
+check_remove_stylus_pages (CcWacomPanel *self)
+{
+ GHashTableIter iter;
+ CcWacomDevice *device;
+ CcWacomTool *tool;
+ GtkWidget *page;
+ GList *tools;
+ g_autoptr(GList) total = NULL;
+
+ /* First. Iterate known devices and get the tools */
+ g_hash_table_iter_init (&iter, self->devices);
+ while (g_hash_table_iter_next (&iter, NULL, (gpointer*) &device)) {
+ tools = cc_tablet_tool_map_list_tools (self->tablet_tool_map, device);
+ total = g_list_concat (total, tools);
+ }
+
+ /* Second. Iterate through stylus pages and remove the ones whose
+ * tool is no longer in the list.
+ */
+ g_hash_table_iter_init (&iter, self->stylus_pages);
+ while (g_hash_table_iter_next (&iter, (gpointer*) &tool, (gpointer*) &page)) {
+ if (g_list_find (total, tool))
+ continue;
+ gtk_widget_destroy (page);
+ g_hash_table_iter_remove (&iter);
+ }
+}
+
+static gboolean
+add_stylus (CcWacomPanel *self,
+ CcWacomTool *tool)
+{
+ GtkWidget *page;
+
+ if (g_hash_table_lookup (self->stylus_pages, tool))
+ return FALSE;
+
+ page = cc_wacom_stylus_page_new (tool);
+ cc_wacom_stylus_page_set_navigation (CC_WACOM_STYLUS_PAGE (page),
+ GTK_NOTEBOOK (self->stylus_notebook));
+ gtk_widget_show (page);
+ gtk_notebook_append_page (GTK_NOTEBOOK (self->stylus_notebook), page, NULL);
+ g_hash_table_insert (self->stylus_pages, tool, page);
+
+ if (gtk_notebook_get_current_page (GTK_NOTEBOOK (self->stylus_notebook)) == 0)
+ gtk_notebook_set_current_page (GTK_NOTEBOOK (self->stylus_notebook), 1);
+
+ return TRUE;
+}
+
+static void
+update_test_button (CcWacomPanel *self)
+{
+ if (!self->test_button)
+ return;
+
+ if (g_hash_table_size (self->devices) == 0) {
+ gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (self->test_button), FALSE);
+ gtk_widget_set_sensitive (self->test_button, FALSE);
+ } else {
+ gtk_widget_set_sensitive (self->test_button, TRUE);
+ }
+}
+
+static void
+update_current_tool (CcWacomPanel *panel,
+ GdkDevice *device,
+ GdkDeviceTool *tool)
+{
+ GsdDeviceManager *device_manager;
+ CcWacomDevice *wacom_device;
+ CcWacomTool *stylus;
+ GsdDevice *gsd_device;
+ guint64 serial, id;
+ gboolean added;
+
+ if (!tool)
+ return;
+
+ /* Work our way to the CcWacomDevice */
+ device_manager = gsd_device_manager_get ();
+ gsd_device = gsd_device_manager_lookup_gdk_device (device_manager,
+ device);
+ if (!gsd_device)
+ return;
+
+ wacom_device = g_hash_table_lookup (panel->devices, gsd_device);
+ if (!wacom_device)
+ return;
+
+ /* Check whether we already know this tool, nothing to do then */
+ serial = gdk_device_tool_get_serial (tool);
+
+ /* The wacom driver sends serial-less tools with a serial of
+ * 1, libinput uses 0. No device exists with serial 1, let's reset
+ * it here so everything else works as expected.
+ */
+ if (serial == 1)
+ serial = 0;
+
+ stylus = cc_tablet_tool_map_lookup_tool (panel->tablet_tool_map,
+ wacom_device, serial);
+
+ if (!stylus) {
+ id = gdk_device_tool_get_hardware_id (tool);
+
+ /* The wacom driver sends a hw id of 0x2 for stylus and 0xa
+ * for eraser for devices that don't have a true HW id.
+ * Reset those to 0 so we can use the same code-paths
+ * libinput uses.
+ * The touch ID is 0x3, let's ignore that because we don't
+ * have a touch tool and it only happens when the wacom
+ * driver handles the touch device.
+ */
+ if (id == 0x2 || id == 0xa)
+ id = 0;
+ else if (id == 0x3)
+ return;
+
+ stylus = cc_wacom_tool_new (serial, id, wacom_device);
+ if (!stylus)
+ return;
+ }
+
+ added = add_stylus (panel, stylus);
+
+ if (added) {
+ if (panel->stylus_notebook ==
+ gtk_stack_get_visible_child (GTK_STACK (panel->stack))) {
+ GtkWidget *widget;
+ gint page;
+
+ widget = g_hash_table_lookup (panel->stylus_pages, stylus);
+ page = gtk_notebook_page_num (GTK_NOTEBOOK (panel->stylus_notebook), widget);
+ gtk_notebook_set_current_page (GTK_NOTEBOOK (panel->stylus_notebook), page);
+ } else {
+ gtk_container_child_set (GTK_CONTAINER (panel->stack),
+ panel->stylus_notebook,
+ "needs-attention", TRUE,
+ NULL);
+ }
+ }
+
+ cc_tablet_tool_map_add_relation (panel->tablet_tool_map,
+ wacom_device, stylus);
+}
+
+static gboolean
+on_shell_event_cb (CcWacomPanel *panel,
+ GdkEvent *event)
+{
+ if (event->type == GDK_MOTION_NOTIFY) {
+ update_current_tool (panel,
+ gdk_event_get_source_device (event),
+ gdk_event_get_device_tool (event));
+ }
+
+ return GDK_EVENT_PROPAGATE;
+}
+
+static void
+cc_wacom_panel_constructed (GObject *object)
+{
+ CcWacomPanel *self = CC_WACOM_PANEL (object);
+ GtkWidget *button;
+ CcShell *shell;
+
+ G_OBJECT_CLASS (cc_wacom_panel_parent_class)->constructed (object);
+
+ /* Add test area button to shell header. */
+ shell = cc_panel_get_shell (CC_PANEL (self));
+
+ button = gtk_toggle_button_new_with_mnemonic (_("Test Your _Settings"));
+ gtk_style_context_add_class (gtk_widget_get_style_context (button),
+ "text-button");
+ gtk_widget_set_valign (button, GTK_ALIGN_CENTER);
+ gtk_widget_set_visible (button, TRUE);
+
+ cc_shell_embed_widget_in_header (shell, button, GTK_POS_RIGHT);
+
+ self->test_popover = gtk_popover_new (button);
+ gtk_container_set_border_width (GTK_CONTAINER (self->test_popover), 6);
+
+ self->test_draw_area = cc_drawing_area_new ();
+ gtk_widget_set_size_request (self->test_draw_area, 400, 300);
+ gtk_container_add (GTK_CONTAINER (self->test_popover),
+ self->test_draw_area);
+ gtk_widget_show (self->test_draw_area);
+
+ g_object_bind_property (button, "active",
+ self->test_popover, "visible",
+ G_BINDING_BIDIRECTIONAL);
+
+ g_signal_connect_object (shell, "event",
+ G_CALLBACK (on_shell_event_cb), self, G_CONNECT_SWAPPED);
+
+ self->test_button = button;
+ update_test_button (self);
+}
+
+static const char *
+cc_wacom_panel_get_help_uri (CcPanel *panel)
+{
+ return "help:gnome-help/wacom";
+}
+
+static GtkWidget *
+cc_wacom_panel_get_title_widget (CcPanel *panel)
+{
+ CcWacomPanel *self = CC_WACOM_PANEL (panel);
+
+ return self->switcher;
+}
+
+static void
+cc_wacom_panel_class_init (CcWacomPanelClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ CcPanelClass *panel_class = CC_PANEL_CLASS (klass);
+
+ object_class->get_property = cc_wacom_panel_get_property;
+ object_class->set_property = cc_wacom_panel_set_property;
+ object_class->dispose = cc_wacom_panel_dispose;
+ object_class->constructed = cc_wacom_panel_constructed;
+
+ panel_class->get_help_uri = cc_wacom_panel_get_help_uri;
+ panel_class->get_title_widget = cc_wacom_panel_get_title_widget;
+
+ g_object_class_override_property (object_class, PROP_PARAMETERS, "parameters");
+}
+
+static void
+update_current_page (CcWacomPanel *self)
+{
+ int num_pages;
+
+ num_pages = gtk_notebook_get_n_pages (GTK_NOTEBOOK (self->tablet_notebook));
+ if (num_pages > 1)
+ gtk_notebook_set_current_page (GTK_NOTEBOOK (self->tablet_notebook), 1);
+
+ update_test_button (self);
+}
+
+static void
+add_known_device (CcWacomPanel *self,
+ GsdDevice *gsd_device)
+{
+ CcWacomDevice *device;
+ GsdDeviceType device_type;
+ g_autoptr(GList) tools = NULL;
+ GtkWidget *page;
+ GList *l;
+
+ device_type = gsd_device_get_device_type (gsd_device);
+
+ if ((device_type & GSD_DEVICE_TYPE_TABLET) == 0)
+ return;
+
+ if ((device_type &
+ (GSD_DEVICE_TYPE_PAD |
+ GSD_DEVICE_TYPE_TOUCHSCREEN |
+ GSD_DEVICE_TYPE_TOUCHPAD)) != 0) {
+ return;
+ }
+
+ device = cc_wacom_device_new (gsd_device);
+ if (!device)
+ return;
+
+ g_hash_table_insert (self->devices, gsd_device, device);
+
+ tools = cc_tablet_tool_map_list_tools (self->tablet_tool_map, device);
+
+ for (l = tools; l != NULL; l = l->next) {
+ add_stylus (self, l->data);
+ }
+
+ page = cc_wacom_page_new (self, device);
+ cc_wacom_page_set_navigation (CC_WACOM_PAGE (page), GTK_NOTEBOOK (self->tablet_notebook), TRUE);
+ gtk_widget_show (page);
+ gtk_notebook_append_page (GTK_NOTEBOOK (self->tablet_notebook), page, NULL);
+ g_hash_table_insert (self->pages, device, page);
+}
+
+static void
+device_removed_cb (CcWacomPanel *self,
+ GsdDevice *gsd_device)
+{
+ CcWacomDevice *device;
+ GtkWidget *page;
+
+ device = g_hash_table_lookup (self->devices, gsd_device);
+ if (!device)
+ return;
+
+ page = g_hash_table_lookup (self->pages, device);
+ if (page) {
+ g_hash_table_remove (self->pages, device);
+ gtk_widget_destroy (page);
+ }
+
+ g_hash_table_remove (self->devices, gsd_device);
+ check_remove_stylus_pages (self);
+ update_current_page (self);
+}
+
+static void
+device_added_cb (CcWacomPanel *self,
+ GsdDevice *device)
+{
+ add_known_device (self, device);
+ update_current_page (self);
+}
+
+static gboolean
+link_activated (CcWacomPanel *self)
+{
+ cc_wacom_panel_switch_to_panel (self, "bluetooth");
+ return TRUE;
+}
+
+void
+cc_wacom_panel_switch_to_panel (CcWacomPanel *self,
+ const char *panel)
+{
+ CcShell *shell;
+ g_autoptr(GError) error = NULL;
+
+ g_return_if_fail (self);
+
+ shell = cc_panel_get_shell (CC_PANEL (self));
+ if (!cc_shell_set_active_panel_from_id (shell, panel, NULL, &error))
+ g_warning ("Failed to activate '%s' panel: %s", panel, error->message);
+}
+
+static void
+got_osd_proxy_cb (GObject *source_object,
+ GAsyncResult *res,
+ gpointer data)
+{
+ g_autoptr(GError) error = NULL;
+ CcWacomPanel *self;
+
+ self = CC_WACOM_PANEL (data);
+ self->proxy = g_dbus_proxy_new_for_bus_finish (res, &error);
+
+ if (self->proxy == NULL) {
+ g_printerr ("Error creating proxy: %s\n", error->message);
+ return;
+ }
+}
+
+static void
+enbiggen_label (GtkLabel *label)
+{
+ const char *str;
+ g_autofree char *new_str = NULL;
+
+ str = gtk_label_get_text (label);
+ new_str = g_strdup_printf ("<big>%s</big>", str);
+ gtk_label_set_markup (label, new_str);
+}
+
+static void
+on_stack_visible_child_notify_cb (CcWacomPanel *panel)
+{
+ GtkWidget *child;
+
+ child = gtk_stack_get_visible_child (GTK_STACK (panel->stack));
+
+ if (child == panel->stylus_notebook) {
+ gtk_container_child_set (GTK_CONTAINER (panel->stack),
+ panel->stylus_notebook,
+ "needs-attention", FALSE,
+ NULL);
+ }
+}
+
+static void
+cc_wacom_panel_init (CcWacomPanel *self)
+{
+ GtkWidget *widget;
+ GsdDeviceManager *device_manager;
+ g_autoptr(GList) devices = NULL;
+ GList *l;
+ g_autoptr(GError) error = NULL;
+ char *objects[] = {
+ "main-box",
+ "no-stylus-page",
+ NULL
+ };
+
+ g_resources_register (cc_wacom_get_resource ());
+
+ self->builder = gtk_builder_new ();
+
+ gtk_builder_add_objects_from_resource (self->builder,
+ "/org/gnome/control-center/wacom/gnome-wacom-properties.ui",
+ objects,
+ &error);
+ gtk_builder_add_objects_from_resource (self->builder,
+ "/org/gnome/control-center/wacom/wacom-stylus-page.ui",
+ objects,
+ &error);
+ if (error != NULL) {
+ g_warning ("Error loading UI file: %s", error->message);
+ return;
+ }
+
+ self->tablet_tool_map = cc_tablet_tool_map_new ();
+
+ g_dbus_proxy_new_for_bus (G_BUS_TYPE_SESSION,
+ G_DBUS_PROXY_FLAGS_NONE,
+ NULL,
+ "org.gnome.Shell",
+ "/org/gnome/Shell/Wacom",
+ "org.gnome.Shell.Wacom.PadOsd",
+ cc_panel_get_cancellable (CC_PANEL (self)),
+ got_osd_proxy_cb,
+ self);
+
+ /* Stack + Switcher */
+ self->stack = gtk_stack_new ();
+ g_object_set (G_OBJECT (self->stack),
+ "margin-top", 30,
+ "margin-end", 30,
+ "margin-start", 30,
+ "margin-bottom", 30,
+ NULL);
+
+ g_signal_connect_object (self->stack, "notify::visible-child",
+ G_CALLBACK (on_stack_visible_child_notify_cb), self, G_CONNECT_SWAPPED);
+
+ self->switcher = gtk_stack_switcher_new ();
+ gtk_stack_switcher_set_stack (GTK_STACK_SWITCHER (self->switcher),
+ GTK_STACK (self->stack));
+ gtk_widget_show (self->switcher);
+
+ gtk_container_add (GTK_CONTAINER (self), GTK_WIDGET (self->stack));
+ gtk_widget_show (self->stack);
+
+ self->tablet_notebook = gtk_notebook_new ();
+ gtk_widget_show (self->tablet_notebook);
+ gtk_notebook_set_show_tabs (GTK_NOTEBOOK (self->tablet_notebook), FALSE);
+ gtk_notebook_set_show_border (GTK_NOTEBOOK (self->tablet_notebook), FALSE);
+ gtk_widget_set_vexpand (self->tablet_notebook, TRUE);
+
+ self->stylus_notebook = gtk_notebook_new ();
+ gtk_widget_show (self->stylus_notebook);
+ gtk_notebook_set_show_tabs (GTK_NOTEBOOK (self->stylus_notebook), FALSE);
+ gtk_notebook_set_show_border (GTK_NOTEBOOK (self->stylus_notebook), FALSE);
+ gtk_container_set_border_width (GTK_CONTAINER (self->stylus_notebook), 0);
+ gtk_widget_set_vexpand (self->stylus_notebook, TRUE);
+
+ gtk_stack_add_titled (GTK_STACK (self->stack),
+ self->stylus_notebook, "stylus",
+ _("Stylus"));
+ gtk_stack_add_titled (GTK_STACK (self->stack),
+ self->tablet_notebook, "tablet",
+ _("Tablet"));
+
+ /* No styli page */
+ widget = WID ("no-stylus-page");
+ enbiggen_label (GTK_LABEL (WID ("no-stylus-label1")));
+ gtk_notebook_append_page (GTK_NOTEBOOK (self->stylus_notebook), widget, NULL);
+
+ /* No tablets page */
+ widget = WID ("main-box");
+ enbiggen_label (GTK_LABEL (WID ("advice-label1")));
+ gtk_notebook_append_page (GTK_NOTEBOOK (self->tablet_notebook), widget, NULL);
+
+ g_signal_connect_object (WID ("linkbutton"), "activate-link",
+ G_CALLBACK (link_activated), self, G_CONNECT_SWAPPED);
+
+ self->devices = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, g_object_unref);
+ self->pages = g_hash_table_new (NULL, NULL);
+ self->stylus_pages = g_hash_table_new (NULL, NULL);
+
+ device_manager = gsd_device_manager_get ();
+ g_signal_connect_object (device_manager, "device-added",
+ G_CALLBACK (device_added_cb), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (device_manager, "device-removed",
+ G_CALLBACK (device_removed_cb), self, G_CONNECT_SWAPPED);
+
+ devices = gsd_device_manager_list_devices (device_manager,
+ GSD_DEVICE_TYPE_TABLET);
+ for (l = devices; l ; l = l->next)
+ add_known_device (self, l->data);
+
+ update_current_page (self);
+}
+
+GDBusProxy *
+cc_wacom_panel_get_gsd_wacom_bus_proxy (CcWacomPanel *self)
+{
+ g_return_val_if_fail (CC_IS_WACOM_PANEL (self), NULL);
+
+ return self->proxy;
+}
diff --git a/panels/wacom/cc-wacom-panel.h b/panels/wacom/cc-wacom-panel.h
new file mode 100644
index 0000000..55bf1f9
--- /dev/null
+++ b/panels/wacom/cc-wacom-panel.h
@@ -0,0 +1,40 @@
+/*
+ * Copyright © 2011 Red Hat, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Peter Hutterer <peter.hutterer@redhat.com>
+ * Bastien Nocera <hadess@hadess.net>
+ */
+
+#pragma once
+
+#include <shell/cc-panel.h>
+
+G_BEGIN_DECLS
+
+#define CC_TYPE_WACOM_PANEL (cc_wacom_panel_get_type ())
+G_DECLARE_FINAL_TYPE (CcWacomPanel, cc_wacom_panel, CC, WACOM_PANEL, CcPanel)
+
+void cc_wacom_panel_static_init_func (void);
+
+void cc_wacom_panel_switch_to_panel (CcWacomPanel *self,
+ const char *panel);
+
+void cc_wacom_panel_set_osd_visibility (CcWacomPanel *self,
+ guint32 device_id);
+
+GDBusProxy * cc_wacom_panel_get_gsd_wacom_bus_proxy (CcWacomPanel *self);
+
+G_END_DECLS
diff --git a/panels/wacom/cc-wacom-stylus-page.c b/panels/wacom/cc-wacom-stylus-page.c
new file mode 100644
index 0000000..2fc6c5a
--- /dev/null
+++ b/panels/wacom/cc-wacom-stylus-page.c
@@ -0,0 +1,503 @@
+/*
+ * Copyright © 2011 Red Hat, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Peter Hutterer <peter.hutterer@redhat.com>
+ * Bastien Nocera <hadess@hadess.net>
+ *
+ */
+
+#include <config.h>
+
+#include <glib/gi18n.h>
+#include "cc-wacom-stylus-page.h"
+#include "cc-wacom-nav-button.h"
+#include <gtk/gtk.h>
+#include <gdesktop-enums.h>
+
+#include <string.h>
+
+#define WID(x) (GtkWidget *) gtk_builder_get_object (page->builder, x)
+#define CWID(x) (GtkContainer *) gtk_builder_get_object (page->builder, x)
+
+struct _CcWacomStylusPage
+{
+ GtkBox parent_instance;
+
+ CcWacomTool *stylus;
+ GtkBuilder *builder;
+ GtkWidget *nav;
+ GSettings *stylus_settings;
+};
+
+G_DEFINE_TYPE (CcWacomStylusPage, cc_wacom_stylus_page, GTK_TYPE_BOX)
+
+/* Button combo box storage columns */
+enum {
+ BUTTONNUMBER_COLUMN,
+ BUTTONNAME_COLUMN,
+ N_BUTTONCOLUMNS
+};
+
+/* GSettings stores pressurecurve as 4 values like the driver. We map slider
+ * scale to these values given the array below. These settings were taken from
+ * wacomcpl, where they've been around for years.
+ */
+#define N_PRESSURE_CURVES 7
+static const gint32 PRESSURE_CURVES[N_PRESSURE_CURVES][4] = {
+ { 0, 75, 25, 100 }, /* soft */
+ { 0, 50, 50, 100 },
+ { 0, 25, 75, 100 },
+ { 0, 0, 100, 100 }, /* neutral */
+ { 25, 0, 100, 75 },
+ { 50, 0, 100, 50 },
+ { 75, 0, 100, 25 } /* firm */
+};
+
+static void
+set_pressurecurve (GtkRange *range, GSettings *settings, const gchar *key)
+{
+ gint slider_val = gtk_range_get_value (range);
+ GVariant *values[4],
+ *array;
+ int i;
+
+ for (i = 0; i < G_N_ELEMENTS (values); i++)
+ values[i] = g_variant_new_int32 (PRESSURE_CURVES[slider_val][i]);
+
+ array = g_variant_new_array (G_VARIANT_TYPE_INT32, values, G_N_ELEMENTS (values));
+
+ g_settings_set_value (settings, key, array);
+}
+
+static void
+tip_feel_value_changed_cb (CcWacomStylusPage *page)
+{
+ set_pressurecurve (GTK_RANGE (WID ("scale-tip-feel")), page->stylus_settings, "pressure-curve");
+}
+
+static void
+eraser_feel_value_changed_cb (CcWacomStylusPage *page)
+{
+ set_pressurecurve (GTK_RANGE (WID ("scale-eraser-feel")), page->stylus_settings, "eraser-pressure-curve");
+}
+
+static void
+set_feel_from_gsettings (GtkAdjustment *adjustment, GSettings *settings, const gchar *key)
+{
+ GVariant *variant;
+ const gint32 *values;
+ gsize nvalues;
+ int i;
+
+ variant = g_settings_get_value (settings, key);
+ values = g_variant_get_fixed_array (variant, &nvalues, sizeof (gint32));
+
+ if (nvalues != 4) {
+ g_warning ("Invalid pressure curve format, expected 4 values (got %"G_GSIZE_FORMAT")", nvalues);
+ return;
+ }
+
+ for (i = 0; i < N_PRESSURE_CURVES; i++) {
+ if (memcmp (PRESSURE_CURVES[i], values, sizeof (gint32) * 4) == 0) {
+ gtk_adjustment_set_value (adjustment, i);
+ break;
+ }
+ }
+}
+
+static void
+set_button_mapping_from_gsettings (GtkComboBox *combo, GSettings* settings, const gchar *key)
+{
+ GDesktopStylusButtonAction action;
+ GtkTreeModel *model;
+ GtkTreeIter iter;
+ gboolean valid;
+
+ action = g_settings_get_enum (settings, key);
+ model = gtk_combo_box_get_model (combo);
+ valid = gtk_tree_model_get_iter_first (model, &iter);
+
+ while (valid) {
+ gint button;
+
+ gtk_tree_model_get (model, &iter,
+ BUTTONNUMBER_COLUMN, &button,
+ -1);
+
+ /* Currently button values match logical X buttons. If we
+ * introduce things like double-click, this code must
+ * change. Recommendation: use negative buttons numbers for
+ * special ones.
+ */
+
+ if (button == action) {
+ gtk_combo_box_set_active_iter (combo, &iter);
+ break;
+ }
+
+ valid = gtk_tree_model_iter_next (model, &iter);
+ }
+}
+
+static void
+button_changed_cb (CcWacomStylusPage *page)
+{
+ GtkTreeIter iter;
+ GtkListStore *liststore;
+ gint mapping_b2,
+ mapping_b3,
+ mapping_b4;
+
+ if (!gtk_combo_box_get_active_iter (GTK_COMBO_BOX (WID ("combo-bottombutton")), &iter))
+ return;
+
+ liststore = GTK_LIST_STORE (WID ("liststore-buttons"));
+ gtk_tree_model_get (GTK_TREE_MODEL (liststore), &iter,
+ BUTTONNUMBER_COLUMN, &mapping_b2,
+ -1);
+
+ if (cc_wacom_tool_get_num_buttons (page->stylus) > 1) {
+ if (!gtk_combo_box_get_active_iter (GTK_COMBO_BOX (WID ("combo-topbutton")), &iter))
+ return;
+
+ gtk_tree_model_get (GTK_TREE_MODEL (liststore), &iter,
+ BUTTONNUMBER_COLUMN, &mapping_b3,
+ -1);
+ } else {
+ mapping_b3 = 0;
+ }
+
+ if (cc_wacom_tool_get_num_buttons (page->stylus) > 2) {
+ if (!gtk_combo_box_get_active_iter (GTK_COMBO_BOX (WID ("combo-thirdbutton")), &iter))
+ return;
+
+ gtk_tree_model_get (GTK_TREE_MODEL (liststore), &iter,
+ BUTTONNUMBER_COLUMN, &mapping_b4,
+ -1);
+ } else {
+ mapping_b4 = 0;
+ }
+
+ g_settings_set_enum (page->stylus_settings, "button-action", mapping_b2);
+ g_settings_set_enum (page->stylus_settings, "secondary-button-action", mapping_b3);
+ g_settings_set_enum (page->stylus_settings, "tertiary-button-action", mapping_b4);
+}
+
+static void
+combobox_text_cellrenderer (GtkComboBox *combo, int name_column)
+{
+ GtkCellRenderer *renderer;
+
+ renderer = gtk_cell_renderer_text_new ();
+ gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (combo), renderer, TRUE);
+ gtk_cell_layout_set_attributes (GTK_CELL_LAYOUT (combo), renderer,
+ "text", BUTTONNAME_COLUMN, NULL);
+}
+
+/* Boilerplate code goes below */
+
+static void
+cc_wacom_stylus_page_get_property (GObject *object,
+ guint property_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ switch (property_id)
+ {
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ }
+}
+
+static void
+cc_wacom_stylus_page_set_property (GObject *object,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ switch (property_id)
+ {
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ }
+}
+
+static void
+cc_wacom_stylus_page_dispose (GObject *object)
+{
+ CcWacomStylusPage *page = CC_WACOM_STYLUS_PAGE (object);
+
+ g_clear_object (&page->builder);
+
+ G_OBJECT_CLASS (cc_wacom_stylus_page_parent_class)->dispose (object);
+}
+
+static void
+cc_wacom_stylus_page_class_init (CcWacomStylusPageClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->get_property = cc_wacom_stylus_page_get_property;
+ object_class->set_property = cc_wacom_stylus_page_set_property;
+ object_class->dispose = cc_wacom_stylus_page_dispose;
+}
+
+static void
+add_marks (GtkScale *scale)
+{
+ gint i;
+
+ for (i = 0; i < N_PRESSURE_CURVES; i++)
+ gtk_scale_add_mark (scale, i, GTK_POS_BOTTOM, NULL);
+}
+
+static void
+cc_wacom_stylus_page_init (CcWacomStylusPage *page)
+{
+ g_autoptr(GError) error = NULL;
+ GtkComboBox *combo;
+ GtkWidget *box;
+ char *objects[] = {
+ "stylus-grid",
+ "liststore-buttons",
+ "adjustment-tip-feel",
+ "adjustment-eraser-feel",
+ NULL
+ };
+
+ page->builder = gtk_builder_new ();
+
+ gtk_builder_add_objects_from_resource (page->builder,
+ "/org/gnome/control-center/wacom/wacom-stylus-page.ui",
+ objects,
+ &error);
+ if (error != NULL) {
+ g_warning ("Error loading UI file: %s", error->message);
+ return;
+ }
+
+ box = WID ("stylus-grid");
+ gtk_container_add (GTK_CONTAINER (page), box);
+ gtk_widget_set_vexpand (GTK_WIDGET (box), TRUE);
+
+ add_marks (GTK_SCALE (WID ("scale-tip-feel")));
+ add_marks (GTK_SCALE (WID ("scale-eraser-feel")));
+
+ g_signal_connect_object (WID ("scale-tip-feel"), "value-changed",
+ G_CALLBACK (tip_feel_value_changed_cb), page, G_CONNECT_SWAPPED);
+ g_signal_connect_object (WID ("scale-eraser-feel"), "value-changed",
+ G_CALLBACK (eraser_feel_value_changed_cb), page, G_CONNECT_SWAPPED);
+
+ combo = GTK_COMBO_BOX (WID ("combo-topbutton"));
+ combobox_text_cellrenderer (combo, BUTTONNAME_COLUMN);
+ g_signal_connect_object (combo, "changed",
+ G_CALLBACK (button_changed_cb), page, G_CONNECT_SWAPPED);
+
+ combo = GTK_COMBO_BOX (WID ("combo-bottombutton"));
+ combobox_text_cellrenderer (combo, BUTTONNAME_COLUMN);
+ g_signal_connect_object (combo, "changed",
+ G_CALLBACK (button_changed_cb), page, G_CONNECT_SWAPPED);
+
+ combo = GTK_COMBO_BOX (WID ("combo-thirdbutton"));
+ combobox_text_cellrenderer (combo, BUTTONNAME_COLUMN);
+ g_signal_connect_object (G_OBJECT (combo), "changed",
+ G_CALLBACK (button_changed_cb), page, G_CONNECT_SWAPPED);
+
+ page->nav = cc_wacom_nav_button_new ();
+ gtk_widget_set_halign (page->nav, GTK_ALIGN_END);
+ gtk_widget_set_margin_start (page->nav, 10);
+ gtk_widget_show (page->nav);
+ gtk_container_add (CWID ("navigation-placeholder"), page->nav);
+}
+
+static void
+set_icon_name (CcWacomStylusPage *page,
+ const char *widget_name,
+ const char *icon_name)
+{
+ g_autofree gchar *resource = NULL;
+
+ resource = g_strdup_printf ("/org/gnome/control-center/wacom/%s.svg", icon_name);
+ gtk_image_set_from_resource (GTK_IMAGE (WID (widget_name)), resource);
+}
+
+/* Different types of layout for the stylus config */
+enum {
+ LAYOUT_NORMAL, /* eraser, 2 buttons, tip */
+ LAYOUT_INKING, /* tip */
+ LAYOUT_AIRBRUSH, /* eraser, 1 button, tip */
+ LAYOUT_GENERIC_2_BUTTONS_NO_ERASER, /* 2 buttons, tip, no eraser */
+ LAYOUT_3DPEN, /* 3 buttons, tip, no eraser */
+ LAYOUT_OTHER
+};
+
+static void
+remove_buttons (CcWacomStylusPage *page, int n)
+{
+ if (n < 3) {
+ gtk_widget_destroy (WID ("combo-thirdbutton"));
+ gtk_widget_destroy (WID ("label-third-button"));
+ }
+ if (n < 2) {
+ gtk_widget_destroy (WID ("combo-topbutton"));
+ gtk_widget_destroy (WID ("label-top-button"));
+ gtk_label_set_text (GTK_LABEL (WID ("label-lower-button")), _("Button"));
+ }
+ if (n < 1) {
+ gtk_widget_destroy (WID ("combo-bottombutton"));
+ gtk_widget_destroy (WID ("label-lower-button"));
+ }
+}
+
+static void
+remove_eraser (CcWacomStylusPage *page)
+{
+ gtk_widget_destroy (WID ("eraser-box"));
+ gtk_widget_destroy (WID ("label-eraser-feel"));
+}
+
+static void
+update_stylus_ui (CcWacomStylusPage *page,
+ int layout)
+{
+ switch (layout) {
+ case LAYOUT_NORMAL:
+ remove_buttons (page, 2);
+ break;
+ case LAYOUT_INKING:
+ remove_buttons (page, 0);
+ remove_eraser (page);
+ gtk_container_child_set (CWID ("stylus-controls-grid"),
+ WID ("label-tip-feel"),
+ "top_attach", 0, NULL);
+ gtk_container_child_set (CWID ("stylus-controls-grid"),
+ WID ("box-tip-feel"),
+ "top_attach", 0, NULL);
+ break;
+ case LAYOUT_AIRBRUSH:
+ remove_buttons (page, 1);
+ gtk_container_child_set (CWID ("stylus-controls-grid"),
+ WID ("label-lower-button"),
+ "top_attach", 1, NULL);
+ gtk_container_child_set (CWID ("stylus-controls-grid"),
+ WID ("combo-bottombutton"),
+ "top_attach", 1, NULL);
+ gtk_container_child_set (CWID ("stylus-controls-grid"),
+ WID ("label-tip-feel"),
+ "top_attach", 2, NULL);
+ gtk_container_child_set (CWID ("stylus-controls-grid"),
+ WID ("box-tip-feel"),
+ "top_attach", 2, NULL);
+ break;
+ case LAYOUT_GENERIC_2_BUTTONS_NO_ERASER:
+ remove_buttons (page, 2);
+ remove_eraser (page);
+ break;
+ case LAYOUT_3DPEN:
+ remove_buttons (page, 3);
+ remove_eraser (page);
+ break;
+ case LAYOUT_OTHER:
+ /* We already warn about it in cc_wacom_stylus_page_new () */
+ break;
+ }
+}
+
+GtkWidget *
+cc_wacom_stylus_page_new (CcWacomTool *stylus)
+{
+ CcWacomStylusPage *page;
+ guint num_buttons;
+ int layout;
+ gboolean has_eraser;
+
+ g_return_val_if_fail (CC_IS_WACOM_TOOL (stylus), NULL);
+
+ page = g_object_new (CC_TYPE_WACOM_STYLUS_PAGE, NULL);
+
+ page->stylus = stylus;
+
+ /* Icon */
+ set_icon_name (page, "image-stylus", cc_wacom_tool_get_icon_name (stylus));
+
+ /* Settings */
+ page->stylus_settings = cc_wacom_tool_get_settings (stylus);
+ has_eraser = cc_wacom_tool_get_has_eraser (stylus);
+
+ /* Stylus name */
+ gtk_label_set_text (GTK_LABEL (WID ("label-stylus")), cc_wacom_tool_get_name (stylus));
+
+ num_buttons = cc_wacom_tool_get_num_buttons (stylus);
+ if (num_buttons == 0 && !has_eraser)
+ layout = LAYOUT_INKING;
+ else if (num_buttons == 2 && has_eraser)
+ layout = LAYOUT_NORMAL;
+ else if (num_buttons == 1 && has_eraser)
+ layout = LAYOUT_AIRBRUSH;
+ else if (num_buttons == 2 && !has_eraser)
+ layout = LAYOUT_GENERIC_2_BUTTONS_NO_ERASER;
+ else if (num_buttons == 3 && !has_eraser)
+ layout = LAYOUT_3DPEN;
+ else {
+ layout = LAYOUT_OTHER;
+ remove_buttons (page, num_buttons);
+
+ /* Gray out eraser if not available */
+ gtk_widget_set_sensitive (WID ("eraser-box"), has_eraser);
+ gtk_widget_set_sensitive (WID ("label-eraser-feel"), has_eraser);
+
+ g_warning ("The layout of this page is not known, %d buttons, %s eraser",
+ num_buttons, has_eraser ? "with" : "without");
+ }
+
+ update_stylus_ui (page, layout);
+
+ if (num_buttons >= 3)
+ set_button_mapping_from_gsettings (GTK_COMBO_BOX (WID ("combo-thirdbutton")),
+ page->stylus_settings, "tertiary-button-action");
+ if (num_buttons >= 2)
+ set_button_mapping_from_gsettings (GTK_COMBO_BOX (WID ("combo-topbutton")),
+ page->stylus_settings, "secondary-button-action");
+ if (num_buttons >= 1)
+ set_button_mapping_from_gsettings (GTK_COMBO_BOX (WID ("combo-bottombutton")),
+ page->stylus_settings, "button-action");
+ set_feel_from_gsettings (GTK_ADJUSTMENT (WID ("adjustment-tip-feel")),
+ page->stylus_settings, "pressure-curve");
+
+ if (has_eraser)
+ set_feel_from_gsettings (GTK_ADJUSTMENT (WID ("adjustment-eraser-feel")),
+ page->stylus_settings, "eraser-pressure-curve");
+
+ return GTK_WIDGET (page);
+}
+
+CcWacomTool *
+cc_wacom_stylus_page_get_tool (CcWacomStylusPage *page)
+{
+ return page->stylus;
+}
+
+void
+cc_wacom_stylus_page_set_navigation (CcWacomStylusPage *page,
+ GtkNotebook *notebook)
+{
+ g_return_if_fail (CC_IS_WACOM_STYLUS_PAGE (page));
+
+ g_object_set (G_OBJECT (page->nav),
+ "notebook", notebook,
+ "ignore-first", TRUE,
+ NULL);
+}
diff --git a/panels/wacom/cc-wacom-stylus-page.h b/panels/wacom/cc-wacom-stylus-page.h
new file mode 100644
index 0000000..23810d9
--- /dev/null
+++ b/panels/wacom/cc-wacom-stylus-page.h
@@ -0,0 +1,38 @@
+/*
+ * Copyright © 2011 Red Hat, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Peter Hutterer <peter.hutterer@redhat.com>
+ * Bastien Nocera <hadess@hadess.net>
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+#include "cc-wacom-tool.h"
+
+G_BEGIN_DECLS
+
+#define CC_TYPE_WACOM_STYLUS_PAGE (cc_wacom_stylus_page_get_type ())
+G_DECLARE_FINAL_TYPE (CcWacomStylusPage, cc_wacom_stylus_page, CC, WACOM_STYLUS_PAGE, GtkBox)
+
+GtkWidget * cc_wacom_stylus_page_new (CcWacomTool *stylus);
+
+CcWacomTool * cc_wacom_stylus_page_get_tool (CcWacomStylusPage *page);
+
+void cc_wacom_stylus_page_set_navigation (CcWacomStylusPage *page,
+ GtkNotebook *notebook);
+
+G_END_DECLS
diff --git a/panels/wacom/cc-wacom-tool.c b/panels/wacom/cc-wacom-tool.c
new file mode 100644
index 0000000..d54de61
--- /dev/null
+++ b/panels/wacom/cc-wacom-tool.c
@@ -0,0 +1,309 @@
+/*
+ * Copyright © 2016 Red Hat, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Carlos Garnacho <carlosg@gnome.org>
+ *
+ */
+
+#include "config.h"
+
+#include "cc-wacom-tool.h"
+
+enum {
+ PROP_0,
+ PROP_SERIAL,
+ PROP_ID,
+ PROP_DEVICE,
+ N_PROPS
+};
+
+static GParamSpec *props[N_PROPS] = { 0 };
+
+typedef struct _CcWacomTool CcWacomTool;
+
+struct _CcWacomTool {
+ GObject parent_instance;
+ guint64 serial;
+ guint64 id;
+
+ CcWacomDevice *device; /* Only set for tools with no serial */
+
+ GSettings *settings;
+ const WacomStylus *wstylus;
+};
+
+static void cc_wacom_tool_initable_iface_init (GInitableIface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (CcWacomTool, cc_wacom_tool, G_TYPE_OBJECT,
+ G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE,
+ cc_wacom_tool_initable_iface_init))
+
+static void
+cc_wacom_tool_init (CcWacomTool *tool)
+{
+}
+
+static void
+cc_wacom_tool_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ CcWacomTool *tool = CC_WACOM_TOOL (object);
+
+ switch (prop_id) {
+ case PROP_SERIAL:
+ tool->serial = g_value_get_uint64 (value);
+ break;
+ case PROP_ID:
+ tool->id = g_value_get_uint64 (value);
+ break;
+ case PROP_DEVICE:
+ tool->device = g_value_get_object (value);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+cc_wacom_tool_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ CcWacomTool *tool = CC_WACOM_TOOL (object);
+
+ switch (prop_id) {
+ case PROP_SERIAL:
+ g_value_set_uint64 (value, tool->serial);
+ break;
+ case PROP_ID:
+ g_value_set_uint64 (value, tool->id);
+ break;
+ case PROP_DEVICE:
+ g_value_set_object (value, tool->device);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+cc_wacom_tool_finalize (GObject *object)
+{
+ CcWacomTool *tool = CC_WACOM_TOOL (object);
+
+ g_clear_object (&tool->settings);
+
+ G_OBJECT_CLASS (cc_wacom_tool_parent_class)->finalize (object);
+}
+
+static void
+cc_wacom_tool_class_init (CcWacomToolClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->set_property = cc_wacom_tool_set_property;
+ object_class->get_property = cc_wacom_tool_get_property;
+ object_class->finalize = cc_wacom_tool_finalize;
+
+ props[PROP_SERIAL] =
+ g_param_spec_uint64 ("serial",
+ "serial",
+ "serial",
+ 0, G_MAXUINT64, 0,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
+ props[PROP_ID] =
+ g_param_spec_uint64 ("id",
+ "id",
+ "id",
+ 0, G_MAXUINT64, 0,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
+ props[PROP_DEVICE] =
+ g_param_spec_object ("device",
+ "device",
+ "device",
+ CC_TYPE_WACOM_DEVICE,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
+
+ g_object_class_install_properties (object_class, N_PROPS, props);
+}
+
+static gboolean
+cc_wacom_tool_initable_init (GInitable *initable,
+ GCancellable *cancellable,
+ GError **error)
+{
+ CcWacomTool *tool = CC_WACOM_TOOL (initable);
+ WacomDeviceDatabase *wacom_db;
+ gchar *path;
+
+ wacom_db = cc_wacom_device_database_get ();
+
+ if (tool->id == 0 && tool->device) {
+ const gint *ids;
+ gint n_supported;
+
+ ids = cc_wacom_device_get_supported_tools (tool->device, &n_supported);
+ if (n_supported > 0)
+ tool->id = ids[0];
+ }
+
+ if (tool->id == 0)
+ tool->wstylus = libwacom_stylus_get_for_id (wacom_db, 0xfffff);
+ else
+ tool->wstylus = libwacom_stylus_get_for_id (wacom_db, tool->id);
+
+ if (!tool->wstylus) {
+ g_set_error (error, 0, 0, "Stylus description not found");
+ return FALSE;
+ }
+
+ if (tool->serial == 0) {
+ const gchar *vendor, *product;
+ GsdDevice *gsd_device;
+
+ gsd_device = cc_wacom_device_get_device (tool->device);
+ gsd_device_get_device_ids (gsd_device, &vendor, &product);
+ path = g_strdup_printf ("/org/gnome/desktop/peripherals/stylus/default-%s:%s/",
+ vendor, product);
+ } else {
+ path = g_strdup_printf ("/org/gnome/desktop/peripherals/stylus/%lx/", tool->serial);
+ }
+
+ tool->settings = g_settings_new_with_path ("org.gnome.desktop.peripherals.tablet.stylus",
+ path);
+ g_free (path);
+
+ return TRUE;
+}
+
+static void
+cc_wacom_tool_initable_iface_init (GInitableIface *iface)
+{
+ iface->init = cc_wacom_tool_initable_init;
+}
+
+CcWacomTool *
+cc_wacom_tool_new (guint64 serial,
+ guint64 id,
+ CcWacomDevice *device)
+{
+ g_return_val_if_fail (serial != 0 || CC_IS_WACOM_DEVICE (device), NULL);
+
+ return g_initable_new (CC_TYPE_WACOM_TOOL,
+ NULL, NULL,
+ "serial", serial,
+ "id", id,
+ "device", device,
+ NULL);
+}
+
+guint64
+cc_wacom_tool_get_serial (CcWacomTool *tool)
+{
+ g_return_val_if_fail (CC_IS_WACOM_TOOL (tool), 0);
+
+ return tool->serial;
+}
+
+guint64
+cc_wacom_tool_get_id (CcWacomTool *tool)
+{
+ g_return_val_if_fail (CC_IS_WACOM_TOOL (tool), 0);
+
+ return tool->id;
+}
+
+const gchar *
+cc_wacom_tool_get_name (CcWacomTool *tool)
+{
+ g_return_val_if_fail (CC_IS_WACOM_TOOL (tool), NULL);
+
+ return libwacom_stylus_get_name (tool->wstylus);
+}
+
+static const char *
+get_icon_name_from_type (const WacomStylus *wstylus)
+{
+ WacomStylusType type = libwacom_stylus_get_type (wstylus);
+
+ switch (type) {
+ case WSTYLUS_INKING:
+ case WSTYLUS_STROKE:
+ /* The stroke pen is the same as the inking pen with
+ * a different nib */
+ return "wacom-stylus-inking";
+ case WSTYLUS_AIRBRUSH:
+ return "wacom-stylus-airbrush";
+ case WSTYLUS_MARKER:
+ return "wacom-stylus-art-pen";
+ case WSTYLUS_CLASSIC:
+ return "wacom-stylus-classic";
+#ifdef HAVE_WACOM_3D_STYLUS
+ case WSTYLUS_3D:
+ return "wacom-stylus-3btn-no-eraser";
+#endif
+ default:
+ if (!libwacom_stylus_has_eraser (wstylus)) {
+ if (libwacom_stylus_get_num_buttons (wstylus) >= 3)
+ return "wacom-stylus-3btn-no-eraser";
+ else
+ return "wacom-stylus-no-eraser";
+ }
+ else {
+ if (libwacom_stylus_get_num_buttons (wstylus) >= 3)
+ return "wacom-stylus-3btn";
+ else
+ return "wacom-stylus";
+ }
+ }
+}
+
+const gchar *
+cc_wacom_tool_get_icon_name (CcWacomTool *tool)
+{
+ g_return_val_if_fail (CC_IS_WACOM_TOOL (tool), NULL);
+
+ return get_icon_name_from_type (tool->wstylus);
+}
+
+GSettings *
+cc_wacom_tool_get_settings (CcWacomTool *tool)
+{
+ g_return_val_if_fail (CC_IS_WACOM_TOOL (tool), NULL);
+
+ return tool->settings;
+}
+
+guint
+cc_wacom_tool_get_num_buttons (CcWacomTool *tool)
+{
+ g_return_val_if_fail (CC_IS_WACOM_TOOL (tool), 0);
+
+ return libwacom_stylus_get_num_buttons (tool->wstylus);
+}
+
+gboolean
+cc_wacom_tool_get_has_eraser (CcWacomTool *tool)
+{
+ g_return_val_if_fail (CC_IS_WACOM_TOOL (tool), FALSE);
+
+ return libwacom_stylus_has_eraser (tool->wstylus);
+}
diff --git a/panels/wacom/cc-wacom-tool.h b/panels/wacom/cc-wacom-tool.h
new file mode 100644
index 0000000..f988e1f
--- /dev/null
+++ b/panels/wacom/cc-wacom-tool.h
@@ -0,0 +1,44 @@
+/*
+ * Copyright © 2016 Red Hat, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Carlos Garnacho <carlosg@gnome.org>
+ *
+ */
+
+#pragma once
+
+#include "config.h"
+#include "gsd-device-manager.h"
+#include "cc-wacom-device.h"
+#include <glib.h>
+
+#define CC_TYPE_WACOM_TOOL (cc_wacom_tool_get_type ())
+G_DECLARE_FINAL_TYPE (CcWacomTool, cc_wacom_tool, CC, WACOM_TOOL, GObject)
+
+CcWacomTool * cc_wacom_tool_new (guint64 serial,
+ guint64 id,
+ CcWacomDevice *device);
+
+guint64 cc_wacom_tool_get_serial (CcWacomTool *tool);
+guint64 cc_wacom_tool_get_id (CcWacomTool *tool);
+
+const gchar * cc_wacom_tool_get_name (CcWacomTool *tool);
+const gchar * cc_wacom_tool_get_icon_name (CcWacomTool *tool);
+
+GSettings * cc_wacom_tool_get_settings (CcWacomTool *tool);
+
+guint cc_wacom_tool_get_num_buttons (CcWacomTool *tool);
+gboolean cc_wacom_tool_get_has_eraser (CcWacomTool *tool);
diff --git a/panels/wacom/gnome-wacom-panel.desktop.in.in b/panels/wacom/gnome-wacom-panel.desktop.in.in
new file mode 100644
index 0000000..c8358ee
--- /dev/null
+++ b/panels/wacom/gnome-wacom-panel.desktop.in.in
@@ -0,0 +1,18 @@
+[Desktop Entry]
+Name=Wacom Tablet
+Comment=Set button mappings and adjust stylus sensitivity for graphics tablets
+Exec=gnome-control-center wacom
+# Translators: Do NOT translate or transliterate this text (this is an icon file name)!
+Icon=input-tablet
+Terminal=false
+Type=Application
+NoDisplay=true
+StartupNotify=true
+Categories=GNOME;GTK;Settings;HardwareSettings;X-GNOME-Settings-Panel;X-GNOME-DevicesSettings;
+OnlyShowIn=GNOME;Unity;
+X-GNOME-Bugzilla-Bugzilla=GNOME
+X-GNOME-Bugzilla-Product=gnome-control-center
+X-GNOME-Bugzilla-Component=wacom
+X-GNOME-Bugzilla-Version=@VERSION@
+# Translators: Search terms to find the Wacom Tablet panel. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon!
+Keywords=Tablet;Wacom;Stylus;Eraser;Mouse;
diff --git a/panels/wacom/gnome-wacom-properties.ui b/panels/wacom/gnome-wacom-properties.ui
new file mode 100644
index 0000000..8679bd2
--- /dev/null
+++ b/panels/wacom/gnome-wacom-properties.ui
@@ -0,0 +1,465 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.22.1 -->
+<interface>
+ <requires lib="gtk+" version="3.20"/>
+ <object class="GtkListStore" id="liststore-tabletmode">
+ <columns>
+ <!-- column-name tabletmode -->
+ <column type="guint"/>
+ <!-- column-name tabletmode-label -->
+ <column type="gchararray"/>
+ </columns>
+ <data>
+ <row>
+ <col id="0">0</col>
+ <col id="1" translatable="yes">Tablet (absolute)</col>
+ </row>
+ <row>
+ <col id="0">1</col>
+ <col id="1" translatable="yes">Touchpad (relative)</col>
+ </row>
+ </data>
+ </object>
+ <object class="GtkDialog" id="wacom_properties_dialog">
+ <property name="can_focus">False</property>
+ <property name="vexpand">True</property>
+ <property name="border_width">5</property>
+ <property name="title" translatable="yes">Tablet Preferences</property>
+ <property name="resizable">False</property>
+ <property name="default_width">675</property>
+ <property name="default_height">460</property>
+ <property name="icon_name">input-tablet</property>
+ <property name="type_hint">dialog</property>
+ <child internal-child="vbox">
+ <object class="GtkBox" id="dialog-vbox1">
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">2</property>
+ <child internal-child="action_area">
+ <object class="GtkButtonBox" id="dialog-action_area1">
+ <property name="can_focus">False</property>
+ <property name="layout_style">end</property>
+ <child>
+ <object class="GtkButton" id="helpbutton1">
+ <property name="label" translatable="yes">_Help</property>
+ <property name="use_action_appearance">False</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="can_default">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_underline">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="closebutton1">
+ <property name="label" translatable="yes">_Close</property>
+ <property name="use_action_appearance">False</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="can_default">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_underline">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="pack_type">end</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkNotebook" id="main-notebook">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="vexpand">True</property>
+ <property name="show_tabs">False</property>
+ <property name="show_border">False</property>
+ <child>
+ <object class="GtkGrid" id="main-box">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="hexpand">True</property>
+ <property name="vexpand">True</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="sensitive">False</property>
+ <property name="can_focus">False</property>
+ <property name="valign">end</property>
+ <property name="vexpand">True</property>
+ <property name="pixel_size">96</property>
+ <property name="icon_name">input-tablet-symbolic</property>
+ <property name="icon_size">0</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="advice-label1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="vexpand">False</property>
+ <property name="label" translatable="yes">No tablet detected</property>
+ <property name="justify">center</property>
+ <property name="yalign">1</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="advice-label2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="valign">start</property>
+ <property name="vexpand">True</property>
+ <property name="label" translatable="yes">Please plug in or turn on your Wacom tablet</property>
+ <property name="justify">center</property>
+ <property name="yalign">0</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ <property name="width">3</property>
+ <property name="height">3</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLinkButton" id="linkbutton">
+ <property name="label" translatable="yes">Bluetooth Settings</property>
+ <property name="use_action_appearance">False</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="halign">end</property>
+ <property name="valign">end</property>
+ <property name="vexpand">True</property>
+ <property name="relief">none</property>
+ </object>
+ <packing>
+ <property name="left_attach">2</property>
+ <property name="top_attach">2</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <child type="tab">
+ <object class="GtkLabel" id="label4">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label">Plugin</property>
+ </object>
+ <packing>
+ <property name="tab_fill">False</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkGrid" id="main-grid">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="row_spacing">10</property>
+ <property name="column_spacing">10</property>
+ <child>
+ <object class="GtkLabel" id="label-tabletmodel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">start</property>
+ <property name="valign">center</property>
+ <property name="label" translatable="yes">Wacom Tablet</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ <property name="width">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkImage" id="image-tablet">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">center</property>
+ <property name="valign">start</property>
+ <property name="pixbuf">wacom-tablet.svg</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkGrid" id="main-controls-grid">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_start">16</property>
+ <property name="orientation">vertical</property>
+ <property name="row_spacing">10</property>
+ <property name="column_spacing">10</property>
+ <child>
+ <object class="GtkLabel" id="label-trackingmode">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">end</property>
+ <property name="valign">center</property>
+ <property name="label" translatable="yes">Tracking Mode</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkComboBox" id="combo-tabletmode">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="hexpand">True</property>
+ <property name="model">liststore-tabletmode</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label-left-handed">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">end</property>
+ <property name="valign">center</property>
+ <property name="label" translatable="yes">Left-Handed Orientation</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSwitch" id="switch-left-handed">
+ <property name="use_action_appearance">False</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="halign">start</property>
+ <property name="valign">center</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="tablet-buttons-box">
+ <property name="visible">True</property>
+ <property name="halign">start</property>
+ <property name="spacing">10</property>
+ <child>
+ <object class="GtkButton" id="display-mapping-button">
+ <property name="label" translatable="yes">Map to Monitor…</property>
+ <property name="use_action_appearance">False</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ </object>
+ <packing>
+ <property name="pack_type">end</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="map-buttons-button">
+ <property name="label" translatable="yes">Map Buttons…</property>
+ <property name="use_action_appearance">False</property>
+ <property name="visible">False</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ </object>
+ <packing>
+ <property name="pack_type">end</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="button-calibrate">
+ <property name="label" translatable="yes">Calibrate…</property>
+ <property name="use_action_appearance">False</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ </object>
+ <packing>
+ <property name="pack_type">end</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLinkButton" id="mouse-link">
+ <property name="label" translatable="yes">Adjust mouse settings</property>
+ <property name="use_action_appearance">False</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="halign">start</property>
+ <property name="valign">start</property>
+ <property name="relief">none</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">3</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLinkButton" id="display-link">
+ <property name="label" translatable="yes">Adjust display resolution</property>
+ <property name="use_action_appearance">False</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="halign">start</property>
+ <property name="valign">start</property>
+ <property name="relief">none</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">3</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label-decouple-display">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">end</property>
+ <property name="valign">center</property>
+ <property name="label" translatable="yes">Decouple Display</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">4</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSwitch" id="switch-decouple-display">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="halign">start</property>
+ <property name="valign">center</property>
+ <property name="active">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">4</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="display-mapping-button-2">
+ <property name="label" translatable="yes">Map to Monitor…</property>
+ <property name="use_action_appearance">False</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="halign">start</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">6</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkRevealer" id="navigation-placeholder">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">end</property>
+ <property name="transition_type">crossfade</property>
+ <property name="transition_duration">100</property>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child type="tab">
+ <object class="GtkLabel" id="label1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label">Wacom</property>
+ </object>
+ <packing>
+ <property name="position">1</property>
+ <property name="tab_fill">False</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <action-widgets>
+ <action-widget response="0">helpbutton1</action-widget>
+ <action-widget response="0">closebutton1</action-widget>
+ </action-widgets>
+ </object>
+</interface>
diff --git a/panels/wacom/gsd-enums.h b/panels/wacom/gsd-enums.h
new file mode 100644
index 0000000..9cfa48f
--- /dev/null
+++ b/panels/wacom/gsd-enums.h
@@ -0,0 +1,7 @@
+/* We copy gsd-wacom-device from gnome-settings-daemon.
+ * It include "gsd-enums.h" because the include directory
+ * is known. As gnome-settings-daemon's pkg-config file
+ * prefixes this, we need a little help to avoid this
+ * one line difference */
+
+#include <gnome-settings-daemon/gsd-enums.h>
diff --git a/panels/wacom/gsd-wacom-key-shortcut-button.c b/panels/wacom/gsd-wacom-key-shortcut-button.c
new file mode 100644
index 0000000..fcc5313
--- /dev/null
+++ b/panels/wacom/gsd-wacom-key-shortcut-button.c
@@ -0,0 +1,571 @@
+/*
+ * gsd-wacom-key-shortcut-button.c
+ *
+ * Copyright © 2013 Red Hat, Inc.
+ *
+ * Author: Joaquim Rocha <jrocha@redhat.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+#include <glib/gi18n-lib.h>
+
+#include "gsd-wacom-key-shortcut-button.h"
+
+/**
+ * SECTION:gsd-wacom-key-shortcut-button
+ * @short_description: A button which captures and displays a keyboard shortcut
+ * @title: GsdWacomKeyShortcutButton
+ *
+ * GsdWacomKeyShortcutButton is a button which, when clicked, captures a keyboard
+ * shortcut and displays it.
+ * It works in a similar way to #GtkCellRendererAccel but, being a #GtkWidget,
+ * can be added to e.g. containers.
+ */
+
+#define DEFAULT_CANCEL_KEY GDK_KEY_Escape
+#define DEFAULT_CLEAR_KEY GDK_KEY_BackSpace
+
+enum {
+ KEY_SHORTCUT_EDITED,
+ KEY_SHORTCUT_CLEARED,
+ LAST_SIGNAL
+};
+
+enum {
+ PROP_0,
+ PROP_SHORTCUT_KEY_VAL,
+ PROP_SHORTCUT_KEY_MODS,
+ PROP_SHORTCUT_MODE,
+ PROP_SHORTCUT_CANCEL_KEY,
+ PROP_SHORTCUT_CLEAR_KEY,
+ N_PROPERTIES
+};
+
+struct _GsdWacomKeyShortcutButton
+{
+ GtkButton parent_instance;
+
+ gboolean editing_mode;
+
+ GdkSeat *grab_seat;
+
+ guint keyval;
+ guint keycode;
+ GdkModifierType mods;
+
+ /* Temporary shortcut info used for allowing
+ * modifier-only shortcuts */
+ guint tmp_shortcut_keyval;
+ GdkModifierType tmp_shortcut_mods;
+ guint32 tmp_shortcut_time;
+
+ GsdWacomKeyShortcutButtonMode mode;
+
+ guint cancel_keyval;
+ guint clear_keyval;
+};
+
+G_DEFINE_TYPE (GsdWacomKeyShortcutButton, gsd_wacom_key_shortcut_button, GTK_TYPE_BUTTON);
+
+static guint signals[LAST_SIGNAL] = { 0 };
+
+static GParamSpec *obj_properties[N_PROPERTIES] = { NULL, };
+
+static void gsd_wacom_key_shortcut_button_changed (GsdWacomKeyShortcutButton *self);
+
+static void
+gsd_wacom_key_shortcut_button_set_property (GObject *object,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GsdWacomKeyShortcutButton *self = GSD_WACOM_KEY_SHORTCUT_BUTTON (object);
+ gboolean changed = FALSE;
+
+ switch (property_id)
+ {
+ case PROP_SHORTCUT_KEY_VAL:
+ self->keyval = g_value_get_uint (value);
+ changed = TRUE;
+ break;
+
+ case PROP_SHORTCUT_KEY_MODS:
+ self->mods = g_value_get_uint (value);
+ changed = TRUE;
+ break;
+
+ case PROP_SHORTCUT_MODE:
+ self->mode = g_value_get_enum (value);
+ break;
+
+ case PROP_SHORTCUT_CANCEL_KEY:
+ self->cancel_keyval = g_value_get_uint (value);
+ break;
+
+ case PROP_SHORTCUT_CLEAR_KEY:
+ self->clear_keyval = g_value_get_uint (value);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ break;
+ }
+
+ if (changed)
+ gsd_wacom_key_shortcut_button_changed (self);
+}
+
+static void
+gsd_wacom_key_shortcut_button_get_property (GObject *object,
+ guint property_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GsdWacomKeyShortcutButton *self = GSD_WACOM_KEY_SHORTCUT_BUTTON (object);
+
+ switch (property_id)
+ {
+ case PROP_SHORTCUT_KEY_VAL:
+ g_value_set_uint (value, self->keyval);
+ break;
+
+ case PROP_SHORTCUT_KEY_MODS:
+ g_value_set_uint (value, self->mods);
+ break;
+
+ case PROP_SHORTCUT_MODE:
+ g_value_set_enum (value, self->mode);
+ break;
+
+ case PROP_SHORTCUT_CANCEL_KEY:
+ g_value_set_uint (value, self->cancel_keyval);
+ break;
+
+ case PROP_SHORTCUT_CLEAR_KEY:
+ g_value_set_uint (value, self->clear_keyval);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ break;
+ }
+}
+
+static void
+gsd_wacom_key_shortcut_set_editing_mode (GsdWacomKeyShortcutButton *self,
+ GdkEvent *event)
+{
+ GdkWindow *window;
+ GdkSeat *seat;
+
+ self->editing_mode = TRUE;
+ gsd_wacom_key_shortcut_button_changed (self);
+
+ window = gtk_widget_get_window (GTK_WIDGET (self));
+
+ g_return_if_fail (window != NULL);
+
+ seat = gdk_event_get_seat (event);
+
+ if (gdk_seat_grab (seat, window, GDK_SEAT_CAPABILITY_ALL,
+ FALSE, NULL, event, NULL, NULL) != GDK_GRAB_SUCCESS)
+ return;
+
+ gtk_widget_grab_focus (GTK_WIDGET (self));
+
+ self->grab_seat = seat;
+}
+
+static void
+gsd_wacom_key_shortcut_remove_editing_mode (GsdWacomKeyShortcutButton *self)
+{
+ self->editing_mode = FALSE;
+
+ self->editing_mode = FALSE;
+
+ if (self->grab_seat)
+ {
+ gdk_seat_ungrab (self->grab_seat);
+ self->grab_seat = NULL;
+ }
+
+ self->tmp_shortcut_keyval = 0;
+ self->tmp_shortcut_mods = 0;
+ self->tmp_shortcut_time = 0;
+}
+
+static void
+gsd_wacom_key_shortcut_button_changed (GsdWacomKeyShortcutButton *self)
+{
+ g_autofree gchar *text = NULL;
+
+ if (self->editing_mode)
+ {
+ gtk_button_set_label (GTK_BUTTON (self), _("New shortcut…"));
+
+ gtk_widget_set_state_flags (GTK_WIDGET (self),
+ GTK_STATE_FLAG_ACTIVE | GTK_STATE_FLAG_PRELIGHT,
+ FALSE);
+
+ return;
+ }
+
+ if (self->keyval == 0 && self->mods == 0)
+ {
+ gtk_button_set_label (GTK_BUTTON (self), "");
+ return;
+ }
+
+ text = gtk_accelerator_get_label (self->keyval, self->mods);
+ gtk_button_set_label (GTK_BUTTON (self), text);
+}
+
+static void
+gsd_wacom_key_shortcut_button_activate (GtkButton *self)
+{
+ gsd_wacom_key_shortcut_set_editing_mode (GSD_WACOM_KEY_SHORTCUT_BUTTON (self), NULL);
+
+ GTK_BUTTON_CLASS (gsd_wacom_key_shortcut_button_parent_class)->activate (self);
+}
+
+static void
+gsd_wacom_key_shortcut_button_init (GsdWacomKeyShortcutButton *self)
+{
+ gtk_button_set_relief (GTK_BUTTON (self), GTK_RELIEF_NONE);
+
+ self->cancel_keyval = DEFAULT_CANCEL_KEY;
+ self->clear_keyval = DEFAULT_CLEAR_KEY;
+}
+
+static void
+key_shortcut_finished_editing (GsdWacomKeyShortcutButton *self,
+ guint32 time)
+{
+ gdk_seat_ungrab (self->grab_seat);
+ self->grab_seat = NULL;
+
+ self->editing_mode = FALSE;
+
+ gsd_wacom_key_shortcut_remove_editing_mode (self);
+
+ gsd_wacom_key_shortcut_button_changed (self);
+}
+
+static gboolean
+gsd_wacom_key_shortcut_button_key_release (GtkWidget *widget,
+ GdkEventKey *event)
+{
+ GsdWacomKeyShortcutButton *self = GSD_WACOM_KEY_SHORTCUT_BUTTON (widget);
+
+ if (self->tmp_shortcut_keyval == 0)
+ {
+ GTK_WIDGET_CLASS (gsd_wacom_key_shortcut_button_parent_class)->key_release_event (widget, event);
+
+ return FALSE;
+ }
+
+ self->keyval = self->tmp_shortcut_keyval;
+ self->mods = self->tmp_shortcut_mods;
+
+ key_shortcut_finished_editing (self, self->tmp_shortcut_time);
+
+ g_signal_emit (self, signals[KEY_SHORTCUT_EDITED], 0);
+
+ return TRUE;
+}
+
+static gboolean
+gsd_wacom_key_shortcut_button_key_press (GtkWidget *widget,
+ GdkEventKey *event)
+{
+ /* This code is based on the gtk_cell_renderer_accel_start_editing */
+ GsdWacomKeyShortcutButton *self = GSD_WACOM_KEY_SHORTCUT_BUTTON (widget);
+ GdkModifierType mods = 0;
+ guint shortcut_keyval;
+ guint keyval;
+ gboolean edited;
+ gboolean cleared;
+
+ /* GTK and OTHER modes don't allow modifier keyvals */
+ if (event->is_modifier && self->mode != GSD_WACOM_KEY_SHORTCUT_BUTTON_MODE_ALL)
+ return TRUE;
+
+ if (!self->editing_mode)
+ {
+ GTK_WIDGET_CLASS (gsd_wacom_key_shortcut_button_parent_class)->key_press_event (widget, event);
+
+ return FALSE;
+ }
+
+ edited = FALSE;
+ cleared = FALSE;
+
+ mods = event->state;
+
+ keyval = event->keyval;
+ if (keyval == GDK_KEY_Sys_Req &&
+ (mods & GDK_MOD1_MASK) != 0)
+ {
+ /* HACK: we don't want to use SysRq as a keybinding (but we do
+ * want Alt+Print), so we avoid translation from Alt+Print to SysRq
+ */
+ keyval = GDK_KEY_Print;
+ }
+
+ shortcut_keyval = gdk_keyval_to_lower (keyval);
+
+ if (shortcut_keyval == GDK_KEY_ISO_Left_Tab)
+ shortcut_keyval = GDK_KEY_Tab;
+
+ mods &= gtk_accelerator_get_default_mod_mask ();
+
+ /* Put shift back if it changed the case of the key, not otherwise.
+ */
+ if (shortcut_keyval != keyval)
+ mods |= GDK_SHIFT_MASK;
+
+ if (mods == 0)
+ {
+ if (keyval == self->cancel_keyval)
+ {
+ /* cancel the edition */
+ goto out;
+ }
+ else if (keyval == self->clear_keyval)
+ {
+ /* clear the current shortcut */
+ cleared = TRUE;
+ goto out;
+ }
+ }
+
+ self->tmp_shortcut_keyval = 0;
+ self->tmp_shortcut_mods = 0;
+ self->tmp_shortcut_time = 0;
+
+ if (event->is_modifier)
+ {
+ /* when the user presses a non-modifier key, it readily assigns the
+ * shortcut but since we also support modifiers-only shortcuts, we
+ * cannot assign the shortcut right when the user presses a modifier
+ * key because the user might assign e.g. Alt, Alt+Ctrl, Alt+Ctrl+Shift, etc.
+ * So, we keep track of the pressed shortcut's (keyval, mods and time) if
+ * it is a modifier shortcut and assign them when a key-release happens */
+ self->tmp_shortcut_keyval = shortcut_keyval;
+ self->tmp_shortcut_mods = mods;
+ self->tmp_shortcut_time = event->time;
+
+ return TRUE;
+ }
+
+ edited = TRUE;
+
+ out:
+
+ if (edited)
+ {
+ self->keyval = shortcut_keyval;
+ self->mods = mods;
+ }
+
+ if (cleared)
+ {
+ self->keyval = 0;
+ self->mods = 0;
+ }
+
+ key_shortcut_finished_editing (GSD_WACOM_KEY_SHORTCUT_BUTTON (widget), event->time);
+
+ if (edited)
+ g_signal_emit (self, signals[KEY_SHORTCUT_EDITED], 0);
+ else if (cleared)
+ g_signal_emit (self, signals[KEY_SHORTCUT_CLEARED], 0);
+
+ return TRUE;
+}
+
+static gboolean
+gsd_wacom_key_shortcut_button_button_press (GtkWidget *widget,
+ GdkEventButton *event)
+{
+ GsdWacomKeyShortcutButton *self;
+
+ self = GSD_WACOM_KEY_SHORTCUT_BUTTON (widget);
+
+ if (self->editing_mode)
+ return TRUE;
+
+ gsd_wacom_key_shortcut_set_editing_mode (self, NULL);
+
+ GTK_WIDGET_CLASS (gsd_wacom_key_shortcut_button_parent_class)->button_press_event (widget,
+ event);
+
+ return TRUE;
+}
+
+static void
+gsd_wacom_key_shortcut_button_unrealize (GtkWidget *widget)
+{
+ GsdWacomKeyShortcutButton *self;
+
+ self = GSD_WACOM_KEY_SHORTCUT_BUTTON (widget);
+
+ gsd_wacom_key_shortcut_remove_editing_mode (self);
+
+ GTK_WIDGET_CLASS (gsd_wacom_key_shortcut_button_parent_class)->unrealize (widget);
+}
+
+static void
+gsd_wacom_key_shortcut_button_class_init (GsdWacomKeyShortcutButtonClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+ GtkButtonClass *button_class = GTK_BUTTON_CLASS (klass);
+
+ gobject_class->set_property = gsd_wacom_key_shortcut_button_set_property;
+ gobject_class->get_property = gsd_wacom_key_shortcut_button_get_property;
+
+ obj_properties[PROP_SHORTCUT_KEY_VAL] =
+ g_param_spec_uint ("key-value",
+ "The key value",
+ "The key value of the shortcut currently set",
+ 0,
+ G_MAXUINT,
+ 0,
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS);
+
+ obj_properties[PROP_SHORTCUT_KEY_MODS] =
+ g_param_spec_uint ("key-mods",
+ "The key modifiers",
+ "The key modifiers of the shortcut currently set",
+ 0,
+ G_MAXUINT,
+ 0,
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS);
+
+ obj_properties[PROP_SHORTCUT_CANCEL_KEY] =
+ g_param_spec_uint ("cancel-key",
+ "The cancel key",
+ "The key which cancels the edition of the shortcut",
+ 0,
+ G_MAXUINT,
+ DEFAULT_CANCEL_KEY,
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS);
+
+ obj_properties[PROP_SHORTCUT_CLEAR_KEY] =
+ g_param_spec_uint ("clear-key",
+ "The clear key",
+ "The key which clears the currently set shortcut",
+ 0,
+ G_MAXUINT,
+ DEFAULT_CLEAR_KEY,
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS);
+
+ /**
+ * GsdWacomKeyShortcutButton:mode:
+ *
+ * Determines which type of keys are allowed in the captured shortcuts.
+ * %GSD_WACOM_KEY_SHORTCUT_BUTTON_MODE_ALL is the same as
+ * %GSD_WACOM_KEY_SHORTCUT_BUTTON_MODE_OTHER but allows shortcuts composed of
+ * only modifier keys.
+ */
+ obj_properties[PROP_SHORTCUT_MODE] =
+ g_param_spec_enum ("mode",
+ "The shortcut mode",
+ "The mode with which the shortcuts are captured",
+ GSD_WACOM_TYPE_KEY_SHORTCUT_BUTTON_MODE,
+ GSD_WACOM_KEY_SHORTCUT_BUTTON_MODE_OTHER,
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS);
+
+ g_object_class_install_properties (gobject_class,
+ N_PROPERTIES,
+ obj_properties);
+
+ widget_class->key_press_event = gsd_wacom_key_shortcut_button_key_press;
+ widget_class->button_press_event = gsd_wacom_key_shortcut_button_button_press;
+ widget_class->key_release_event = gsd_wacom_key_shortcut_button_key_release;
+ widget_class->unrealize = gsd_wacom_key_shortcut_button_unrealize;
+
+ button_class->activate = gsd_wacom_key_shortcut_button_activate;
+
+ /**
+ * GsdWacomKeyShortcutButton::key-shortcut-edited:
+ * @keyshortcutbutton: the #GsdWacomKeyShortcutButton
+ *
+ * Emitted when the key shortcut of the @keyshortcutbutton is edited.
+ *
+ * The new shortcut can be retrieved by using the #GsdWacomKeyShortcutButton:key-value
+ * and #GsdWacomKeyShortcutButton:key-mods properties.
+ */
+ signals[KEY_SHORTCUT_EDITED] = g_signal_new ("key-shortcut-edited",
+ GSD_WACOM_TYPE_KEY_SHORTCUT_BUTTON,
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL, NULL,
+ g_cclosure_marshal_VOID__VOID,
+ G_TYPE_NONE, 0);
+
+ /**
+ * GsdWacomKeyShortcutButton::key-shortcut-cleared:
+ * @keyshortcutbutton: the #GsdWacomKeyShortcutButton
+ *
+ * Emitted when the key shortcut of the @keyshortcutbutton is cleared.
+ */
+ signals[KEY_SHORTCUT_CLEARED] = g_signal_new ("key-shortcut-cleared",
+ GSD_WACOM_TYPE_KEY_SHORTCUT_BUTTON,
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL, NULL,
+ g_cclosure_marshal_VOID__VOID,
+ G_TYPE_NONE, 0);
+}
+
+/**
+ * gsd_wacom_key_shortcut_button_new:
+ *
+ * Creates a new #GsdWacomKeyShortcutButton.
+ *
+ * Returns: a new #GsdWacomKeyShortcutButton object.
+ *
+ * Since: 3.10
+ */
+GtkWidget *
+gsd_wacom_key_shortcut_button_new (void)
+{
+ return g_object_new (GSD_WACOM_TYPE_KEY_SHORTCUT_BUTTON, NULL);
+}
+
+GType
+gsd_wacom_key_shortcut_button_mode_type (void)
+{
+ static GType enum_type_id = 0;
+ if (G_UNLIKELY (!enum_type_id))
+ {
+ static const GEnumValue values[] =
+ {
+ { GSD_WACOM_KEY_SHORTCUT_BUTTON_MODE_OTHER, "OTHER", "other" },
+ { GSD_WACOM_KEY_SHORTCUT_BUTTON_MODE_ALL, "ALL", "all" },
+ { 0, NULL, NULL }
+ };
+ enum_type_id = g_enum_register_static ("GsdWacomKeyShortcutButtonMode", values);
+ }
+ return enum_type_id;
+}
diff --git a/panels/wacom/gsd-wacom-key-shortcut-button.h b/panels/wacom/gsd-wacom-key-shortcut-button.h
new file mode 100644
index 0000000..f4d5525
--- /dev/null
+++ b/panels/wacom/gsd-wacom-key-shortcut-button.h
@@ -0,0 +1,40 @@
+/*
+ * gsd-wacom-key-shortcut-button.h
+ *
+ * Copyright © 2013 Red Hat, Inc.
+ *
+ * Author: Joaquim Rocha <jrocha@redhat.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define GSD_WACOM_TYPE_KEY_SHORTCUT_BUTTON (gsd_wacom_key_shortcut_button_get_type ())
+G_DECLARE_FINAL_TYPE (GsdWacomKeyShortcutButton, gsd_wacom_key_shortcut_button, GSD, WACOM_KEY_SHORTCUT_BUTTON, GtkButton)
+
+GType gsd_wacom_key_shortcut_button_mode_type (void) G_GNUC_CONST;
+#define GSD_WACOM_TYPE_KEY_SHORTCUT_BUTTON_MODE (gsd_wacom_key_shortcut_button_mode_type ())
+
+typedef enum
+{
+ GSD_WACOM_KEY_SHORTCUT_BUTTON_MODE_OTHER,
+ GSD_WACOM_KEY_SHORTCUT_BUTTON_MODE_ALL
+} GsdWacomKeyShortcutButtonMode;
+
+GtkWidget * gsd_wacom_key_shortcut_button_new (void);
diff --git a/panels/wacom/meson.build b/panels/wacom/meson.build
new file mode 100644
index 0000000..5f62220
--- /dev/null
+++ b/panels/wacom/meson.build
@@ -0,0 +1,108 @@
+deps = wacom_deps + [
+ gnome_desktop_dep,
+ gnome_settings_dep,
+ x11_dep,
+ xi_dep
+]
+
+cflags += ['-DGNOMELOCALEDIR="@0@"'.format(control_center_localedir)]
+
+test_cflags = cflags + ['-DFAKE_AREA']
+
+wacom_gresource = gnome.compile_resources(
+ 'cc-' + cappletname + '-resources',
+ cappletname + '.gresource.xml',
+ source_dir : '.',
+ c_name : 'cc_' + cappletname,
+ dependencies : resource_data,
+ export : true
+)
+
+subdir('calibrator')
+
+panels_list += cappletname
+desktop = 'gnome-@0@-panel.desktop'.format(cappletname)
+
+desktop_in = configure_file(
+ input : desktop + '.in.in',
+ output : desktop + '.in',
+ configuration : desktop_conf
+)
+
+i18n.merge_file(
+ desktop,
+ type : 'desktop',
+ input : desktop_in,
+ output : desktop,
+ po_dir : po_dir,
+ install : true,
+ install_dir : control_center_desktopdir
+)
+
+common_sources = files(
+ 'cc-tablet-tool-map.c',
+ 'cc-wacom-button-row.c',
+ 'cc-wacom-device.c',
+ 'cc-wacom-mapping-panel.c',
+ 'cc-wacom-nav-button.c',
+ 'cc-wacom-page.c',
+ 'cc-wacom-stylus-page.c',
+ 'cc-wacom-tool.c',
+ 'gsd-wacom-key-shortcut-button.c'
+)
+
+resource_data = files(
+ 'calibrator/calibrator.ui',
+ 'calibrator/calibrator.css',
+ 'calibrator/target.svg',
+ 'button-mapping.ui',
+ 'gnome-wacom-properties.ui',
+ 'wacom-stylus-3btn.svg',
+ 'wacom-stylus-3btn-no-eraser.svg',
+ 'wacom-stylus-airbrush.svg',
+ 'wacom-stylus-art-pen.svg',
+ 'wacom-stylus-classic.svg',
+ 'wacom-stylus-inking.svg',
+ 'wacom-stylus-no-eraser.svg',
+ 'wacom-stylus-page.ui',
+ 'wacom-stylus.svg',
+ 'wacom-tablet-cintiq.svg',
+ 'wacom-tablet-pc.svg',
+ 'wacom-tablet.svg'
+)
+
+common_sources += wacom_gresource
+
+sources = common_sources + files(
+ 'cc-' + cappletname + '-panel.c',
+ 'cc-drawing-area.c'
+)
+
+deps += libdevice_dep
+
+incs = [
+ top_inc,
+ calibrator_inc
+]
+
+panels_libs += static_library(
+ cappletname + '-properties',
+ sources : sources,
+ include_directories : incs,
+ dependencies : deps,
+ c_args : cflags,
+ link_with : [ libwacom_calibrator ]
+)
+
+name = 'test-wacom'
+
+sources = common_sources + files(name + '.c')
+
+executable(
+ name,
+ sources,
+ include_directories : incs,
+ dependencies : deps,
+ c_args : test_cflags,
+ link_with : [ libwacom_calibrator_test ]
+)
diff --git a/panels/wacom/test-wacom.c b/panels/wacom/test-wacom.c
new file mode 100644
index 0000000..a8d4f69
--- /dev/null
+++ b/panels/wacom/test-wacom.c
@@ -0,0 +1,160 @@
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+
+#include "cc-wacom-page.h"
+
+#define FIXED_WIDTH 675
+
+void
+cc_wacom_panel_switch_to_panel (CcWacomPanel *self, const char *panel)
+{
+ g_message ("Should launch %s preferences here", panel);
+}
+
+GDBusProxy *
+cc_wacom_panel_get_gsd_wacom_bus_proxy (CcWacomPanel *self)
+{
+ g_message ("Should get the g-s-d wacom dbus proxy here");
+
+ return NULL;
+}
+
+static void
+add_page (GList *devices,
+ GtkWidget *notebook)
+{
+ GtkWidget *widget;
+ CcWacomDevice *stylus = NULL;
+ GList *l;
+
+ if (devices == NULL)
+ return;
+
+ for (l = devices; l ; l = l->next) {
+ stylus = l->data;
+ }
+ g_list_free (devices);
+
+ widget = cc_wacom_page_new (NULL, stylus);
+ cc_wacom_page_set_navigation (CC_WACOM_PAGE (widget), GTK_NOTEBOOK (notebook), FALSE);
+ gtk_notebook_append_page (GTK_NOTEBOOK (notebook), widget, NULL);
+ gtk_widget_show (widget);
+}
+
+static gboolean
+delete_event_cb (GtkWidget *widget,
+ GdkEvent *event,
+ gpointer user_data)
+{
+ gtk_main_quit ();
+
+ return FALSE;
+}
+
+static GList *
+create_fake_cintiq (void)
+{
+ CcWacomDevice *device;
+ GList *devices;
+
+ device = cc_wacom_device_new_fake ("Wacom Cintiq 21UX2");
+ devices = g_list_prepend (NULL, device);
+
+ return devices;
+}
+
+static GList *
+create_fake_bt (void)
+{
+ CcWacomDevice *device;
+ GList *devices;
+
+ device = cc_wacom_device_new_fake ("Wacom Graphire Wireless");
+ devices = g_list_prepend (NULL, device);
+
+ return devices;
+}
+
+static GList *
+create_fake_x201 (void)
+{
+ CcWacomDevice *device;
+ GList *devices;
+
+ device = cc_wacom_device_new_fake ("Wacom Serial Tablet WACf004");
+ devices = g_list_prepend (NULL, device);
+
+ return devices;
+}
+
+static GList *
+create_fake_intuos4 (void)
+{
+ CcWacomDevice *device;
+ GList *devices;
+
+ device = cc_wacom_device_new_fake ("Wacom Intuos4 6x9");
+ devices = g_list_prepend (NULL, device);
+
+ return devices;
+}
+
+static GList *
+create_fake_h610pro (void)
+{
+ CcWacomDevice *device;
+ GList *devices;
+
+ device = cc_wacom_device_new_fake ("Huion H610 Pro");
+ devices = g_list_prepend (NULL, device);
+
+ return devices;
+}
+
+int main (int argc, char **argv)
+{
+ GtkWidget *window, *notebook;
+ GList *devices;
+
+ bindtextdomain (GETTEXT_PACKAGE, GNOMELOCALEDIR);
+ bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
+ textdomain (GETTEXT_PACKAGE);
+
+ gtk_init (&argc, &argv);
+
+ window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
+ gtk_window_set_resizable (GTK_WINDOW (window), FALSE);
+ gtk_window_set_default_size (GTK_WINDOW (window), FIXED_WIDTH, -1);
+ g_signal_connect (G_OBJECT (window), "delete-event",
+ G_CALLBACK (delete_event_cb), NULL);
+ notebook = gtk_notebook_new ();
+ gtk_notebook_set_show_tabs (GTK_NOTEBOOK (notebook), FALSE);
+ gtk_notebook_set_show_border (GTK_NOTEBOOK (notebook), FALSE);
+ gtk_widget_set_vexpand (notebook, TRUE);
+ gtk_container_set_border_width (GTK_CONTAINER (notebook), 24);
+ gtk_container_add (GTK_CONTAINER (window), notebook);
+ gtk_widget_show (notebook);
+
+ devices = create_fake_intuos4 ();
+ add_page (devices, notebook);
+
+ devices = create_fake_cintiq ();
+ add_page (devices, notebook);
+
+ devices = create_fake_bt ();
+ add_page (devices, notebook);
+
+ devices = create_fake_x201 ();
+ add_page (devices, notebook);
+
+ devices = create_fake_h610pro ();
+ add_page (devices, notebook);
+
+ gtk_widget_show (window);
+
+ gtk_main ();
+
+ return 0;
+}
diff --git a/panels/wacom/wacom-stylus-3btn-no-eraser.svg b/panels/wacom/wacom-stylus-3btn-no-eraser.svg
new file mode 100644
index 0000000..60642d7
--- /dev/null
+++ b/panels/wacom/wacom-stylus-3btn-no-eraser.svg
@@ -0,0 +1,132 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ id="svg86343"
+ version="1.1"
+ inkscape:version="0.91 r13725"
+ width="148"
+ height="192"
+ sodipodi:docname="wacom-stylus-3btn-no-eraser.svg">
+ <metadata
+ id="metadata86349">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs86347" />
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="960"
+ inkscape:window-height="1014"
+ id="namedview86345"
+ showgrid="false"
+ inkscape:snap-nodes="false"
+ inkscape:snap-bbox="true"
+ inkscape:zoom="2.4748737"
+ inkscape:cx="176.8759"
+ inkscape:cy="81.687362"
+ inkscape:window-x="1920"
+ inkscape:window-y="27"
+ inkscape:window-maximized="0"
+ inkscape:current-layer="g10631"
+ borderlayer="true"
+ inkscape:showpageshadow="false">
+ <inkscape:grid
+ type="xygrid"
+ id="grid86802"
+ empspacing="5"
+ visible="true"
+ enabled="true"
+ snapvisiblegridlinesonly="true" />
+ </sodipodi:namedview>
+ <g
+ style="display:inline"
+ transform="translate(592.43375,-287.62088)"
+ id="g10545">
+ <path
+ sodipodi:nodetypes="cscscccccccccccccscscc"
+ inkscape:connector-curvature="0"
+ id="rect10526"
+ transform="translate(-928.4063,-95.84375)"
+ d="m 344.125,384.88832 c -1.9944,0 -3.59375,1.59935 -3.59375,3.59375 L 340.53125,516 338,545.125 c -0.1873,2.15512 1.62589,3.92035 3.75,4.125 l 4.625,10.90625 1.53125,0 0,2.15625 3.61536,8.57242 1.18546,0.0214 0.44918,3.78119 0.33938,-3.7414 1.14797,-0.0687 3.76265,-8.53366 0,-2.1875 1.53125,0 4.65625,-10.96875 c 1.96694,-0.35188 3.54637,-2.02216 3.40625,-4.0625 L 365.53125,516 l 0,-127.51793 c 0,-1.9944 -1.59935,-3.59375 -3.59375,-3.59375 z"
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:#d3d7cf;fill-opacity:1;fill-rule:nonzero;stroke:#babdb6;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate" />
+ <rect
+ style="color:#000000;fill:#eeeeec;fill-opacity:1;fill-rule:nonzero;stroke:#babdb6;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ id="rect10541"
+ width="8.75"
+ height="22"
+ x="-579.65631"
+ y="385.90625"
+ rx="3.25"
+ ry="3.25" />
+ <rect
+ style="color:#000000;fill:#eeeeec;fill-opacity:1;fill-rule:nonzero;stroke:#babdb6;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ id="rect10543"
+ width="8.75"
+ height="12.25"
+ x="-579.65631"
+ y="410.90625"
+ rx="3.25"
+ ry="3.2500002" />
+ <rect
+ style="color:#000000;fill:#eeeeec;fill-opacity:1;fill-rule:nonzero;stroke:#babdb6;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ id="rect10544"
+ width="8.75"
+ height="8.75"
+ x="-579.65631"
+ y="429.15625"
+ rx="3.25"
+ ry="3.2500002" />
+ </g>
+ <g
+ style="opacity:0.2;display:inline"
+ id="g10631"
+ transform="translate(592.43375,-302.48416)">
+ <path
+ style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m -571.1563,413.06786 35.28033,0 0,-89.17947 82.46967,0"
+ id="path3342"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccc" />
+ <path
+ style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m -571.1563,430.06786 48.28033,0 0,-64.17947 69.46967,0"
+ id="path3344"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccc" />
+ <path
+ style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m -571.1563,448.31786 61.28033,0 0,-40.43396 56.46967,0"
+ id="path3344"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccc" />
+ <path
+ style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m -575.4063,492.06786 78.53033,0 0,-43.17947 43.46967,0"
+ id="path10629"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccc" />
+ </g>
+</svg>
diff --git a/panels/wacom/wacom-stylus-3btn.svg b/panels/wacom/wacom-stylus-3btn.svg
new file mode 100644
index 0000000..2f3db9a
--- /dev/null
+++ b/panels/wacom/wacom-stylus-3btn.svg
@@ -0,0 +1,138 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ id="svg86343"
+ version="1.1"
+ inkscape:version="0.48.2 r9819"
+ width="148"
+ height="192"
+ sodipodi:docname="wacom-stylus-3btn.svg">
+ <metadata
+ id="metadata86349">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs86347" />
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1280"
+ inkscape:window-height="742"
+ id="namedview86345"
+ showgrid="false"
+ inkscape:snap-nodes="false"
+ inkscape:snap-bbox="true"
+ inkscape:zoom="7"
+ inkscape:cx="93.888956"
+ inkscape:cy="186.10424"
+ inkscape:window-x="0"
+ inkscape:window-y="26"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="svg86343"
+ borderlayer="true"
+ inkscape:showpageshadow="false">
+ <inkscape:grid
+ type="xygrid"
+ id="grid86802"
+ empspacing="5"
+ visible="true"
+ enabled="true"
+ snapvisiblegridlinesonly="true" />
+ </sodipodi:namedview>
+ <g
+ style="display:inline"
+ transform="translate(592.43375,-287.62088)"
+ id="g10545">
+ <path
+ sodipodi:nodetypes="sscsscscccccccccccccscsscsss"
+ inkscape:connector-curvature="0"
+ id="rect10526"
+ transform="translate(-928.4063,-95.84375)"
+ d="m 349.69531,384.96463 c -2.3083,0 -3.2326,1.49535 -3.69531,4.51323 L 345.53125,396 344.125,396 c -1.9944,0 -3.59375,1.59935 -3.59375,3.59375 L 340.53125,516 338,545.125 c -0.1873,2.15512 1.62589,3.92035 3.75,4.125 l 4.625,10.90625 1.53125,0 0,2.15625 3.61536,8.57242 1.18546,0.0214 0.44918,3.78119 0.33938,-3.7414 1.14797,-0.0687 3.76265,-8.53366 0,-2.1875 1.53125,0 4.65625,-10.96875 c 1.96694,-0.35188 3.54637,-2.02216 3.40625,-4.0625 L 365.53125,516 l 0,-116.40625 c 0,-1.9944 -1.59935,-3.59375 -3.59375,-3.59375 l -1.40625,0 L 360,389.47786 c -0.23272,-2.85711 -1.26201,-4.51323 -3.69531,-4.51323 z"
+ style="color:#000000;fill:#d3d7cf;fill-opacity:1;fill-rule:nonzero;stroke:#babdb6;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+ <rect
+ style="color:#000000;fill:#eeeeec;fill-opacity:1;fill-rule:nonzero;stroke:#babdb6;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ id="rect10541"
+ width="8.75"
+ height="22"
+ x="-579.65631"
+ y="385.90625"
+ rx="3.25"
+ ry="3.25" />
+ <rect
+ style="color:#000000;fill:#eeeeec;fill-opacity:1;fill-rule:nonzero;stroke:#babdb6;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ id="rect10543"
+ width="8.75"
+ height="12.25"
+ x="-579.65631"
+ y="410.90625"
+ rx="3.25"
+ ry="3.2500002" />
+ <rect
+ style="color:#000000;fill:#eeeeec;fill-opacity:1;fill-rule:nonzero;stroke:#babdb6;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ id="rect10544"
+ width="8.75"
+ height="8.75"
+ x="-579.65631"
+ y="429.15625"
+ rx="3.25"
+ ry="3.2500002" />
+ </g>
+ <g
+ style="opacity:0.2;display:inline"
+ id="g10631"
+ transform="translate(592.43375,-302.48416)">
+ <path
+ style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m -568.1563,309.03125 32.25,0 0,14.85714 82.5,0"
+ id="path10556"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccc" />
+ <path
+ style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m -571.1563,413.06786 35.28033,0 0,-52.14286 82.46967,0"
+ id="path10552"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccc" />
+ <path
+ style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m -571.1563,430.06786 48.28033,0 0,-27.14286 69.46967,0"
+ id="path86913"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccc" />
+ <path
+ style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m -571.1563,448.31786 61.28033,0 0,-3.39286 56.46967,0"
+ id="path3344"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccc" />
+ <path
+ style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m -575.4063,492.06786 78.53033,0 0,-5.14286 43.46967,0"
+ id="path10629"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccc" />
+ </g>
+</svg>
diff --git a/panels/wacom/wacom-stylus-airbrush.svg b/panels/wacom/wacom-stylus-airbrush.svg
new file mode 100644
index 0000000..7d35a51
--- /dev/null
+++ b/panels/wacom/wacom-stylus-airbrush.svg
@@ -0,0 +1,94 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ id="svg86343"
+ version="1.1"
+ inkscape:version="0.48.2 r9819"
+ width="148"
+ height="192"
+ sodipodi:docname="wacom-stylus-airbrush.svg">
+ <metadata
+ id="metadata86349">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs86347" />
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1037"
+ inkscape:window-height="741"
+ id="namedview86345"
+ showgrid="false"
+ inkscape:snap-nodes="false"
+ inkscape:snap-bbox="true"
+ inkscape:zoom="7"
+ inkscape:cx="111.67742"
+ inkscape:cy="-1.9993707"
+ inkscape:window-x="241"
+ inkscape:window-y="26"
+ inkscape:window-maximized="0"
+ inkscape:current-layer="svg86343"
+ borderlayer="true"
+ inkscape:showpageshadow="false">
+ <inkscape:grid
+ type="xygrid"
+ id="grid86802"
+ empspacing="5"
+ visible="true"
+ enabled="true"
+ snapvisiblegridlinesonly="true" />
+ </sodipodi:namedview>
+ <g
+ style="opacity:0.2;display:inline"
+ id="g10631"
+ transform="translate(592.43375,-302.48416)">
+ <path
+ style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 384.35014,508.88388 8.18019,0 0,-68.25798 82.46967,0"
+ id="path10552"
+ inkscape:connector-curvature="0"
+ transform="translate(-928.4063,-79.84375)"
+ sodipodi:nodetypes="cccc" />
+ <path
+ sodipodi:nodetypes="cccc"
+ inkscape:connector-curvature="0"
+ id="path10556"
+ d="m -553.62983,305.002 16.78159,0 0,18.8605 83.44194,0"
+ style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+ <path
+ style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m -557.78929,491.98468 42.82049,0 0,-88.95343 61.5625,0"
+ id="path10629"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccc" />
+ </g>
+ <path
+ style="color:#000000;fill:#d3d7cf;fill-opacity:1;fill-rule:nonzero;stroke:#babdb6;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ d="m 36.76199,1.22591 c -1.43276,0 -2.59423,1.88644 -2.59423,4.21347 0,0.06 -0.002,0.11387 0,0.17315 l -0.9224,0 -9.22393,88.19416 c 0.9224,9.23499 -17.8868963,29.69978 -17.8868963,29.69978 -4.0295528,5.27125 -3.9083456,22.05579 3.5898163,27.55716 l 12.4523,8.31149 5.07316,15.23774 0.5765,0 c 0.4809,6.59867 3.7818,16.16123 5.30375,16.16123 2.11004,0 4.72727,-11.12955 4.72727,-19.16261 0,-0.1448 0.002,-0.28896 0,-0.43293 0,0 5.38613,-9.2445 8.12859,-20.17264 6.283146,-2.81141 5.782898,-13.64962 0.49916,-16.4982 l -0.0094,-3.91974 c 0.83292,-0.84067 1.75859,-2.00027 1.75859,-3.53531 0,-1.88597 -0.81769,-3.49443 -1.98891,-4.21347 L 45.09235,108.09211 40.62451,5.61253 l -1.26829,0 c 0.002,-0.0591 0,-0.11322 0,-0.17315 0,-2.32703 -1.16148,-4.21347 -2.59423,-4.21347 z"
+ id="path5306"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ssccccccccssccccsccccss" />
+</svg>
diff --git a/panels/wacom/wacom-stylus-art-pen.svg b/panels/wacom/wacom-stylus-art-pen.svg
new file mode 100644
index 0000000..e1b5b83
--- /dev/null
+++ b/panels/wacom/wacom-stylus-art-pen.svg
@@ -0,0 +1,127 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ id="svg86343"
+ version="1.1"
+ inkscape:version="0.48.2 r9819"
+ width="148"
+ height="192"
+ sodipodi:docname="wacom-stylus-art-pen.svg">
+ <metadata
+ id="metadata86349">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs86347">
+ <inkscape:path-effect
+ effect="spiro"
+ id="path-effect31574"
+ is_visible="true" />
+ </defs>
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1037"
+ inkscape:window-height="741"
+ id="namedview86345"
+ showgrid="false"
+ inkscape:snap-nodes="false"
+ inkscape:snap-bbox="true"
+ inkscape:zoom="4"
+ inkscape:cx="84.005601"
+ inkscape:cy="140.80529"
+ inkscape:window-x="241"
+ inkscape:window-y="26"
+ inkscape:window-maximized="0"
+ inkscape:current-layer="svg86343"
+ borderlayer="true"
+ inkscape:showpageshadow="false">
+ <inkscape:grid
+ type="xygrid"
+ id="grid86802"
+ empspacing="5"
+ visible="true"
+ enabled="true"
+ snapvisiblegridlinesonly="true" />
+ </sodipodi:namedview>
+ <g
+ style="display:inline"
+ transform="translate(592.43375,-287.62088)"
+ id="g10545">
+ <path
+ sodipodi:nodetypes="sscsscsccccccccccccccscsscsss"
+ inkscape:connector-curvature="0"
+ id="rect10526"
+ transform="translate(-928.4063,-95.84375)"
+ d="m 351.16741,385.06603 c -2.25232,0 -3.61834,1.53072 -4.16741,4.08946 L 345.53125,396 344.125,396 c -1.9944,0 -3.59375,1.59935 -3.59375,3.59375 L 340.53125,516 338,545.125 c -0.1873,2.15512 1.62589,3.92035 3.75,4.125 l 1.625,6.6639 2.20868,0 -1.40241,1.58029 c 0,0 3.53964,7.3799 5.83774,7.46829 l 1.42852,-6.9e-4 0.0512,7.31672 3.97602,-2.37893 -0.0292,-4.98639 1.38503,0.0197 c 2.82843,0 5.42789,-7.64858 5.42789,-7.64858 l -1.5299,-1.37038 2.20868,0 1.65625,-6.72641 c 1.96694,-0.35188 3.54637,-2.02216 3.40625,-4.0625 L 365.53125,516 l 0,-116.40625 c 0,-1.9944 -1.59935,-3.59375 -3.59375,-3.59375 l -1.40625,0 L 359,389.15549 c -0.53643,-2.39779 -1.88996,-4.08946 -4.16741,-4.08946 z"
+ style="color:#000000;fill:#d3d7cf;fill-opacity:1;fill-rule:nonzero;stroke:#babdb6;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+ <rect
+ style="color:#000000;fill:#eeeeec;fill-opacity:1;fill-rule:nonzero;stroke:#babdb6;stroke-width:0.99999994px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ id="rect10541"
+ width="9.0059462"
+ height="35.214661"
+ x="-579.93378"
+ y="385.90622"
+ rx="3.25"
+ ry="3.2499998" />
+ </g>
+ <g
+ style="opacity:0.2;display:inline"
+ id="g10631"
+ transform="translate(592.43375,-302.48416)">
+ <path
+ style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 357.25,492.91161 35.28033,0 0,-52.04275 82.46967,0"
+ id="path10552"
+ inkscape:connector-curvature="0"
+ transform="translate(-928.4063,-79.84375)"
+ sodipodi:nodetypes="cccc" />
+ <path
+ sodipodi:nodetypes="cccc"
+ inkscape:connector-curvature="0"
+ id="path10556"
+ d="m -568.1563,309.03125 32.25,0 0,14.94921 82.5,0"
+ style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+ <path
+ style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m -577.26246,492.06786 41.38649,0 0,-48.24593 82.46967,0"
+ id="path10629"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccc" />
+ <path
+ sodipodi:nodetypes="cccc"
+ inkscape:connector-curvature="0"
+ id="path86913"
+ d="m -571.1563,430.06786 48.10785,0 0,-27.00932 69.64215,0"
+ style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+ </g>
+ <path
+ style="opacity:0.5;color:#000000;fill:#eeeeec;fill-opacity:1;fill-rule:nonzero;stroke:#babdb6;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ d="m 12.427494,118.46089 8.909363,0"
+ id="path31572"
+ inkscape:path-effect="#path-effect31574"
+ inkscape:original-d="m 12.427494,118.46089 8.909363,0"
+ inkscape:connector-curvature="0" />
+</svg>
diff --git a/panels/wacom/wacom-stylus-classic.svg b/panels/wacom/wacom-stylus-classic.svg
new file mode 100644
index 0000000..d07906e
--- /dev/null
+++ b/panels/wacom/wacom-stylus-classic.svg
@@ -0,0 +1,103 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ id="svg86343"
+ version="1.1"
+ inkscape:version="0.48.2 r9819"
+ width="148"
+ height="192"
+ sodipodi:docname="wacom-stylus-classic.svg">
+ <metadata
+ id="metadata86349">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs86347">
+ <inkscape:path-effect
+ effect="spiro"
+ id="path-effect14408"
+ is_visible="true" />
+ </defs>
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1037"
+ inkscape:window-height="741"
+ id="namedview86345"
+ showgrid="false"
+ inkscape:snap-nodes="false"
+ inkscape:snap-bbox="true"
+ inkscape:zoom="6"
+ inkscape:cx="72.723556"
+ inkscape:cy="29.510722"
+ inkscape:window-x="241"
+ inkscape:window-y="26"
+ inkscape:window-maximized="0"
+ inkscape:current-layer="svg86343"
+ borderlayer="true"
+ inkscape:showpageshadow="false">
+ <inkscape:grid
+ type="xygrid"
+ id="grid86802"
+ empspacing="5"
+ visible="true"
+ enabled="true"
+ snapvisiblegridlinesonly="true" />
+ </sodipodi:namedview>
+ <g
+ style="opacity:0.2;display:inline"
+ id="g10631"
+ transform="translate(592.43375,-302.48416)" />
+ <path
+ style="color:#000000;fill:#d3d7cf;fill-opacity:1;fill-rule:nonzero;stroke:#babdb6;stroke-width:0.99999994px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ d="m 9.320334,143.05408 c 0.5048552,10.77062 -1.425426,24.64726 -1.425426,24.64726 l 1.559872,1.05225 c 0,0 1.362729,5.49117 1.850845,8.2077 0.488109,2.71653 3.679579,7.5712 3.679579,7.5712 l 1.545431,1e-5 -8e-6,3.89851 c -0.03586,1.30926 1.983788,1.38546 1.983788,9e-5 l 8e-6,-3.89851 1.451666,0 c 0,0 3.191479,-4.85469 3.679602,-7.57122 0.488109,-2.71652 1.850829,-8.20768 1.850829,-8.20768 l 1.559865,-1.05226 c 0,0 -1.786488,-11.863 -1.545414,-21.63127 0.02477,-1.0038 2.597475,-2.07462 2.650333,-3.20226 0.270987,-5.78108 0.707625,-18.64287 1.089793,-26.63126 0.05442,-1.13744 -1.422603,-2.29772 -1.371031,-3.48097 0.344388,-7.90158 0.611194,-10.82762 0.611194,-20.817562 0,-18.010758 -2.769382,-60.275057 -4.161591,-77.215202 -0.04797,-0.583659 -1.395782,-0.703789 -1.431722,-1.120751 -0.194665,-2.258415 -1.008414,-7.2278662 -1.084445,-7.5212464 -0.488463,-1.884831 -2.23225,-3.5368788 -4.278453,-3.4701457 -0.03684,-0.00219 -0.07529,0.00133 -0.11193,0 -2.047734,-0.068768 -3.789705,1.5842322 -4.278452,3.470141 -0.07358,0.2839198 -0.882492,5.1712001 -1.068191,7.3053231 -0.04236,0.486815 -1.400479,0.674009 -1.457914,1.373508 -1.3931925,16.967387 -4.1585528,59.180639 -4.158534,77.178329 2.3e-5,21.992366 2.3554437,40.345416 2.860306,51.116016 z"
+ id="path14406"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="sccsccccccsccssssssssccssscs" />
+ <path
+ style="opacity:0.2;fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;display:inline"
+ d="m 28.911333,120.55337 27.646447,0 0,-61.96967 82.46967,0"
+ id="path10552"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccc" />
+ <path
+ sodipodi:nodetypes="cccc"
+ inkscape:connector-curvature="0"
+ id="path10556"
+ d="m 24.27745,6.54709 32.25,0 0,15 82.5,0"
+ style="opacity:0.2;fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;display:inline" />
+ <path
+ style="opacity:0.2;fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;display:inline"
+ d="m 17.02745,189.5837 64.53033,0 0,-48 57.46967,0"
+ id="path10629"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccc" />
+ <path
+ sodipodi:nodetypes="cccc"
+ inkscape:connector-curvature="0"
+ id="path86913"
+ d="m 28.02745,138.52564 41.53033,0 0,-37.94194 69.46967,0"
+ style="opacity:0.2;fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;display:inline" />
+</svg>
diff --git a/panels/wacom/wacom-stylus-inking.svg b/panels/wacom/wacom-stylus-inking.svg
new file mode 100644
index 0000000..aca5ca4
--- /dev/null
+++ b/panels/wacom/wacom-stylus-inking.svg
@@ -0,0 +1,87 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ id="svg86343"
+ version="1.1"
+ inkscape:version="0.48.2 r9819"
+ width="148"
+ height="192"
+ sodipodi:docname="wacom-stylus-inking.svg">
+ <metadata
+ id="metadata86349">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs86347">
+ <inkscape:path-effect
+ effect="spiro"
+ id="path-effect14408"
+ is_visible="true" />
+ </defs>
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1280"
+ inkscape:window-height="742"
+ id="namedview86345"
+ showgrid="false"
+ inkscape:snap-nodes="false"
+ inkscape:snap-bbox="true"
+ inkscape:zoom="7"
+ inkscape:cx="100.36114"
+ inkscape:cy="19.260911"
+ inkscape:window-x="0"
+ inkscape:window-y="26"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="svg86343"
+ borderlayer="true"
+ inkscape:showpageshadow="false">
+ <inkscape:grid
+ type="xygrid"
+ id="grid86802"
+ empspacing="5"
+ visible="true"
+ enabled="true"
+ snapvisiblegridlinesonly="true" />
+ </sodipodi:namedview>
+ <g
+ style="opacity:0.2;display:inline"
+ id="g10631"
+ transform="translate(592.43375,-302.48416)">
+ <path
+ style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 353.5,569.875 38.0625,0 0,-166.14286 83.4375,0"
+ id="path10552"
+ inkscape:connector-curvature="0"
+ transform="translate(-928.4063,-79.84375)"
+ sodipodi:nodetypes="cccc" />
+ </g>
+ <path
+ style="color:#000000;fill:#d3d7cf;fill-opacity:1;fill-rule:nonzero;stroke:#babdb6;stroke-width:0.99999994px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ d="m 9.320334,142.86781 c 0.5052639,10.77933 -1.425426,24.83353 -1.425426,24.83353 l 1.559872,1.05225 c 0,0 1.362729,5.49117 1.850845,8.2077 0.488109,2.71653 3.679579,7.5712 3.679579,7.5712 l 1.545431,1e-5 -8e-6,3.89851 c -0.03586,1.30926 1.983788,1.38546 1.983788,9e-5 l 8e-6,-3.89851 1.451666,0 c 0,0 3.191479,-4.85469 3.679602,-7.57122 0.488109,-2.71652 1.850829,-8.20768 1.850829,-8.20768 l 1.559865,-1.05226 c 0,0 -1.930689,-14.0542 -1.425411,-24.83354 C 26.136241,132.08855 28.49126,113.93051 28.49126,91.938108 28.491252,69.945706 24.362076,8.8089377 23.653056,6.0730443 22.956873,3.3867824 20.471645,1.0322717 17.55539,1.12738 c -0.0525,-0.00312 -0.107307,0.0019 -0.159523,0 C 14.477429,1.029372 11.994773,3.3852354 11.298202,6.0730375 10.589174,8.8089377 6.460005,69.945698 6.460028,91.938064 c 2.3e-5,21.992366 2.355035,40.150416 2.860306,50.929746 z"
+ id="path14406"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="sccsccccccsccsssccscs" />
+</svg>
diff --git a/panels/wacom/wacom-stylus-no-eraser.svg b/panels/wacom/wacom-stylus-no-eraser.svg
new file mode 100644
index 0000000..cca7d34
--- /dev/null
+++ b/panels/wacom/wacom-stylus-no-eraser.svg
@@ -0,0 +1,118 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ id="svg86343"
+ version="1.1"
+ inkscape:version="0.91 r13725"
+ width="148"
+ height="192"
+ sodipodi:docname="wacom-stylus-no-eraser.svg">
+ <metadata
+ id="metadata86349">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs86347" />
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="960"
+ inkscape:window-height="1014"
+ id="namedview86345"
+ showgrid="false"
+ inkscape:snap-nodes="false"
+ inkscape:snap-bbox="true"
+ inkscape:zoom="2.4748737"
+ inkscape:cx="176.8759"
+ inkscape:cy="81.687362"
+ inkscape:window-x="1920"
+ inkscape:window-y="27"
+ inkscape:window-maximized="0"
+ inkscape:current-layer="g10631"
+ borderlayer="true"
+ inkscape:showpageshadow="false">
+ <inkscape:grid
+ type="xygrid"
+ id="grid86802"
+ empspacing="5"
+ visible="true"
+ enabled="true"
+ snapvisiblegridlinesonly="true" />
+ </sodipodi:namedview>
+ <g
+ style="display:inline"
+ transform="translate(592.43375,-287.62088)"
+ id="g10545">
+ <path
+ sodipodi:nodetypes="cscscccccccccccccscscc"
+ inkscape:connector-curvature="0"
+ id="rect10526"
+ transform="translate(-928.4063,-95.84375)"
+ d="m 344.125,384.88832 c -1.9944,0 -3.59375,1.59935 -3.59375,3.59375 L 340.53125,516 338,545.125 c -0.1873,2.15512 1.62589,3.92035 3.75,4.125 l 4.625,10.90625 1.53125,0 0,2.15625 3.61536,8.57242 1.18546,0.0214 0.44918,3.78119 0.33938,-3.7414 1.14797,-0.0687 3.76265,-8.53366 0,-2.1875 1.53125,0 4.65625,-10.96875 c 1.96694,-0.35188 3.54637,-2.02216 3.40625,-4.0625 L 365.53125,516 l 0,-127.51793 c 0,-1.9944 -1.59935,-3.59375 -3.59375,-3.59375 z"
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:#d3d7cf;fill-opacity:1;fill-rule:nonzero;stroke:#babdb6;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate" />
+ <rect
+ style="color:#000000;fill:#eeeeec;fill-opacity:1;fill-rule:nonzero;stroke:#babdb6;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ id="rect10541"
+ width="8.75"
+ height="22"
+ x="-579.65631"
+ y="385.90625"
+ rx="3.25"
+ ry="3.25" />
+ <rect
+ ry="3.2500002"
+ rx="3.25"
+ y="410.90625"
+ x="-579.65631"
+ height="12.25"
+ width="8.75"
+ id="rect10543"
+ style="color:#000000;fill:#eeeeec;fill-opacity:1;fill-rule:nonzero;stroke:#babdb6;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+ </g>
+ <g
+ style="opacity:0.2;display:inline"
+ id="g10631"
+ transform="translate(592.43375,-302.48416)">
+ <path
+ sodipodi:nodetypes="cccc"
+ transform="translate(-928.4063,-79.84375)"
+ inkscape:connector-curvature="0"
+ id="path3342"
+ d="m 357.25,492.91161 35.28033,0 0,-89.17947 82.46967,0"
+ style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+ <path
+ style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m -575.4063,492.06786 65.53033,0 0,-85.17947 56.46967,0"
+ id="path10629"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccc" />
+ <path
+ style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m -571.1563,430.06786 48.28033,0 0,-64.17947 69.46967,0"
+ id="path3344"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccc" />
+ </g>
+</svg>
diff --git a/panels/wacom/wacom-stylus-page.ui b/panels/wacom/wacom-stylus-page.ui
new file mode 100644
index 0000000..133f3c8
--- /dev/null
+++ b/panels/wacom/wacom-stylus-page.ui
@@ -0,0 +1,417 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.20.0 -->
+<interface>
+ <requires lib="gtk+" version="3.20"/>
+ <object class="GtkAdjustment" id="adjustment-eraser-feel">
+ <property name="upper">6</property>
+ <property name="step_increment">1</property>
+ <property name="page_increment">3</property>
+ </object>
+ <object class="GtkAdjustment" id="adjustment-tip-feel">
+ <property name="upper">6</property>
+ <property name="step_increment">1</property>
+ <property name="page_increment">3</property>
+ </object>
+ <object class="GtkListStore" id="liststore-buttons">
+ <columns>
+ <!-- column-name button -->
+ <column type="gint"/>
+ <!-- column-name button-label -->
+ <column type="gchararray"/>
+ </columns>
+ <data>
+ <row>
+ <col id="0">0</col>
+ <col id="1" translatable="yes">Default</col>
+ </row>
+ <row>
+ <col id="0">1</col>
+ <col id="1" translatable="yes">Middle Mouse Button Click</col>
+ </row>
+ <row>
+ <col id="0">2</col>
+ <col id="1" translatable="yes">Right Mouse Button Click</col>
+ </row>
+ <row>
+ <col id="0">3</col>
+ <col id="1" translatable="yes">Back</col>
+ </row>
+ <row>
+ <col id="0">4</col>
+ <col id="1" translatable="yes">Forward</col>
+ </row>
+ </data>
+ </object>
+ <object class="GtkNotebook" id="stylus-notebook">
+ <property name="name">stylus-notebook</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="show_tabs">False</property>
+ <property name="show_border">False</property>
+ <child>
+ <object class="GtkBox" id="no-stylus-page">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="valign">end</property>
+ <property name="vexpand">True</property>
+ <property name="pixel_size">96</property>
+ <property name="icon_name">input-tablet-symbolic</property>
+ <property name="icon_size">0</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="no-stylus-label1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">No stylus found</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="valign">start</property>
+ <property name="vexpand">True</property>
+ <property name="label" translatable="yes">Please move your stylus to the proximity of the tablet to configure it</property>
+ <property name="justify">center</property>
+ <property name="wrap">True</property>
+ <property name="width_chars">30</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkGrid" id="stylus-grid">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="hexpand">True</property>
+ <property name="row_spacing">10</property>
+ <property name="column_spacing">10</property>
+ <child>
+ <object class="GtkLabel" id="label-stylus">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">start</property>
+ <property name="valign">center</property>
+ <property name="label" translatable="yes">Stylus</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ <property name="width">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkImage" id="image-stylus">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">end</property>
+ <property name="valign">start</property>
+ <property name="pixbuf">wacom-stylus.svg</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkGrid" id="stylus-controls-grid">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_start">16</property>
+ <property name="margin_top">12</property>
+ <property name="hexpand">True</property>
+ <property name="orientation">vertical</property>
+ <property name="row_spacing">6</property>
+ <property name="column_spacing">10</property>
+ <child>
+ <object class="GtkLabel" id="label-eraser-feel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">end</property>
+ <property name="valign">center</property>
+ <property name="label" translatable="yes">Eraser Pressure Feel</property>
+ <property name="justify">right</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="eraser-box">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="hexpand">True</property>
+ <property name="spacing">10</property>
+ <child>
+ <object class="GtkLabel" id="label-eraser-soft">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Soft</property>
+ <attributes>
+ <attribute name="scale" value="0.82999999999999996"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkScale" id="scale-eraser-feel">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="valign">center</property>
+ <property name="adjustment">adjustment-eraser-feel</property>
+ <property name="digits">0</property>
+ <property name="draw_value">False</property>
+ <property name="round_digits">0</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label-eraser-firm">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Firm</property>
+ <attributes>
+ <attribute name="scale" value="0.82999999999999996"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label-top-button">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">end</property>
+ <property name="valign">center</property>
+ <property name="label" translatable="yes">Top Button</property>
+ <property name="justify">right</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkComboBox" id="combo-topbutton">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="valign">center</property>
+ <property name="model">liststore-buttons</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label-lower-button">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">end</property>
+ <property name="valign">center</property>
+ <property name="label" translatable="yes">Lower Button</property>
+ <property name="justify">right</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkComboBox" id="combo-bottombutton">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="valign">center</property>
+ <property name="model">liststore-buttons</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label-third-button">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">end</property>
+ <property name="valign">center</property>
+ <property name="label" translatable="yes">Lowest Button</property>
+ <property name="justify">right</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">3</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkComboBox" id="combo-thirdbutton">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="valign">center</property>
+ <property name="model">liststore-buttons</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">3</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label-tip-feel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">end</property>
+ <property name="valign">center</property>
+ <property name="label" translatable="yes">Tip Pressure Feel</property>
+ <property name="justify">right</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">4</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="box-tip-feel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">10</property>
+ <child>
+ <object class="GtkLabel" id="label-tip-soft">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Soft</property>
+ <attributes>
+ <attribute name="scale" value="0.82999999999999996"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkScale" id="scale-tip-feel">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="hexpand">True</property>
+ <property name="adjustment">adjustment-tip-feel</property>
+ <property name="digits">0</property>
+ <property name="draw_value">False</property>
+ <property name="round_digits">0</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label-tip-firm">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Firm</property>
+ <attributes>
+ <attribute name="scale" value="0.82999999999999996"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">4</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkRevealer" id="navigation-placeholder">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">end</property>
+ <property name="transition_type">crossfade</property>
+ <property name="transition_duration">100</property>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+</interface>
diff --git a/panels/wacom/wacom-stylus.svg b/panels/wacom/wacom-stylus.svg
new file mode 100644
index 0000000..63dfa6e
--- /dev/null
+++ b/panels/wacom/wacom-stylus.svg
@@ -0,0 +1,124 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ id="svg86343"
+ version="1.1"
+ inkscape:version="0.48.2 r9819"
+ width="148"
+ height="192"
+ sodipodi:docname="wacom-stylus.svg">
+ <metadata
+ id="metadata86349">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs86347" />
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1280"
+ inkscape:window-height="742"
+ id="namedview86345"
+ showgrid="false"
+ inkscape:snap-nodes="false"
+ inkscape:snap-bbox="true"
+ inkscape:zoom="7"
+ inkscape:cx="93.888956"
+ inkscape:cy="186.10424"
+ inkscape:window-x="0"
+ inkscape:window-y="26"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="svg86343"
+ borderlayer="true"
+ inkscape:showpageshadow="false">
+ <inkscape:grid
+ type="xygrid"
+ id="grid86802"
+ empspacing="5"
+ visible="true"
+ enabled="true"
+ snapvisiblegridlinesonly="true" />
+ </sodipodi:namedview>
+ <g
+ style="display:inline"
+ transform="translate(592.43375,-287.62088)"
+ id="g10545">
+ <path
+ sodipodi:nodetypes="sscsscscccccccccccccscsscsss"
+ inkscape:connector-curvature="0"
+ id="rect10526"
+ transform="translate(-928.4063,-95.84375)"
+ d="m 349.69531,384.96463 c -2.3083,0 -3.2326,1.49535 -3.69531,4.51323 L 345.53125,396 344.125,396 c -1.9944,0 -3.59375,1.59935 -3.59375,3.59375 L 340.53125,516 338,545.125 c -0.1873,2.15512 1.62589,3.92035 3.75,4.125 l 4.625,10.90625 1.53125,0 0,2.15625 3.61536,8.57242 1.18546,0.0214 0.44918,3.78119 0.33938,-3.7414 1.14797,-0.0687 3.76265,-8.53366 0,-2.1875 1.53125,0 4.65625,-10.96875 c 1.96694,-0.35188 3.54637,-2.02216 3.40625,-4.0625 L 365.53125,516 l 0,-116.40625 c 0,-1.9944 -1.59935,-3.59375 -3.59375,-3.59375 l -1.40625,0 L 360,389.47786 c -0.23272,-2.85711 -1.26201,-4.51323 -3.69531,-4.51323 z"
+ style="color:#000000;fill:#d3d7cf;fill-opacity:1;fill-rule:nonzero;stroke:#babdb6;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+ <rect
+ style="color:#000000;fill:#eeeeec;fill-opacity:1;fill-rule:nonzero;stroke:#babdb6;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ id="rect10541"
+ width="8.75"
+ height="22"
+ x="-579.65631"
+ y="385.90625"
+ rx="3.25"
+ ry="3.25" />
+ <rect
+ ry="3.2500002"
+ rx="3.25"
+ y="410.90625"
+ x="-579.65631"
+ height="12.25"
+ width="8.75"
+ id="rect10543"
+ style="color:#000000;fill:#eeeeec;fill-opacity:1;fill-rule:nonzero;stroke:#babdb6;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+ </g>
+ <g
+ style="opacity:0.2;display:inline"
+ id="g10631"
+ transform="translate(592.43375,-302.48416)">
+ <path
+ style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 357.25,492.91161 35.28033,0 0,-52.14286 82.46967,0"
+ id="path10552"
+ inkscape:connector-curvature="0"
+ transform="translate(-928.4063,-79.84375)"
+ sodipodi:nodetypes="cccc" />
+ <path
+ sodipodi:nodetypes="cccc"
+ inkscape:connector-curvature="0"
+ id="path10556"
+ d="m -568.1563,309.03125 32.25,0 0,14.85714 82.5,0"
+ style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+ <path
+ style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m -575.4063,492.06786 39.53033,0 0,-48.14286 82.46967,0"
+ id="path10629"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccc" />
+ <path
+ sodipodi:nodetypes="cccc"
+ inkscape:connector-curvature="0"
+ id="path86913"
+ d="m -571.1563,430.06786 48.28033,0 0,-27.14286 69.46967,0"
+ style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+ </g>
+</svg>
diff --git a/panels/wacom/wacom-tablet-cintiq.svg b/panels/wacom/wacom-tablet-cintiq.svg
new file mode 100644
index 0000000..e9ae5c3
--- /dev/null
+++ b/panels/wacom/wacom-tablet-cintiq.svg
@@ -0,0 +1,83 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ id="svg86858"
+ version="1.1"
+ inkscape:version="0.48.1 r9760"
+ width="148"
+ height="95"
+ sodipodi:docname="wacom-tablet-cintiq.svg">
+ <metadata
+ id="metadata86864">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs86862" />
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1199"
+ inkscape:window-height="807"
+ id="namedview86860"
+ showgrid="false"
+ borderlayer="true"
+ inkscape:showpageshadow="false"
+ inkscape:zoom="1"
+ inkscape:cx="-123.95096"
+ inkscape:cy="-15.993957"
+ inkscape:window-x="2858"
+ inkscape:window-y="395"
+ inkscape:window-maximized="0"
+ inkscape:current-layer="svg86858"
+ inkscape:snap-nodes="false"
+ inkscape:snap-bbox="true">
+ <inkscape:grid
+ type="xygrid"
+ id="grid86892"
+ empspacing="5"
+ visible="true"
+ enabled="true"
+ snapvisiblegridlinesonly="true" />
+ </sodipodi:namedview>
+ <g
+ style="display:inline;enable-background:new"
+ transform="matrix(0.68936116,0,0,0.68936115,465.30952,-75.491812)"
+ id="g4353-3">
+ <path
+ inkscape:connector-curvature="0"
+ style="color:#000000;fill:#d3d7cf;fill-opacity:1;fill-rule:nonzero;stroke:#babdb6;stroke-width:1.45061844px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ d="m 191,253 0,122 30.45602,0 0,10 20.32523,0 0,-10 65.25887,0 0,10 20.4159,0 0,-10 31.81597,0 0,-122 z"
+ transform="translate(-860.90625,-139.8605)"
+ id="rect22528"
+ sodipodi:nodetypes="ccccccccccccc" />
+ <rect
+ style="opacity:1;color:#000000;fill:#babdb6;fill-opacity:1;fill-rule:nonzero;stroke:#babdb6;stroke-width:1.45061835000000006;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ id="rect4133-4"
+ width="126.20383"
+ height="79.784019"
+ x="-649.60083"
+ y="130.54378" />
+ </g>
+</svg>
diff --git a/panels/wacom/wacom-tablet-pc.svg b/panels/wacom/wacom-tablet-pc.svg
new file mode 100644
index 0000000..091b9b1
--- /dev/null
+++ b/panels/wacom/wacom-tablet-pc.svg
@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ id="svg86858"
+ version="1.1"
+ inkscape:version="0.48.1 r9760"
+ width="148"
+ height="95"
+ sodipodi:docname="wacom-tablet-pc.svg">
+ <metadata
+ id="metadata86864">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs86862" />
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1199"
+ inkscape:window-height="807"
+ id="namedview86860"
+ showgrid="false"
+ borderlayer="true"
+ inkscape:showpageshadow="false"
+ inkscape:zoom="1"
+ inkscape:cx="218.7114"
+ inkscape:cy="-21.332194"
+ inkscape:window-x="1873"
+ inkscape:window-y="310"
+ inkscape:window-maximized="0"
+ inkscape:current-layer="svg86858"
+ inkscape:snap-nodes="false"
+ inkscape:snap-bbox="true">
+ <inkscape:grid
+ type="xygrid"
+ id="grid86892"
+ empspacing="5"
+ visible="true"
+ enabled="true"
+ snapvisiblegridlinesonly="true" />
+ </sodipodi:namedview>
+ <path
+ style="color:#000000;fill:#d3d7cf;fill-opacity:1;fill-rule:nonzero;stroke:#babdb6;stroke-width:0.99999994000000003px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ d="m 17.536356,5.4999999 0,59.7141961 -12.9738561,25.238058 115.6736201,0 -14.68849,-26.666627 -0.012,-58.2856271 z"
+ id="rect22528"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccccc" />
+ <rect
+ y="14.5"
+ x="28.499998"
+ height="42"
+ width="66"
+ id="rect4133-4"
+ style="opacity:1;color:#000000;fill:#babdb6;fill-opacity:1;fill-rule:nonzero;stroke:#babdb6;stroke-width:0.99999994000000003;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+</svg>
diff --git a/panels/wacom/wacom-tablet.svg b/panels/wacom/wacom-tablet.svg
new file mode 100644
index 0000000..35e5a4b
--- /dev/null
+++ b/panels/wacom/wacom-tablet.svg
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ id="svg86858"
+ version="1.1"
+ inkscape:version="0.48.1 r9760"
+ width="148"
+ height="95"
+ sodipodi:docname="wacom-tablet.svg">
+ <metadata
+ id="metadata86864">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs86862" />
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1199"
+ inkscape:window-height="807"
+ id="namedview86860"
+ showgrid="false"
+ borderlayer="true"
+ inkscape:showpageshadow="false"
+ inkscape:zoom="1.4142136"
+ inkscape:cx="42.197473"
+ inkscape:cy="89.570991"
+ inkscape:window-x="1873"
+ inkscape:window-y="310"
+ inkscape:window-maximized="0"
+ inkscape:current-layer="g4353-0">
+ <inkscape:grid
+ type="xygrid"
+ id="grid86892"
+ empspacing="5"
+ visible="true"
+ enabled="true"
+ snapvisiblegridlinesonly="true" />
+ </sodipodi:namedview>
+ <g
+ style="display:inline"
+ transform="matrix(0.59415025,0,0,0.67531282,405.44733,-74.059104)"
+ id="g4353-0">
+ <path
+ style="fill:#d3d7cf;fill-opacity:1;stroke:#babdb6;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="M 4.2812501,3.375 3.53125,85.5 C 31.076436,90.740667 83.579166,92.281216 115,85.5 L 114.25,3.375 z"
+ transform="matrix(1.683076,0,0,1.4807952,-682.39865,109.66637)"
+ id="path3568-6"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccc" />
+ <rect
+ style="color:#000000;fill:#babdb6;fill-opacity:1;fill-rule:nonzero;stroke:#babdb6;stroke-width:1.57869899;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ id="rect10889"
+ width="138.6996"
+ height="82.952255"
+ x="-651.99988"
+ y="129.88136" />
+ </g>
+</svg>
diff --git a/panels/wacom/wacom.gresource.xml b/panels/wacom/wacom.gresource.xml
new file mode 100644
index 0000000..846835f
--- /dev/null
+++ b/panels/wacom/wacom.gresource.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+ <gresource prefix="/org/gnome/control-center/wacom">
+ <file preprocess="xml-stripblanks">gnome-wacom-properties.ui</file>
+ <file preprocess="xml-stripblanks">wacom-stylus-page.ui</file>
+ <file preprocess="xml-stripblanks">button-mapping.ui</file>
+ <file preprocess="xml-stripblanks">calibrator/calibrator.ui</file>
+ <file>calibrator/calibrator.css</file>
+ <file>calibrator/target.svg</file>
+ <file>wacom-tablet.svg</file>
+ <file>wacom-stylus.svg</file>
+ <file>wacom-stylus-3btn-no-eraser.svg</file>
+ <file>wacom-stylus-3btn.svg</file>
+ <file>wacom-stylus-no-eraser.svg</file>
+ <file>wacom-stylus-airbrush.svg</file>
+ <file>wacom-stylus-inking.svg</file>
+ <file>wacom-stylus-art-pen.svg</file>
+ <file>wacom-stylus-classic.svg</file>
+ <file>wacom-tablet-cintiq.svg</file>
+ <file>wacom-tablet-pc.svg</file>
+ </gresource>
+</gresources>