From 937128697fbbef6b21e2d23a4785f1334f62b9e3 Mon Sep 17 00:00:00 2001 From: James Ross-Gowan Date: Mon, 24 Apr 2017 22:10:03 +1000 Subject: [PATCH] input: pre-process MP_AXIS_* input This adds some logic for pre-processing MP_AXIS_* events before the corresponding input command is generated. Firstly, the events are filtered. A lot of touchpad drivers and operating systems don't seem to filter axis events, which makes it difficult to use the verical axis (MP_AXIS_UP/MP_AXIS_DOWN) without accidentally triggering commands bound to the horizontal axis (MP_AXIS_LEFT/MP_AXIS_RIGHT) and vice-versa. To fix this, a small deadzone is used. When one axis breaks out of the deadzone, events on the other axis are ignored until the user stops scrolling (determined by a timer.) Secondly, the scale_units value is determined, which is the integer number of "units" the user has scrolled, as opposed to scale, which is the fractional number of units. It's determed by accumulating the fractional scale values. If an axis is bound to a "non-scalable" command that doesn't understand fractional units, interpret_key() will queue that many commands, each with scale = 1.0. --- input/input.c | 107 +++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 101 insertions(+), 6 deletions(-) diff --git a/input/input.c b/input/input.c index b8cc142da2..04b61cf3b4 100644 --- a/input/input.c +++ b/input/input.c @@ -95,6 +95,11 @@ struct cmd_queue { struct mp_cmd *first; }; +struct axis_state { + double dead_zone_accum; + double unit_accum; +}; + struct input_ctx { pthread_mutex_t mutex; struct mp_log *log; @@ -130,6 +135,12 @@ struct input_ctx { bool mouse_mangle, mouse_src_mangle; struct mp_rect mouse_src, mouse_dst; + // Axis state (MP_AXIS_*) + struct axis_state axis_state_y; // MP_AXIS_UP/MP_AXIS_DOWN + struct axis_state axis_state_x; // MP_AXIS_LEFT/MP_AXIS_RIGHT + struct axis_state *axis_current; // Points to axis currently being scrolled + double last_axis_time; // mp_time_sec() of the last axis event + // List of command binding sections struct cmd_bind_section *cmd_bind_sections; @@ -542,7 +553,8 @@ static struct mp_cmd *resolve_key(struct input_ctx *ictx, int code) return NULL; } -static void interpret_key(struct input_ctx *ictx, int code, double scale) +static void interpret_key(struct input_ctx *ictx, int code, double scale, + int scale_units) { int state = code & (MP_KEY_STATE_DOWN | MP_KEY_STATE_UP); code = code & ~(unsigned)state; @@ -604,8 +616,86 @@ static void interpret_key(struct input_ctx *ictx, int code, double scale) memset(ictx->key_history, 0, sizeof(ictx->key_history)); - cmd->scale = scale; - mp_input_queue_cmd(ictx, cmd); + if (mp_input_is_scalable_cmd(cmd)) { + cmd->scale = scale; + mp_input_queue_cmd(ictx, cmd); + } else { + // Non-scalable commands won't understand cmd->scale, so synthesize + // multiple commands with cmd->scale = 1 + cmd->scale = 1; + // Avoid spamming the player with too many commands + scale_units = FFMIN(scale_units, 20); + for (int i = 0; i < scale_units - 1; i++) + mp_input_queue_cmd(ictx, mp_cmd_clone(cmd)); + if (scale_units) + mp_input_queue_cmd(ictx, cmd); + } +} + +// Pre-processing for MP_AXIS_* events. If this returns false, the caller +// should discard the event. +static bool process_axis(struct input_ctx *ictx, int code, double *scale, + int *scale_units) +{ + // Size of the deadzone in scroll units. The user must scroll at least this + // much in any direction before their scroll is registered. + static const double DEADZONE_DIST = 0.125; + // The deadzone accumulator is reset if no scrolls happened in this many + // seconds, eg. the user is assumed to have finished scrolling. + static const double DEADZONE_SCROLL_TIME = 0.2; + // The scale_units accumulator is reset if no scrolls happened in this many + // seconds. This value should be fairly large, so commands will still be + // sent when the user scrolls slowly. + static const double UNIT_SCROLL_TIME = 0.5; + + // Determine which axis is being scrolled + double dir; + struct axis_state *state; + switch (code) { + case MP_AXIS_UP: dir = -1; state = &ictx->axis_state_y; break; + case MP_AXIS_DOWN: dir = +1; state = &ictx->axis_state_y; break; + case MP_AXIS_LEFT: dir = -1; state = &ictx->axis_state_x; break; + case MP_AXIS_RIGHT: dir = +1; state = &ictx->axis_state_x; break; + default: + return true; + } + + // Reset accumulators if it's determined that the user finished scrolling + double now = mp_time_sec(); + if (now > ictx->last_axis_time + DEADZONE_SCROLL_TIME) { + ictx->axis_current = NULL; + ictx->axis_state_y.dead_zone_accum = 0; + ictx->axis_state_x.dead_zone_accum = 0; + } + if (now > ictx->last_axis_time + UNIT_SCROLL_TIME) { + ictx->axis_state_y.unit_accum = 0; + ictx->axis_state_x.unit_accum = 0; + } + ictx->last_axis_time = now; + + // Process axis deadzone. A lot of touchpad drivers don't filter scroll + // input, which makes it difficult for the user to send AXIS_UP/DOWN + // without accidentally triggering AXIS_LEFT/RIGHT. We try to fix this by + // implementing a deadzone. When the value of either axis breaks out of the + // deadzone, events from the other axis will be ignored until the user + // finishes scrolling. + if (ictx->axis_current == NULL) { + state->dead_zone_accum += *scale * dir; + if (state->dead_zone_accum * dir > DEADZONE_DIST) { + ictx->axis_current = state; + *scale = state->dead_zone_accum * dir; + } + } + if (ictx->axis_current != state) + return false; + + // Determine scale_units. This is incremented every time the accumulated + // scale value crosses 1.0. Non-scalable input commands will be ran that + // many times. + state->unit_accum += *scale * dir; + *scale_units = trunc(state->unit_accum * dir); + state->unit_accum -= *scale_units * dir; + return true; } static void mp_input_feed_key(struct input_ctx *ictx, int code, double scale, @@ -631,14 +721,19 @@ static void mp_input_feed_key(struct input_ctx *ictx, int code, double scale, // ignore system-doubleclick if we generate these events ourselves if (!force_mouse && opts->doubleclick_time && MP_KEY_IS_MOUSE_BTN_DBL(unmod)) return; - interpret_key(ictx, code, scale); + int units = 1; + if (MP_KEY_IS_AXIS(unmod) && !process_axis(ictx, unmod, &scale, &units)) + return; + interpret_key(ictx, code, scale, units); if (code & MP_KEY_STATE_DOWN) { code &= ~MP_KEY_STATE_DOWN; if (ictx->last_doubleclick_key_down == code && now - ictx->last_doubleclick_time < opts->doubleclick_time / 1000.0) { - if (code >= MP_MOUSE_BTN0 && code <= MP_MOUSE_BTN2) - interpret_key(ictx, code - MP_MOUSE_BTN0 + MP_MOUSE_BTN0_DBL, 1); + if (code >= MP_MOUSE_BTN0 && code <= MP_MOUSE_BTN2) { + interpret_key(ictx, code - MP_MOUSE_BTN0 + MP_MOUSE_BTN0_DBL, + 1, 1); + } } ictx->last_doubleclick_key_down = code; ictx->last_doubleclick_time = now;