/* Touch and Drawing Tablets * * Demonstrates advanced handling of event information from exotic * input devices. * * On one hand, this snippet demonstrates management of drawing tablets, * those contain additional information for the pointer other than * X/Y coordinates. Tablet pads events are mapped to actions, which * are both defined and interpreted by the application. * * Input axes are dependent on hardware devices, on linux/unix you * can see the device axes through xinput list . Each time * a different hardware device is used to move the pointer, the * master device will be updated to match the axes it provides, * these changes can be tracked through GdkDevice::changed, or * checking gdk_event_get_source_device(). * * On the other hand, this demo handles basic multitouch events, * each event coming from an specific touchpoint will contain a * GdkEventSequence that's unique for its lifetime, so multiple * touchpoints can be tracked. */ #include #include typedef struct { GdkDevice *last_source; GdkDeviceTool *last_tool; gdouble *axes; GdkRGBA color; gdouble x; gdouble y; } AxesInfo; typedef struct { GHashTable *pointer_info; /* GdkDevice -> AxesInfo */ GHashTable *touch_info; /* GdkEventSequence -> AxesInfo */ } EventData; const gchar *colors[] = { "black", "orchid", "fuchsia", "indigo", "thistle", "sienna", "azure", "plum", "lime", "navy", "maroon", "burlywood" }; static GtkPadActionEntry pad_actions[] = { { GTK_PAD_ACTION_BUTTON, 1, -1, N_("Nuclear strike"), "pad.nuke" }, { GTK_PAD_ACTION_BUTTON, 2, -1, N_("Release siberian methane reserves"), "pad.heat" }, { GTK_PAD_ACTION_BUTTON, 3, -1, N_("Release solar flare"), "pad.fry" }, { GTK_PAD_ACTION_BUTTON, 4, -1, N_("De-stabilize Oort cloud"), "pad.fall" }, { GTK_PAD_ACTION_BUTTON, 5, -1, N_("Ignite WR-104"), "pad.burst" }, { GTK_PAD_ACTION_BUTTON, 6, -1, N_("Lart whoever asks about this button"), "pad.lart" }, { GTK_PAD_ACTION_RING, -1, -1, N_("Earth axial tilt"), "pad.tilt" }, { GTK_PAD_ACTION_STRIP, -1, -1, N_("Extent of weak nuclear force"), "pad.dissolve" }, }; static const gchar *pad_action_results[] = { "☢", "♨", "☼", "☄", "⚡", "💫", "◑", "⚛" }; static guint cur_color = 0; static guint pad_action_timeout_id = 0; static AxesInfo * axes_info_new (void) { AxesInfo *info; info = g_new0 (AxesInfo, 1); gdk_rgba_parse (&info->color, colors[cur_color]); cur_color = (cur_color + 1) % G_N_ELEMENTS (colors); return info; } static EventData * event_data_new (void) { EventData *data; data = g_new0 (EventData, 1); data->pointer_info = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify) g_free); data->touch_info = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify) g_free); return data; } static void event_data_free (EventData *data) { g_hash_table_destroy (data->pointer_info); g_hash_table_destroy (data->touch_info); g_free (data); } static void update_axes_from_event (GdkEvent *event, EventData *data) { GdkDevice *device, *source_device; GdkEventSequence *sequence; GdkDeviceTool *tool; gdouble x, y; AxesInfo *info; device = gdk_event_get_device (event); source_device = gdk_event_get_source_device (event); sequence = gdk_event_get_event_sequence (event); tool = gdk_event_get_device_tool (event); if (event->type == GDK_TOUCH_END || event->type == GDK_TOUCH_CANCEL) { g_hash_table_remove (data->touch_info, sequence); return; } else if (event->type == GDK_LEAVE_NOTIFY) { g_hash_table_remove (data->pointer_info, device); return; } if (!sequence) { info = g_hash_table_lookup (data->pointer_info, device); if (!info) { info = axes_info_new (); g_hash_table_insert (data->pointer_info, device, info); } } else { info = g_hash_table_lookup (data->touch_info, sequence); if (!info) { info = axes_info_new (); g_hash_table_insert (data->touch_info, sequence, info); } } if (info->last_source != source_device) info->last_source = source_device; if (info->last_tool != tool) info->last_tool = tool; g_clear_pointer (&info->axes, g_free); if (event->type == GDK_TOUCH_BEGIN || event->type == GDK_TOUCH_UPDATE) { if (sequence && event->touch.emulating_pointer) g_hash_table_remove (data->pointer_info, device); } if (event->type == GDK_MOTION_NOTIFY) { info->axes = g_memdup (event->motion.axes, sizeof (gdouble) * gdk_device_get_n_axes (source_device)); } else if (event->type == GDK_BUTTON_PRESS || event->type == GDK_BUTTON_RELEASE) { info->axes = g_memdup (event->button.axes, sizeof (gdouble) * gdk_device_get_n_axes (source_device)); } if (gdk_event_get_coords (event, &x, &y)) { info->x = x; info->y = y; } } static gboolean event_cb (GtkWidget *widget, GdkEvent *event, gpointer user_data) { update_axes_from_event (event, user_data); gtk_widget_queue_draw (widget); return FALSE; } static void render_arrow (cairo_t *cr, gdouble x_diff, gdouble y_diff, const gchar *label) { cairo_save (cr); cairo_set_source_rgb (cr, 0, 0, 0); cairo_new_path (cr); cairo_move_to (cr, 0, 0); cairo_line_to (cr, x_diff, y_diff); cairo_stroke (cr); cairo_move_to (cr, x_diff, y_diff); cairo_show_text (cr, label); cairo_restore (cr); } static void draw_axes_info (cairo_t *cr, AxesInfo *info, GtkAllocation *allocation) { gdouble pressure, tilt_x, tilt_y, distance, wheel, rotation, slider; GdkAxisFlags axes = gdk_device_get_axes (info->last_source); cairo_save (cr); cairo_set_line_width (cr, 1); gdk_cairo_set_source_rgba (cr, &info->color); cairo_move_to (cr, 0, info->y); cairo_line_to (cr, allocation->width, info->y); cairo_move_to (cr, info->x, 0); cairo_line_to (cr, info->x, allocation->height); cairo_stroke (cr); cairo_translate (cr, info->x, info->y); if (!info->axes) { cairo_restore (cr); return; } if (axes & GDK_AXIS_FLAG_PRESSURE) { cairo_pattern_t *pattern; gdk_device_get_axis (info->last_source, info->axes, GDK_AXIS_PRESSURE, &pressure); pattern = cairo_pattern_create_radial (0, 0, 0, 0, 0, 100); cairo_pattern_add_color_stop_rgba (pattern, pressure, 1, 0, 0, pressure); cairo_pattern_add_color_stop_rgba (pattern, 1, 0, 0, 1, 0); cairo_set_source (cr, pattern); cairo_arc (cr, 0, 0, 100, 0, 2 * G_PI); cairo_fill (cr); cairo_pattern_destroy (pattern); } if (axes & GDK_AXIS_FLAG_XTILT && axes & GDK_AXIS_FLAG_YTILT) { gdk_device_get_axis (info->last_source, info->axes, GDK_AXIS_XTILT, &tilt_x); gdk_device_get_axis (info->last_source, info->axes, GDK_AXIS_YTILT, &tilt_y); render_arrow (cr, tilt_x * 100, tilt_y * 100, "Tilt"); } if (axes & GDK_AXIS_FLAG_DISTANCE) { double dashes[] = { 5.0, 5.0 }; cairo_text_extents_t extents; gdk_device_get_axis (info->last_source, info->axes, GDK_AXIS_DISTANCE, &distance); cairo_save (cr); cairo_move_to (cr, distance * 100, 0); cairo_set_source_rgb (cr, 0.0, 0.0, 0.0); cairo_set_dash (cr, dashes, 2, 0.0); cairo_arc (cr, 0, 0, distance * 100, 0, 2 * G_PI); cairo_stroke (cr); cairo_move_to (cr, 0, -distance * 100); cairo_text_extents (cr, "Distance", &extents); cairo_rel_move_to (cr, -extents.width / 2, 0); cairo_show_text (cr, "Distance"); cairo_move_to (cr, 0, 0); cairo_restore (cr); } if (axes & GDK_AXIS_FLAG_WHEEL) { gdk_device_get_axis (info->last_source, info->axes, GDK_AXIS_WHEEL, &wheel); cairo_save (cr); cairo_set_line_width (cr, 10); cairo_set_source_rgba (cr, 0, 0, 0, 0.5); cairo_new_sub_path (cr); cairo_arc (cr, 0, 0, 100, 0, wheel * 2 * G_PI); cairo_stroke (cr); cairo_restore (cr); } if (axes & GDK_AXIS_FLAG_ROTATION) { gdk_device_get_axis (info->last_source, info->axes, GDK_AXIS_ROTATION, &rotation); rotation *= 2 * G_PI; cairo_save (cr); cairo_rotate (cr, - G_PI / 2); cairo_set_line_cap (cr, CAIRO_LINE_CAP_ROUND); cairo_set_line_width (cr, 5); cairo_new_sub_path (cr); cairo_arc (cr, 0, 0, 100, 0, rotation); cairo_stroke (cr); cairo_restore (cr); } if (axes & GDK_AXIS_FLAG_SLIDER) { cairo_pattern_t *pattern, *mask; gdk_device_get_axis (info->last_source, info->axes, GDK_AXIS_SLIDER, &slider); cairo_save (cr); cairo_move_to (cr, 0, -10); cairo_rel_line_to (cr, 0, -50); cairo_rel_line_to (cr, 10, 0); cairo_rel_line_to (cr, -5, 50); cairo_close_path (cr); cairo_clip_preserve (cr); pattern = cairo_pattern_create_linear (0, -10, 0, -60); cairo_pattern_add_color_stop_rgb (pattern, 0, 0, 1, 0); cairo_pattern_add_color_stop_rgb (pattern, 1, 1, 0, 0); cairo_set_source (cr, pattern); cairo_pattern_destroy (pattern); mask = cairo_pattern_create_linear (0, -10, 0, -60); cairo_pattern_add_color_stop_rgba (mask, 0, 0, 0, 0, 1); cairo_pattern_add_color_stop_rgba (mask, slider, 0, 0, 0, 1); cairo_pattern_add_color_stop_rgba (mask, slider, 0, 0, 0, 0); cairo_pattern_add_color_stop_rgba (mask, 1, 0, 0, 0, 0); cairo_mask (cr, mask); cairo_pattern_destroy (mask); cairo_set_source_rgb (cr, 0, 0, 0); cairo_stroke (cr); cairo_restore (cr); } cairo_restore (cr); } static const gchar * tool_type_to_string (GdkDeviceToolType tool_type) { switch (tool_type) { case GDK_DEVICE_TOOL_TYPE_PEN: return "Pen"; case GDK_DEVICE_TOOL_TYPE_ERASER: return "Eraser"; case GDK_DEVICE_TOOL_TYPE_BRUSH: return "Brush"; case GDK_DEVICE_TOOL_TYPE_PENCIL: return "Pencil"; case GDK_DEVICE_TOOL_TYPE_AIRBRUSH: return "Airbrush"; case GDK_DEVICE_TOOL_TYPE_MOUSE: return "Mouse"; case GDK_DEVICE_TOOL_TYPE_LENS: return "Lens cursor"; case GDK_DEVICE_TOOL_TYPE_UNKNOWN: default: return "Unknown"; } } static void draw_device_info (GtkWidget *widget, cairo_t *cr, GdkEventSequence *sequence, gint *y, AxesInfo *info) { PangoLayout *layout; GString *string; gint height; cairo_save (cr); string = g_string_new (NULL); g_string_append_printf (string, "Source: %s", gdk_device_get_name (info->last_source)); if (sequence) g_string_append_printf (string, "\nSequence: %d", GPOINTER_TO_UINT (sequence)); if (info->last_tool) { const gchar *tool_type; guint64 serial; tool_type = tool_type_to_string (gdk_device_tool_get_tool_type (info->last_tool)); serial = gdk_device_tool_get_serial (info->last_tool); g_string_append_printf (string, "\nTool: %s", tool_type); if (serial != 0) g_string_append_printf (string, ", Serial: %lx", serial); } cairo_move_to (cr, 10, *y); layout = gtk_widget_create_pango_layout (widget, string->str); pango_cairo_show_layout (cr, layout); cairo_stroke (cr); pango_layout_get_pixel_size (layout, NULL, &height); gdk_cairo_set_source_rgba (cr, &info->color); cairo_set_line_width (cr, 10); cairo_move_to (cr, 0, *y); *y = *y + height; cairo_line_to (cr, 0, *y); cairo_stroke (cr); cairo_restore (cr); g_object_unref (layout); g_string_free (string, TRUE); } static gboolean draw_cb (GtkWidget *widget, cairo_t *cr, gpointer user_data) { EventData *data = user_data; GtkAllocation allocation; AxesInfo *info; GHashTableIter iter; gpointer key, value; gint y = 0; gtk_widget_get_allocation (widget, &allocation); /* Draw Abs info */ g_hash_table_iter_init (&iter, data->pointer_info); while (g_hash_table_iter_next (&iter, NULL, &value)) { info = value; draw_axes_info (cr, info, &allocation); } g_hash_table_iter_init (&iter, data->touch_info); while (g_hash_table_iter_next (&iter, NULL, &value)) { info = value; draw_axes_info (cr, info, &allocation); } /* Draw name, color legend and misc data */ g_hash_table_iter_init (&iter, data->pointer_info); while (g_hash_table_iter_next (&iter, NULL, &value)) { info = value; draw_device_info (widget, cr, NULL, &y, info); } g_hash_table_iter_init (&iter, data->touch_info); while (g_hash_table_iter_next (&iter, &key, &value)) { info = value; draw_device_info (widget, cr, key, &y, info); } return FALSE; } static void update_label_text (GtkWidget *label, const gchar *text) { gchar *markup = NULL; if (text) markup = g_strdup_printf ("%s", text); gtk_label_set_markup (GTK_LABEL (label), markup); g_free (markup); } static gboolean reset_label_text_timeout_cb (gpointer user_data) { GtkWidget *label = user_data; update_label_text (label, NULL); pad_action_timeout_id = 0; return G_SOURCE_REMOVE; } static void update_label_and_timeout (GtkWidget *label, const gchar *text) { if (pad_action_timeout_id) g_source_remove (pad_action_timeout_id); update_label_text (label, text); pad_action_timeout_id = g_timeout_add (200, reset_label_text_timeout_cb, label); } static void on_action_activate (GSimpleAction *action, GVariant *parameter, gpointer user_data) { GtkWidget *label = user_data; const gchar *result; gchar *str; result = g_object_get_data (G_OBJECT (action), "action-result"); if (!parameter) update_label_and_timeout (label, result); else { str = g_strdup_printf ("%s %.2f", result, g_variant_get_double (parameter)); update_label_and_timeout (label, str); g_free (str); } } static void init_pad_controller (GtkWidget *window, GtkWidget *label) { GtkPadController *pad_controller; GSimpleActionGroup *action_group; GSimpleAction *action; gint i; action_group = g_simple_action_group_new (); pad_controller = gtk_pad_controller_new (GTK_WINDOW (window), G_ACTION_GROUP (action_group), NULL); for (i = 0; i < G_N_ELEMENTS (pad_actions); i++) { if (pad_actions[i].type == GTK_PAD_ACTION_BUTTON) { action = g_simple_action_new (pad_actions[i].action_name, NULL); } else { action = g_simple_action_new_stateful (pad_actions[i].action_name, G_VARIANT_TYPE_DOUBLE, NULL); } g_signal_connect (action, "activate", G_CALLBACK (on_action_activate), label); g_object_set_data (G_OBJECT (action), "action-result", (gpointer) pad_action_results[i]); g_action_map_add_action (G_ACTION_MAP (action_group), G_ACTION (action)); g_object_unref (action); } gtk_pad_controller_set_action_entries (pad_controller, pad_actions, G_N_ELEMENTS (pad_actions)); g_object_set_data_full (G_OBJECT (window), "pad-controller", pad_controller, g_object_unref); g_object_unref (action_group); } GtkWidget * do_event_axes (GtkWidget *toplevel) { static GtkWidget *window = NULL; EventData *event_data; GtkWidget *box, *label; if (!window) { window = gtk_window_new (GTK_WINDOW_TOPLEVEL); gtk_window_set_title (GTK_WINDOW (window), "Event Axes"); gtk_window_set_default_size (GTK_WINDOW (window), 400, 400); g_signal_connect (window, "destroy", G_CALLBACK (gtk_widget_destroyed), &window); box = gtk_event_box_new (); gtk_container_add (GTK_CONTAINER (window), box); gtk_widget_set_support_multidevice (box, TRUE); gtk_widget_add_events (box, GDK_POINTER_MOTION_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_SMOOTH_SCROLL_MASK | GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK | GDK_TOUCH_MASK); event_data = event_data_new (); g_object_set_data_full (G_OBJECT (box), "gtk-demo-event-data", event_data, (GDestroyNotify) event_data_free); g_signal_connect (box, "event", G_CALLBACK (event_cb), event_data); g_signal_connect (box, "draw", G_CALLBACK (draw_cb), event_data); label = gtk_label_new (""); gtk_label_set_use_markup (GTK_LABEL (label), TRUE); gtk_container_add (GTK_CONTAINER (box), label); init_pad_controller (window, label); } if (!gtk_widget_get_visible (window)) gtk_widget_show_all (window); else gtk_widget_destroy (window); return window; }