summaryrefslogtreecommitdiffstats
path: root/panels/wacom
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--panels/wacom/button-mapping.ui78
-rw-r--r--panels/wacom/calibrator/COPYING27
-rw-r--r--panels/wacom/calibrator/calibrator-gui.c429
-rw-r--r--panels/wacom/calibrator/calibrator-gui.h63
-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.ui146
-rw-r--r--panels/wacom/calibrator/cc-clock.c289
-rw-r--r--panels/wacom/calibrator/cc-clock.h41
-rw-r--r--panels/wacom/calibrator/main.c421
-rw-r--r--panels/wacom/calibrator/meson.build35
-rw-r--r--panels/wacom/calibrator/target.svg93
-rw-r--r--panels/wacom/cc-drawing-area.c182
-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.c437
-rw-r--r--panels/wacom/cc-wacom-device.h66
-rw-r--r--panels/wacom/cc-wacom-ekr-page.c195
-rw-r--r--panels/wacom/cc-wacom-ekr-page.h34
-rw-r--r--panels/wacom/cc-wacom-ekr-page.ui32
-rw-r--r--panels/wacom/cc-wacom-page.c865
-rw-r--r--panels/wacom/cc-wacom-page.h39
-rw-r--r--panels/wacom/cc-wacom-page.ui89
-rw-r--r--panels/wacom/cc-wacom-panel.c753
-rw-r--r--panels/wacom/cc-wacom-panel.h42
-rw-r--r--panels/wacom/cc-wacom-panel.ui88
-rw-r--r--panels/wacom/cc-wacom-stylus-page.c274
-rw-r--r--panels/wacom/cc-wacom-stylus-page.h35
-rw-r--r--panels/wacom/cc-wacom-stylus-page.ui140
-rw-r--r--panels/wacom/cc-wacom-tool.c330
-rw-r--r--panels/wacom/cc-wacom-tool.h46
-rw-r--r--panels/wacom/gnome-wacom-panel.desktop.in.in18
-rw-r--r--panels/wacom/gsd-enums.h7
-rw-r--r--panels/wacom/gsd-wacom-key-shortcut-button.c535
-rw-r--r--panels/wacom/gsd-wacom-key-shortcut-button.h40
-rw-r--r--panels/wacom/icons/meson.build4
-rw-r--r--panels/wacom/icons/scalable/org.gnome.Settings-wacom-symbolic.svg4
-rw-r--r--panels/wacom/meson.build106
-rw-r--r--panels/wacom/test-wacom.c155
-rwxr-xr-xpanels/wacom/wacom-panel-scenario-tester.py75
-rw-r--r--panels/wacom/wacom-stylus-3btn-no-eraser.svg110
-rw-r--r--panels/wacom/wacom-stylus-3btn.svg106
-rw-r--r--panels/wacom/wacom-stylus-airbrush.svg73
-rw-r--r--panels/wacom/wacom-stylus-art-pen.svg94
-rw-r--r--panels/wacom/wacom-stylus-classic.svg83
-rw-r--r--panels/wacom/wacom-stylus-inking.svg79
-rw-r--r--panels/wacom/wacom-stylus-no-eraser.svg101
-rw-r--r--panels/wacom/wacom-stylus.svg100
-rw-r--r--panels/wacom/wacom-tablet-cintiq.svg86
-rw-r--r--panels/wacom/wacom-tablet-pc.svg80
-rw-r--r--panels/wacom/wacom-tablet.svg84
-rw-r--r--panels/wacom/wacom.gresource.xml24
56 files changed, 8358 insertions, 0 deletions
diff --git a/panels/wacom/button-mapping.ui b/panels/wacom/button-mapping.ui
new file mode 100644
index 0000000..401aa12
--- /dev/null
+++ b/panels/wacom/button-mapping.ui
@@ -0,0 +1,78 @@
+<?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="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>
+ <child>
+ <object class="GtkBox" id="top_vbox">
+ <property name="orientation">vertical</property>
+ <property name="spacing">2</property>
+ <child>
+ <object class="GtkBox" id="shortcuts_vbox">
+ <property name="orientation">vertical</property>
+ <property name="spacing">6</property>
+ <property name="hexpand">True</property>
+ <property name="vexpand">True</property>
+ <child>
+ <object class="GtkLabel" id="label1">
+ <property name="xalign">0</property>
+ <property name="margin-top">12</property>
+ <property name="label" translatable="yes">Map buttons to functions</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ </object>
+ </child>
+ <child>
+ <object class="GtkScrolledWindow" id="actions_swindow">
+ <property name="hscrollbar_policy">never</property>
+ <child>
+ <object class="GtkListBox" id="shortcuts_list">
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox" id="hbox1">
+ <property name="margin-top">5</property>
+ <property name="margin-bottom">5</property>
+ <property name="margin-start">5</property>
+ <property name="margin-end">5</property>
+ <property name="spacing">12</property>
+ <child>
+ <object class="GtkLabel" id="label12">
+ <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>
+ <property name="hexpand">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkButtonBox" id="dialog-action_area1">
+ <property name="layout_style">end</property>
+ <child>
+ <object class="GtkButton" id="close_button">
+ <property name="label" translatable="yes">_Close</property>
+ <property name="use_underline">True</property>
+ </object>
+ </child>
+ </object>
+ </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..2d5d7ec
--- /dev/null
+++ b/panels/wacom/calibrator/calibrator-gui.c
@@ -0,0 +1,429 @@
+/*
+ * 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/x11/gdkx.h>
+#include <gtk/gtk.h>
+
+#include "calibrator.h"
+#include "calibrator-gui.h"
+#include "cc-clock.h"
+
+struct _CcCalibArea
+{
+ GtkWindow parent_instance;
+
+ struct Calib calibrator;
+ XYinfo axis;
+ gboolean swap;
+ gboolean success;
+ GdkDevice *device;
+
+ GtkWidget *error_revealer;
+ GtkWidget *title_revealer;
+ GtkWidget *subtitle_revealer;
+ GtkWidget *clock;
+ GtkWidget *target1, *target2, *target3, *target4;
+ GtkWidget *stack;
+ GtkWidget *success_page;
+ GtkCssProvider *style_provider;
+
+ FinishCallback callback;
+ gpointer user_data;
+};
+
+G_DEFINE_TYPE (CcCalibArea, cc_calib_area, GTK_TYPE_WINDOW)
+
+/* Timeout parameters */
+#define MAX_TIME 15000 /* 15000 = 15 sec */
+#define END_TIME 750 /* 750 = 0.75 sec */
+
+static void
+cc_calib_area_notify_finish (CcCalibArea *area)
+{
+ gtk_widget_hide (GTK_WIDGET (area));
+
+ (*area->callback) (area, area->user_data);
+}
+
+static gboolean
+on_close_request (GtkWidget *widget,
+ CcCalibArea *area)
+{
+ cc_calib_area_notify_finish (area);
+ return GDK_EVENT_PROPAGATE;
+}
+
+static gboolean
+cc_calib_area_finish_idle_cb (CcCalibArea *area)
+{
+ cc_calib_area_notify_finish (area);
+ return FALSE;
+}
+
+static void
+set_success (CcCalibArea *area)
+{
+ gtk_stack_set_visible_child (GTK_STACK (area->stack), area->success_page);
+}
+
+static void
+set_calibration_status (CcCalibArea *area)
+{
+ area->success = finish (&area->calibrator, &area->axis, &area->swap);
+
+ if (area->success)
+ {
+ set_success (area);
+ g_timeout_add (END_TIME,
+ (GSourceFunc) cc_calib_area_finish_idle_cb,
+ area);
+ }
+ else
+ {
+ g_idle_add ((GSourceFunc) cc_calib_area_finish_idle_cb, area);
+ }
+}
+
+static void
+show_error_message (CcCalibArea *area)
+{
+ gtk_revealer_set_reveal_child (GTK_REVEALER (area->error_revealer), TRUE);
+}
+
+static void
+hide_error_message (CcCalibArea *area)
+{
+ gtk_revealer_set_reveal_child (GTK_REVEALER (area->error_revealer), FALSE);
+}
+
+static void
+set_active_target (CcCalibArea *area,
+ int n_target)
+{
+ GtkWidget *targets[] = {
+ area->target1,
+ area->target2,
+ area->target3,
+ area->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 (GtkGestureClick *gesture,
+ guint n_press,
+ gdouble x,
+ gdouble y,
+ CcCalibArea *area)
+{
+ gint num_clicks;
+ gboolean success;
+ GdkDevice *source;
+
+ if (area->success)
+ return;
+
+ source = gtk_gesture_get_device (GTK_GESTURE (gesture));
+
+ if (gdk_device_get_source (source) == GDK_SOURCE_TOUCHSCREEN)
+ return;
+
+ /* 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 (GtkEventControllerKey *controller,
+ guint keyval,
+ guint keycode,
+ GdkModifierType state,
+ CcCalibArea *area)
+{
+ if (area->success || keyval != GDK_KEY_Escape)
+ return GDK_EVENT_PROPAGATE;
+
+ cc_calib_area_notify_finish (area);
+ return GDK_EVENT_STOP;
+}
+
+static void
+on_clock_finished (CcClock *clock,
+ CcCalibArea *area)
+{
+ set_calibration_status (area);
+}
+
+static void
+on_title_revealed (CcCalibArea *area)
+{
+ gtk_revealer_set_reveal_child (GTK_REVEALER (area->subtitle_revealer), TRUE);
+}
+
+static void
+on_fullscreen (GtkWindow *window,
+ GParamSpec *pspec,
+ CcCalibArea *area)
+{
+ if (!gtk_window_is_fullscreen (window))
+ return;
+
+ g_signal_connect_swapped (area->title_revealer,
+ "notify::child-revealed",
+ G_CALLBACK (on_title_revealed),
+ area);
+ gtk_revealer_set_reveal_child (GTK_REVEALER (area->title_revealer), TRUE);
+
+ set_active_target (area, 0);
+}
+
+static void
+cc_calib_area_finalize (GObject *object)
+{
+ CcCalibArea *area = CC_CALIB_AREA (object);
+
+ gtk_style_context_remove_provider_for_display (gtk_widget_get_display (GTK_WIDGET (area)),
+ GTK_STYLE_PROVIDER (area->style_provider));
+
+ G_OBJECT_CLASS (cc_calib_area_parent_class)->finalize (object);
+}
+
+static void
+cc_calib_area_size_allocate (GtkWidget *widget,
+ int width,
+ int height,
+ int baseline)
+{
+ CcCalibArea *calib_area = CC_CALIB_AREA (widget);
+
+ if (calib_area->calibrator.geometry.width != width ||
+ calib_area->calibrator.geometry.height != height)
+ {
+ calib_area->calibrator.geometry.width = width;
+ calib_area->calibrator.geometry.height = height;
+
+ /* reset calibration if already started */
+ reset (&calib_area->calibrator);
+ set_active_target (calib_area, 0);
+ }
+
+ GTK_WIDGET_CLASS (cc_calib_area_parent_class)->size_allocate (widget,
+ width,
+ height,
+ baseline);
+}
+
+static void
+cc_calib_area_class_init (CcCalibAreaClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->finalize = cc_calib_area_finalize;
+
+ widget_class->size_allocate = cc_calib_area_size_allocate;
+
+ g_type_ensure (CC_TYPE_CLOCK);
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/control-center/wacom/calibrator/calibrator.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, CcCalibArea, error_revealer);
+ gtk_widget_class_bind_template_child (widget_class, CcCalibArea, title_revealer);
+ gtk_widget_class_bind_template_child (widget_class, CcCalibArea, subtitle_revealer);
+ gtk_widget_class_bind_template_child (widget_class, CcCalibArea, clock);
+ gtk_widget_class_bind_template_child (widget_class, CcCalibArea, target1);
+ gtk_widget_class_bind_template_child (widget_class, CcCalibArea, target2);
+ gtk_widget_class_bind_template_child (widget_class, CcCalibArea, target3);
+ gtk_widget_class_bind_template_child (widget_class, CcCalibArea, target4);
+ gtk_widget_class_bind_template_child (widget_class, CcCalibArea, stack);
+ gtk_widget_class_bind_template_child (widget_class, CcCalibArea, success_page);
+}
+
+static void
+cc_calib_area_init (CcCalibArea *calib_area)
+{
+ GtkGesture *click;
+ GtkEventController *key;
+
+ gtk_widget_init_template (GTK_WIDGET (calib_area));
+
+ 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_display (gtk_widget_get_display (GTK_WIDGET (calib_area)),
+ 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 (GTK_WIDGET (calib_area));
+ gtk_widget_set_cursor_from_name (GTK_WIDGET (calib_area), "blank");
+
+ gtk_widget_set_can_focus (GTK_WIDGET (calib_area), TRUE);
+#endif /* FAKE_AREA */
+
+ g_signal_connect (calib_area,
+ "close-request",
+ G_CALLBACK (on_close_request),
+ calib_area);
+ g_signal_connect (calib_area,
+ "notify::fullscreened",
+ G_CALLBACK (on_fullscreen),
+ calib_area);
+
+ click = gtk_gesture_click_new ();
+ gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (click), GDK_BUTTON_PRIMARY);
+ g_signal_connect (click, "pressed",
+ G_CALLBACK (on_gesture_press), calib_area);
+ gtk_widget_add_controller (GTK_WIDGET (calib_area),
+ GTK_EVENT_CONTROLLER (click));
+
+ key = gtk_event_controller_key_new ();
+ g_signal_connect (key, "key-released",
+ G_CALLBACK (on_key_release), calib_area);
+ gtk_widget_add_controller (GTK_WIDGET (calib_area), key);
+}
+
+/**
+ * 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
+ * cc_calib_area_finish().
+ */
+CcCalibArea *
+cc_calib_area_new (GdkDisplay *display,
+ GdkMonitor *monitor,
+ GdkDevice *device,
+ FinishCallback callback,
+ gpointer user_data,
+ int threshold_doubleclick,
+ int threshold_misclick)
+{
+ CcCalibArea *calib_area;
+
+ g_return_val_if_fail (callback, NULL);
+
+ calib_area = g_object_new (CC_TYPE_CALIB_AREA, NULL);
+ 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;
+
+ /* Move to correct screen */
+ if (monitor)
+ gtk_window_fullscreen_on_monitor (GTK_WINDOW (calib_area), monitor);
+ else
+ gtk_window_fullscreen (GTK_WINDOW (calib_area));
+
+ gtk_widget_show (GTK_WIDGET (calib_area));
+
+ return calib_area;
+}
+
+/* Finishes the calibration. Note that CalibArea
+ * needs to be destroyed with Cccalib_area_free() afterwards */
+gboolean
+cc_calib_area_finish (CcCalibArea *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
+cc_calib_area_free (CcCalibArea *area)
+{
+ gtk_window_destroy (GTK_WINDOW (area));
+}
+
+void
+cc_calib_area_get_axis (CcCalibArea *area,
+ XYinfo *new_axis,
+ gboolean *swap_xy)
+{
+ g_return_if_fail (area != NULL);
+
+ *new_axis = area->axis;
+ *swap_xy = area->swap;
+}
+
+void
+cc_calib_area_get_padding (CcCalibArea *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..5d6d1ae
--- /dev/null
+++ b/panels/wacom/calibrator/calibrator-gui.h
@@ -0,0 +1,63 @@
+/*
+ * 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;
+
+#define CC_TYPE_CALIB_AREA cc_calib_area_get_type ()
+G_DECLARE_FINAL_TYPE (CcCalibArea, cc_calib_area, CC, CALIB_AREA, GtkWindow)
+
+typedef void (*FinishCallback) (CcCalibArea *area, gpointer user_data);
+
+CcCalibArea * cc_calib_area_new (GdkDisplay *display,
+ GdkMonitor *monitor,
+ GdkDevice *device,
+ FinishCallback callback,
+ gpointer user_data,
+ int threshold_doubleclick,
+ int threshold_misclick);
+
+gboolean cc_calib_area_finish (CcCalibArea *area);
+
+void cc_calib_area_free (CcCalibArea *area);
+
+void cc_calib_area_get_axis (CcCalibArea *area,
+ XYinfo *new_axis,
+ gboolean *swap_xy);
+
+void cc_calib_area_get_padding (CcCalibArea *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..462c766
--- /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..89d4865
--- /dev/null
+++ b/panels/wacom/calibrator/calibrator.ui
@@ -0,0 +1,146 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="CcCalibArea" parent="GtkWindow">
+ <property name="name">calibrator</property>
+ <property name="child">
+ <object class="GtkStack" id="stack">
+ <property name="transition_duration">0</property>
+ <child>
+ <object class="GtkGrid">
+ <property name="row_homogeneous">1</property>
+ <property name="column_homogeneous">1</property>
+ <child>
+ <object class="GtkBox">
+ <property name="orientation">vertical</property>
+ <layout>
+ <property name="column">0</property>
+ <property name="row">0</property>
+ <property name="column-span">8</property>
+ <property name="row-span">8</property>
+ </layout>
+ <child>
+ <object class="GtkBox" id="box1">
+ <property name="orientation">vertical</property>
+ <property name="vexpand">1</property>
+ </object>
+ </child>
+ <child>
+ <object class="CcClock" id="clock"/>
+ </child>
+ <child>
+ <object class="GtkBox" id="box2">
+ <property name="orientation">vertical</property>
+ <property name="vexpand">1</property>
+ <child>
+ <object class="GtkRevealer" id="title_revealer">
+ <property name="transition_duration">300</property>
+ <property name="child">
+ <object class="GtkLabel">
+ <property name="name">title</property>
+ <property name="label" translatable="1">Screen Calibration</property>
+ </object>
+ </property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkRevealer" id="subtitle_revealer">
+ <property name="transition_duration">300</property>
+ <property name="child">
+ <object class="GtkLabel">
+ <property name="name">subtitle</property>
+ <property name="label" translatable="1">Please tap the target markers as they appear on screen to calibrate the tablet.</property>
+ </object>
+ </property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkRevealer" id="error_revealer">
+ <property name="transition_type">crossfade</property>
+ <property name="transition_duration">500</property>
+ <property name="child">
+ <object class="GtkLabel">
+ <property name="name">error</property>
+ <property name="label" translatable="1">Mis-click detected, restarting…</property>
+ </object>
+ </property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImage" id="target1">
+ <property name="name">target</property>
+ <property name="width_request">100</property>
+ <property name="height_request">100</property>
+ <property name="sensitive">0</property>
+ <layout>
+ <property name="column">0</property>
+ <property name="row">0</property>
+ <property name="column-span">2</property>
+ <property name="row-span">2</property>
+ </layout>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImage" id="target2">
+ <property name="name">target</property>
+ <property name="width_request">100</property>
+ <property name="height_request">100</property>
+ <property name="sensitive">0</property>
+ <layout>
+ <property name="column">6</property>
+ <property name="row">0</property>
+ <property name="column-span">2</property>
+ <property name="row-span">2</property>
+ </layout>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImage" id="target3">
+ <property name="name">target</property>
+ <property name="width_request">100</property>
+ <property name="height_request">100</property>
+ <property name="sensitive">0</property>
+ <layout>
+ <property name="column">0</property>
+ <property name="row">6</property>
+ <property name="column-span">2</property>
+ <property name="row-span">2</property>
+ </layout>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImage" id="target4">
+ <property name="name">target</property>
+ <property name="width_request">100</property>
+ <property name="height_request">100</property>
+ <property name="sensitive">0</property>
+ <layout>
+ <property name="column">6</property>
+ <property name="row">6</property>
+ <property name="column-span">2</property>
+ <property name="row-span">2</property>
+ </layout>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImage" id="success_page">
+ <property name="pixel_size">300</property>
+ <property name="icon_name">emblem-ok-symbolic</property>
+ </object>
+ </child>
+ </object>
+ </property>
+ </template>
+ <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..26afd81
--- /dev/null
+++ b/panels/wacom/calibrator/cc-clock.c
@@ -0,0 +1,289 @@
+/*
+ * 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 void
+cc_clock_snapshot (GtkWidget *widget,
+ GtkSnapshot *snapshot)
+{
+ GtkAllocation allocation;
+ cairo_t *cr;
+ gdouble angle;
+
+ gtk_widget_get_allocation (widget, &allocation);
+ angle = cc_clock_get_angle (CC_CLOCK (widget));
+
+ cr = gtk_snapshot_append_cairo (snapshot,
+ &GRAPHENE_RECT_INIT (0, 0, allocation.width, allocation.height));
+
+ /* 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);
+}
+
+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_measure (GtkWidget *widget,
+ GtkOrientation orientation,
+ gint for_size,
+ gint *minimum,
+ gint *natural,
+ gint *minimum_baseline,
+ gint *natural_baseline)
+{
+ 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->snapshot = cc_clock_snapshot;
+ widget_class->measure = cc_clock_measure;
+
+ 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)
+{
+}
+
+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..71421c7
--- /dev/null
+++ b/panels/wacom/calibrator/main.c
@@ -0,0 +1,421 @@
+/*
+ * 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"
+
+static GMainLoop *mainloop = NULL;
+
+/**
+ * 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 (CcCalibArea *area,
+ gpointer user_data)
+{
+ gboolean success;
+ XYinfo axis;
+ gboolean swap_xy;
+
+ success = cc_calib_area_finish (area);
+ if (success)
+ {
+ cc_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");
+
+ g_main_loop_quit (mainloop);
+}
+
+int main(int argc, char** argv)
+{
+
+ struct Calib* calibrator = main_common(argc, argv);
+ CcCalibArea *calib_area;
+
+ bindtextdomain (GETTEXT_PACKAGE, GNOMELOCALEDIR);
+ bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
+ textdomain (GETTEXT_PACKAGE);
+
+ gtk_init ();
+
+ g_setenv ("G_MESSAGES_DEBUG", "all", TRUE);
+
+ calib_area = cc_calib_area_new (NULL,
+ NULL, /* monitor */
+ NULL, /* NULL to accept input from any device */
+ calibration_finished_cb,
+ NULL,
+ calibrator->threshold_doubleclick,
+ calibrator->threshold_misclick);
+
+ mainloop = g_main_loop_new (NULL, FALSE);
+ g_main_loop_run (mainloop);
+
+ cc_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..3f570ad
--- /dev/null
+++ b/panels/wacom/cc-drawing-area.c
@@ -0,0 +1,182 @@
+/*
+ * 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 {
+ GtkDrawingArea parent;
+ GtkGesture *stylus_gesture;
+ cairo_surface_t *surface;
+ cairo_t *cr;
+};
+
+G_DEFINE_TYPE (CcDrawingArea, cc_drawing_area, GTK_TYPE_DRAWING_AREA)
+
+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_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 void
+draw_cb (GtkDrawingArea *drawing_area,
+ cairo_t *cr,
+ gint width,
+ gint height,
+ gpointer user_data)
+{
+ CcDrawingArea *area = CC_DRAWING_AREA (drawing_area);
+
+ ensure_drawing_surface (area, width, height);
+
+ 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, width, height);
+ cairo_stroke (cr);
+}
+
+static void
+stylus_down_cb (GtkGestureStylus *gesture,
+ double x,
+ double y,
+ CcDrawingArea *area)
+{
+ cairo_new_path (area->cr);
+}
+
+static void
+stylus_motion_cb (GtkGestureStylus *gesture,
+ double x,
+ double y,
+ CcDrawingArea *area)
+{
+ GdkDeviceTool *tool;
+ gdouble pressure;
+
+ tool = gtk_gesture_stylus_get_device_tool (gesture);
+ gtk_gesture_stylus_get_axis (gesture,
+ 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 (GTK_WIDGET (area));
+}
+
+static void
+cc_drawing_area_class_init (CcDrawingAreaClass *klass)
+{
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ widget_class->map = cc_drawing_area_map;
+ widget_class->unmap = cc_drawing_area_unmap;
+}
+
+static void
+cc_drawing_area_init (CcDrawingArea *area)
+{
+ gtk_drawing_area_set_draw_func (GTK_DRAWING_AREA (area), draw_cb, NULL, NULL);
+ area->stylus_gesture = gtk_gesture_stylus_new ();
+ g_signal_connect (area->stylus_gesture, "down",
+ G_CALLBACK (stylus_down_cb), area);
+ g_signal_connect (area->stylus_gesture, "motion",
+ G_CALLBACK (stylus_motion_cb), area);
+ gtk_widget_add_controller (GTK_WIDGET (area),
+ GTK_EVENT_CONTROLLER (area->stylus_gesture));
+}
+
+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..1d3d6ba
--- /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, GtkDrawingArea)
+
+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..3c8536b
--- /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_list_box_row_set_child (GTK_LIST_BOX_ROW (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..695b85b
--- /dev/null
+++ b/panels/wacom/cc-wacom-device.c
@@ -0,0 +1,437 @@
+/*
+ * 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"
+
+#include <glib/gi18n.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;
+ WacomError *wacom_error;
+ const gchar *node_path;
+
+ wacom_db = cc_wacom_device_database_get ();
+ node_path = gsd_device_get_device_file (device->device);
+ wacom_error = libwacom_error_new ();
+ device->wdevice = libwacom_new_from_path (wacom_db, node_path, FALSE, wacom_error);
+
+ if (!device->wdevice) {
+ g_debug ("libwacom_new_from_path() failed: %s", libwacom_error_get_message (wacom_error));
+ libwacom_error_free (&wacom_error);
+ g_set_error (error, 0, 0, "Tablet description not found");
+ return FALSE;
+ }
+ libwacom_error_free (&wacom_error);
+
+ 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;
+ WacomError *wacom_error;
+
+ device = g_object_new (CC_TYPE_WACOM_DEVICE,
+ NULL);
+
+ wacom_error = libwacom_error_new ();
+ wacom_device = libwacom_new_from_name (cc_wacom_device_database_get(),
+ name, wacom_error);
+ if (wacom_device == NULL) {
+ g_debug ("libwacom_new_fake() failed: %s", libwacom_error_get_message (wacom_error));
+ libwacom_error_free (&wacom_error);
+ return NULL;
+ }
+ libwacom_error_free (&wacom_error);
+
+ 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_RR_IS_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;
+}
+
+const gchar *
+cc_wacom_device_get_description (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 _("Tablet mounted on laptop panel");
+ } else if (integration_flags & WACOM_DEVICE_INTEGRATED_DISPLAY) {
+ return _("Tablet mounted on external display");
+ } else {
+ return _("External tablet device");
+ }
+}
diff --git a/panels/wacom/cc-wacom-device.h b/panels/wacom/cc-wacom-device.h
new file mode 100644
index 0000000..63d38fb
--- /dev/null
+++ b/panels/wacom/cc-wacom-device.h
@@ -0,0 +1,66 @@
+/*
+ * 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 <gnome-rr/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);
+
+const gchar * cc_wacom_device_get_description (CcWacomDevice *device);
+
diff --git a/panels/wacom/cc-wacom-ekr-page.c b/panels/wacom/cc-wacom-ekr-page.c
new file mode 100644
index 0000000..fb2f3e3
--- /dev/null
+++ b/panels/wacom/cc-wacom-ekr-page.c
@@ -0,0 +1,195 @@
+/*
+ * Copyright © 2022 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-ekr-page.h"
+#include "cc-wacom-panel.h"
+
+struct _CcWacomEkrPage
+{
+ GtkBox parent_instance;
+ GtkWidget *ekr_section;
+ GtkWidget *ekr_icon;
+ GtkWidget *ekr_map_buttons;
+ CcWacomPanel *panel;
+ CcWacomDevice *device;
+};
+
+enum {
+ PROP_0,
+ PROP_PANEL,
+ PROP_DEVICE,
+ N_PROPS,
+};
+
+static GParamSpec *props[N_PROPS] = { 0, };
+
+G_DEFINE_TYPE (CcWacomEkrPage, cc_wacom_ekr_page, GTK_TYPE_BOX)
+
+static void
+set_osd_visibility_cb (GObject *source_object,
+ GAsyncResult *res,
+ gpointer data)
+{
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GVariant) result = NULL;
+
+ result = g_dbus_proxy_call_finish (G_DBUS_PROXY (source_object), res, &error);
+
+ if (error && !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ g_warning ("Error invoking pad button mapping OSK: %s\n", error->message);
+}
+
+static void
+set_osd_visibility (CcWacomEkrPage *page)
+{
+ GDBusProxy *proxy;
+ GsdDevice *gsd_device;
+ const gchar *device_path;
+
+ proxy = cc_wacom_panel_get_gsd_wacom_bus_proxy (page->panel);
+
+ if (proxy == NULL) {
+ g_warning ("Wacom D-Bus interface is not available");
+ return;
+ }
+
+ gsd_device = cc_wacom_device_get_device (page->device);
+ device_path = gsd_device_get_device_file (gsd_device);
+
+ g_dbus_proxy_call (proxy,
+ "Show",
+ g_variant_new ("(ob)", device_path, TRUE),
+ G_DBUS_CALL_FLAGS_NONE,
+ -1,
+ NULL,
+ set_osd_visibility_cb,
+ page);
+}
+
+static void
+on_map_buttons_activated (CcWacomEkrPage *self)
+{
+ set_osd_visibility (self);
+}
+
+static void
+cc_wacom_ekr_page_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ CcWacomEkrPage *page = CC_WACOM_EKR_PAGE (object);
+
+ switch (prop_id) {
+ case PROP_PANEL:
+ g_value_set_object (value, page->panel);
+ break;
+ case PROP_DEVICE:
+ g_value_set_object (value, page->device);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+cc_wacom_ekr_page_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ CcWacomEkrPage *page = CC_WACOM_EKR_PAGE (object);
+
+ switch (prop_id) {
+ case PROP_PANEL:
+ page->panel = g_value_get_object (value);
+ break;
+ case PROP_DEVICE:
+ page->device = g_value_get_object (value);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+cc_wacom_ekr_page_constructed (GObject *object)
+{
+ CcWacomEkrPage *page = CC_WACOM_EKR_PAGE (object);
+
+ G_OBJECT_CLASS (cc_wacom_ekr_page_parent_class)->constructed (object);
+
+ adw_preferences_group_set_title (ADW_PREFERENCES_GROUP (page->ekr_section),
+ cc_wacom_device_get_name (page->device));
+}
+
+static void
+cc_wacom_ekr_page_class_init (CcWacomEkrPageClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->get_property = cc_wacom_ekr_page_get_property;
+ object_class->set_property = cc_wacom_ekr_page_set_property;
+ object_class->constructed = cc_wacom_ekr_page_constructed;
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/control-center/wacom/cc-wacom-ekr-page.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, CcWacomEkrPage, ekr_section);
+ gtk_widget_class_bind_template_child (widget_class, CcWacomEkrPage, ekr_icon);
+ gtk_widget_class_bind_template_child (widget_class, CcWacomEkrPage, ekr_map_buttons);
+
+ gtk_widget_class_bind_template_callback (widget_class, on_map_buttons_activated);
+
+ props[PROP_PANEL] = g_param_spec_object ("panel",
+ "panel",
+ "panel",
+ CC_TYPE_WACOM_PANEL,
+ 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 void
+cc_wacom_ekr_page_init (CcWacomEkrPage *page)
+{
+ gtk_widget_init_template (GTK_WIDGET (page));
+}
+
+GtkWidget *
+cc_wacom_ekr_page_new (CcWacomPanel *panel,
+ CcWacomDevice *ekr)
+{
+ return g_object_new (CC_TYPE_WACOM_EKR_PAGE,
+ "panel", panel,
+ "device", ekr,
+ NULL);
+}
diff --git a/panels/wacom/cc-wacom-ekr-page.h b/panels/wacom/cc-wacom-ekr-page.h
new file mode 100644
index 0000000..f8944a1
--- /dev/null
+++ b/panels/wacom/cc-wacom-ekr-page.h
@@ -0,0 +1,34 @@
+/*
+ * Copyright © 2022 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 <gtk/gtk.h>
+#include "cc-wacom-panel.h"
+#include "cc-wacom-device.h"
+
+G_BEGIN_DECLS
+
+#define CC_TYPE_WACOM_EKR_PAGE (cc_wacom_ekr_page_get_type ())
+G_DECLARE_FINAL_TYPE (CcWacomEkrPage, cc_wacom_ekr_page, CC, WACOM_EKR_PAGE, GtkBox)
+
+GtkWidget * cc_wacom_ekr_page_new (CcWacomPanel *panel,
+ CcWacomDevice *stylus);
+
+G_END_DECLS
diff --git a/panels/wacom/cc-wacom-ekr-page.ui b/panels/wacom/cc-wacom-ekr-page.ui
new file mode 100644
index 0000000..1d1e403
--- /dev/null
+++ b/panels/wacom/cc-wacom-ekr-page.ui
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="CcWacomEkrPage" parent="GtkBox">
+ <property name="orientation">vertical</property>
+ <property name="spacing">24</property>
+ <child>
+ <object class="AdwPreferencesGroup" id="ekr_section">
+ <property name="description" translatable="yes"
+ comments="translators: this is a drawing tablet pad, i.e. a collection of buttons and knobs">External pad device</property>
+ <property name="header-suffix">
+ <object class="GtkPicture" id="ekr_icon">
+ <property name="halign">end</property>
+ <property name="valign">start</property>
+ <property name="file">resource:///org/gnome/control-center/wacom/wacom-tablet.svg</property>
+ </object>
+ </property>
+ <child>
+ <object class="AdwActionRow" id="ekr_map_buttons">
+ <property name="title" translatable="yes">Map Buttons</property>
+ <property name="activatable">True</property>
+ <signal name="activated" handler="on_map_buttons_activated" object="CcWacomEkrPage" swapped="yes" />
+ <child type="suffix">
+ <object class="GtkImage">
+ <property name="icon-name">go-next-symbolic</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/panels/wacom/cc-wacom-page.c b/panels/wacom/cc-wacom-page.c
new file mode 100644
index 0000000..696d71d
--- /dev/null
+++ b/panels/wacom/cc-wacom-page.c
@@ -0,0 +1,865 @@
+/*
+ * 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/x11/gdkx.h>
+#endif
+#ifdef GDK_WINDOWING_WAYLAND
+#include <gdk/wayland/gdkwayland.h>
+#endif
+
+#include "cc-wacom-device.h"
+#include "cc-wacom-button-row.h"
+#include "cc-wacom-page.h"
+#include "cc-wacom-stylus-page.h"
+#include "gsd-enums.h"
+#include "calibrator-gui.h"
+#include "gsd-input-helper.h"
+
+#include <string.h>
+
+#define MWID(x) (GtkWidget *) gtk_builder_get_object (page->mapping_builder, x)
+
+#define THRESHOLD_MISCLICK 15
+#define THRESHOLD_DOUBLECLICK 7
+
+struct _CcWacomPage
+{
+ GtkBox parent_instance;
+
+ CcWacomPanel *panel;
+ CcWacomDevice *stylus;
+ GList *pads;
+ CcCalibArea *area;
+ GSettings *wacom_settings;
+
+ GtkWidget *tablet_section;
+ GtkWidget *tablet_icon;
+ GtkWidget *tablet_display;
+ GtkWidget *tablet_calibrate;
+ GtkWidget *tablet_map_buttons;
+ GtkWidget *tablet_mode;
+ GtkWidget *tablet_mode_switch;
+ GtkWidget *tablet_left_handed;
+ GtkWidget *tablet_left_handed_switch;
+ GtkWidget *tablet_aspect_ratio;
+ GtkWidget *tablet_aspect_ratio_switch;
+ GtkWidget *display_section;
+
+ GnomeRRScreen *rr_screen;
+
+ /* Button mapping */
+ GtkBuilder *mapping_builder;
+ GtkWindow *button_map;
+ GtkListStore *action_store;
+
+ GCancellable *cancellable;
+
+ /* To reach other grouped devices */
+ GsdDeviceManager *manager;
+};
+
+G_DEFINE_TYPE (CcWacomPage, cc_wacom_page, GTK_TYPE_BOX)
+
+/* 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 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,
+ 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)",
+ cal[0], cal[1], cal[2], cal[3]);
+}
+
+static void
+finish_calibration (CcCalibArea *area,
+ gpointer user_data)
+{
+ CcWacomPage *page = (CcWacomPage *) user_data;
+ XYinfo axis;
+ gdouble cal[4];
+
+ if (cc_calib_area_finish (area)) {
+ cc_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;
+
+ set_calibration (page->stylus,
+ 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);
+ }
+
+ cc_calib_area_free (area);
+ page->area = NULL;
+ gtk_widget_set_sensitive (page->tablet_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_devices (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)
+{
+ g_assert (page->area == NULL);
+
+ page->area = cc_calib_area_new (NULL,
+ 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 GdkMonitor *
+find_monitor_at_point (GdkDisplay *display,
+ gint x,
+ gint y)
+{
+ GListModel *monitors;
+ int i;
+
+ monitors = gdk_display_get_monitors (display);
+
+ for (i = 0; i < g_list_model_get_n_items (monitors); i++) {
+ g_autoptr(GdkMonitor) m = g_list_model_get_item (monitors, i);
+ GdkRectangle geometry;
+
+ gdk_monitor_get_geometry (m, &geometry);
+ if (gdk_rectangle_contains_point (&geometry, x, y))
+ return g_steal_pointer (&m);
+ }
+
+ return NULL;
+}
+
+static void
+calibrate (CcWacomPage *page)
+{
+ int i;
+ GVariant *old_calibration, *array;
+ g_autofree GVariant **tmp = NULL;
+ g_autofree gdouble *calibration = NULL;
+ gsize ncal;
+ GdkDisplay *display;
+ g_autoptr(GdkMonitor) monitor = NULL;
+ g_autoptr(GnomeRRScreen) rr_screen = NULL;
+ GnomeRROutput *output;
+ g_autoptr(GError) error = NULL;
+ GDBusProxy *input_mapping_proxy;
+ gint x, y;
+
+ display = gdk_display_get_default ();
+ rr_screen = gnome_rr_screen_new (display, &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);
+ input_mapping_proxy = cc_wacom_panel_get_input_mapping_bus_proxy (page->panel);
+
+ if (output) {
+ gnome_rr_output_get_position (output, &x, &y);
+ monitor = find_monitor_at_point (display, x, y);
+ } else if (input_mapping_proxy) {
+ GsdDevice *gsd_device;
+ GVariant *mapping;
+
+ gsd_device = cc_wacom_device_get_device (page->stylus);
+
+ if (gsd_device) {
+ mapping = g_dbus_proxy_call_sync (input_mapping_proxy,
+ "GetDeviceMapping",
+ g_variant_new ("(o)", gsd_device_get_device_file (gsd_device)),
+ G_DBUS_CALL_FLAGS_NONE,
+ -1,
+ NULL,
+ NULL);
+ if (mapping) {
+ gint x, y, width, height;
+
+ g_variant_get (mapping, "((iiii))", &x, &y, &width, &height);
+ monitor = find_monitor_at_point (display, 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. Calibration may appear in wrong monitor.");
+ }
+
+ 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 (page->tablet_calibrate, FALSE);
+}
+
+static void
+on_calibrate_activated (CcWacomPage *self)
+{
+ calibrate (self);
+}
+
+/* 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)
+{
+ gtk_list_box_append (GTK_LIST_BOX (list_box),
+ cc_wacom_button_row_new (button, settings));
+}
+
+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_window_destroy (GTK_WINDOW (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 (gtk_widget_get_native (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 = GTK_WINDOW (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
+on_map_buttons_activated (CcWacomPage *self)
+{
+ set_osd_visibility (self);
+}
+
+static void
+on_display_selected (GtkWidget *widget,
+ GParamSpec *pspec,
+ CcWacomPage *page)
+{
+ GListModel *list;
+ g_autoptr (GObject) obj = NULL;
+ GVariant *variant;
+ gint idx;
+
+ list = adw_combo_row_get_model (ADW_COMBO_ROW (widget));
+ idx = adw_combo_row_get_selected (ADW_COMBO_ROW (widget));
+ obj = g_list_model_get_item (list, idx);
+
+ variant = g_object_get_data (obj, "value-output");
+
+ if (variant)
+ g_settings_set_value (page->wacom_settings, "output", g_variant_ref (variant));
+ else
+ g_settings_reset (page->wacom_settings, "output");
+
+ gtk_widget_set_sensitive (page->tablet_calibrate, variant == NULL);
+}
+
+/* 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, cc_calib_area_free);
+ g_clear_pointer (&self->button_map, gtk_window_destroy);
+ g_list_free_full (self->pads, g_object_unref);
+ g_clear_object (&self->rr_screen);
+ 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);
+ GtkWidgetClass *widget_class = GTK_WIDGET_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;
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/control-center/wacom/cc-wacom-page.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, CcWacomPage, tablet_section);
+ gtk_widget_class_bind_template_child (widget_class, CcWacomPage, tablet_icon);
+ gtk_widget_class_bind_template_child (widget_class, CcWacomPage, tablet_display);
+ gtk_widget_class_bind_template_child (widget_class, CcWacomPage, tablet_calibrate);
+ gtk_widget_class_bind_template_child (widget_class, CcWacomPage, tablet_map_buttons);
+ gtk_widget_class_bind_template_child (widget_class, CcWacomPage, tablet_mode);
+ gtk_widget_class_bind_template_child (widget_class, CcWacomPage, tablet_mode_switch);
+ gtk_widget_class_bind_template_child (widget_class, CcWacomPage, tablet_left_handed);
+ gtk_widget_class_bind_template_child (widget_class, CcWacomPage, tablet_left_handed_switch);
+ gtk_widget_class_bind_template_child (widget_class, CcWacomPage, tablet_aspect_ratio);
+ gtk_widget_class_bind_template_child (widget_class, CcWacomPage, tablet_aspect_ratio_switch);
+ gtk_widget_class_bind_template_child (widget_class, CcWacomPage, display_section);
+
+ gtk_widget_class_bind_template_callback (widget_class, on_map_buttons_activated);
+ gtk_widget_class_bind_template_callback (widget_class, on_calibrate_activated);
+ gtk_widget_class_bind_template_callback (widget_class, on_display_selected);
+}
+
+static void
+update_displays_model (CcWacomPage *page)
+{
+ g_autoptr (GtkStringList) list = NULL;
+ GnomeRROutput **outputs, *cur_output;
+ int i, idx = 0, cur = -1, automatic_item = -1;
+ g_autoptr (GObject) obj = NULL;
+ GVariant *variant;
+
+ outputs = gnome_rr_screen_list_outputs (page->rr_screen);
+ list = gtk_string_list_new (NULL);
+ cur_output = cc_wacom_device_get_output (page->stylus,
+ page->rr_screen);
+
+ for (i = 0; outputs[i] != NULL; i++) {
+ GnomeRROutput *output = outputs[i];
+ GnomeRRCrtc *crtc = gnome_rr_output_get_crtc (output);
+ g_autofree gchar *text = NULL;
+ g_autofree gchar *vendor = NULL;
+ g_autofree gchar *product = NULL;
+ g_autofree gchar *serial = NULL;
+ const gchar *name, *disp_name;
+
+ /* Output is turned on? */
+ if (!crtc || gnome_rr_crtc_get_current_mode (crtc) == NULL)
+ continue;
+
+ if (output == cur_output)
+ cur = idx;
+
+ 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);
+
+ gnome_rr_output_get_ids_from_edid (output,
+ &vendor,
+ &product,
+ &serial);
+ variant = g_variant_new_strv ((const gchar *[]) { vendor, product, serial }, 3);
+
+ gtk_string_list_append (list, text);
+ obj = g_list_model_get_item (G_LIST_MODEL (list), idx);
+ g_object_set_data_full (G_OBJECT (obj), "value-output",
+ variant, (GDestroyNotify) g_variant_unref);
+ idx++;
+ }
+
+ /* All displays item */
+ gtk_string_list_append (list, _("All Displays"));
+ variant = g_variant_new_strv ((const gchar *[]) { "", "", "" }, 3);
+ obj = g_list_model_get_item (G_LIST_MODEL (list), idx);
+ g_object_set_data_full (G_OBJECT (obj), "value-output",
+ variant, (GDestroyNotify) g_variant_unref);
+ if (cur_output == NULL)
+ cur = idx;
+
+ /* "Automatic" item */
+ if (get_layout_type (page->stylus) == LAYOUT_SCREEN) {
+ g_autoptr (GVariant) user_value = NULL;
+
+ idx++;
+ gtk_string_list_append (list, _("Automatic"));
+ automatic_item = idx;
+
+ user_value = g_settings_get_user_value (page->wacom_settings, "output");
+ if (!user_value)
+ cur = idx;
+ }
+
+ g_signal_handlers_block_by_func (page->tablet_display, on_display_selected, page);
+ adw_combo_row_set_model (ADW_COMBO_ROW (page->tablet_display), G_LIST_MODEL (list));
+ adw_combo_row_set_selected (ADW_COMBO_ROW (page->tablet_display), cur);
+ g_signal_handlers_unblock_by_func (page->tablet_display, on_display_selected, page);
+
+ gtk_widget_set_sensitive (page->tablet_calibrate, cur == automatic_item);
+}
+
+static void
+cc_wacom_page_init (CcWacomPage *page)
+{
+ g_autoptr (GError) error = NULL;
+
+ gtk_widget_init_template (GTK_WIDGET (page));
+ page->rr_screen = gnome_rr_screen_new (gdk_display_get_default (), &error);
+
+ if (error)
+ g_warning ("Could not get RR screen: %s", error->message);
+
+ g_signal_connect_object (page->rr_screen, "changed",
+ G_CALLBACK (update_displays_model),
+ page, G_CONNECT_SWAPPED);
+}
+
+static void
+set_icon_name (CcWacomPage *page,
+ GtkWidget *widget,
+ const char *icon_name)
+{
+ g_autofree gchar *resource = NULL;
+
+ resource = g_strdup_printf ("/org/gnome/control-center/wacom/%s.svg", icon_name);
+ gtk_picture_set_resource (GTK_PICTURE (widget), resource);
+}
+
+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
+update_pad_availability (CcWacomPage *page)
+{
+ gtk_widget_set_visible (page->tablet_map_buttons, 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);
+}
+
+static GVariant *
+tablet_mode_bind_set (const GValue *value,
+ const GVariantType *expected_type,
+ gpointer user_data)
+{
+ gboolean setting;
+
+ setting = g_value_get_boolean (value);
+
+ return g_variant_new_string (setting ? "absolute" : "relative");
+}
+
+static gboolean
+tablet_mode_bind_get (GValue *value,
+ GVariant *variant,
+ gpointer user_data)
+{
+ g_value_set_boolean (value,
+ g_strcmp0 (g_variant_get_string (variant, NULL),
+ "absolute") == 0);
+ return TRUE;
+}
+
+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;
+
+ gtk_widget_set_visible (page->tablet_left_handed,
+ get_layout_type (stylus) == LAYOUT_REVERSIBLE);
+ gtk_widget_set_visible (page->tablet_calibrate,
+ get_layout_type (stylus) == LAYOUT_SCREEN);
+
+ /* FIXME move this to construct */
+ page->wacom_settings = cc_wacom_device_get_settings (stylus);
+
+ /* Tablet name */
+ adw_preferences_group_set_title (ADW_PREFERENCES_GROUP (page->tablet_section),
+ cc_wacom_device_get_name (stylus));
+ adw_preferences_group_set_description (ADW_PREFERENCES_GROUP (page->tablet_section),
+ cc_wacom_device_get_description (stylus));
+
+ g_settings_bind_with_mapping (page->wacom_settings, "mapping",
+ page->tablet_mode_switch, "active",
+ G_SETTINGS_BIND_DEFAULT,
+ tablet_mode_bind_get,
+ tablet_mode_bind_set,
+ NULL, NULL);
+ g_settings_bind_with_mapping (page->wacom_settings, "mapping",
+ page->display_section, "sensitive",
+ G_SETTINGS_BIND_DEFAULT,
+ tablet_mode_bind_get,
+ tablet_mode_bind_set,
+ NULL, NULL);
+ g_settings_bind (page->wacom_settings, "left-handed",
+ page->tablet_left_handed_switch, "active",
+ G_SETTINGS_BIND_DEFAULT);
+ g_settings_bind (page->wacom_settings, "keep-aspect",
+ page->tablet_aspect_ratio_switch, "active",
+ G_SETTINGS_BIND_DEFAULT);
+
+ /* Tablet icon */
+ set_icon_name (page, page->tablet_icon, 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);
+
+ update_pad_availability (page);
+ update_displays_model (page);
+
+ return GTK_WIDGET (page);
+}
+
+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..76a16ec
--- /dev/null
+++ b/panels/wacom/cc-wacom-page.h
@@ -0,0 +1,39 @@
+/*
+ * 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_calibrate (CcWacomPage *page);
+
+gboolean cc_wacom_page_can_calibrate (CcWacomPage *page);
+
+G_END_DECLS
diff --git a/panels/wacom/cc-wacom-page.ui b/panels/wacom/cc-wacom-page.ui
new file mode 100644
index 0000000..327e5a9
--- /dev/null
+++ b/panels/wacom/cc-wacom-page.ui
@@ -0,0 +1,89 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="CcWacomPage" parent="GtkBox">
+ <property name="orientation">vertical</property>
+ <property name="spacing">24</property>
+ <child>
+ <object class="AdwPreferencesGroup" id="tablet_section">
+ <property name="header-suffix">
+ <object class="GtkPicture" id="tablet_icon">
+ <property name="halign">end</property>
+ <property name="valign">start</property>
+ </object>
+ </property>
+ <child>
+ <object class="AdwActionRow" id="tablet_mode">
+ <property name="title" translatable="yes">Tablet Mode</property>
+ <property name="subtitle" translatable="yes">Use absolute positioning for the pen</property>
+ <property name="activatable_widget">tablet_mode_switch</property>
+ <child>
+ <object class="GtkSwitch" id="tablet_mode_switch">
+ <property name="valign">center</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="AdwActionRow" id="tablet_left_handed">
+ <property name="title" translatable="yes">Left Hand Orientation</property>
+ <property name="subtitle" translatable="yes">Tablet and Express Keys™ are rotated for left hand use</property>
+ <property name="activatable_widget">tablet_left_handed_switch</property>
+ <child>
+ <object class="GtkSwitch" id="tablet_left_handed_switch">
+ <property name="valign">center</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="AdwActionRow" id="tablet_map_buttons">
+ <property name="title" translatable="yes">Map Buttons</property>
+ <property name="activatable">True</property>
+ <signal name="activated" handler="on_map_buttons_activated" object="CcWacomPage" swapped="yes" />
+ <child type="suffix">
+ <object class="GtkImage">
+ <property name="icon-name">go-next-symbolic</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+
+ <child>
+ <object class="AdwPreferencesGroup" id="display_section">
+ <child>
+ <object class="AdwComboRow" id="tablet_display">
+ <property name="width_request">100</property>
+ <property name="title" translatable="yes" context="display setting">Map to Monitor</property>
+ <signal name="notify::selected-item" handler="on_display_selected" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="AdwActionRow" id="tablet_aspect_ratio">
+ <property name="title" translatable="yes">Keep Aspect Ratio</property>
+ <property name="subtitle" translatable="yes">Only use a portion of the tablet surface to keep monitor aspect ratio</property>
+ <property name="activatable_widget">tablet_aspect_ratio_switch</property>
+ <child>
+ <object class="GtkSwitch" id="tablet_aspect_ratio_switch">
+ <property name="valign">center</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="AdwActionRow" id="tablet_calibrate">
+ <property name="title" translatable="yes">Calibrate</property>
+ <property name="activatable">True</property>
+ <signal name="activated" handler="on_calibrate_activated" object="CcWacomPage" swapped="yes" />
+ <child type="suffix">
+ <object class="GtkImage">
+ <property name="icon-name">go-next-symbolic</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/panels/wacom/cc-wacom-panel.c b/panels/wacom/cc-wacom-panel.c
new file mode 100644
index 0000000..36aca06
--- /dev/null
+++ b/panels/wacom/cc-wacom-panel.c
@@ -0,0 +1,753 @@
+/*
+ * 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-ekr-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/wayland/gdkwayland.h>
+#endif
+
+#define EKR_VENDOR "056a"
+#define EKR_PRODUCT "0331"
+
+struct _CcWacomPanel
+{
+ CcPanel parent_instance;
+
+ GtkWidget *test_popover;
+ GtkWidget *test_draw_area;
+ GtkWidget *test_button;
+ GtkWidget *scrollable;
+ GtkWidget *tablets;
+ GtkWidget *styli;
+ GtkWidget *initial_state_stack;
+ GtkWidget *panel_view;
+ GtkWidget *panel_empty_state;
+ GHashTable *devices; /* key=GsdDevice, value=CcWacomDevice */
+ GHashTable *pages; /* key=CcWacomDevice, value=GtkWidget */
+ GHashTable *stylus_pages; /* key=CcWacomTool, value=GtkWidget */
+ guint mock_stylus_id;
+
+ CcTabletToolMap *tablet_tool_map;
+
+ GtkAdjustment *vadjustment;
+ GtkGesture *stylus_gesture;
+
+ GtkWidget *highlighted_widget;
+
+ /* DBus */
+ GDBusProxy *proxy;
+ GDBusProxy *input_mapping_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 void
+highlight_widget (CcWacomPanel *self, GtkWidget *widget)
+{
+ double y;
+
+ if (self->highlighted_widget == widget)
+ return;
+
+ gtk_widget_translate_coordinates (widget,
+ self->scrollable,
+ 0, 0,
+ NULL, &y);
+ gtk_adjustment_set_value (self->vadjustment, y);
+ self->highlighted_widget = widget;
+}
+
+static CcWacomPage *
+update_highlighted_device (CcWacomPanel *self, const gchar *device_name)
+{
+ CcWacomPage *page;
+ CcWacomDevice *wacom_device;
+
+ 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);
+ highlight_widget (self, GTK_WIDGET (page));
+
+ 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 = update_highlighted_device (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:
+ update_highlighted_device (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);
+ CcShell *shell;
+
+ shell = cc_panel_get_shell (CC_PANEL (self));
+ if (shell) {
+ gtk_widget_remove_controller (GTK_WIDGET (shell),
+ GTK_EVENT_CONTROLLER (self->stylus_gesture));
+ }
+
+
+ g_clear_pointer (&self->devices, g_hash_table_unref);
+ g_clear_object (&self->proxy);
+ g_clear_object (&self->input_mapping_proxy);
+ g_clear_pointer (&self->pages, g_hash_table_unref);
+ g_clear_pointer (&self->stylus_pages, g_hash_table_unref);
+ g_clear_handle_id (&self->mock_stylus_id, g_source_remove);
+
+ 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_box_remove (GTK_BOX (self->styli), 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);
+ gtk_box_append (GTK_BOX (self->styli), page);
+ g_hash_table_insert (self->stylus_pages, tool, page);
+
+ return TRUE;
+}
+
+static void
+update_test_button (CcWacomPanel *self)
+{
+ if (!self->test_button)
+ return;
+
+ if (g_hash_table_size (self->devices) == 0) {
+ gtk_popover_popdown (GTK_POPOVER (self->test_popover));
+ gtk_widget_set_sensitive (self->test_button, FALSE);
+ } else {
+ gtk_widget_set_sensitive (self->test_button, TRUE);
+ }
+}
+
+static void
+update_initial_state (CcWacomPanel *self)
+{
+ gtk_stack_set_visible_child (GTK_STACK (self->initial_state_stack),
+ g_hash_table_size (self->devices) == 0 ?
+ self->panel_empty_state :
+ self->panel_view);
+}
+
+static void
+update_highlighted_stylus (CcWacomPanel *panel,
+ CcWacomTool *stylus)
+{
+ GtkWidget *widget;
+
+ widget = g_hash_table_lookup (panel->stylus_pages, stylus);
+ highlight_widget (panel, widget);
+}
+
+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;
+
+ 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;
+ }
+
+ add_stylus (panel, stylus);
+
+ update_highlighted_stylus (panel, stylus);
+
+ cc_tablet_tool_map_add_relation (panel->tablet_tool_map,
+ wacom_device, stylus);
+}
+
+static void
+on_stylus_proximity_cb (GtkGestureStylus *gesture,
+ double x,
+ double y,
+ CcWacomPanel *panel)
+{
+ GdkDevice *device;
+ GdkDeviceTool *tool;
+
+ device = gtk_event_controller_get_current_event_device (GTK_EVENT_CONTROLLER (gesture));
+ tool = gtk_gesture_stylus_get_device_tool (gesture);
+ update_current_tool (panel, device, tool);
+}
+
+static gboolean
+show_mock_stylus_cb (gpointer user_data)
+{
+ CcWacomPanel *panel = user_data;
+ GList *device_list;
+ CcWacomDevice *wacom_device;
+ CcWacomTool *stylus;
+
+ panel->mock_stylus_id = 0;
+
+ device_list = g_hash_table_get_values (panel->devices);
+ if (device_list == NULL) {
+ g_warning ("Could not create fake stylus event because could not find tablet device");
+ return G_SOURCE_REMOVE;
+ }
+
+ wacom_device = device_list->data;
+ g_list_free (device_list);
+
+ stylus = cc_wacom_tool_new (0, 0, wacom_device);
+ add_stylus (panel, stylus);
+ update_highlighted_stylus (panel, stylus);
+ cc_tablet_tool_map_add_relation (panel->tablet_tool_map,
+ wacom_device, stylus);
+
+ return G_SOURCE_REMOVE;
+}
+
+static void
+cc_wacom_panel_constructed (GObject *object)
+{
+ CcWacomPanel *self = CC_WACOM_PANEL (object);
+ 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));
+
+ self->stylus_gesture = gtk_gesture_stylus_new ();
+ g_signal_connect (self->stylus_gesture, "proximity",
+ G_CALLBACK (on_stylus_proximity_cb), self);
+ gtk_widget_add_controller (GTK_WIDGET (shell),
+ GTK_EVENT_CONTROLLER (self->stylus_gesture));
+
+ if (g_getenv ("UMOCKDEV_DIR") != NULL)
+ self->mock_stylus_id = g_idle_add (show_mock_stylus_cb, self);
+}
+
+static const char *
+cc_wacom_panel_get_help_uri (CcPanel *panel)
+{
+ return "help:gnome-help/wacom";
+}
+
+static void
+cc_wacom_panel_class_init (CcWacomPanelClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ CcPanelClass *panel_class = CC_PANEL_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_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;
+
+ g_object_class_override_property (object_class, PROP_PARAMETERS, "parameters");
+
+ g_type_ensure (CC_TYPE_DRAWING_AREA);
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/control-center/wacom/cc-wacom-panel.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, CcWacomPanel, scrollable);
+ gtk_widget_class_bind_template_child (widget_class, CcWacomPanel, test_button);
+ gtk_widget_class_bind_template_child (widget_class, CcWacomPanel, test_popover);
+ gtk_widget_class_bind_template_child (widget_class, CcWacomPanel, test_draw_area);
+ gtk_widget_class_bind_template_child (widget_class, CcWacomPanel, tablets);
+ gtk_widget_class_bind_template_child (widget_class, CcWacomPanel, styli);
+ gtk_widget_class_bind_template_child (widget_class, CcWacomPanel, initial_state_stack);
+ gtk_widget_class_bind_template_child (widget_class, CcWacomPanel, panel_empty_state);
+ gtk_widget_class_bind_template_child (widget_class, CcWacomPanel, panel_view);
+ gtk_widget_class_bind_template_child (widget_class, CcWacomPanel, vadjustment);
+}
+
+static void
+add_known_device (CcWacomPanel *self,
+ GsdDevice *gsd_device)
+{
+ CcWacomDevice *device;
+ GsdDeviceType device_type;
+ g_autoptr(GList) tools = NULL;
+ GtkWidget *page;
+ gboolean is_ekr = FALSE;
+ 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_TOUCHSCREEN |
+ GSD_DEVICE_TYPE_TOUCHPAD)) != 0) {
+ return;
+ }
+
+ if ((device_type & GSD_DEVICE_TYPE_PAD) != 0) {
+ const char *vendor, *product;
+
+ gsd_device_get_device_ids (gsd_device, &vendor, &product);
+ is_ekr = (g_strcmp0 (vendor, EKR_VENDOR) == 0 &&
+ g_strcmp0 (product, EKR_PRODUCT) == 0);
+
+ /* Express key remote is an special case, as it is an
+ * external pad device, we want to distinctly show it
+ * in the list. Other pads are mounted on a tablet, which
+ * get their own entries.
+ */
+ if (!is_ekr)
+ 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);
+ }
+
+ if (is_ekr)
+ page = cc_wacom_ekr_page_new (self, device);
+ else
+ page = cc_wacom_page_new (self, device);
+
+ gtk_box_append (GTK_BOX (self->tablets), page);
+ 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_box_remove (GTK_BOX (self->tablets), page);
+ }
+
+ g_hash_table_remove (self->devices, gsd_device);
+ check_remove_stylus_pages (self);
+ update_test_button (self);
+ update_initial_state (self);
+}
+
+static void
+device_added_cb (CcWacomPanel *self,
+ GsdDevice *device)
+{
+ add_known_device (self, device);
+ update_test_button (self);
+ update_initial_state (self);
+}
+
+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
+got_input_mapping_proxy_cb (GObject *source_object,
+ GAsyncResult *res,
+ gpointer data)
+{
+ g_autoptr(GError) error = NULL;
+ CcWacomPanel *self;
+
+ self = CC_WACOM_PANEL (data);
+ self->input_mapping_proxy = g_dbus_proxy_new_for_bus_finish (res, &error);
+
+ if (self->input_mapping_proxy == NULL) {
+ g_printerr ("Error creating input mapping proxy: %s\n", error->message);
+ return;
+ }
+}
+
+static void
+cc_wacom_panel_init (CcWacomPanel *self)
+{
+ GsdDeviceManager *device_manager;
+ g_autoptr(GList) devices = NULL;
+ GList *l;
+ g_autoptr(GError) error = NULL;
+
+ g_resources_register (cc_wacom_get_resource ());
+
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ 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);
+
+ g_dbus_proxy_new_for_bus (G_BUS_TYPE_SESSION,
+ G_DBUS_PROXY_FLAGS_NONE,
+ NULL,
+ "org.gnome.Shell",
+ "/org/gnome/Mutter/InputMapping",
+ "org.gnome.Mutter.InputMapping",
+ cc_panel_get_cancellable (CC_PANEL (self)),
+ got_input_mapping_proxy_cb,
+ self);
+
+ 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_test_button (self);
+ update_initial_state (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;
+}
+
+GDBusProxy *
+cc_wacom_panel_get_input_mapping_bus_proxy (CcWacomPanel *self)
+{
+ g_return_val_if_fail (CC_IS_WACOM_PANEL (self), NULL);
+
+ return self->input_mapping_proxy;
+}
diff --git a/panels/wacom/cc-wacom-panel.h b/panels/wacom/cc-wacom-panel.h
new file mode 100644
index 0000000..68f46aa
--- /dev/null
+++ b/panels/wacom/cc-wacom-panel.h
@@ -0,0 +1,42 @@
+/*
+ * 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);
+
+GDBusProxy * cc_wacom_panel_get_input_mapping_bus_proxy (CcWacomPanel *self);
+
+G_END_DECLS
diff --git a/panels/wacom/cc-wacom-panel.ui b/panels/wacom/cc-wacom-panel.ui
new file mode 100644
index 0000000..77e7e16
--- /dev/null
+++ b/panels/wacom/cc-wacom-panel.ui
@@ -0,0 +1,88 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="CcWacomPanel" parent="CcPanel">
+
+ <child type="titlebar-end">
+ <object class="GtkMenuButton" id="test_button">
+ <property name="use_underline">True</property>
+ <property name="valign">center</property>
+ <property name="label" translatable="yes">Test Your _Settings</property>
+ <property name="popover">test_popover</property>
+ <style>
+ <class name="text-button"/>
+ </style>
+ </object>
+ </child>
+
+ <child type="content">
+ <object class="GtkStack" id="initial_state_stack">
+ <property name="transition_duration">0</property>
+ <child>
+ <object class="GtkScrolledWindow" id="panel_view">
+ <property name="hscrollbar-policy">never</property>
+ <property name="vadjustment">vadjustment</property>
+ <child>
+ <object class="AdwClamp" id="scrollable">
+ <property name="margin_top">32</property>
+ <property name="margin_bottom">32</property>
+ <property name="margin_start">12</property>
+ <property name="margin_end">12</property>
+
+ <child>
+ <object class="GtkBox">
+ <property name="spacing">48</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkBox" id="tablets">
+ <property name="orientation">vertical</property>
+ <property name="spacing">48</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox" id="styli">
+ <property name="orientation">vertical</property>
+ <property name="spacing">48</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="AdwStatusPage" id="panel_empty_state">
+ <property name="icon-name">input-tablet-symbolic</property>
+ <property name="title" translatable="yes">No tablet detected</property>
+ <property name="description" translatable="yes">Please plug in or turn on your Wacom tablet.</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+
+ <object class="GtkAdjustment" id="vadjustment" />
+
+ <!-- Test Popover -->
+ <object class="GtkPopover" id="test_popover">
+ <style>
+ <class name="menu" />
+ </style>
+ <child>
+ <object class="GtkBox">
+ <property name="margin-top">12</property>
+ <property name="margin-bottom">12</property>
+ <property name="margin-start">12</property>
+ <property name="margin-end">12</property>
+ <property name="spacing">6</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="CcDrawingArea" id="test_draw_area">
+ <property name="width-request">400</property>
+ <property name="height-request">300</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+</interface>
diff --git a/panels/wacom/cc-wacom-stylus-page.c b/panels/wacom/cc-wacom-stylus-page.c
new file mode 100644
index 0000000..01819ad
--- /dev/null
+++ b/panels/wacom/cc-wacom-stylus-page.c
@@ -0,0 +1,274 @@
+/*
+ * 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 <adwaita.h>
+#include <glib/gi18n.h>
+#include "cc-wacom-stylus-page.h"
+#include <gtk/gtk.h>
+#include <gdesktop-enums.h>
+
+#include <string.h>
+
+struct _CcWacomStylusPage
+{
+ GtkBox parent_instance;
+
+ GtkWidget *stylus_section;
+ GtkWidget *stylus_icon;
+ GtkWidget *stylus_button1_action;
+ GtkWidget *stylus_button2_action;
+ GtkWidget *stylus_button3_action;
+ GtkWidget *stylus_eraser_pressure;
+ GtkWidget *stylus_tip_pressure_scale;
+ GtkWidget *stylus_eraser_pressure_scale;
+ GtkAdjustment *stylus_tip_pressure_adjustment;
+ GtkAdjustment *stylus_eraser_pressure_adjustment;
+ CcWacomTool *stylus;
+ GSettings *stylus_settings;
+};
+
+G_DEFINE_TYPE (CcWacomStylusPage, cc_wacom_stylus_page, GTK_TYPE_BOX)
+
+/* 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
+on_tip_pressure_value_changed (CcWacomStylusPage *page)
+{
+ set_pressurecurve (GTK_RANGE (page->stylus_tip_pressure_scale), page->stylus_settings, "pressure-curve");
+}
+
+static void
+on_eraser_pressure_value_changed (CcWacomStylusPage *page)
+{
+ set_pressurecurve (GTK_RANGE (page->stylus_eraser_pressure_scale), 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
+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
+on_stylus_action_selected (GtkWidget *widget,
+ GParamSpec *pspec,
+ CcWacomStylusPage *page)
+{
+ gint idx;
+
+ idx = adw_combo_row_get_selected (ADW_COMBO_ROW (widget));
+
+ if (widget == page->stylus_button1_action)
+ g_settings_set_enum (page->stylus_settings, "button-action", idx);
+ else if (widget == page->stylus_button2_action)
+ g_settings_set_enum (page->stylus_settings, "secondary-button-action", idx);
+ else if (widget == page->stylus_button3_action)
+ g_settings_set_enum (page->stylus_settings, "tertiary-button-action", idx);
+}
+
+static void
+cc_wacom_stylus_page_class_init (CcWacomStylusPageClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->get_property = cc_wacom_stylus_page_get_property;
+ object_class->set_property = cc_wacom_stylus_page_set_property;
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/control-center/wacom/cc-wacom-stylus-page.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, CcWacomStylusPage, stylus_section);
+ gtk_widget_class_bind_template_child (widget_class, CcWacomStylusPage, stylus_icon);
+ gtk_widget_class_bind_template_child (widget_class, CcWacomStylusPage, stylus_button1_action);
+ gtk_widget_class_bind_template_child (widget_class, CcWacomStylusPage, stylus_button2_action);
+ gtk_widget_class_bind_template_child (widget_class, CcWacomStylusPage, stylus_button3_action);
+ gtk_widget_class_bind_template_child (widget_class, CcWacomStylusPage, stylus_eraser_pressure);
+ gtk_widget_class_bind_template_child (widget_class, CcWacomStylusPage, stylus_tip_pressure_scale);
+ gtk_widget_class_bind_template_child (widget_class, CcWacomStylusPage, stylus_eraser_pressure_scale);
+ gtk_widget_class_bind_template_child (widget_class, CcWacomStylusPage, stylus_tip_pressure_adjustment);
+ gtk_widget_class_bind_template_child (widget_class, CcWacomStylusPage, stylus_eraser_pressure_adjustment);
+
+ gtk_widget_class_bind_template_callback (widget_class, on_stylus_action_selected);
+ gtk_widget_class_bind_template_callback (widget_class, on_tip_pressure_value_changed);
+ gtk_widget_class_bind_template_callback (widget_class, on_eraser_pressure_value_changed);
+}
+
+static void
+add_marks (GtkScale *scale)
+{
+#if 0
+ gint i;
+
+ for (i = 0; i < N_PRESSURE_CURVES; i++)
+ gtk_scale_add_mark (scale, i, GTK_POS_BOTTOM, NULL);
+#endif
+}
+
+static void
+cc_wacom_stylus_page_init (CcWacomStylusPage *page)
+{
+ gtk_widget_init_template (GTK_WIDGET (page));
+
+ add_marks (GTK_SCALE (page->stylus_tip_pressure_scale));
+ add_marks (GTK_SCALE (page->stylus_eraser_pressure_scale));
+}
+
+static void
+set_icon_name (CcWacomStylusPage *page,
+ const char *icon_name)
+{
+ g_autofree gchar *resource = NULL;
+
+ resource = g_strdup_printf ("/org/gnome/control-center/wacom/%s.svg", icon_name);
+ gtk_picture_set_resource (GTK_PICTURE (page->stylus_icon), resource);
+}
+
+GtkWidget *
+cc_wacom_stylus_page_new (CcWacomTool *stylus)
+{
+ CcWacomStylusPage *page;
+ guint num_buttons;
+ 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;
+
+ /* Stylus name */
+ adw_preferences_group_set_title (ADW_PREFERENCES_GROUP (page->stylus_section),
+ cc_wacom_tool_get_name (stylus));
+ adw_preferences_group_set_description (ADW_PREFERENCES_GROUP (page->stylus_section),
+ cc_wacom_tool_get_description (stylus));
+
+ /* Icon */
+ set_icon_name (page, 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);
+
+ num_buttons = cc_wacom_tool_get_num_buttons (stylus);
+ gtk_widget_set_visible (page->stylus_button3_action,
+ num_buttons >= 3);
+ gtk_widget_set_visible (page->stylus_button2_action,
+ num_buttons >= 2);
+ gtk_widget_set_visible (page->stylus_button1_action,
+ num_buttons >= 1);
+ gtk_widget_set_visible (page->stylus_eraser_pressure,
+ has_eraser);
+
+ adw_combo_row_set_selected (ADW_COMBO_ROW (page->stylus_button1_action),
+ g_settings_get_enum (page->stylus_settings, "button-action"));
+ adw_combo_row_set_selected (ADW_COMBO_ROW (page->stylus_button2_action),
+ g_settings_get_enum (page->stylus_settings, "secondary-button-action"));
+ adw_combo_row_set_selected (ADW_COMBO_ROW (page->stylus_button3_action),
+ g_settings_get_enum (page->stylus_settings, "tertiary-button-action"));
+
+ set_feel_from_gsettings (page->stylus_tip_pressure_adjustment,
+ page->stylus_settings, "pressure-curve");
+ set_feel_from_gsettings (page->stylus_eraser_pressure_adjustment,
+ page->stylus_settings, "eraser-pressure-curve");
+
+ return GTK_WIDGET (page);
+}
+
+CcWacomTool *
+cc_wacom_stylus_page_get_tool (CcWacomStylusPage *page)
+{
+ return page->stylus;
+}
diff --git a/panels/wacom/cc-wacom-stylus-page.h b/panels/wacom/cc-wacom-stylus-page.h
new file mode 100644
index 0000000..51ad7d3
--- /dev/null
+++ b/panels/wacom/cc-wacom-stylus-page.h
@@ -0,0 +1,35 @@
+/*
+ * 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);
+
+G_END_DECLS
diff --git a/panels/wacom/cc-wacom-stylus-page.ui b/panels/wacom/cc-wacom-stylus-page.ui
new file mode 100644
index 0000000..a7850f7
--- /dev/null
+++ b/panels/wacom/cc-wacom-stylus-page.ui
@@ -0,0 +1,140 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="CcWacomStylusPage" parent="GtkBox">
+ <property name="orientation">vertical</property>
+ <property name="spacing">24</property>
+ <child>
+ <object class="AdwPreferencesGroup" id="stylus_section">
+ <property name="header-suffix">
+ <object class="GtkPicture" id="stylus_icon">
+ <property name="halign">end</property>
+ <property name="valign">start</property>
+ </object>
+ </property>
+ <child>
+ <object class="AdwActionRow" id="stylus_tip_pressure">
+ <property name="title" translatable="yes">Tip Pressure Feel</property>
+ <child>
+ <object class="GtkBox" id="stylus_tip_pressure_box">
+ <child>
+ <object class="GtkLabel">
+ <property name="label" translatable="yes">Soft</property>
+ <style>
+ <class name="caption"/>
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkScale" id="stylus_tip_pressure_scale">
+ <property name="adjustment">stylus_tip_pressure_adjustment</property>
+ <property name="draw_value">False</property>
+ <property name="hexpand">True</property>
+ <property name="vexpand">True</property>
+ <signal name="value-changed" handler="on_tip_pressure_value_changed" swapped="yes"/>
+ <accessibility>
+ <property name="label" translatable="yes">Stylus tip pressure</property>
+ </accessibility>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="label" translatable="yes">Firm</property>
+ <style>
+ <class name="caption"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="AdwComboRow" id="stylus_button1_action">
+ <property name="width_request">100</property>
+ <property name="title" translatable="yes" context="display setting">Button 1</property>
+ <property name="model">button_model</property>
+ <signal name="notify::selected-item" handler="on_stylus_action_selected" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="AdwComboRow" id="stylus_button2_action">
+ <property name="width_request">100</property>
+ <property name="title" translatable="yes" context="display setting">Button 2</property>
+ <property name="model">button_model</property>
+ <signal name="notify::selected-item" handler="on_stylus_action_selected" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="AdwComboRow" id="stylus_button3_action">
+ <property name="width_request">100</property>
+ <property name="title" translatable="yes" context="display setting">Button 3</property>
+ <property name="model">button_model</property>
+ <signal name="notify::selected-item" handler="on_stylus_action_selected" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="AdwActionRow" id="stylus_eraser_pressure">
+ <property name="title" translatable="yes">Eraser Pressure Feel</property>
+ <child>
+ <object class="GtkBox" id="stylus_eraser_pressure_box">
+ <child>
+ <object class="GtkLabel">
+ <property name="label" translatable="yes">Soft</property>
+ <style>
+ <class name="caption"/>
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkScale" id="stylus_eraser_pressure_scale">
+ <property name="adjustment">stylus_eraser_pressure_adjustment</property>
+ <property name="draw_value">False</property>
+ <property name="hexpand">True</property>
+ <property name="vexpand">True</property>
+ <signal name="value-changed" handler="on_eraser_pressure_value_changed" swapped="yes"/>
+ <accessibility>
+ <property name="label" translatable="yes">Eraser pressure</property>
+ </accessibility>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="label" translatable="yes">Firm</property>
+ <style>
+ <class name="caption"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+ <object class="GtkAdjustment" id="stylus_tip_pressure_adjustment">
+ <property name="upper">6</property>
+ <property name="step_increment">1</property>
+ <property name="page_increment">3</property>
+ </object>
+ <object class="GtkAdjustment" id="stylus_eraser_pressure_adjustment">
+ <property name="upper">6</property>
+ <property name="step_increment">1</property>
+ <property name="page_increment">3</property>
+ </object>
+ <object class="GtkSizeGroup" id="sizegroup_pressure">
+ <widgets>
+ <widget name="stylus_tip_pressure_box" />
+ <widget name="stylus_eraser_pressure_box" />
+ </widgets>
+ </object>
+ <object class="GtkStringList" id="button_model">
+ <items>
+ <item translatable="yes">Default</item>
+ <item translatable="yes">Middle Mouse Button Click</item>
+ <item translatable="yes">Right Mouse Button Click</item>
+ <item translatable="yes">Back</item>
+ <item translatable="yes">Forward</item>
+ </items>
+ </object>
+</interface>
diff --git a/panels/wacom/cc-wacom-tool.c b/panels/wacom/cc-wacom-tool.c
new file mode 100644
index 0000000..5633f5b
--- /dev/null
+++ b/panels/wacom/cc-wacom-tool.c
@@ -0,0 +1,330 @@
+/*
+ * 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"
+
+#include <glib/gi18n.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_is_eraser (tool->wstylus);
+}
+
+const gchar *
+cc_wacom_tool_get_description (CcWacomTool *tool)
+{
+ WacomAxisTypeFlags axes;
+
+ axes = libwacom_stylus_get_axes (tool->wstylus);
+
+ if ((~axes & (WACOM_AXIS_TYPE_TILT | WACOM_AXIS_TYPE_PRESSURE | WACOM_AXIS_TYPE_SLIDER)) == 0)
+ return _("Airbrush stylus with pressure, tilt, and integrated slider");
+ else if ((~axes & (WACOM_AXIS_TYPE_TILT | WACOM_AXIS_TYPE_PRESSURE | WACOM_AXIS_TYPE_ROTATION_Z)) == 0)
+ return _("Airbrush stylus with pressure, tilt, and rotation");
+ else if ((~axes & (WACOM_AXIS_TYPE_TILT | WACOM_AXIS_TYPE_PRESSURE)) == 0)
+ return _("Standard stylus with pressure and tilt");
+ else if ((~axes & WACOM_AXIS_TYPE_PRESSURE) == 0)
+ return _("Standard stylus with pressure");
+
+ return NULL;
+}
diff --git a/panels/wacom/cc-wacom-tool.h b/panels/wacom/cc-wacom-tool.h
new file mode 100644
index 0000000..a07659f
--- /dev/null
+++ b/panels/wacom/cc-wacom-tool.h
@@ -0,0 +1,46 @@
+/*
+ * 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);
+
+const gchar * cc_wacom_tool_get_description (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..87cebd7
--- /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=org.gnome.Settings-wacom-symbolic
+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/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..aa15b0f
--- /dev/null
+++ b/panels/wacom/gsd-wacom-key-shortcut-button.c
@@ -0,0 +1,535 @@
+/*
+ * 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;
+
+ 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)
+{
+ self->editing_mode = TRUE;
+ gsd_wacom_key_shortcut_button_changed (self);
+ gtk_widget_grab_focus (GTK_WIDGET (self));
+}
+
+static void
+gsd_wacom_key_shortcut_remove_editing_mode (GsdWacomKeyShortcutButton *self)
+{
+ self->editing_mode = FALSE;
+ 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
+key_shortcut_finished_editing (GsdWacomKeyShortcutButton *self,
+ guint32 time)
+{
+ 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_released_cb (GtkEventController *controller,
+ guint keyval,
+ guint keycode,
+ GdkModifierType state,
+ GsdWacomKeyShortcutButton *self)
+{
+ if (self->tmp_shortcut_keyval == 0)
+ 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_pressed_cb (GtkEventController *controller,
+ guint keyval,
+ guint keycode,
+ GdkModifierType state,
+ GsdWacomKeyShortcutButton *self)
+{
+ /* This code is based on the gtk_cell_renderer_accel_start_editing */
+ GdkModifierType mods = 0;
+ GdkEvent *event;
+ guint shortcut_keyval;
+ gboolean edited;
+ gboolean cleared;
+
+ event = gtk_event_controller_get_current_event (controller);
+
+ /* GTK and OTHER modes don't allow modifier keyvals */
+ if (gdk_key_event_is_modifier (event) && self->mode != GSD_WACOM_KEY_SHORTCUT_BUTTON_MODE_ALL)
+ return TRUE;
+
+ if (!self->editing_mode)
+ return FALSE;
+
+ edited = FALSE;
+ cleared = FALSE;
+
+ mods = state;
+
+ if (keyval == GDK_KEY_Sys_Req && (mods & GDK_ALT_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 (gdk_key_event_is_modifier (event))
+ {
+ /* 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 = gtk_event_controller_get_current_event_time (controller);
+
+ 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 (self, gtk_event_controller_get_current_event_time (controller));
+
+ 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 void
+gsd_wacom_key_shortcut_button_button_pressed_cb (GtkGestureClick *gesture,
+ gint n_press,
+ gdouble x,
+ gdouble y,
+ GsdWacomKeyShortcutButton *self)
+{
+ if (!self->editing_mode)
+ gsd_wacom_key_shortcut_set_editing_mode (self, NULL);
+}
+
+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->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);
+}
+
+static void
+gsd_wacom_key_shortcut_button_init (GsdWacomKeyShortcutButton *self)
+{
+ GtkEventController *controller;
+ GtkGesture *gesture;
+
+ self->cancel_keyval = DEFAULT_CANCEL_KEY;
+ self->clear_keyval = DEFAULT_CLEAR_KEY;
+
+ controller = gtk_event_controller_key_new ();
+ g_signal_connect (controller, "key-pressed", G_CALLBACK (gsd_wacom_key_shortcut_button_key_pressed_cb), self);
+ g_signal_connect (controller, "key-released", G_CALLBACK (gsd_wacom_key_shortcut_button_key_released_cb), self);
+ gtk_widget_add_controller (GTK_WIDGET (self), controller);
+
+ gesture = gtk_gesture_click_new ();
+ g_signal_connect (gesture, "pressed", G_CALLBACK (gsd_wacom_key_shortcut_button_button_pressed_cb), self);
+ gtk_widget_add_controller (GTK_WIDGET (self), GTK_EVENT_CONTROLLER (gesture));
+}
+
+/**
+ * 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/icons/meson.build b/panels/wacom/icons/meson.build
new file mode 100644
index 0000000..7437a1d
--- /dev/null
+++ b/panels/wacom/icons/meson.build
@@ -0,0 +1,4 @@
+install_data(
+ 'scalable/org.gnome.Settings-wacom-symbolic.svg',
+ install_dir: join_paths(control_center_icondir, 'hicolor', 'scalable', 'apps')
+)
diff --git a/panels/wacom/icons/scalable/org.gnome.Settings-wacom-symbolic.svg b/panels/wacom/icons/scalable/org.gnome.Settings-wacom-symbolic.svg
new file mode 100644
index 0000000..bc66c02
--- /dev/null
+++ b/panels/wacom/icons/scalable/org.gnome.Settings-wacom-symbolic.svg
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
+ <path d="m 8 0 v 1.5 c 0 0.132812 -0.054688 0.261719 -0.144531 0.355469 c -0.09375 0.09375 -0.21875 0.144531 -0.347657 0.144531 h -3.011718 c -0.535156 0 -1.03125 0.289062 -1.296875 0.75 c -0.265625 0.464844 -0.265625 1.035156 0 1.5 c 0.265625 0.460938 0.761719 0.75 1.296875 0.75 h 3.007812 c 0.179688 0 0.339844 0.09375 0.429688 0.25 c 0.042968 0.074219 0.066406 0.164062 0.066406 0.25 v 0.5 h -5 c -1.644531 0 -3 1.355469 -3 3 v 4 c 0 1.644531 1.355469 3 3 3 h 10 c 1.644531 0 3 -1.355469 3 -3 v -4 c 0 -0.867188 -0.378906 -1.648438 -0.972656 -2.199219 l -1.410156 1.402344 c 0.234374 0.179687 0.382812 0.460937 0.382812 0.796875 v 3 c 0 0.554688 -0.445312 1 -1 1 h -10 c -0.554688 0 -1 -0.445312 -1 -1 v -3 c 0 -0.570312 0.5 -1 1 -1 h 5.191406 l 1.992188 -2 h -1.183594 v -0.5 c 0 -0.257812 -0.066406 -0.515625 -0.199219 -0.75 c -0.265625 -0.460938 -0.761719 -0.75 -1.296875 -0.75 h -3.007812 c -0.179688 0 -0.339844 -0.09375 -0.429688 -0.25 c -0.089844 -0.152344 -0.089844 -0.347656 0 -0.5 c 0.089844 -0.15625 0.25 -0.25 0.429688 -0.25 h 3.011718 c 0.398438 0 0.777344 -0.160156 1.054688 -0.441406 c 0.28125 -0.28125 0.4375 -0.660156 0.4375 -1.058594 v -1.5 z m 7 3 c -0.265625 0 -0.519531 0.105469 -0.703125 0.292969 h -0.003906 l -6.292969 6.316406 v 1.390625 h 1.390625 l 6.316406 -6.292969 l -0.003906 -0.003906 c 0.191406 -0.183594 0.296875 -0.4375 0.296875 -0.703125 c 0 -0.550781 -0.449219 -1 -1 -1 z m 0 0" fill="#2e3436"/>
+</svg>
diff --git a/panels/wacom/meson.build b/panels/wacom/meson.build
new file mode 100644
index 0000000..f65a3db
--- /dev/null
+++ b/panels/wacom/meson.build
@@ -0,0 +1,106 @@
+deps = common_deps + wacom_deps + [
+ gnome_rr_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(
+ 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-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',
+ '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.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',
+ 'cc-wacom-ekr-page.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 ]
+)
+
+subdir('icons')
diff --git a/panels/wacom/test-wacom.c b/panels/wacom/test-wacom.c
new file mode 100644
index 0000000..5e4cf7e
--- /dev/null
+++ b/panels/wacom/test-wacom.c
@@ -0,0 +1,155 @@
+
+#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;
+}
+
+GDBusProxy *
+cc_wacom_panel_get_input_mapping_bus_proxy (CcWacomPanel *self)
+{
+ g_message ("Should get the mutter input mapping 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);
+ gtk_notebook_append_page (GTK_NOTEBOOK (notebook), widget, NULL);
+ gtk_widget_show (widget);
+}
+
+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 ();
+
+ window = gtk_window_new ();
+ gtk_window_set_resizable (GTK_WINDOW (window), FALSE);
+ gtk_window_set_default_size (GTK_WINDOW (window), FIXED_WIDTH, -1);
+ 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_window_set_child (GTK_WINDOW (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);
+
+ while (g_list_model_get_n_items (gtk_window_get_toplevels ()) > 0)
+ g_main_context_iteration (NULL, TRUE);
+
+ return 0;
+}
diff --git a/panels/wacom/wacom-panel-scenario-tester.py b/panels/wacom/wacom-panel-scenario-tester.py
new file mode 100755
index 0000000..731d780
--- /dev/null
+++ b/panels/wacom/wacom-panel-scenario-tester.py
@@ -0,0 +1,75 @@
+#!/usr/bin/env python3
+
+# Copyright (C) 2021 Red Hat Inc.
+#
+# Author: Bastien Nocera <hadess@hadess.net>
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+import os
+import gi
+import subprocess
+import sys
+
+gi.require_version('UMockdev', '1.0')
+from gi.repository import UMockdev
+
+def setup_devices(testbed):
+ dev = testbed.add_device('hid',
+ '/devices/pci0000:00/0000:00:14.0/usb3/3-10/3-10:1.2/0003:046D:C52B.0009/0003:046D:4101.000A',
+ None,
+ [], [])
+
+ parent = dev
+ testbed.add_device('input',
+ '/devices/pci0000:00/0000:00:14.0/usb3/3-10/3-10:1.2/0003:046D:C52B.0009/0003:046D:4101.000A/input/input3',
+ parent,
+ ['name', 'Wacom Cintiq 24HD Pad'],
+ ['DEVNAME', 'input/event3',
+ 'ID_INPUT', '1',
+ 'ID_INPUT_TABLET', '1',
+ 'ID_INPUT_TABLET_PAD', '1',
+ 'ID_VENDOR_ID', '0x56a',
+ 'ID_MODEL_ID', '0x0f4',
+ 'ID_INPUT_WIDTH_MM', '50',
+ 'ID_INPUT_HEIGHT_MM', '40',
+ 'PRODUCT', '3/56a/f4/100',
+ 'LIBINPUT_DEVICE_GROUP', '3/56a/f4:usb-0000:00:14.0-5'])
+ testbed.add_device('input',
+ '/devices/pci0000:00/0000:00:14.0/usb3/3-10/3-10:1.2/0003:046D:C52B.0009/0003:046D:4101.000A/input/input4',
+ parent,
+ ['name', 'Wacom Cintiq 24HD Pen'],
+ ['DEVNAME', 'input/event4',
+ 'ID_INPUT', '1',
+ 'ID_INPUT_TABLET', '1',
+ 'ID_VENDOR_ID', '0x56a',
+ 'ID_MODEL_ID', '0x0f4',
+ 'ID_INPUT_WIDTH_MM', '50',
+ 'ID_INPUT_HEIGHT_MM', '40',
+ 'PRODUCT', '3/56a/f4/100',
+ 'LIBINPUT_DEVICE_GROUP', '3/56a/f4:usb-0000:00:14.0-5'])
+
+def wrap_call(testbed):
+ os.environ['GSETTINGS_BACKEND'] = 'memory'
+ os.environ['UMOCKDEV_DIR'] = testbed.get_root_dir()
+
+ wrapper = os.environ.get('WRAPPER')
+ args = ['gnome-control-center', '-v', 'wacom']
+ if wrapper == 'gdb':
+ args = ['gdb', '-ex', 'r', '-ex', 'bt full', '--args'] + args
+ elif wrapper:
+ args = wrapper.split(' ') + args
+
+ print(os.environ)
+
+ p = subprocess.Popen(args, env=os.environ)
+ p.wait()
+
+if __name__ == '__main__':
+ if 'umockdev' not in os.environ.get('LD_PRELOAD', ''):
+ os.execvp('umockdev-wrapper', ['umockdev-wrapper'] + sys.argv)
+
+ # Start mock udev
+ testbed = UMockdev.Testbed.new()
+ setup_devices(testbed)
+ wrap_call(testbed)
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..b52c208
--- /dev/null
+++ b/panels/wacom/wacom-stylus-3btn-no-eraser.svg
@@ -0,0 +1,110 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ id="svg86343"
+ version="1.1"
+ inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
+ width="190.35817"
+ height="31.02216"
+ sodipodi:docname="wacom-stylus-3btn-no-eraser.svg"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <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="1080"
+ inkscape:window-height="651"
+ id="namedview86345"
+ showgrid="false"
+ inkscape:snap-nodes="false"
+ inkscape:snap-bbox="true"
+ inkscape:zoom="1.9143497"
+ inkscape:cx="237.93981"
+ inkscape:cy="17.23823"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="svg86343"
+ borderlayer="true"
+ inkscape:showpageshadow="false"
+ inkscape:pagecheckerboard="0">
+ <inkscape:grid
+ type="xygrid"
+ id="grid86802"
+ empspacing="5"
+ visible="true"
+ enabled="true"
+ snapvisiblegridlinesonly="true"
+ originx="77.933462"
+ originy="-80.812147" />
+ </sodipodi:namedview>
+ <g
+ style="display:inline"
+ transform="rotate(90,-56.008515,534.91126)"
+ 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 V 516 L 338,545.125 c -0.1873,2.15512 1.62589,3.92035 3.75,4.125 l 4.625,10.90625 h 1.53125 v 2.15625 l 3.61536,8.57242 1.18546,0.0214 0.44918,3.78119 0.33938,-3.7414 1.14797,-0.0687 3.76265,-8.53366 v -2.1875 h 1.53125 l 4.65625,-10.96875 c 1.96694,-0.35188 3.54637,-2.02216 3.40625,-4.0625 L 365.53125,516 V 388.48207 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;display:inline;overflow:visible;visibility:visible;fill:#eeeeec;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"
+ id="rect10541"
+ width="8.75"
+ height="22"
+ x="-579.65631"
+ y="385.90625"
+ rx="3.25"
+ ry="3.25" />
+ <rect
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:#eeeeec;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"
+ id="rect10543"
+ width="8.75"
+ height="12.25"
+ x="-579.65631"
+ y="410.90625"
+ rx="3.25"
+ ry="3.2500002" />
+ <rect
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:#eeeeec;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"
+ id="rect10544"
+ width="8.75"
+ height="8.75"
+ x="-579.65631"
+ y="429.15625"
+ rx="3.25"
+ ry="3.2500002" />
+ </g>
+ <g
+ style="display:inline;opacity:0.2"
+ id="g10631"
+ transform="translate(670.36721,-383.29631)" />
+</svg>
diff --git a/panels/wacom/wacom-stylus-3btn.svg b/panels/wacom/wacom-stylus-3btn.svg
new file mode 100644
index 0000000..99a72fb
--- /dev/null
+++ b/panels/wacom/wacom-stylus-3btn.svg
@@ -0,0 +1,106 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ id="svg86343"
+ version="1.1"
+ inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
+ width="190.28186"
+ height="31.02216"
+ sodipodi:docname="wacom-stylus-3btn.svg"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <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="1080"
+ inkscape:window-height="651"
+ id="namedview86345"
+ showgrid="false"
+ inkscape:snap-nodes="false"
+ inkscape:snap-bbox="true"
+ inkscape:zoom="1.8154258"
+ inkscape:cx="167.72924"
+ inkscape:cy="14.872544"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="svg86343"
+ borderlayer="true"
+ inkscape:showpageshadow="false"
+ inkscape:pagecheckerboard="0">
+ <inkscape:grid
+ type="xygrid"
+ id="grid86802"
+ empspacing="5"
+ visible="true"
+ enabled="true"
+ snapvisiblegridlinesonly="true"
+ originx="77.895312"
+ originy="-80.850297" />
+ </sodipodi:namedview>
+ <g
+ style="display:inline"
+ transform="rotate(90,-56.008515,534.91126)"
+ 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 H 344.125 c -1.9944,0 -3.59375,1.59935 -3.59375,3.59375 V 516 L 338,545.125 c -0.1873,2.15512 1.62589,3.92035 3.75,4.125 l 4.625,10.90625 h 1.53125 v 2.15625 l 3.61536,8.57242 1.18546,0.0214 0.44918,3.78119 0.33938,-3.7414 1.14797,-0.0687 3.76265,-8.53366 v -2.1875 h 1.53125 l 4.65625,-10.96875 c 1.96694,-0.35188 3.54637,-2.02216 3.40625,-4.0625 L 365.53125,516 V 399.59375 c 0,-1.9944 -1.59935,-3.59375 -3.59375,-3.59375 h -1.40625 L 360,389.47786 c -0.23272,-2.85711 -1.26201,-4.51323 -3.69531,-4.51323 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;display:inline;overflow:visible;visibility:visible;fill:#eeeeec;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"
+ id="rect10541"
+ width="8.75"
+ height="22"
+ x="-579.65631"
+ y="385.90625"
+ rx="3.25"
+ ry="3.25" />
+ <rect
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:#eeeeec;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"
+ id="rect10543"
+ width="8.75"
+ height="12.25"
+ x="-579.65631"
+ y="410.90625"
+ rx="3.25"
+ ry="3.2500002" />
+ <rect
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:#eeeeec;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"
+ id="rect10544"
+ width="8.75"
+ height="8.75"
+ x="-579.65631"
+ y="429.15625"
+ rx="3.25"
+ ry="3.2500002" />
+ </g>
+</svg>
diff --git a/panels/wacom/wacom-stylus-airbrush.svg b/panels/wacom/wacom-stylus-airbrush.svg
new file mode 100644
index 0000000..763fc7b
--- /dev/null
+++ b/panels/wacom/wacom-stylus-airbrush.svg
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ id="svg86343"
+ version="1.1"
+ inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
+ width="190.54817"
+ height="48.084461"
+ sodipodi:docname="wacom-stylus-airbrush.svg"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <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="1080"
+ inkscape:window-height="651"
+ id="namedview86345"
+ showgrid="false"
+ inkscape:snap-nodes="false"
+ inkscape:snap-bbox="true"
+ inkscape:zoom="1.566601"
+ inkscape:cx="202.66806"
+ inkscape:cy="10.213194"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="svg86343"
+ borderlayer="true"
+ inkscape:showpageshadow="false"
+ inkscape:pagecheckerboard="0">
+ <inkscape:grid
+ type="xygrid"
+ id="grid86802"
+ empspacing="5"
+ visible="true"
+ enabled="true"
+ snapvisiblegridlinesonly="true"
+ originx="68.241145"
+ originy="-71.955672" />
+ </sodipodi:namedview>
+ <path
+ 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"
+ d="m 190.04818,14.311086 c 0,1.43276 -1.88644,2.59423 -4.21348,2.59423 -0.06,0 -0.11387,0.002 -0.17315,0 v 0.9224 L 97.4674,27.051651 c -9.23499,-0.9224 -29.69978,17.886897 -29.69978,17.886897 -5.27125,4.02955 -22.05579,3.90834 -27.55716,-3.58982 l -8.31149,-12.4523 -15.23774,-5.073157 v -0.5765 C 10.06256,22.765871 0.5,19.464966 0.5,17.943016 c 0,-2.11004 11.12955,-4.72727 19.16261,-4.72727 0.1448,0 0.28896,-0.002 0.43293,0 0,0 9.2445,-5.3861297 20.17264,-8.1285897 2.81141,-6.28314 13.64962,-5.78289004 16.4982,-0.49916 l 3.91974,0.009 c 0.84067,-0.83292 2.00027,-1.75859 3.53531,-1.75859 1.88597,0 3.49443,0.81769 4.21347,1.98891 l 14.74708,1.15341 102.47958,4.4678397 v 1.26829 c 0.0591,-0.002 0.11321,0 0.17315,0 2.32703,0 4.21347,1.16148 4.21347,2.59423 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..bb0be38
--- /dev/null
+++ b/panels/wacom/wacom-stylus-art-pen.svg
@@ -0,0 +1,94 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ id="svg86343"
+ version="1.1"
+ inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
+ width="188.59064"
+ height="31.02191"
+ sodipodi:docname="wacom-stylus-art-pen.svg"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <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"
+ lpeversion="0" />
+ </defs>
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1080"
+ inkscape:window-height="651"
+ id="namedview86345"
+ showgrid="false"
+ inkscape:snap-nodes="false"
+ inkscape:snap-bbox="true"
+ inkscape:zoom="1.670234"
+ inkscape:cx="146.38667"
+ inkscape:cy="34.725672"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="svg86343"
+ borderlayer="true"
+ inkscape:showpageshadow="false"
+ inkscape:pagecheckerboard="0">
+ <inkscape:grid
+ type="xygrid"
+ id="grid86802"
+ empspacing="5"
+ visible="true"
+ enabled="true"
+ snapvisiblegridlinesonly="true"
+ originx="77.459407"
+ originy="-79.696633" />
+ </sodipodi:namedview>
+ <g
+ style="display:inline"
+ transform="rotate(90,-56.803429,534.11634)"
+ 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 H 344.125 c -1.9944,0 -3.59375,1.59935 -3.59375,3.59375 V 516 L 338,545.125 c -0.1873,2.15512 1.62589,3.92035 3.75,4.125 l 1.625,6.6639 h 2.20868 l -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 h 2.20868 l 1.65625,-6.72641 c 1.96694,-0.35188 3.54637,-2.02216 3.40625,-4.0625 L 365.53125,516 V 399.59375 c 0,-1.9944 -1.59935,-3.59375 -3.59375,-3.59375 h -1.40625 L 359,389.15549 c -0.53643,-2.39779 -1.88996,-4.08946 -4.16741,-4.08946 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;display:inline;overflow:visible;visibility:visible;fill:#eeeeec;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"
+ id="rect10541"
+ width="9.0059462"
+ height="35.214661"
+ x="-579.93378"
+ y="385.90622"
+ rx="3.25"
+ ry="3.2499998" />
+ </g>
+</svg>
diff --git a/panels/wacom/wacom-stylus-classic.svg b/panels/wacom/wacom-stylus-classic.svg
new file mode 100644
index 0000000..4a82793
--- /dev/null
+++ b/panels/wacom/wacom-stylus-classic.svg
@@ -0,0 +1,83 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ id="svg86343"
+ version="1.1"
+ inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
+ width="187.83311"
+ height="23.792789"
+ sodipodi:docname="wacom-stylus-classic.svg"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <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"
+ lpeversion="0" />
+ </defs>
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1080"
+ inkscape:window-height="651"
+ id="namedview86345"
+ showgrid="false"
+ inkscape:snap-nodes="false"
+ inkscape:snap-bbox="true"
+ inkscape:zoom="1.3696365"
+ inkscape:cx="152.96029"
+ inkscape:cy="14.602414"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="svg86343"
+ borderlayer="true"
+ inkscape:showpageshadow="false"
+ inkscape:pagecheckerboard="0">
+ <inkscape:grid
+ type="xygrid"
+ id="grid86802"
+ empspacing="5"
+ visible="true"
+ enabled="true"
+ snapvisiblegridlinesonly="true"
+ originx="76.060233"
+ originy="-84.128949" />
+ </sodipodi:namedview>
+ <g
+ style="display:inline;opacity:0.2"
+ id="g10631"
+ transform="translate(668.49398,-386.61311)" />
+ <path
+ 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"
+ d="M 46.887667,3.360306 C 36.117047,3.865161 22.240407,1.93488 22.240407,1.93488 l -1.05225,1.559872 c 0,0 -5.49117,1.362729 -8.2077,1.850845 -2.71653,0.488109 -7.5711996,3.679579 -7.5711996,3.679579 l -1e-5,1.545431 -3.89851,-8e-6 c -1.30925997,-0.03586 -1.38545997,1.983788 -9e-5,1.983788 l 3.89851,8e-6 v 1.451666 c 0,0 4.8546896,3.19148 7.5712196,3.6796 2.71652,0.48811 8.20768,1.85083 8.20768,1.85083 l 1.05226,1.55987 c 0,0 11.863,-1.78649 21.63127,-1.54542 1.0038,0.0248 2.07462,2.59748 3.20226,2.65033 5.78108,0.27099 18.64287,0.70763 26.63126,1.0898 1.13744,0.0544 2.29772,-1.42261 3.48097,-1.37103 7.90158,0.34438 10.82762,0.61119 20.817562,0.61119 18.010761,0 60.275061,-2.76938 77.215201,-4.16159 0.58366,-0.048 0.70379,-1.39578 1.12075,-1.43172 2.25841,-0.19467 7.22786,-1.00842 7.52125,-1.084447 1.88483,-0.488463 3.53687,-2.23225 3.47014,-4.278453 0.002,-0.03684 -0.001,-0.07529 0,-0.11193 0.0688,-2.047734 -1.58423,-3.789705 -3.47014,-4.278452 -0.28392,-0.07358 -5.1712,-0.882492 -7.30532,-1.068191 C 176.0687,6.074088 175.88151,4.715969 175.18201,4.658534 158.21463,3.265341 116.00137,0.499981 98.003683,0.5 76.011317,0.500023 57.658267,2.855443 46.887667,3.360306 Z"
+ id="path14406"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="sccsccccccsccssssssssccssscs" />
+</svg>
diff --git a/panels/wacom/wacom-stylus-inking.svg b/panels/wacom/wacom-stylus-inking.svg
new file mode 100644
index 0000000..aa67724
--- /dev/null
+++ b/panels/wacom/wacom-stylus-inking.svg
@@ -0,0 +1,79 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ id="svg86343"
+ version="1.1"
+ inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
+ width="189.31738"
+ height="23.031231"
+ sodipodi:docname="wacom-stylus-inking.svg"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <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"
+ lpeversion="0" />
+ </defs>
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1080"
+ inkscape:window-height="651"
+ id="namedview86345"
+ showgrid="false"
+ inkscape:snap-nodes="false"
+ inkscape:snap-bbox="true"
+ inkscape:zoom="1.2481956"
+ inkscape:cx="90.93126"
+ inkscape:cy="28.841633"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="svg86343"
+ borderlayer="true"
+ inkscape:showpageshadow="false"
+ inkscape:pagecheckerboard="0">
+ <inkscape:grid
+ type="xygrid"
+ id="grid86802"
+ empspacing="5"
+ visible="true"
+ enabled="true"
+ snapvisiblegridlinesonly="true"
+ originx="77.183018"
+ originy="-83.767469" />
+ </sodipodi:namedview>
+ <path
+ 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"
+ d="M 47.073937,3.360306 C 36.294607,3.86557 22.240407,1.93488 22.240407,1.93488 l -1.05225,1.559872 c 0,0 -5.49117,1.362729 -8.2077,1.850845 -2.71653,0.488109 -7.5711996,3.679579 -7.5711996,3.679579 l -1e-5,1.545431 -3.89851,-8e-6 c -1.30925997,-0.03586 -1.38545997,1.983788 -9e-5,1.983788 l 3.89851,8e-6 v 1.451666 c 0,0 4.8546896,3.19148 7.5712196,3.6796 2.71652,0.48811 8.20768,1.85083 8.20768,1.85083 l 1.05226,1.55987 c 0,0 14.0542,-1.93069 24.83354,-1.42541 10.77934,0.50526 28.937381,2.86028 50.929782,2.86028 21.992401,-10e-6 83.129171,-4.12918 85.865071,-4.8382 2.68626,-0.69619 5.04077,-3.181414 4.94566,-6.097669 0.003,-0.0525 -0.002,-0.107307 0,-0.159523 0.098,-2.918438 -2.25786,-5.401094 -4.94566,-6.097665 C 181.13281,4.629146 119.99605,0.499977 98.003683,0.5 76.011318,0.500023 57.853267,2.855035 47.073937,3.360306 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..5436cf8
--- /dev/null
+++ b/panels/wacom/wacom-stylus-no-eraser.svg
@@ -0,0 +1,101 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ id="svg86343"
+ version="1.1"
+ inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
+ width="190.35817"
+ height="31.02216"
+ sodipodi:docname="wacom-stylus-no-eraser.svg"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <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="1080"
+ inkscape:window-height="651"
+ id="namedview86345"
+ showgrid="false"
+ inkscape:snap-nodes="false"
+ inkscape:snap-bbox="true"
+ inkscape:zoom="1.5080049"
+ inkscape:cx="262.26706"
+ inkscape:cy="141.90936"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="svg86343"
+ borderlayer="true"
+ inkscape:showpageshadow="false"
+ inkscape:pagecheckerboard="0">
+ <inkscape:grid
+ type="xygrid"
+ id="grid86802"
+ empspacing="5"
+ visible="true"
+ enabled="true"
+ snapvisiblegridlinesonly="true"
+ originx="77.933462"
+ originy="-80.812147" />
+ </sodipodi:namedview>
+ <g
+ style="display:inline"
+ transform="rotate(90,-56.008515,534.91126)"
+ 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 V 516 L 338,545.125 c -0.1873,2.15512 1.62589,3.92035 3.75,4.125 l 4.625,10.90625 h 1.53125 v 2.15625 l 3.61536,8.57242 1.18546,0.0214 0.44918,3.78119 0.33938,-3.7414 1.14797,-0.0687 3.76265,-8.53366 v -2.1875 h 1.53125 l 4.65625,-10.96875 c 1.96694,-0.35188 3.54637,-2.02216 3.40625,-4.0625 L 365.53125,516 V 388.48207 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;display:inline;overflow:visible;visibility:visible;fill:#eeeeec;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"
+ 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;display:inline;overflow:visible;visibility:visible;fill:#eeeeec;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" />
+ </g>
+ <g
+ style="display:inline;opacity:0.2"
+ id="g10631"
+ transform="translate(670.36721,-383.29631)" />
+</svg>
diff --git a/panels/wacom/wacom-stylus.svg b/panels/wacom/wacom-stylus.svg
new file mode 100644
index 0000000..19122b2
--- /dev/null
+++ b/panels/wacom/wacom-stylus.svg
@@ -0,0 +1,100 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ id="svg86343"
+ version="1.1"
+ inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
+ width="190.28186"
+ height="31.02216"
+ sodipodi:docname="wacom-stylus.svg"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <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" />
+ </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="1080"
+ inkscape:window-height="651"
+ id="namedview86345"
+ showgrid="false"
+ inkscape:snap-nodes="false"
+ inkscape:snap-bbox="true"
+ inkscape:zoom="1.9779054"
+ inkscape:cx="84.685547"
+ inkscape:cy="92.016534"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="svg86343"
+ borderlayer="true"
+ inkscape:showpageshadow="false"
+ inkscape:pagecheckerboard="0"
+ fit-margin-top="0"
+ fit-margin-left="0"
+ fit-margin-right="0"
+ fit-margin-bottom="0">
+ <inkscape:grid
+ type="xygrid"
+ id="grid86802"
+ empspacing="5"
+ visible="true"
+ enabled="true"
+ snapvisiblegridlinesonly="true"
+ originx="-0.20774507"
+ originy="-0.50172038" />
+ </sodipodi:namedview>
+ <g
+ style="display:inline"
+ transform="rotate(90,-56.008515,534.91126)"
+ 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 H 344.125 c -1.9944,0 -3.59375,1.59935 -3.59375,3.59375 V 516 L 338,545.125 c -0.1873,2.15512 1.62589,3.92035 3.75,4.125 l 4.625,10.90625 h 1.53125 v 2.15625 l 3.61536,8.57242 1.18546,0.0214 0.44918,3.78119 0.33938,-3.7414 1.14797,-0.0687 3.76265,-8.53366 v -2.1875 h 1.53125 l 4.65625,-10.96875 c 1.96694,-0.35188 3.54637,-2.02216 3.40625,-4.0625 L 365.53125,516 V 399.59375 c 0,-1.9944 -1.59935,-3.59375 -3.59375,-3.59375 h -1.40625 L 360,389.47786 c -0.23272,-2.85711 -1.26201,-4.51323 -3.69531,-4.51323 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;display:inline;overflow:visible;visibility:visible;fill:#eeeeec;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"
+ 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;display:inline;overflow:visible;visibility:visible;fill:#eeeeec;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" />
+ </g>
+</svg>
diff --git a/panels/wacom/wacom-tablet-cintiq.svg b/panels/wacom/wacom-tablet-cintiq.svg
new file mode 100644
index 0000000..42d9005
--- /dev/null
+++ b/panels/wacom/wacom-tablet-cintiq.svg
@@ -0,0 +1,86 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ id="svg86858"
+ version="1.1"
+ inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
+ width="117.00018"
+ height="91.995674"
+ sodipodi:docname="wacom-tablet-cintiq.svg"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <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="1080"
+ inkscape:window-height="651"
+ id="namedview86860"
+ showgrid="false"
+ borderlayer="true"
+ inkscape:showpageshadow="false"
+ inkscape:zoom="1"
+ inkscape:cx="-127.5"
+ inkscape:cy="-18"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="svg86858"
+ inkscape:snap-nodes="false"
+ inkscape:snap-bbox="true"
+ inkscape:pagecheckerboard="0">
+ <inkscape:grid
+ type="xygrid"
+ id="grid86892"
+ empspacing="5"
+ visible="true"
+ enabled="true"
+ snapvisiblegridlinesonly="true"
+ originx="-3.0021704"
+ originy="-2.0021639" />
+ </sodipodi:namedview>
+ <g
+ style="display:inline;enable-background:new"
+ transform="matrix(0.68936116,0,0,0.68936115,462.30735,-77.493976)"
+ id="g4353-3">
+ <path
+ inkscape:connector-curvature="0"
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:#d3d7cf;fill-opacity:1;fill-rule:nonzero;stroke:#babdb6;stroke-width:1.45062px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"
+ d="m 191,253 v 122 h 30.45602 v 10 h 20.32523 v -10 h 65.25887 v 10 h 20.4159 v -10 h 31.81597 V 253 Z"
+ transform="translate(-860.90625,-139.8605)"
+ id="rect22528"
+ sodipodi:nodetypes="ccccccccccccc" />
+ <rect
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:#babdb6;fill-opacity:1;fill-rule:nonzero;stroke:#babdb6;stroke-width:1.45062;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;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..fd523eb
--- /dev/null
+++ b/panels/wacom/wacom-tablet-pc.svg
@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ id="svg86858"
+ version="1.1"
+ inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
+ width="117.33909"
+ height="85.952255"
+ sodipodi:docname="wacom-tablet-pc.svg"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <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="1080"
+ inkscape:window-height="651"
+ id="namedview86860"
+ showgrid="false"
+ borderlayer="true"
+ inkscape:showpageshadow="false"
+ inkscape:zoom="1"
+ inkscape:cx="214.5"
+ inkscape:cy="-26"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="svg86858"
+ inkscape:snap-nodes="false"
+ inkscape:snap-bbox="true"
+ inkscape:pagecheckerboard="0">
+ <inkscape:grid
+ type="xygrid"
+ id="grid86892"
+ empspacing="5"
+ visible="true"
+ enabled="true"
+ snapvisiblegridlinesonly="true"
+ originx="-3.7432745"
+ originy="-4.9999999" />
+ </sodipodi:namedview>
+ <path
+ 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"
+ d="M 13.793082,0.49999997 V 60.214196 L 0.81922544,85.452254 H 116.49285 l -14.68849,-26.666627 -0.012,-58.28562703 z"
+ id="rect22528"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccccc" />
+ <rect
+ y="9.5"
+ x="24.756723"
+ height="42"
+ width="66"
+ id="rect4133-4"
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:#babdb6;fill-opacity:1;fill-rule:nonzero;stroke:#babdb6;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate" />
+</svg>
diff --git a/panels/wacom/wacom-tablet.svg b/panels/wacom/wacom-tablet.svg
new file mode 100644
index 0000000..270144f
--- /dev/null
+++ b/panels/wacom/wacom-tablet.svg
@@ -0,0 +1,84 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ id="svg86858"
+ version="1.1"
+ inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
+ width="112.47625"
+ height="87.658791"
+ sodipodi:docname="wacom-tablet.svg"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <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="1080"
+ inkscape:window-height="651"
+ id="namedview86860"
+ showgrid="false"
+ borderlayer="true"
+ inkscape:showpageshadow="false"
+ inkscape:zoom="1.4142136"
+ inkscape:cx="39.244425"
+ inkscape:cy="86.974132"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="g4353-0"
+ inkscape:pagecheckerboard="0">
+ <inkscape:grid
+ type="xygrid"
+ id="grid86892"
+ empspacing="5"
+ visible="true"
+ enabled="true"
+ snapvisiblegridlinesonly="true"
+ originx="-3.027458"
+ originy="-2.8750015" />
+ </sodipodi:namedview>
+ <g
+ style="display:inline"
+ transform="matrix(0.59415025,0,0,0.67531282,402.41987,-76.934106)"
+ 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;display:inline;overflow:visible;visibility:visible;fill:#babdb6;fill-opacity:1;fill-rule:nonzero;stroke:#babdb6;stroke-width:1.5787;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;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..1aa4df2
--- /dev/null
+++ b/panels/wacom/wacom.gresource.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+ <gresource prefix="/org/gnome/control-center/wacom">
+ <file preprocess="xml-stripblanks">cc-wacom-panel.ui</file>
+ <file preprocess="xml-stripblanks">cc-wacom-page.ui</file>
+ <file preprocess="xml-stripblanks">cc-wacom-ekr-page.ui</file>
+ <file preprocess="xml-stripblanks">cc-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>