diff --git a/src/video/gltouchjoy.h b/src/video/gltouchjoy.h index 3f255c26..e25210cf 100644 --- a/src/video/gltouchjoy.h +++ b/src/video/gltouchjoy.h @@ -22,6 +22,13 @@ # define TOUCH_JOY_LOG(...) #endif +#define DEBUG_TOUCH_JOY_GESTURE 0 +#if DEBUG_TOUCH_JOY_GESTURE +# define TOUCH_JOY_GESTURE_LOG(...) LOG(__VA_ARGS__) +#else +# define TOUCH_JOY_GESTURE_LOG(...) +#endif + #define ROSETTE_ROWS 3 #define ROSETTE_COLS 3 diff --git a/src/video/gltouchjoy_joy.c b/src/video/gltouchjoy_joy.c index 0235294a..e158405c 100644 --- a/src/video/gltouchjoy_joy.c +++ b/src/video/gltouchjoy_joy.c @@ -17,16 +17,21 @@ #define BUTTON_TAP_DELAY_NANOS_DEFAULT 50000000 -static GLTouchJoyVariant happyHappyJoyJoy = { 0 }; - -static struct { +typedef struct touch_event_s { + struct touch_event_s *next; + interface_touch_event_t event; uint8_t currJoyButtonValue0; uint8_t currJoyButtonValue1; uint8_t currButtonDisplayChar; +} touch_event_s; + +static touch_event_s *touchEventQ = NULL; + +static GLTouchJoyVariant happyHappyJoyJoy = { 0 }; + +static struct { void (*buttonDrawCallback)(char newChar); - bool trackingButton; - bool trackingButtonMove; pthread_t tapDelayThreadId; pthread_mutex_t tapDelayMutex; pthread_cond_t tapDelayCond; @@ -51,9 +56,6 @@ static inline void _reset_axis_state(void) { static inline void _reset_buttons_state(void) { joy_button0 = 0x0; joy_button1 = 0x0; - joys.currJoyButtonValue0 = 0; - joys.currJoyButtonValue1 = 0; - joys.currButtonDisplayChar = ' '; } static void touchjoy_resetState(void) { @@ -63,12 +65,19 @@ static void touchjoy_resetState(void) { // ---------------------------------------------------------------------------- -// Tap Delay Thread : delays processing of touch-down so that a different joystick button can be fired +// Tap Delay Thread : implements a gesture recognizer that differentiates between a "long touch", "tap", and "swipe +// down/up" gestures. Necessarily delays processing of initial touch down event to make a proper determination. Also +// delays resetting joystick button state after touch up event to avoid resetting joystick button state too soon. +// +// * long touch and tap are interpreted as one (configurable) joystick button fire event +// * swipe up and swipe down are the other two (configurable) joystick button fire events +// -static inline void _signal_tap_delay(void) { - pthread_mutex_lock(&joys.tapDelayMutex); - pthread_cond_signal(&joys.tapDelayCond); - pthread_mutex_unlock(&joys.tapDelayMutex); +static struct timespec *_tap_wait(void) { + static struct timespec wait = { 0 }; + clock_gettime(CLOCK_REALTIME, &wait); + wait = timespec_add(wait, joys.tapDelayNanos); + return &wait; } static void *_button_tap_delayed_thread(void *dummyptr) { @@ -76,65 +85,98 @@ static void *_button_tap_delayed_thread(void *dummyptr) { pthread_mutex_lock(&joys.tapDelayMutex); - bool deepSleep = false; - uint8_t currJoyButtonValue0 = 0x0; - uint8_t currJoyButtonValue1 = 0x0; - uint8_t currButtonDisplayChar = ' '; + int timedOut = ETIMEDOUT; for (;;) { if (UNLIKELY(!joyglobals.isAvailable)) { break; } - struct timespec wait; - clock_gettime(CLOCK_REALTIME, &wait); - wait = timespec_add(wait, joys.tapDelayNanos); - int timedOut = pthread_cond_timedwait(&joys.tapDelayCond, &joys.tapDelayMutex, &wait); // wait and possibly consume event - assert((!timedOut || timedOut == ETIMEDOUT) && "should not fail any other way"); + if (timedOut) { + // reset state and deep sleep waiting for touch down + _reset_buttons_state(); + TOUCH_JOY_GESTURE_LOG(">>> [DELAYEDTAP] deep sleep ..."); + pthread_cond_wait(&joys.tapDelayCond, &joys.tapDelayMutex); + } else { + // delays reset of button state while remaining ready to process a touch down + TOUCH_JOY_GESTURE_LOG(">>> [DELAYEDTAP] event looping ..."); + timedOut = pthread_cond_timedwait(&joys.tapDelayCond, &joys.tapDelayMutex, _tap_wait()); // wait and possibly consume event + assert((!timedOut || timedOut == ETIMEDOUT) && "should not fail any other way"); + if (timedOut) { + // reset state and go into deep sleep + continue; + } + _reset_buttons_state(); + } - if (!timedOut) { - if (!deepSleep) { - if (joys.trackingButtonMove) { - // dragging - currJoyButtonValue0 = 0x0; - currJoyButtonValue1 = 0x0; - currButtonDisplayChar = joys.currButtonDisplayChar; - } else if (joys.trackingButton) { - // touch down -- delay consumption to determine if tap or drag - currJoyButtonValue0 = joys.currJoyButtonValue0; - currJoyButtonValue1 = joys.currJoyButtonValue1; - currButtonDisplayChar = joys.currButtonDisplayChar; - joys.buttonDrawCallback(currButtonDisplayChar); - // zero the buttons before delay - _reset_buttons_state(); - continue; - } else { - // touch up becomes tap - joys.currJoyButtonValue0 = currJoyButtonValue0; - joys.currJoyButtonValue1 = currJoyButtonValue1; - joys.currButtonDisplayChar = currButtonDisplayChar; - } + if (UNLIKELY(!joyglobals.isAvailable)) { + break; + } + + TOUCH_JOY_GESTURE_LOG(">>> [DELAYEDTAP] touch down ..."); + + touch_event_s *touchCurrEvent = NULL; + touch_event_s *touchPrevEvent = touchEventQ; + assert(touchPrevEvent && "should be a touch event ready to consume"); + touchEventQ = touchEventQ->next; + assert(touchPrevEvent->event == TOUCH_DOWN && "event queue head should be a touch down event"); + + for (;;) { + // delay processing of touch down to perform simple gesture recognition + timedOut = pthread_cond_timedwait(&joys.tapDelayCond, &joys.tapDelayMutex, _tap_wait()); + assert((!timedOut || timedOut == ETIMEDOUT) && "should not fail any other way"); + + if (UNLIKELY(!joyglobals.isAvailable)) { + break; + } + + touchCurrEvent = touchEventQ; + if (!touchCurrEvent) { + assert(timedOut); + // touch-down-and-hold + TOUCH_JOY_GESTURE_LOG(">>> [DELAYEDTAP] long touch ..."); + joy_button0 = touchPrevEvent->currJoyButtonValue0; + joy_button1 = touchPrevEvent->currJoyButtonValue1; + joys.buttonDrawCallback(touchPrevEvent->currButtonDisplayChar); + continue; + } + touchEventQ = touchEventQ->next; + + if (touchCurrEvent->event == TOUCH_MOVE) { + // dragging ... + TOUCH_JOY_GESTURE_LOG(">>> [DELAYEDTAP] move ..."); + joy_button0 = touchCurrEvent->currJoyButtonValue0; + joy_button1 = touchCurrEvent->currJoyButtonValue1; + joys.buttonDrawCallback(touchCurrEvent->currButtonDisplayChar); + FREE(touchPrevEvent); + touchPrevEvent = touchCurrEvent; + } else if (touchCurrEvent->event == TOUCH_UP) { + // tap + TOUCH_JOY_GESTURE_LOG(">>> [DELAYEDTAP] touch up ..."); + joy_button0 = touchPrevEvent->currJoyButtonValue0; + joy_button1 = touchPrevEvent->currJoyButtonValue1; + joys.buttonDrawCallback(touchPrevEvent->currButtonDisplayChar); + timedOut = 0; + break; + } else { + LOG("OOPS : unexpected touch event : %d", touchCurrEvent->event); + assert(false); } } - joy_button0 = joys.currJoyButtonValue0; - joy_button1 = joys.currJoyButtonValue1; - joys.buttonDrawCallback(joys.currButtonDisplayChar); + FREE(touchPrevEvent); + FREE(touchCurrEvent); + } - deepSleep = false; - if (timedOut && !joys.trackingButton) { - deepSleep = true; - _reset_buttons_state(); - TOUCH_JOY_LOG(">>> [DELAYEDTAP] end ..."); - pthread_cond_wait(&joys.tapDelayCond, &joys.tapDelayMutex); // consume initial touch down - TOUCH_JOY_LOG(">>> [DELAYEDTAP] begin ..."); - } else { - TOUCH_JOY_LOG(">>> [DELAYEDTAP] event looping ..."); - } + // clear out event queue + touch_event_s *p = touchEventQ; + while (p) { + touch_event_s *dead = p; + p = p->next; + FREE(dead); } pthread_mutex_unlock(&joys.tapDelayMutex); - joys.tapDelayThreadId = 0; joys.tapDelayMutex = (pthread_mutex_t)PTHREAD_MUTEX_INITIALIZER; joys.tapDelayCond = (pthread_cond_t)PTHREAD_COND_INITIALIZER; @@ -145,15 +187,23 @@ static void *_button_tap_delayed_thread(void *dummyptr) { static void touchjoy_setup(void (*buttonDrawCallback)(char newChar)) { assert((joys.tapDelayThreadId == 0) && "setup called multiple times!"); - joys.buttonDrawCallback = buttonDrawCallback; - pthread_create(&joys.tapDelayThreadId, NULL, (void *)&_button_tap_delayed_thread, (void *)NULL); + + long lVal = 0; + const interface_device_t screenOwner = prefs_parseLongValue(PREF_DOMAIN_TOUCHSCREEN, PREF_SCREEN_OWNER, &lVal, /*base:*/10) ? lVal : TOUCH_DEVICE_NONE; + if (screenOwner == TOUCH_DEVICE_JOYSTICK) { + joys.buttonDrawCallback = buttonDrawCallback; + pthread_create(&joys.tapDelayThreadId, NULL, (void *)&_button_tap_delayed_thread, (void *)NULL); + } } static void touchjoy_shutdown(void) { + pthread_mutex_lock(&joys.tapDelayMutex); pthread_cond_signal(&joys.tapDelayCond); - if (pthread_join(joys.tapDelayThreadId, NULL)) { + pthread_mutex_unlock(&joys.tapDelayMutex); + if (joys.tapDelayThreadId && pthread_join(joys.tapDelayThreadId, NULL)) { ERRLOG("OOPS: pthread_join tap delay thread ..."); } + joys.tapDelayThreadId = 0; } // ---------------------------------------------------------------------------- @@ -196,30 +246,46 @@ static void touchjoy_axisUp(int x, int y) { // ---------------------------------------------------------------------------- // button state -static void _set_current_button_state(touchjoy_button_type_t theButtonChar) { +static void _signal_tap_delay_event(interface_touch_event_t type, touchjoy_button_type_t theButtonChar) { + touch_event_s *touchEvent = MALLOC(sizeof(*touchEvent)); + touchEvent->next = NULL; + touchEvent->event = type; if (theButtonChar == TOUCH_BUTTON1) { - joys.currJoyButtonValue0 = 0x80; - joys.currJoyButtonValue1 = 0; - joys.currButtonDisplayChar = MOUSETEXT_OPENAPPLE; + touchEvent->currJoyButtonValue0 = 0x80; + touchEvent->currJoyButtonValue1 = 0; + touchEvent->currButtonDisplayChar = MOUSETEXT_OPENAPPLE; } else if (theButtonChar == TOUCH_BUTTON2) { - joys.currJoyButtonValue0 = 0; - joys.currJoyButtonValue1 = 0x80; - joys.currButtonDisplayChar = MOUSETEXT_CLOSEDAPPLE; + touchEvent->currJoyButtonValue0 = 0; + touchEvent->currJoyButtonValue1 = 0x80; + touchEvent->currButtonDisplayChar = MOUSETEXT_CLOSEDAPPLE; } else if (theButtonChar == TOUCH_BOTH) { - joys.currJoyButtonValue0 = 0x80; - joys.currJoyButtonValue1 = 0x80; - joys.currButtonDisplayChar = '+'; + touchEvent->currJoyButtonValue0 = 0x80; + touchEvent->currJoyButtonValue1 = 0x80; + touchEvent->currButtonDisplayChar = '+'; } else { - joys.currJoyButtonValue0 = 0; - joys.currJoyButtonValue1 = 0; - joys.currButtonDisplayChar = ' '; + touchEvent->currJoyButtonValue0 = 0; + touchEvent->currJoyButtonValue1 = 0; + touchEvent->currButtonDisplayChar = ' '; } + + pthread_mutex_lock(&joys.tapDelayMutex); + touch_event_s *p0 = NULL; + touch_event_s *p = touchEventQ; + while (p) { + p0 = p; + p = p->next; + } + if (p0) { + p0->next = touchEvent; + } else { + touchEventQ = touchEvent; + } + pthread_cond_signal(&joys.tapDelayCond); + pthread_mutex_unlock(&joys.tapDelayMutex); } static void touchjoy_buttonDown(void) { - _set_current_button_state(joys.touchDownChar); - joys.trackingButton = true; - _signal_tap_delay(); + _signal_tap_delay_event(TOUCH_DOWN, joys.touchDownChar); } static void touchjoy_buttonMove(int dx, int dy) { @@ -232,16 +298,12 @@ static void touchjoy_buttonMove(int dx, int dy) { theButtonChar = joys.southChar; } - _set_current_button_state(theButtonChar); - _signal_tap_delay(); + _signal_tap_delay_event(TOUCH_MOVE, theButtonChar); } - joys.trackingButtonMove = true; } static void touchjoy_buttonUp(int dx, int dy) { - joys.trackingButton = false; - joys.trackingButtonMove = false; - _signal_tap_delay(); + _signal_tap_delay_event(TOUCH_UP, ' '); } static void touchjoy_prefsChanged(const char *domain) {