From 1a5c69a4247138ecc2d95c31c159c91c911c12e0 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Fri, 26 May 2023 18:55:26 +0300 Subject: [PATCH] Implemented left & right click support for touch input --- client/eventsSDL/InputHandler.cpp | 1 + client/eventsSDL/InputSourceTouch.cpp | 213 +++++++++++++++++++------- client/eventsSDL/InputSourceTouch.h | 71 ++++++++- 3 files changed, 223 insertions(+), 62 deletions(-) diff --git a/client/eventsSDL/InputHandler.cpp b/client/eventsSDL/InputHandler.cpp index 4ddadd290..ef6323d85 100644 --- a/client/eventsSDL/InputHandler.cpp +++ b/client/eventsSDL/InputHandler.cpp @@ -78,6 +78,7 @@ void InputHandler::processEvents() handleCurrentEvent(currentEvent); eventsQueue.clear(); + fingerHandler->handleUpdate(); } bool InputHandler::ignoreEventsUntilInput() diff --git a/client/eventsSDL/InputSourceTouch.cpp b/client/eventsSDL/InputSourceTouch.cpp index 86210d308..f293af00c 100644 --- a/client/eventsSDL/InputSourceTouch.cpp +++ b/client/eventsSDL/InputSourceTouch.cpp @@ -20,94 +20,175 @@ #include "../gui/MouseButton.h" #include -#include #include +#include InputSourceTouch::InputSourceTouch() - : multifinger(false) - , isPointerRelativeMode(settings["general"]["userRelativePointer"].Bool()) - , pointerSpeedMultiplier(settings["general"]["relativePointerSpeedMultiplier"].Float()) + : lastTapTimeTicks(0) { - if(isPointerRelativeMode) - { - SDL_SetHint(SDL_HINT_MOUSE_TOUCH_EVENTS, "0"); - SDL_SetHint(SDL_HINT_TOUCH_MOUSE_EVENTS, "0"); - } + params.useRelativeMode = settings["general"]["userRelativePointer"].Bool(); + params.relativeModeSpeedFactor = settings["general"]["relativePointerSpeedMultiplier"].Float(); + + if (params.useRelativeMode) + state = TouchState::RELATIVE_MODE; + else + state = TouchState::IDLE; + + SDL_SetHint(SDL_HINT_MOUSE_TOUCH_EVENTS, "0"); + SDL_SetHint(SDL_HINT_TOUCH_MOUSE_EVENTS, "0"); } void InputSourceTouch::handleEventFingerMotion(const SDL_TouchFingerEvent & tfinger) { - if(isPointerRelativeMode) + switch(state) { - Point screenSize = GH.screenDimensions(); + case TouchState::RELATIVE_MODE: + { + Point screenSize = GH.screenDimensions(); - Point moveDistance { - static_cast(screenSize.x * pointerSpeedMultiplier * tfinger.dx), - static_cast(screenSize.y * pointerSpeedMultiplier * tfinger.dy) - }; + Point moveDistance { + static_cast(screenSize.x * params.relativeModeSpeedFactor * tfinger.dx), + static_cast(screenSize.y * params.relativeModeSpeedFactor * tfinger.dy) + }; - GH.input().moveCursorPosition(moveDistance); + GH.input().moveCursorPosition(moveDistance); + break; + } + case TouchState::IDLE: + { + // no-op, might happen in some edge cases, e.g. when fingerdown event was ignored + break; + } + case TouchState::TAP_DOWN_SHORT: + { + state = TouchState::TAP_DOWN_PANNING; + break; + } + case TouchState::TAP_DOWN_PANNING: + { + emitPanningEvent(); + break; + } + case TouchState::TAP_DOWN_DOUBLE: + { + emitPinchEvent(); + break; + } + case TouchState::TAP_DOWN_LONG: + { + // no-op + break; + } } } void InputSourceTouch::handleEventFingerDown(const SDL_TouchFingerEvent & tfinger) { - if(isPointerRelativeMode) - { - if(tfinger.x > 0.5) - { - bool isRightClick = tfinger.y < 0.5; + lastTapTimeTicks = tfinger.timestamp; - if (isRightClick) - GH.events().dispatchMouseButtonPressed(MouseButton::RIGHT, GH.getCursorPosition()); - else - GH.events().dispatchMouseButtonPressed(MouseButton::LEFT, GH.getCursorPosition()); + switch(state) + { + case TouchState::RELATIVE_MODE: + { + if(tfinger.x > 0.5) + { + MouseButton button = tfinger.y < 0.5 ? MouseButton::RIGHT : MouseButton::LEFT; + GH.events().dispatchMouseButtonPressed(button, GH.getCursorPosition()); + } + break; + } + case TouchState::IDLE: + { + GH.input().setCursorPosition(convertTouchToMouse(tfinger)); + state = TouchState::TAP_DOWN_SHORT; + break; + } + case TouchState::TAP_DOWN_SHORT: + { + GH.input().setCursorPosition(convertTouchToMouse(tfinger)); + state = TouchState::TAP_DOWN_DOUBLE; + break; + } + case TouchState::TAP_DOWN_PANNING: + { + GH.input().setCursorPosition(convertTouchToMouse(tfinger)); + state = TouchState::TAP_DOWN_DOUBLE; + break; + } + case TouchState::TAP_DOWN_DOUBLE: + { + // TODO? ignore? + break; + } + case TouchState::TAP_DOWN_LONG: + { + // no-op + break; } } -#ifndef VCMI_IOS - else - { - auto fingerCount = SDL_GetNumTouchFingers(tfinger.touchId); - multifinger = fingerCount > 1; - - if(fingerCount == 2) - { - Point position = convertTouchToMouse(tfinger); - - GH.input().setCursorPosition(position); - GH.events().dispatchMouseButtonPressed(MouseButton::RIGHT, position); - } - } -#endif //VCMI_IOS } void InputSourceTouch::handleEventFingerUp(const SDL_TouchFingerEvent & tfinger) { - if(isPointerRelativeMode) + switch(state) { - if(tfinger.x > 0.5) + case TouchState::RELATIVE_MODE: { - bool isRightClick = tfinger.y < 0.5; + if(tfinger.x > 0.5) + { + MouseButton button = tfinger.y < 0.5 ? MouseButton::RIGHT : MouseButton::LEFT; + GH.events().dispatchMouseButtonReleased(button, GH.getCursorPosition()); + } + break; + } + case TouchState::IDLE: + { + // no-op, might happen in some edge cases, e.g. when fingerdown event was ignored + break; + } + case TouchState::TAP_DOWN_SHORT: + { + GH.input().setCursorPosition(convertTouchToMouse(tfinger)); + GH.events().dispatchMouseButtonPressed(MouseButton::LEFT, convertTouchToMouse(tfinger)); + GH.events().dispatchMouseButtonReleased(MouseButton::LEFT, convertTouchToMouse(tfinger)); + state = TouchState::IDLE; + break; + } + case TouchState::TAP_DOWN_PANNING: + { + state = TouchState::IDLE; + break; + } + case TouchState::TAP_DOWN_DOUBLE: + { + if (SDL_GetNumTouchFingers(tfinger.touchId) == 1) + state = TouchState::TAP_DOWN_PANNING; + break; + } + case TouchState::TAP_DOWN_LONG: + { + if (SDL_GetNumTouchFingers(tfinger.touchId) == 0) + { + GH.input().setCursorPosition(convertTouchToMouse(tfinger)); + GH.events().dispatchMouseButtonReleased(MouseButton::RIGHT, convertTouchToMouse(tfinger)); + state = TouchState::IDLE; + } + break; + } + } +} - if (isRightClick) - GH.events().dispatchMouseButtonReleased(MouseButton::RIGHT, GH.getCursorPosition()); - else - GH.events().dispatchMouseButtonReleased(MouseButton::LEFT, GH.getCursorPosition()); - } - } -#ifndef VCMI_IOS - else +void InputSourceTouch::handleUpdate() +{ + if ( state == TouchState::TAP_DOWN_SHORT) { - if(multifinger) + uint32_t currentTime = SDL_GetTicks(); + if (currentTime > lastTapTimeTicks + params.longPressTimeMilliseconds) { - auto fingerCount = SDL_GetNumTouchFingers(tfinger.touchId); - Point position = convertTouchToMouse(tfinger); - GH.input().setCursorPosition(position); - GH.events().dispatchMouseButtonReleased(MouseButton::RIGHT, position); - multifinger = fingerCount != 0; + state = TouchState::TAP_DOWN_LONG; + GH.events().dispatchMouseButtonPressed(MouseButton::RIGHT, GH.getCursorPosition()); } } -#endif //VCMI_IOS } Point InputSourceTouch::convertTouchToMouse(const SDL_TouchFingerEvent & tfinger) @@ -117,5 +198,21 @@ Point InputSourceTouch::convertTouchToMouse(const SDL_TouchFingerEvent & tfinger bool InputSourceTouch::isMouseButtonPressed(MouseButton button) const { + if (state == TouchState::TAP_DOWN_LONG) + { + if (button == MouseButton::RIGHT) + return true; + } + return false; } + +void InputSourceTouch::emitPanningEvent() +{ + // TODO +} + +void InputSourceTouch::emitPinchEvent() +{ + // TODO +} diff --git a/client/eventsSDL/InputSourceTouch.h b/client/eventsSDL/InputSourceTouch.h index b41bfbcdc..8a7946efa 100644 --- a/client/eventsSDL/InputSourceTouch.h +++ b/client/eventsSDL/InputSourceTouch.h @@ -17,16 +17,77 @@ VCMI_LIB_NAMESPACE_END enum class MouseButton; struct SDL_TouchFingerEvent; +/// Enumeration that describes current state of gesture recognition +enum class TouchState +{ + // special state that allows no transitions + // used when player selects "relative mode" in Launcher + // in this mode touchscreen acts like touchpad, moving cursor at certains speed + // and generates events for positions below cursor instead of positions below touch events + RELATIVE_MODE, + + // no active touch events + // DOWN -> transition to TAP_DOWN_SHORT + // MOTION / UP -> not expected + IDLE, + + // single finger is touching the screen for a short time + // DOWN -> transition to TAP_DOWN_DOUBLE + // MOTION -> transition to TAP_DOWN_PANNING + // UP -> transition to IDLE, emit onLeftClickDown and onLeftClickUp + // on timer -> transition to TAP_DOWN_LONG, emit onRightClickDown event + TAP_DOWN_SHORT, + + // single finger is moving across screen + // DOWN -> transition to TAP_DOWN_DOUBLE + // MOTION -> emit panning event + // UP -> transition to IDLE + TAP_DOWN_PANNING, + + // two fingers are touching the screen + // DOWN -> ??? how to handle 3rd finger? Ignore? + // MOTION -> emit pinch event + // UP -> transition to TAP_DOWN + TAP_DOWN_DOUBLE, + + // single finger is down for long period of time + // DOWN -> ignored + // MOTION -> ignored + // UP -> transition to IDLE, generate onRightClickUp() event + TAP_DOWN_LONG, + + + // Possible transitions: + // -> DOUBLE + // -> PANNING -> IDLE + // IDLE -> DOWN_SHORT -> IDLE + // -> LONG -> IDLE + // -> DOUBLE -> PANNING + // -> IDLE +}; + +struct TouchInputParameters +{ + double relativeModeSpeedFactor = 1.0; + uint32_t longPressTimeMilliseconds = 500; + + bool useHoldGesture = true; + bool usePanGesture = true; + bool usePinchGesture = true; + bool useRelativeMode = false; +}; + /// Class that handles touchscreen input from SDL events class InputSourceTouch { - double pointerSpeedMultiplier; - bool multifinger; - bool isPointerRelativeMode; + TouchInputParameters params; + TouchState state; + uint32_t lastTapTimeTicks; Point convertTouchToMouse(const SDL_TouchFingerEvent & current); - void fakeMouseButtonEventRelativeMode(bool down, bool right); + void emitPanningEvent(); + void emitPinchEvent(); public: InputSourceTouch(); @@ -35,5 +96,7 @@ public: void handleEventFingerDown(const SDL_TouchFingerEvent & current); void handleEventFingerUp(const SDL_TouchFingerEvent & current); + void handleUpdate(); + bool isMouseButtonPressed(MouseButton button) const; };