2016-08-18 07:45:24 +03:00
|
|
|
/*
|
|
|
|
* gui/input.c - Input operations
|
|
|
|
*
|
|
|
|
* Written 2016 by Werner Almesberger
|
|
|
|
* Copyright 2016 by Werner Almesberger
|
|
|
|
*
|
|
|
|
* 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.
|
|
|
|
*/
|
|
|
|
|
|
|
|
#include <stddef.h>
|
|
|
|
#include <stdbool.h>
|
|
|
|
#include <math.h>
|
|
|
|
#include <assert.h>
|
|
|
|
|
|
|
|
#include <gtk/gtk.h>
|
|
|
|
|
|
|
|
#include "misc/util.h"
|
2016-08-18 10:34:51 +03:00
|
|
|
#include "misc/diag.h"
|
2016-08-18 07:45:24 +03:00
|
|
|
#include "gui/input.h"
|
|
|
|
|
|
|
|
|
|
|
|
#define DRAG_RADIUS 5
|
|
|
|
|
|
|
|
|
|
|
|
static struct input {
|
|
|
|
const struct input_ops *ops;
|
|
|
|
void *user;
|
|
|
|
|
|
|
|
enum state {
|
2016-08-18 10:34:51 +03:00
|
|
|
input_idle,
|
2016-08-18 07:45:24 +03:00
|
|
|
input_clicking,
|
|
|
|
input_ignoring, /* click rejected by moving the cursor */
|
|
|
|
input_hovering,
|
2016-08-18 10:56:39 +03:00
|
|
|
input_hovering_down, /* mouse button is pressed */
|
2016-08-18 07:45:24 +03:00
|
|
|
input_dragging,
|
|
|
|
} state;
|
|
|
|
|
|
|
|
struct input *next;
|
|
|
|
} *sp = NULL;
|
|
|
|
|
|
|
|
static int curr_x, curr_y; /* last mouse position */
|
2016-08-18 19:36:06 +03:00
|
|
|
static double clicked_x, clicked_y; /* button down position */
|
2016-08-18 07:45:24 +03:00
|
|
|
|
|
|
|
|
2016-08-18 10:34:51 +03:00
|
|
|
/* ----- Debugging tools --------------------------------------------------- */
|
|
|
|
|
|
|
|
|
|
|
|
static const char *state(void)
|
|
|
|
{
|
|
|
|
switch (sp->state) {
|
|
|
|
case input_idle:
|
|
|
|
return "IDLE";
|
|
|
|
case input_clicking:
|
|
|
|
return "CLICKING";
|
|
|
|
case input_ignoring:
|
|
|
|
return "IGNORING";
|
|
|
|
case input_hovering:
|
|
|
|
return "HOVERING";
|
2016-08-18 10:56:39 +03:00
|
|
|
case input_hovering_down:
|
|
|
|
return "HOVERING_DOWN";
|
2016-08-18 10:34:51 +03:00
|
|
|
case input_dragging:
|
|
|
|
return "DRAGGING";
|
|
|
|
default:
|
|
|
|
abort();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2016-08-18 19:12:09 +03:00
|
|
|
/* ----- Shared operations ------------------------------------------------- */
|
2016-08-18 07:45:24 +03:00
|
|
|
|
|
|
|
|
2016-08-18 10:56:39 +03:00
|
|
|
static bool begin_drag(const GdkEventMotion *event)
|
|
|
|
{
|
|
|
|
const struct input *old_sp = sp;
|
|
|
|
|
|
|
|
if (hypot(event->x - clicked_x, event->y - clicked_y) < DRAG_RADIUS)
|
|
|
|
return 0;
|
|
|
|
if (sp->ops->drag_begin &&
|
|
|
|
sp->ops->drag_begin(sp->user, clicked_x, clicked_y))
|
|
|
|
sp->state = input_dragging;
|
|
|
|
else
|
|
|
|
sp->state = input_ignoring;
|
|
|
|
assert(sp == old_sp);
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2016-08-18 19:12:09 +03:00
|
|
|
static void hover_consider(int x, int y)
|
2016-08-18 07:45:24 +03:00
|
|
|
{
|
|
|
|
const struct input *old_sp = sp;
|
|
|
|
|
2016-08-18 19:12:09 +03:00
|
|
|
assert(sp->state == input_idle);
|
|
|
|
|
|
|
|
if (sp->ops->hover_begin && sp->ops->hover_begin(sp->user, x, y))
|
|
|
|
sp->state = input_hovering;
|
|
|
|
assert(sp == old_sp);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static void hover_update(int x, int y)
|
|
|
|
{
|
|
|
|
const struct input *old_sp = sp;
|
|
|
|
|
2016-08-19 00:29:23 +03:00
|
|
|
assert(sp->state == input_hovering || sp->state == input_hovering_down);
|
2016-08-18 19:12:09 +03:00
|
|
|
|
|
|
|
if (!sp->ops->hover_update)
|
2016-08-19 00:29:23 +03:00
|
|
|
return;
|
2016-08-18 19:12:09 +03:00
|
|
|
|
|
|
|
/*
|
|
|
|
* Caution: hover_update may switch input layers. If this happens,
|
|
|
|
* hovering was already ended when cleaning up the old input layer.
|
|
|
|
*/
|
|
|
|
if (sp->ops->hover_update(sp->user, x, y))
|
|
|
|
return;
|
|
|
|
if (sp != old_sp)
|
|
|
|
return;
|
|
|
|
|
2016-08-22 11:06:05 +03:00
|
|
|
progress(3, "hover_update %s", state());
|
2016-08-19 00:29:23 +03:00
|
|
|
|
|
|
|
switch (sp->state) {
|
|
|
|
case input_idle:
|
|
|
|
case input_hovering:
|
|
|
|
case input_ignoring:
|
|
|
|
sp->state = input_idle;
|
|
|
|
break;
|
|
|
|
case input_clicking:
|
|
|
|
case input_hovering_down:
|
|
|
|
sp->state = input_clicking;
|
|
|
|
break;
|
|
|
|
case input_dragging:
|
|
|
|
default:
|
|
|
|
abort();
|
|
|
|
}
|
|
|
|
|
2016-08-18 19:12:09 +03:00
|
|
|
if (sp->ops->hover_end)
|
|
|
|
sp->ops->hover_end(sp->user);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* ----- Indirect update --------------------------------------------------- */
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Geometry changes may require a reassessment of the hover situation. This is
|
|
|
|
* roughly equivalent to what we would do on a mouse movement over distance
|
|
|
|
* zero.
|
|
|
|
*/
|
|
|
|
|
|
|
|
void input_update(void)
|
|
|
|
{
|
|
|
|
switch (sp->state) {
|
|
|
|
case input_idle:
|
|
|
|
hover_consider(curr_x, curr_y);
|
|
|
|
break;
|
|
|
|
case input_hovering:
|
|
|
|
hover_update(curr_x, curr_y);
|
|
|
|
break;
|
|
|
|
case input_clicking:
|
|
|
|
case input_ignoring:
|
|
|
|
case input_hovering_down:
|
|
|
|
case input_dragging:
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
abort();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* ----- Mouse button ------------------------------------------------------ */
|
|
|
|
|
|
|
|
|
|
|
|
static gboolean motion_notify_event(GtkWidget *widget, GdkEventMotion *event,
|
|
|
|
gpointer data)
|
|
|
|
{
|
2016-08-18 07:45:24 +03:00
|
|
|
curr_x = event->x;
|
|
|
|
curr_y = event->y;
|
|
|
|
|
|
|
|
if (!sp)
|
|
|
|
return TRUE;
|
|
|
|
|
2016-08-22 11:06:05 +03:00
|
|
|
progress(3, "motion %s", state());
|
2016-08-18 10:34:51 +03:00
|
|
|
|
2016-08-18 07:45:24 +03:00
|
|
|
switch (sp->state) {
|
2016-08-18 10:34:51 +03:00
|
|
|
case input_idle:
|
2016-08-18 19:12:09 +03:00
|
|
|
hover_consider(event->x, event->y);
|
2016-08-18 07:45:24 +03:00
|
|
|
break;
|
|
|
|
case input_clicking:
|
2016-08-18 10:56:39 +03:00
|
|
|
begin_drag(event);
|
2016-08-18 07:45:24 +03:00
|
|
|
break;
|
|
|
|
case input_ignoring:
|
|
|
|
break;
|
2016-08-18 10:56:39 +03:00
|
|
|
case input_hovering_down:
|
|
|
|
if (begin_drag(event)) {
|
|
|
|
if (sp->ops->hover_end)
|
|
|
|
sp->ops->hover_end(sp->user);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
/* fall through */
|
2016-08-18 07:45:24 +03:00
|
|
|
case input_hovering:
|
2016-08-18 19:12:09 +03:00
|
|
|
hover_update(event->x, event->y);
|
2016-08-18 07:45:24 +03:00
|
|
|
break;
|
|
|
|
case input_dragging:
|
|
|
|
if (sp->ops->drag_move)
|
|
|
|
sp->ops->drag_move(sp->user,
|
|
|
|
event->x - clicked_x, event->y - clicked_y);
|
|
|
|
clicked_x = event->x;
|
|
|
|
clicked_y = event->y;
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
abort();
|
|
|
|
}
|
|
|
|
return TRUE;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static gboolean button_press_event(GtkWidget *widget, GdkEventButton *event,
|
|
|
|
gpointer data)
|
|
|
|
{
|
|
|
|
if (event->button != 1)
|
|
|
|
return TRUE;
|
|
|
|
|
2016-08-22 11:06:05 +03:00
|
|
|
progress(3, "press %s", state());
|
2016-08-18 10:34:51 +03:00
|
|
|
|
2016-08-18 07:45:24 +03:00
|
|
|
switch (sp->state) {
|
2016-08-18 10:34:51 +03:00
|
|
|
case input_idle:
|
2016-08-18 07:45:24 +03:00
|
|
|
sp->state = input_clicking;
|
|
|
|
clicked_x = event->x;
|
|
|
|
clicked_y = event->y;
|
|
|
|
break;
|
|
|
|
case input_clicking:
|
|
|
|
case input_ignoring:
|
|
|
|
case input_dragging:
|
2016-08-18 10:56:39 +03:00
|
|
|
case input_hovering_down:
|
2016-08-18 07:45:24 +03:00
|
|
|
/* ignore double-click */
|
|
|
|
break;
|
|
|
|
case input_hovering:
|
2016-08-18 10:56:39 +03:00
|
|
|
sp->state = input_hovering_down;
|
|
|
|
clicked_x = event->x;
|
|
|
|
clicked_y = event->y;
|
2016-08-18 07:45:24 +03:00
|
|
|
break;
|
|
|
|
default:
|
|
|
|
abort();
|
|
|
|
}
|
|
|
|
|
|
|
|
return TRUE;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static gboolean button_release_event(GtkWidget *widget, GdkEventButton *event,
|
|
|
|
gpointer data)
|
|
|
|
{
|
2016-08-18 10:56:39 +03:00
|
|
|
const struct input *old_sp = sp;
|
|
|
|
|
2016-08-18 07:45:24 +03:00
|
|
|
if (event->button != 1)
|
|
|
|
return TRUE;
|
|
|
|
|
2016-08-22 11:06:05 +03:00
|
|
|
progress(3, "release %s", state());
|
2016-08-18 10:34:51 +03:00
|
|
|
|
2016-08-18 07:45:24 +03:00
|
|
|
switch (sp->state) {
|
2016-08-18 10:34:51 +03:00
|
|
|
case input_idle:
|
2016-08-18 08:05:43 +03:00
|
|
|
/* hover_click changed the input configuration */
|
|
|
|
break;
|
2016-08-18 07:45:24 +03:00
|
|
|
case input_clicking:
|
2016-08-18 10:34:51 +03:00
|
|
|
sp->state = input_idle;
|
2016-08-18 07:45:24 +03:00
|
|
|
if (sp->ops->click)
|
|
|
|
sp->ops->click(sp->user, clicked_x, clicked_y);
|
|
|
|
break;
|
|
|
|
case input_ignoring:
|
2016-08-18 10:34:51 +03:00
|
|
|
sp->state = input_idle;
|
2016-08-18 07:45:24 +03:00
|
|
|
break;
|
|
|
|
case input_dragging:
|
2016-08-18 10:34:51 +03:00
|
|
|
sp->state = input_idle;
|
2016-08-18 07:45:24 +03:00
|
|
|
if (sp->ops->drag_end)
|
|
|
|
sp->ops->drag_end(sp->user);
|
|
|
|
break;
|
|
|
|
case input_hovering:
|
|
|
|
break;
|
2016-08-18 10:56:39 +03:00
|
|
|
case input_hovering_down:
|
|
|
|
if (sp->ops->hover_click &&
|
|
|
|
sp->ops->hover_click(sp->user, event->x, event->y) &&
|
|
|
|
sp == old_sp) {
|
2016-08-18 23:19:37 +03:00
|
|
|
sp->state = input_idle;
|
2016-08-18 10:56:39 +03:00
|
|
|
if (sp->ops->hover_end)
|
|
|
|
sp->ops->hover_end(sp->user);
|
|
|
|
}
|
|
|
|
break;
|
2016-08-18 07:45:24 +03:00
|
|
|
default:
|
|
|
|
abort();
|
|
|
|
}
|
|
|
|
|
|
|
|
return TRUE;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* ----- Scroll wheel ------------------------------------------------------ */
|
|
|
|
|
|
|
|
|
|
|
|
static gboolean scroll_event(GtkWidget *widget, GdkEventScroll *event,
|
|
|
|
gpointer data)
|
|
|
|
{
|
|
|
|
if (!sp || !sp->ops->scroll)
|
|
|
|
return TRUE;
|
|
|
|
switch (event->direction) {
|
|
|
|
case GDK_SCROLL_UP:
|
|
|
|
sp->ops->scroll(sp->user, event->x, event->y, -1);
|
|
|
|
break;
|
|
|
|
case GDK_SCROLL_DOWN:
|
|
|
|
sp->ops->scroll(sp->user, event->x, event->y, 1);
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
/* ignore */;
|
|
|
|
}
|
|
|
|
return TRUE;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* ----- Keys -------------------------------------------------------------- */
|
|
|
|
|
|
|
|
|
|
|
|
static gboolean key_press_event(GtkWidget *widget, GdkEventKey *event,
|
|
|
|
gpointer data)
|
|
|
|
{
|
|
|
|
if (sp && sp->ops->key)
|
|
|
|
sp->ops->key(sp->user, curr_x, curr_y, event->keyval);
|
|
|
|
return TRUE;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* ----- Covenience function for hover_begin and drag_begin ---------------- */
|
|
|
|
|
|
|
|
|
|
|
|
bool input_accept(void *user, int x, int y)
|
|
|
|
{
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* ----- Adding/removing interaction layers -------------------------------- */
|
|
|
|
|
|
|
|
|
|
|
|
static void cleanup(void)
|
|
|
|
{
|
|
|
|
if (!sp)
|
|
|
|
return;
|
|
|
|
|
|
|
|
switch (sp->state) {
|
|
|
|
case input_hovering:
|
|
|
|
if (sp->ops->hover_end)
|
|
|
|
sp->ops->hover_end(sp->user);
|
|
|
|
break;
|
|
|
|
case input_dragging:
|
|
|
|
if (sp->ops->drag_end)
|
|
|
|
sp->ops->drag_end(sp->user);
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
;
|
|
|
|
}
|
2016-08-18 10:56:39 +03:00
|
|
|
|
|
|
|
sp->state = input_idle;
|
2016-08-18 07:45:24 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void input_push(const struct input_ops *ops, void *user)
|
|
|
|
{
|
|
|
|
struct input *new;
|
|
|
|
|
|
|
|
cleanup();
|
|
|
|
|
|
|
|
new = alloc_type(struct input);
|
|
|
|
new->ops = ops;
|
|
|
|
new->user = user;
|
2016-08-18 10:34:51 +03:00
|
|
|
new->state = input_idle;
|
2016-08-18 07:45:24 +03:00
|
|
|
new->next = sp;
|
|
|
|
sp = new;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void input_pop(void)
|
|
|
|
{
|
|
|
|
struct input *next = sp->next;
|
|
|
|
|
|
|
|
cleanup();
|
|
|
|
free(sp);
|
|
|
|
sp = next;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* ----- Initialization ---------------------------------------------------- */
|
|
|
|
|
|
|
|
|
|
|
|
void input_setup(GtkWidget *da)
|
|
|
|
{
|
|
|
|
gtk_widget_set_can_focus(da, TRUE);
|
|
|
|
|
|
|
|
gtk_widget_add_events(da,
|
|
|
|
GDK_KEY_PRESS_MASK |
|
|
|
|
GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK |
|
|
|
|
GDK_SCROLL_MASK |
|
|
|
|
GDK_POINTER_MOTION_MASK);
|
|
|
|
|
|
|
|
g_signal_connect(G_OBJECT(da), "key_press_event",
|
|
|
|
G_CALLBACK(key_press_event), NULL);
|
|
|
|
g_signal_connect(G_OBJECT(da), "motion_notify_event",
|
|
|
|
G_CALLBACK(motion_notify_event), NULL);
|
|
|
|
g_signal_connect(G_OBJECT(da), "button_press_event",
|
|
|
|
G_CALLBACK(button_press_event), NULL);
|
|
|
|
g_signal_connect(G_OBJECT(da), "button_release_event",
|
|
|
|
G_CALLBACK(button_release_event), NULL);
|
|
|
|
g_signal_connect(G_OBJECT(da), "scroll_event",
|
|
|
|
G_CALLBACK(scroll_event), NULL);
|
|
|
|
}
|