/* * 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 . * * Authors: Peter Hutterer * Bastien Nocera * */ #include #include #include #include #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 #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; }