A GList doesn't make much sense for storing guints, so now a GArray is used internally. gdk_touch_cluster_get_touches() has changed to return an allocated guint*, and gdk_touch_cluster_get_n_touches() has been added too.
699 lines
17 KiB
C
699 lines
17 KiB
C
/* Multitouch
|
|
*
|
|
* Demonstrates some general multitouch event handling,
|
|
* using GdkTouchCluster in order to get grouped motion
|
|
* events for the touches within a cluster. Each of the
|
|
* created rectangles has one of those GdkTouchCluster
|
|
* objects.
|
|
*
|
|
* Touch events are also enabled on additional widgets,
|
|
* enabling simultaneous touch interaction on those. Not
|
|
* all widgets are prepared for multitouch interaction,
|
|
* as there are constraints that not all widgets may
|
|
* apply to.
|
|
*/
|
|
|
|
#include <math.h>
|
|
#include <gtk/gtk.h>
|
|
#include "demo-common.h"
|
|
|
|
#define RECT_BORDER_WIDTH 6
|
|
|
|
static GtkWidget *window = NULL;
|
|
static GtkWidget *area = NULL;
|
|
static GtkWidget *red = NULL;
|
|
static GtkWidget *green = NULL;
|
|
static GtkWidget *blue = NULL;
|
|
static GtkWidget *alpha = NULL;
|
|
|
|
static GQueue *shapes = NULL;
|
|
|
|
typedef struct {
|
|
GdkTouchCluster *cluster;
|
|
GdkRGBA color;
|
|
|
|
gdouble angle;
|
|
gdouble zoom;
|
|
|
|
gdouble center_x;
|
|
gdouble center_y;
|
|
|
|
gdouble x;
|
|
gdouble y;
|
|
gdouble width;
|
|
gdouble height;
|
|
|
|
gdouble base_zoom;
|
|
gdouble base_angle;
|
|
gdouble initial_distance;
|
|
gdouble initial_angle;
|
|
|
|
GdkPoint points[4];
|
|
} ShapeInfo;
|
|
|
|
static void
|
|
calculate_rotated_point (gdouble angle,
|
|
gdouble zoom,
|
|
gdouble center_x,
|
|
gdouble center_y,
|
|
gdouble point_x,
|
|
gdouble point_y,
|
|
gdouble *ret_x,
|
|
gdouble *ret_y)
|
|
{
|
|
gdouble distance, xd, yd, ang;
|
|
|
|
if (angle == 0)
|
|
{
|
|
*ret_x = point_x;
|
|
*ret_y = point_y;
|
|
return;
|
|
}
|
|
|
|
xd = center_x - point_x;
|
|
yd = center_y - point_y;
|
|
|
|
if (xd == 0 && yd == 0)
|
|
{
|
|
*ret_x = center_x;
|
|
*ret_y = center_y;
|
|
return;
|
|
}
|
|
|
|
distance = sqrt ((xd * xd) + (yd * yd));
|
|
distance *= zoom;
|
|
|
|
ang = atan2 (xd, yd);
|
|
|
|
/* Invert angle */
|
|
ang = (2 * G_PI) - ang;
|
|
|
|
/* Shift it 270° */
|
|
ang += 3 * (G_PI / 2);
|
|
|
|
/* And constraint it to 0°-360° */
|
|
ang = fmod (ang, 2 * G_PI);
|
|
ang += angle;
|
|
|
|
*ret_x = center_x + (distance * cos (ang));
|
|
*ret_y = center_y + (distance * sin (ang));
|
|
}
|
|
|
|
static void
|
|
shape_info_allocate_input_rect (ShapeInfo *info)
|
|
{
|
|
gint width, height, i;
|
|
|
|
width = info->width;
|
|
height = info->height;
|
|
|
|
/* Top/left */
|
|
info->points[0].x = info->x - info->center_x;
|
|
info->points[0].y = info->y - info->center_y;
|
|
|
|
/* Top/right */
|
|
info->points[1].x = info->x - info->center_x + width;
|
|
info->points[1].y = info->y - info->center_y;
|
|
|
|
/* Bottom/right */
|
|
info->points[2].x = info->x - info->center_x + width;
|
|
info->points[2].y = info->y - info->center_y + height;
|
|
|
|
/* Bottom/left */
|
|
info->points[3].x = info->x - info->center_x;
|
|
info->points[3].y = info->y - info->center_y + height;
|
|
|
|
for (i = 0; i < 4; i++)
|
|
{
|
|
gdouble ret_x, ret_y;
|
|
|
|
calculate_rotated_point (info->angle,
|
|
info->zoom,
|
|
info->x,
|
|
info->y,
|
|
(gdouble) info->points[i].x,
|
|
(gdouble) info->points[i].y,
|
|
&ret_x,
|
|
&ret_y);
|
|
|
|
info->points[i].x = (gint) ret_x;
|
|
info->points[i].y = (gint) ret_y;
|
|
}
|
|
}
|
|
|
|
static void
|
|
shape_info_bounding_rect (ShapeInfo *info,
|
|
GdkRectangle *rect)
|
|
{
|
|
gint i, left, right, top, bottom;
|
|
|
|
left = top = G_MAXINT;
|
|
right = bottom = 0;
|
|
|
|
for (i = 0; i < 4; i++)
|
|
{
|
|
if (info->points[i].x < left)
|
|
left = info->points[i].x;
|
|
|
|
if (info->points[i].x > right)
|
|
right = info->points[i].x;
|
|
|
|
if (info->points[i].y < top)
|
|
top = info->points[i].y;
|
|
|
|
if (info->points[i].y > bottom)
|
|
bottom = info->points[i].y;
|
|
}
|
|
|
|
rect->x = left - 20;
|
|
rect->y = top - 20;
|
|
rect->width = right - left + 40;
|
|
rect->height = bottom - top + 40;
|
|
}
|
|
|
|
static gboolean
|
|
shape_info_point_in (ShapeInfo *info,
|
|
gint x,
|
|
gint y)
|
|
{
|
|
GdkPoint *left, *right, *top, *bottom;
|
|
gint i;
|
|
|
|
left = right = top = bottom = NULL;
|
|
|
|
for (i = 0; i < 4; i++)
|
|
{
|
|
GdkPoint *p = &info->points[i];
|
|
|
|
if (!left ||
|
|
p->x < left->x ||
|
|
(p->x == left->x && p->y > left->y))
|
|
left = p;
|
|
|
|
if (!right ||
|
|
p->x > right->x ||
|
|
(p->x == right->x && p->y < right->y))
|
|
right = p;
|
|
}
|
|
|
|
for (i = 0; i < 4; i++)
|
|
{
|
|
GdkPoint *p = &info->points[i];
|
|
|
|
if (p == left || p == right)
|
|
continue;
|
|
|
|
if (!top ||
|
|
p->y < top->y)
|
|
top = p;
|
|
|
|
if (!bottom ||
|
|
p->y > bottom->y)
|
|
bottom = p;
|
|
}
|
|
|
|
g_assert (left && right && top && bottom);
|
|
|
|
if (x < left->x ||
|
|
x > right->x ||
|
|
y < top->y ||
|
|
y > bottom->y)
|
|
return FALSE;
|
|
|
|
/* Check whether point is above the sides
|
|
* between leftmost and topmost, and
|
|
* topmost and rightmost corners.
|
|
*/
|
|
if (x <= top->x)
|
|
{
|
|
if (left->y - ((left->y - top->y) * (((gdouble) x - left->x) / (top->x - left->x))) > y)
|
|
return FALSE;
|
|
}
|
|
else
|
|
{
|
|
if (top->y + ((right->y - top->y) * (((gdouble) x - top->x) / (right->x - top->x))) > y)
|
|
return FALSE;
|
|
}
|
|
|
|
/* Check whether point is below the sides
|
|
* between leftmost and bottom, and
|
|
* bottom and rightmost corners.
|
|
*/
|
|
if (x <= bottom->x)
|
|
{
|
|
if (left->y + ((bottom->y - left->y) * (((gdouble) x - left->x) / (bottom->x - left->x))) < y)
|
|
return FALSE;
|
|
}
|
|
else
|
|
{
|
|
if (bottom->y - ((bottom->y - right->y) * (((gdouble) x - bottom->x) / (right->x - bottom->x))) < y)
|
|
return FALSE;
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static ShapeInfo *
|
|
shape_info_new (gdouble x,
|
|
gdouble y,
|
|
gdouble width,
|
|
gdouble height,
|
|
GdkRGBA *color)
|
|
{
|
|
ShapeInfo *info;
|
|
|
|
info = g_slice_new0 (ShapeInfo);
|
|
info->cluster = NULL;
|
|
info->color = *color;
|
|
|
|
info->x = x;
|
|
info->y = y;
|
|
info->width = width;
|
|
info->height = height;
|
|
|
|
info->angle = 0;
|
|
info->zoom = 1;
|
|
|
|
info->base_zoom = 1;
|
|
info->base_angle = 0;
|
|
info->initial_distance = 0;
|
|
info->initial_angle = 0;
|
|
|
|
shape_info_allocate_input_rect (info);
|
|
|
|
g_queue_push_tail (shapes, info);
|
|
|
|
return info;
|
|
}
|
|
|
|
static void
|
|
shape_info_free (ShapeInfo *info)
|
|
{
|
|
g_slice_free (ShapeInfo, info);
|
|
}
|
|
|
|
static void
|
|
shape_info_draw (cairo_t *cr,
|
|
ShapeInfo *info)
|
|
{
|
|
cairo_save (cr);
|
|
|
|
cairo_translate (cr,
|
|
info->points[0].x + RECT_BORDER_WIDTH / 2,
|
|
info->points[0].y + RECT_BORDER_WIDTH / 2);
|
|
|
|
cairo_scale (cr, info->zoom, info->zoom);
|
|
cairo_rotate (cr, info->angle);
|
|
|
|
cairo_rectangle (cr, 0, 0,
|
|
info->width - RECT_BORDER_WIDTH,
|
|
info->height - RECT_BORDER_WIDTH);
|
|
gdk_cairo_set_source_rgba (cr, &info->color);
|
|
cairo_fill_preserve (cr);
|
|
|
|
cairo_set_line_width (cr, RECT_BORDER_WIDTH);
|
|
cairo_set_source_rgb (cr, 0, 0, 0);
|
|
cairo_stroke (cr);
|
|
|
|
cairo_restore (cr);
|
|
}
|
|
|
|
static void
|
|
shape_update_scales (ShapeInfo *info)
|
|
{
|
|
gtk_range_set_value (GTK_RANGE (red), info->color.red);
|
|
gtk_range_set_value (GTK_RANGE (green), info->color.green);
|
|
gtk_range_set_value (GTK_RANGE (blue), info->color.blue);
|
|
gtk_range_set_value (GTK_RANGE (alpha), info->color.alpha);
|
|
}
|
|
|
|
static void
|
|
range_value_changed_cb (GtkRange *range,
|
|
gpointer user_data)
|
|
{
|
|
GtkWidget *widget;
|
|
GdkRectangle rect;
|
|
ShapeInfo *shape;
|
|
gdouble value;
|
|
|
|
widget = GTK_WIDGET (range);
|
|
shape = g_queue_peek_head (shapes);
|
|
|
|
if (!shape)
|
|
return;
|
|
|
|
value = gtk_range_get_value (range);
|
|
|
|
if (widget == red)
|
|
shape->color.red = value;
|
|
else if (widget == green)
|
|
shape->color.green = value;
|
|
else if (widget == blue)
|
|
shape->color.blue = value;
|
|
else if (widget == alpha)
|
|
shape->color.alpha = value;
|
|
|
|
shape_info_bounding_rect (shape, &rect);
|
|
gdk_window_invalidate_rect (gtk_widget_get_window (area),
|
|
&rect, FALSE);
|
|
}
|
|
|
|
static gboolean
|
|
draw_cb (GtkWidget *widget,
|
|
cairo_t *cr,
|
|
gpointer user_data)
|
|
{
|
|
GList *l;
|
|
|
|
cairo_save (cr);
|
|
|
|
cairo_set_source_rgb (cr, 1, 1, 1);
|
|
cairo_paint (cr);
|
|
|
|
for (l = shapes->tail; l; l = l->prev)
|
|
shape_info_draw (cr, l->data);
|
|
|
|
cairo_restore (cr);
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
static gboolean
|
|
button_press_cb (GtkWidget *widget,
|
|
GdkEvent *event,
|
|
gpointer user_data)
|
|
{
|
|
ShapeInfo *shape = NULL;
|
|
guint touch_id;
|
|
|
|
if (gdk_event_get_touch_id (event, &touch_id))
|
|
{
|
|
GList *l;
|
|
|
|
for (l = shapes->tail; l; l = l->prev)
|
|
{
|
|
ShapeInfo *info = l->data;
|
|
|
|
if (shape_info_point_in (info,
|
|
(gint) event->button.x,
|
|
(gint) event->button.y))
|
|
shape = info;
|
|
}
|
|
|
|
if (!shape)
|
|
return FALSE;
|
|
|
|
/* Put on top */
|
|
g_queue_remove (shapes, shape);
|
|
g_queue_push_head (shapes, shape);
|
|
|
|
shape_update_scales (shape);
|
|
|
|
if (!shape->cluster)
|
|
shape->cluster = gdk_window_create_touch_cluster (gtk_widget_get_window (widget),
|
|
gdk_event_get_device (event));
|
|
else if (gdk_touch_cluster_get_n_touches (shape->cluster) == 0)
|
|
{
|
|
/* Only change cluster device if there were no touches */
|
|
gdk_touch_cluster_set_device (shape->cluster,
|
|
gdk_event_get_device (event));
|
|
}
|
|
|
|
gdk_touch_cluster_add_touch (shape->cluster, touch_id);
|
|
return TRUE;
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
static gboolean
|
|
multitouch_cb (GtkWidget *widget,
|
|
GdkEvent *event,
|
|
gpointer user_data)
|
|
{
|
|
ShapeInfo *info = NULL;
|
|
gboolean new_center = FALSE;
|
|
gboolean new_position = FALSE;
|
|
gdouble event_x, event_y;
|
|
cairo_region_t *region;
|
|
GdkRectangle rect;
|
|
GList *l;
|
|
|
|
for (l = shapes->head; l; l = l->next)
|
|
{
|
|
ShapeInfo *shape = l->data;
|
|
|
|
if (event->multitouch.group == shape->cluster)
|
|
{
|
|
info = shape;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!info)
|
|
return FALSE;
|
|
|
|
shape_info_bounding_rect (info, &rect);
|
|
region = cairo_region_create_rectangle ((cairo_rectangle_int_t *) &rect);
|
|
|
|
if (event->multitouch.n_events == 1)
|
|
{
|
|
/* Update center if we just got to
|
|
* this situation from either way */
|
|
if (event->type == GDK_MULTITOUCH_ADDED ||
|
|
event->type == GDK_MULTITOUCH_REMOVED)
|
|
new_center = TRUE;
|
|
|
|
event_x = event->multitouch.events[0]->x;
|
|
event_y = event->multitouch.events[0]->y;
|
|
new_position = TRUE;
|
|
}
|
|
else if (event->multitouch.n_events == 2)
|
|
{
|
|
gdouble distance, angle;
|
|
|
|
gdk_events_get_center ((GdkEvent *) event->multitouch.events[0],
|
|
(GdkEvent *) event->multitouch.events[1],
|
|
&event_x, &event_y);
|
|
|
|
gdk_events_get_distance ((GdkEvent *) event->multitouch.events[0],
|
|
(GdkEvent *) event->multitouch.events[1],
|
|
&distance);
|
|
|
|
gdk_events_get_angle ((GdkEvent *) event->multitouch.events[0],
|
|
(GdkEvent *) event->multitouch.events[1],
|
|
&angle);
|
|
|
|
if (event->type == GDK_MULTITOUCH_ADDED)
|
|
{
|
|
/* Second touch was just added, update base zoom/angle */
|
|
info->base_zoom = info->zoom;
|
|
info->base_angle = info->angle;
|
|
info->initial_angle = angle;
|
|
info->initial_distance = distance;
|
|
new_center = TRUE;
|
|
}
|
|
|
|
info->zoom = MAX (info->base_zoom * (distance / info->initial_distance), 1.0);
|
|
info->angle = info->base_angle + (angle - info->initial_angle);
|
|
new_position = TRUE;
|
|
}
|
|
|
|
if (new_center)
|
|
{
|
|
gdouble origin_x, origin_y;
|
|
|
|
origin_x = info->x - info->center_x;
|
|
origin_y = info->y - info->center_y;
|
|
|
|
calculate_rotated_point (- info->angle,
|
|
1 / info->zoom,
|
|
info->x - origin_x,
|
|
info->y - origin_y,
|
|
event_x - origin_x,
|
|
event_y - origin_y,
|
|
&info->center_x,
|
|
&info->center_y);
|
|
}
|
|
|
|
if (new_position)
|
|
{
|
|
info->x = event_x;
|
|
info->y = event_y;
|
|
}
|
|
|
|
shape_info_allocate_input_rect (info);
|
|
|
|
shape_info_bounding_rect (info, &rect);
|
|
cairo_region_union_rectangle (region, (cairo_rectangle_int_t *) &rect);
|
|
gdk_window_invalidate_region (gtk_widget_get_window (widget), region, FALSE);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static void
|
|
window_destroyed_cb (GtkWidget *widget,
|
|
gpointer user_data)
|
|
{
|
|
g_queue_foreach (shapes, (GFunc) shape_info_free, NULL);
|
|
g_queue_free (shapes);
|
|
|
|
shapes = NULL;
|
|
window = NULL;
|
|
}
|
|
|
|
static void
|
|
new_rectangle_clicked_cb (GtkButton *button,
|
|
gpointer user_data)
|
|
{
|
|
GdkRectangle rect;
|
|
ShapeInfo *info;
|
|
GdkRGBA color;
|
|
|
|
color.red = color.green = color.blue = color.alpha = 0.5;
|
|
info = shape_info_new (0, 0, 100, 150, &color);
|
|
|
|
shape_info_bounding_rect (info, &rect);
|
|
gdk_window_invalidate_rect (gtk_widget_get_window (area), &rect, FALSE);
|
|
}
|
|
|
|
GtkWidget *
|
|
create_drawing_area (void)
|
|
{
|
|
area = gtk_drawing_area_new ();
|
|
|
|
gtk_widget_add_events (area,
|
|
GDK_TOUCH_MASK |
|
|
GDK_POINTER_MOTION_MASK |
|
|
GDK_BUTTON_PRESS_MASK |
|
|
GDK_BUTTON_RELEASE_MASK);
|
|
|
|
gtk_widget_set_size_request (area, 600, 600);
|
|
|
|
g_signal_connect (area, "draw",
|
|
G_CALLBACK (draw_cb), NULL);
|
|
g_signal_connect (area, "button-press-event",
|
|
G_CALLBACK (button_press_cb), NULL);
|
|
g_signal_connect (area, "multitouch-event",
|
|
G_CALLBACK (multitouch_cb), NULL);
|
|
|
|
return area;
|
|
}
|
|
|
|
GtkWidget *
|
|
create_scale (void)
|
|
{
|
|
GtkWidget *scale;
|
|
|
|
scale = gtk_scale_new_with_range (GTK_ORIENTATION_VERTICAL, 0, 1, 0.01);
|
|
gtk_range_set_inverted (GTK_RANGE (scale), TRUE);
|
|
|
|
gtk_widget_set_vexpand (scale, TRUE);
|
|
gtk_widget_set_margin_left (scale, 15);
|
|
gtk_widget_set_margin_right (scale, 15);
|
|
|
|
gtk_widget_add_events (scale, GDK_TOUCH_MASK);
|
|
|
|
g_signal_connect (scale, "value-changed",
|
|
G_CALLBACK (range_value_changed_cb), NULL);
|
|
return scale;
|
|
}
|
|
|
|
GtkWidget *
|
|
create_window (void)
|
|
{
|
|
GtkWidget *grid, *label, *button;
|
|
|
|
window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
|
|
gtk_window_set_title (GTK_WINDOW (window), "Multitouch demo");
|
|
g_signal_connect (window, "destroy",
|
|
G_CALLBACK (window_destroyed_cb), NULL);
|
|
|
|
grid = gtk_grid_new ();
|
|
gtk_container_add (GTK_CONTAINER (window), grid);
|
|
|
|
area = create_drawing_area ();
|
|
gtk_grid_attach (GTK_GRID (grid),
|
|
area, 0, 0, 1, 3);
|
|
gtk_widget_set_hexpand (area, TRUE);
|
|
gtk_widget_set_vexpand (area, TRUE);
|
|
|
|
/* "red" label/scale */
|
|
label = gtk_label_new ("Red");
|
|
gtk_widget_set_vexpand (label, FALSE);
|
|
gtk_grid_attach (GTK_GRID (grid),
|
|
label, 1, 0, 1, 1);
|
|
|
|
red = create_scale ();
|
|
gtk_grid_attach (GTK_GRID (grid),
|
|
red, 1, 1, 1, 1);
|
|
|
|
/* "green" label/scale */
|
|
label = gtk_label_new ("Green");
|
|
gtk_widget_set_vexpand (label, FALSE);
|
|
gtk_grid_attach (GTK_GRID (grid),
|
|
label, 2, 0, 1, 1);
|
|
|
|
green = create_scale ();
|
|
gtk_grid_attach (GTK_GRID (grid),
|
|
green, 2, 1, 1, 1);
|
|
|
|
/* "blue" label/scale */
|
|
label = gtk_label_new ("Blue");
|
|
gtk_widget_set_vexpand (label, FALSE);
|
|
gtk_grid_attach (GTK_GRID (grid),
|
|
label, 3, 0, 1, 1);
|
|
|
|
blue = create_scale ();
|
|
gtk_grid_attach (GTK_GRID (grid),
|
|
blue, 3, 1, 1, 1);
|
|
|
|
/* "alpha" label/scale */
|
|
label = gtk_label_new ("Alpha");
|
|
gtk_widget_set_vexpand (label, FALSE);
|
|
gtk_grid_attach (GTK_GRID (grid),
|
|
label, 4, 0, 1, 1);
|
|
|
|
alpha = create_scale ();
|
|
gtk_grid_attach (GTK_GRID (grid),
|
|
alpha, 4, 1, 1, 1);
|
|
|
|
/* button */
|
|
button = gtk_button_new_from_stock (GTK_STOCK_NEW);
|
|
gtk_widget_add_events (button, GDK_TOUCH_MASK);
|
|
gtk_grid_attach (GTK_GRID (grid),
|
|
button, 1, 2, 4, 1);
|
|
gtk_widget_set_vexpand (button, FALSE);
|
|
|
|
g_signal_connect (button, "clicked",
|
|
G_CALLBACK (new_rectangle_clicked_cb), NULL);
|
|
|
|
gtk_widget_show_all (grid);
|
|
|
|
return window;
|
|
}
|
|
|
|
GtkWidget *
|
|
do_multitouch (GtkWidget *do_widget)
|
|
{
|
|
if (!shapes)
|
|
shapes = g_queue_new ();
|
|
|
|
if (!window)
|
|
window = create_window ();
|
|
|
|
if (!gtk_widget_get_visible (window))
|
|
gtk_widget_show (window);
|
|
else
|
|
{
|
|
gtk_widget_destroy (window);
|
|
window = NULL;
|
|
|
|
g_queue_foreach (shapes, (GFunc) shape_info_free, NULL);
|
|
g_queue_free (shapes);
|
|
shapes = NULL;
|
|
}
|
|
|
|
return window;
|
|
}
|