From c2c2bd2df687c3b37a542744c83b5068f1f97ce5 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Thu, 30 Apr 2020 20:22:43 -0700 Subject: [PATCH] Split input.cpp into smaller files --- app/app.pro | 4 + app/streaming/input/abstouch.cpp | 122 +++ app/streaming/input/gamepad.cpp | 595 ++++++++++++++ app/streaming/input/input.cpp | 1241 ------------------------------ app/streaming/input/keyboard.cpp | 420 ++++++++++ app/streaming/input/mouse.cpp | 126 +++ 6 files changed, 1267 insertions(+), 1241 deletions(-) create mode 100644 app/streaming/input/abstouch.cpp create mode 100644 app/streaming/input/gamepad.cpp create mode 100644 app/streaming/input/keyboard.cpp create mode 100644 app/streaming/input/mouse.cpp diff --git a/app/app.pro b/app/app.pro index 102a7580..24ffc076 100644 --- a/app/app.pro +++ b/app/app.pro @@ -146,7 +146,11 @@ SOURCES += \ cli/quitstream.cpp \ cli/startstream.cpp \ settings/streamingpreferences.cpp \ + streaming/input/abstouch.cpp \ + streaming/input/gamepad.cpp \ streaming/input/input.cpp \ + streaming/input/keyboard.cpp \ + streaming/input/mouse.cpp \ streaming/session.cpp \ streaming/audio/audio.cpp \ streaming/audio/renderers/sdlaud.cpp \ diff --git a/app/streaming/input/abstouch.cpp b/app/streaming/input/abstouch.cpp new file mode 100644 index 00000000..dd0ef4b4 --- /dev/null +++ b/app/streaming/input/abstouch.cpp @@ -0,0 +1,122 @@ +#include "input.h" + +#include +#include +#include "streaming/streamutils.h" + +#include + +// How long the fingers must be stationary to start a right click +#define LONG_PRESS_ACTIVATION_DELAY 650 + +// How far the finger can move before it cancels a right click +#define LONG_PRESS_ACTIVATION_DELTA 0.01f + +// How long the double tap deadzone stays in effect between touch up and touch down +#define DOUBLE_TAP_DEAD_ZONE_DELAY 250 + +// How far the finger can move before it can override the double tap deadzone +#define DOUBLE_TAP_DEAD_ZONE_DELTA 0.025f + +Uint32 SdlInputHandler::longPressTimerCallback(Uint32, void*) +{ + // Raise the left click and start a right click + LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT); + LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT); + + return 0; +} + +void SdlInputHandler::handleTouchFingerEvent(SDL_TouchFingerEvent* event) +{ +#if SDL_VERSION_ATLEAST(2, 0, 10) + if (SDL_GetTouchDeviceType(event->touchId) != SDL_TOUCH_DEVICE_DIRECT) { + // Ignore anything that isn't a touchscreen. We may get callbacks + // for trackpads, but we want to handle those in the mouse path. + return; + } +#elif defined(Q_OS_DARWIN) + // SDL2 sends touch events from trackpads by default on + // macOS. This totally screws our actual mouse handling, + // so we must explicitly ignore touch events on macOS + // until SDL 2.0.10 where we have SDL_GetTouchDeviceType() + // to tell them apart. + return; +#endif + + // Observations on Windows 10: x and y appear to be relative to 0,0 of the window client area. + // Although SDL documentation states they are 0.0 - 1.0 float values, they can actually be higher + // or lower than those values as touch events continue for touches started within the client area that + // leave the client area during a drag motion. + // dx and dy are deltas from the last touch event, not the first touch down. + + // Ignore touch down events with more than one finger + if (event->type == SDL_FINGERDOWN && SDL_GetNumTouchFingers(event->touchId) > 1) { + return; + } + + // Ignore touch move and touch up events from the non-primary finger + if (event->type != SDL_FINGERDOWN && event->fingerId != m_LastTouchDownEvent.fingerId) { + return; + } + + SDL_Rect src, dst; + int windowWidth, windowHeight; + + SDL_GetWindowSize(m_Window, &windowWidth, &windowHeight); + + src.x = src.y = 0; + src.w = m_StreamWidth; + src.h = m_StreamHeight; + + dst.x = dst.y = 0; + dst.w = windowWidth; + dst.h = windowHeight; + + // Use the stream and window sizes to determine the video region + StreamUtils::scaleSourceToDestinationSurface(&src, &dst); + + if (qSqrt(qPow(event->x - m_LastTouchDownEvent.x, 2) + qPow(event->y - m_LastTouchDownEvent.y, 2)) > LONG_PRESS_ACTIVATION_DELTA) { + // Moved too far since touch down. Cancel the long press timer. + SDL_RemoveTimer(m_LongPressTimer); + m_LongPressTimer = 0; + } + + // Don't reposition for finger down events within the deadzone. This makes double-clicking easier. + if (event->type != SDL_FINGERDOWN || + event->timestamp - m_LastTouchUpEvent.timestamp > DOUBLE_TAP_DEAD_ZONE_DELAY || + qSqrt(qPow(event->x - m_LastTouchUpEvent.x, 2) + qPow(event->y - m_LastTouchUpEvent.y, 2)) > DOUBLE_TAP_DEAD_ZONE_DELTA) { + // Scale window-relative events to be video-relative and clamp to video region + short x = qMin(qMax((int)(event->x * windowWidth), dst.x), dst.x + dst.w); + short y = qMin(qMax((int)(event->y * windowHeight), dst.y), dst.y + dst.h); + + // Update the cursor position relative to the video region + LiSendMousePositionEvent(x - dst.x, y - dst.y, dst.w, dst.h); + } + + if (event->type == SDL_FINGERDOWN) { + m_LastTouchDownEvent = *event; + + // Start/restart the long press timer + SDL_RemoveTimer(m_LongPressTimer); + m_LongPressTimer = SDL_AddTimer(LONG_PRESS_ACTIVATION_DELAY, + longPressTimerCallback, + nullptr); + + // Left button down on finger down + LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT); + } + else if (event->type == SDL_FINGERUP) { + m_LastTouchUpEvent = *event; + + // Cancel the long press timer + SDL_RemoveTimer(m_LongPressTimer); + m_LongPressTimer = 0; + + // Left button up on finger up + LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT); + + // Raise right button too in case we triggered a long press gesture + LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT); + } +} diff --git a/app/streaming/input/gamepad.cpp b/app/streaming/input/gamepad.cpp new file mode 100644 index 00000000..2c2ed981 --- /dev/null +++ b/app/streaming/input/gamepad.cpp @@ -0,0 +1,595 @@ +#include "streaming/session.h" + +#include +#include +#include "settings/mappingmanager.h" + +#include + +// How long the Start button must be pressed to toggle mouse emulation +#define MOUSE_EMULATION_LONG_PRESS_TIME 750 + +// How long between polling the gamepad to send virtual mouse input +#define MOUSE_EMULATION_POLLING_INTERVAL 50 + +// Determines how fast the mouse will move each interval +#define MOUSE_EMULATION_MOTION_MULTIPLIER 4 + +// Determines the maximum motion amount before allowing movement +#define MOUSE_EMULATION_DEADZONE 2 + +// Haptic capabilities (in addition to those from SDL_HapticQuery()) +#define ML_HAPTIC_GC_RUMBLE (1U << 16) +#define ML_HAPTIC_SIMPLE_RUMBLE (1U << 17) + +const int SdlInputHandler::k_ButtonMap[] = { + A_FLAG, B_FLAG, X_FLAG, Y_FLAG, + BACK_FLAG, SPECIAL_FLAG, PLAY_FLAG, + LS_CLK_FLAG, RS_CLK_FLAG, + LB_FLAG, RB_FLAG, + UP_FLAG, DOWN_FLAG, LEFT_FLAG, RIGHT_FLAG +}; + +GamepadState* +SdlInputHandler::findStateForGamepad(SDL_JoystickID id) +{ + int i; + + for (i = 0; i < MAX_GAMEPADS; i++) { + if (m_GamepadState[i].jsId == id) { + SDL_assert(!m_MultiController || m_GamepadState[i].index == i); + return &m_GamepadState[i]; + } + } + + // This should only happen with > 4 gamepads + SDL_assert(SDL_NumJoysticks() > 4); + return nullptr; +} + +void SdlInputHandler::sendGamepadState(GamepadState* state) +{ + SDL_assert(m_GamepadMask == 0x1 || m_MultiController); + LiSendMultiControllerEvent(state->index, + m_GamepadMask, + state->buttons, + state->lt, + state->rt, + state->lsX, + state->lsY, + state->rsX, + state->rsY); +} + +Uint32 SdlInputHandler::mouseEmulationTimerCallback(Uint32 interval, void *param) +{ + auto gamepad = reinterpret_cast(param); + + short rawX; + short rawY; + + // Determine which analog stick is currently receiving the strongest input + if ((uint32_t)qAbs(gamepad->lsX) + qAbs(gamepad->lsY) > (uint32_t)qAbs(gamepad->rsX) + qAbs(gamepad->rsY)) { + rawX = gamepad->lsX; + rawY = -gamepad->lsY; + } + else { + rawX = gamepad->rsX; + rawY = -gamepad->rsY; + } + + float deltaX; + float deltaY; + + // Produce a base vector for mouse movement with increased speed as we deviate further from center + deltaX = qPow(rawX / 32766.0f * MOUSE_EMULATION_MOTION_MULTIPLIER, 3); + deltaY = qPow(rawY / 32766.0f * MOUSE_EMULATION_MOTION_MULTIPLIER, 3); + + // Enforce deadzones + deltaX = qAbs(deltaX) > MOUSE_EMULATION_DEADZONE ? deltaX - MOUSE_EMULATION_DEADZONE : 0; + deltaY = qAbs(deltaY) > MOUSE_EMULATION_DEADZONE ? deltaY - MOUSE_EMULATION_DEADZONE : 0; + + if (deltaX != 0 || deltaY != 0) { + LiSendMouseMoveEvent((short)deltaX, (short)deltaY); + } + + return interval; +} + +void SdlInputHandler::handleControllerAxisEvent(SDL_ControllerAxisEvent* event) +{ + SDL_JoystickID gameControllerId = event->which; + GamepadState* state = findStateForGamepad(gameControllerId); + if (state == NULL) { + return; + } + + // Batch all pending axis motion events for this gamepad to save CPU time + SDL_Event nextEvent; + for (;;) { + switch (event->axis) + { + case SDL_CONTROLLER_AXIS_LEFTX: + state->lsX = event->value; + break; + case SDL_CONTROLLER_AXIS_LEFTY: + // Signed values have one more negative value than + // positive value, so inverting the sign on -32768 + // could actually cause the value to overflow and + // wrap around to be negative again. Avoid that by + // capping the value at 32767. + state->lsY = -qMax(event->value, (short)-32767); + break; + case SDL_CONTROLLER_AXIS_RIGHTX: + state->rsX = event->value; + break; + case SDL_CONTROLLER_AXIS_RIGHTY: + state->rsY = -qMax(event->value, (short)-32767); + break; + case SDL_CONTROLLER_AXIS_TRIGGERLEFT: + state->lt = (unsigned char)(event->value * 255UL / 32767); + break; + case SDL_CONTROLLER_AXIS_TRIGGERRIGHT: + state->rt = (unsigned char)(event->value * 255UL / 32767); + break; + default: + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, + "Unhandled controller axis: %d", + event->axis); + return; + } + + // Check for another event to batch with + if (SDL_PeepEvents(&nextEvent, 1, SDL_PEEKEVENT, SDL_CONTROLLERAXISMOTION, SDL_CONTROLLERAXISMOTION) <= 0) { + break; + } + + event = &nextEvent.caxis; + if (event->which != gameControllerId) { + // Stop batching if a different gamepad interrupts us + break; + } + + // Remove the next event to batch + SDL_PeepEvents(&nextEvent, 1, SDL_GETEVENT, SDL_CONTROLLERAXISMOTION, SDL_CONTROLLERAXISMOTION); + } + + // Only send the gamepad state to the host if it's not in mouse emulation mode + if (state->mouseEmulationTimer == 0) { + sendGamepadState(state); + } +} + +void SdlInputHandler::handleControllerButtonEvent(SDL_ControllerButtonEvent* event) +{ + GamepadState* state = findStateForGamepad(event->which); + if (state == NULL) { + return; + } + + if (event->state == SDL_PRESSED) { + state->buttons |= k_ButtonMap[event->button]; + + if (event->button == SDL_CONTROLLER_BUTTON_START) { + state->lastStartDownTime = SDL_GetTicks(); + } + else if (state->mouseEmulationTimer != 0) { + if (event->button == SDL_CONTROLLER_BUTTON_A) { + LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT); + } + else if (event->button == SDL_CONTROLLER_BUTTON_B) { + LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT); + } + else if (event->button == SDL_CONTROLLER_BUTTON_X) { + LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_MIDDLE); + } + else if (event->button == SDL_CONTROLLER_BUTTON_LEFTSHOULDER) { + LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_X1); + } + else if (event->button == SDL_CONTROLLER_BUTTON_RIGHTSHOULDER) { + LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_X2); + } + else if (event->button == SDL_CONTROLLER_BUTTON_DPAD_UP) { + LiSendScrollEvent(1); + } + else if (event->button == SDL_CONTROLLER_BUTTON_DPAD_DOWN) { + LiSendScrollEvent(-1); + } + } + } + else { + state->buttons &= ~k_ButtonMap[event->button]; + + if (event->button == SDL_CONTROLLER_BUTTON_START) { + if (SDL_GetTicks() - state->lastStartDownTime > MOUSE_EMULATION_LONG_PRESS_TIME) { + if (state->mouseEmulationTimer != 0) { + SDL_RemoveTimer(state->mouseEmulationTimer); + state->mouseEmulationTimer = 0; + + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, + "Mouse emulation deactivated"); + Session::get()->notifyMouseEmulationMode(false); + } + else if (m_GamepadMouse) { + // Send the start button up event to the host, since we won't do it below + sendGamepadState(state); + + state->mouseEmulationTimer = SDL_AddTimer(MOUSE_EMULATION_POLLING_INTERVAL, SdlInputHandler::mouseEmulationTimerCallback, state); + + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, + "Mouse emulation active"); + Session::get()->notifyMouseEmulationMode(true); + } + } + } + else if (state->mouseEmulationTimer != 0) { + if (event->button == SDL_CONTROLLER_BUTTON_A) { + LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT); + } + else if (event->button == SDL_CONTROLLER_BUTTON_B) { + LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT); + } + else if (event->button == SDL_CONTROLLER_BUTTON_X) { + LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_MIDDLE); + } + else if (event->button == SDL_CONTROLLER_BUTTON_LEFTSHOULDER) { + LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_X1); + } + else if (event->button == SDL_CONTROLLER_BUTTON_RIGHTSHOULDER) { + LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_X2); + } + } + } + + // Handle Start+Select+L1+R1 as a gamepad quit combo + if (state->buttons == (PLAY_FLAG | BACK_FLAG | LB_FLAG | RB_FLAG)) { + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, + "Detected quit gamepad button combo"); + + // Push a quit event to the main loop + SDL_Event event; + event.type = SDL_QUIT; + event.quit.timestamp = SDL_GetTicks(); + SDL_PushEvent(&event); + + // Clear buttons down on this gameapd + LiSendMultiControllerEvent(state->index, m_GamepadMask, + 0, 0, 0, 0, 0, 0, 0); + return; + } + + // Only send the gamepad state to the host if it's not in mouse emulation mode + if (state->mouseEmulationTimer == 0) { + sendGamepadState(state); + } +} + +void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* event) +{ + GamepadState* state; + + if (event->type == SDL_CONTROLLERDEVICEADDED) { + int i; + const char* name; + SDL_GameController* controller; + const char* mapping; + char guidStr[33]; + uint32_t hapticCaps; + + controller = SDL_GameControllerOpen(event->which); + if (controller == NULL) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, + "Failed to open gamepad: %s", + SDL_GetError()); + return; + } + + // We used to use SDL_GameControllerGetPlayerIndex() here but that + // can lead to strange issues due to bugs in Windows where an Xbox + // controller will join as player 2, even though no player 1 controller + // is connected at all. This pretty much screws any attempt to use + // the gamepad in single player games, so just assign them in order from 0. + i = 0; + + for (; i < MAX_GAMEPADS; i++) { + SDL_assert(m_GamepadState[i].controller != controller); + if (m_GamepadState[i].controller == NULL) { + // Found an empty slot + break; + } + } + + if (i == MAX_GAMEPADS) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, + "No open gamepad slots found!"); + SDL_GameControllerClose(controller); + return; + } + + state = &m_GamepadState[i]; + if (m_MultiController) { + state->index = i; + +#if SDL_VERSION_ATLEAST(2, 0, 12) + // This will change indicators on the controller to show the assigned + // player index. For Xbox 360 controllers, that means updating the LED + // ring to light up the corresponding quadrant for this player. + SDL_GameControllerSetPlayerIndex(controller, state->index); +#endif + } + else { + // Always player 1 in single controller mode + state->index = 0; + } + + state->controller = controller; + state->jsId = SDL_JoystickInstanceID(SDL_GameControllerGetJoystick(state->controller)); + +#if SDL_VERSION_ATLEAST(2, 0, 9) + // Perform a tiny rumble to see if haptics are supported. + // NB: We cannot use zeros for rumble intensity or SDL will not actually call the JS driver + // and we'll get a (potentially false) success value returned. + hapticCaps = SDL_GameControllerRumble(controller, 1, 1, 1) == 0 ? + ML_HAPTIC_GC_RUMBLE : 0; +#else + state->haptic = SDL_HapticOpenFromJoystick(SDL_GameControllerGetJoystick(state->controller)); + state->hapticEffectId = -1; + state->hapticMethod = GAMEPAD_HAPTIC_METHOD_NONE; + if (state->haptic != nullptr) { + // Query for supported haptic effects + hapticCaps = SDL_HapticQuery(state->haptic); + hapticCaps |= SDL_HapticRumbleSupported(state->haptic) ? + ML_HAPTIC_SIMPLE_RUMBLE : 0; + + if ((SDL_HapticQuery(state->haptic) & SDL_HAPTIC_LEFTRIGHT) == 0) { + if (SDL_HapticRumbleSupported(state->haptic)) { + if (SDL_HapticRumbleInit(state->haptic) == 0) { + state->hapticMethod = GAMEPAD_HAPTIC_METHOD_SIMPLERUMBLE; + } + } + if (state->hapticMethod == GAMEPAD_HAPTIC_METHOD_NONE) { + SDL_HapticClose(state->haptic); + state->haptic = nullptr; + } + } else { + state->hapticMethod = GAMEPAD_HAPTIC_METHOD_LEFTRIGHT; + } + } + else { + hapticCaps = 0; + } +#endif + + SDL_JoystickGetGUIDString(SDL_JoystickGetGUID(SDL_GameControllerGetJoystick(state->controller)), + guidStr, sizeof(guidStr)); + mapping = SDL_GameControllerMapping(state->controller); + name = SDL_GameControllerName(state->controller); + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, + "Gamepad %d (player %d) is: %s (haptic capabilities: 0x%x) (mapping: %s -> %s)", + i, + state->index, + name != nullptr ? name : "", + hapticCaps, + guidStr, + mapping != nullptr ? mapping : ""); + if (mapping != nullptr) { + SDL_free((void*)mapping); + } + + // Add this gamepad to the gamepad mask + if (m_MultiController) { + // NB: Don't assert that it's unset here because we will already + // have the mask set for initially attached gamepads to avoid confusing + // apps running on the host. + m_GamepadMask |= (1 << state->index); + } + else { + SDL_assert(m_GamepadMask == 0x1); + } + + // Send an empty event to tell the PC we've arrived + sendGamepadState(state); + } + else if (event->type == SDL_CONTROLLERDEVICEREMOVED) { + state = findStateForGamepad(event->which); + if (state != NULL) { + if (state->mouseEmulationTimer != 0) { + Session::get()->notifyMouseEmulationMode(false); + SDL_RemoveTimer(state->mouseEmulationTimer); + } + + SDL_GameControllerClose(state->controller); + +#if !SDL_VERSION_ATLEAST(2, 0, 9) + if (state->haptic != nullptr) { + SDL_HapticClose(state->haptic); + } +#endif + + // Remove this from the gamepad mask in MC-mode + if (m_MultiController) { + SDL_assert(m_GamepadMask & (1 << state->index)); + m_GamepadMask &= ~(1 << state->index); + } + else { + SDL_assert(m_GamepadMask == 0x1); + } + + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, + "Gamepad %d is gone", + state->index); + + // Send a final event to let the PC know this gamepad is gone + LiSendMultiControllerEvent(state->index, m_GamepadMask, + 0, 0, 0, 0, 0, 0, 0); + + // Clear all remaining state from this slot + SDL_memset(state, 0, sizeof(*state)); + } + } +} + +void SdlInputHandler::handleJoystickArrivalEvent(SDL_JoyDeviceEvent* event) +{ + SDL_assert(event->type == SDL_JOYDEVICEADDED); + + if (!SDL_IsGameController(event->which)) { + char guidStr[33]; + SDL_JoystickGetGUIDString(SDL_JoystickGetDeviceGUID(event->which), + guidStr, sizeof(guidStr)); + const char* name = SDL_JoystickNameForIndex(event->which); + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "Joystick discovered with no mapping: %s %s", + name ? name : "", + guidStr); + SDL_Joystick* joy = SDL_JoystickOpen(event->which); + if (joy != nullptr) { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "Number of axes: %d | Number of buttons: %d | Number of hats: %d", + SDL_JoystickNumAxes(joy), SDL_JoystickNumButtons(joy), + SDL_JoystickNumHats(joy)); + SDL_JoystickClose(joy); + } + else { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "Unable to open joystick for query: %s", + SDL_GetError()); + } + } +} + +void SdlInputHandler::rumble(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor) +{ + // Make sure the controller number is within our supported count + if (controllerNumber >= MAX_GAMEPADS) { + return; + } + +#if SDL_VERSION_ATLEAST(2, 0, 9) + if (m_GamepadState[controllerNumber].controller != nullptr) { + SDL_GameControllerRumble(m_GamepadState[controllerNumber].controller, lowFreqMotor, highFreqMotor, 30000); + } +#else + // Check if the controller supports haptics (and if the controller exists at all) + SDL_Haptic* haptic = m_GamepadState[controllerNumber].haptic; + if (haptic == nullptr) { + return; + } + + // Stop the last effect we played + if (m_GamepadState[controllerNumber].hapticMethod == GAMEPAD_HAPTIC_METHOD_LEFTRIGHT) { + if (m_GamepadState[controllerNumber].hapticEffectId >= 0) { + SDL_HapticDestroyEffect(haptic, m_GamepadState[controllerNumber].hapticEffectId); + } + } else if (m_GamepadState[controllerNumber].hapticMethod == GAMEPAD_HAPTIC_METHOD_SIMPLERUMBLE) { + SDL_HapticRumbleStop(haptic); + } + + // If this callback is telling us to stop both motors, don't bother queuing a new effect + if (lowFreqMotor == 0 && highFreqMotor == 0) { + return; + } + + if (m_GamepadState[controllerNumber].hapticMethod == GAMEPAD_HAPTIC_METHOD_LEFTRIGHT) { + SDL_HapticEffect effect; + SDL_memset(&effect, 0, sizeof(effect)); + effect.type = SDL_HAPTIC_LEFTRIGHT; + + // The effect should last until we are instructed to stop or change it + effect.leftright.length = SDL_HAPTIC_INFINITY; + + // SDL haptics range from 0-32767 but XInput uses 0-65535, so divide by 2 to correct for SDL's scaling + effect.leftright.large_magnitude = lowFreqMotor / 2; + effect.leftright.small_magnitude = highFreqMotor / 2; + + // Play the new effect + m_GamepadState[controllerNumber].hapticEffectId = SDL_HapticNewEffect(haptic, &effect); + if (m_GamepadState[controllerNumber].hapticEffectId >= 0) { + SDL_HapticRunEffect(haptic, m_GamepadState[controllerNumber].hapticEffectId, 1); + } + } else if (m_GamepadState[controllerNumber].hapticMethod == GAMEPAD_HAPTIC_METHOD_SIMPLERUMBLE) { + SDL_HapticRumblePlay(haptic, + std::min(1.0, (GAMEPAD_HAPTIC_SIMPLE_HIFREQ_MOTOR_WEIGHT*highFreqMotor + + GAMEPAD_HAPTIC_SIMPLE_LOWFREQ_MOTOR_WEIGHT*lowFreqMotor) / 65535.0), + SDL_HAPTIC_INFINITY); + } +#endif +} + +QString SdlInputHandler::getUnmappedGamepads() +{ + QString ret; + + if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) != 0) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, + "SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) failed: %s", + SDL_GetError()); + } + + MappingManager mappingManager; + mappingManager.applyMappings(); + + for (int i = 0; i < SDL_NumJoysticks(); i++) { + if (!SDL_IsGameController(i)) { + char guidStr[33]; + SDL_JoystickGetGUIDString(SDL_JoystickGetDeviceGUID(i), + guidStr, sizeof(guidStr)); + const char* name = SDL_JoystickNameForIndex(i); + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, + "Unmapped joystick: %s %s", + name ? name : "", + guidStr); + SDL_Joystick* joy = SDL_JoystickOpen(i); + if (joy != nullptr) { + int numButtons = SDL_JoystickNumButtons(joy); + int numHats = SDL_JoystickNumHats(joy); + int numAxes = SDL_JoystickNumAxes(joy); + + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, + "Number of axes: %d | Number of buttons: %d | Number of hats: %d", + numAxes, numButtons, numHats); + + if ((numAxes >= 4 && numAxes <= 8) && numButtons >= 8 && numHats <= 1) { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "Joystick likely to be an unmapped game controller"); + if (!ret.isEmpty()) { + ret += ", "; + } + + ret += name; + } + + SDL_JoystickClose(joy); + } + else { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "Unable to open joystick for query: %s", + SDL_GetError()); + } + } + } + + SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER); + + return ret; +} + +int SdlInputHandler::getAttachedGamepadMask() +{ + int count; + int mask; + + if (!m_MultiController) { + // Player 1 is always present in non-MC mode + return 0x1; + } + + count = mask = 0; + for (int i = 0; i < SDL_NumJoysticks(); i++) { + if (SDL_IsGameController(i)) { + mask |= (1 << count++); + } + } + + return mask; +} diff --git a/app/streaming/input/input.cpp b/app/streaming/input/input.cpp index 38a67ae3..d3d5ab1a 100644 --- a/app/streaming/input/input.cpp +++ b/app/streaming/input/input.cpp @@ -3,65 +3,12 @@ #include "streaming/session.h" #include "settings/mappingmanager.h" #include "path.h" -#include "streaming/streamutils.h" #include -#include #include -#include - -#define VK_0 0x30 -#define VK_A 0x41 - -// These are real Windows VK_* codes -#ifndef VK_F1 -#define VK_F1 0x70 -#define VK_F13 0x7C -#define VK_NUMPAD0 0x60 -#endif - -// Until we can fully capture these on all platforms (without conflicting with -// OS-provided shortcuts), we should avoid passing them through to the host. -//#define ENABLE_META #define MOUSE_POLLING_INTERVAL 5 -// How long the fingers must be stationary to start a right click -#define LONG_PRESS_ACTIVATION_DELAY 650 - -// How far the finger can move before it cancels a right click -#define LONG_PRESS_ACTIVATION_DELTA 0.01f - -// How long the double tap deadzone stays in effect between touch up and touch down -#define DOUBLE_TAP_DEAD_ZONE_DELAY 250 - -// How far the finger can move before it can override the double tap deadzone -#define DOUBLE_TAP_DEAD_ZONE_DELTA 0.025f - -// How long the Start button must be pressed to toggle mouse emulation -#define MOUSE_EMULATION_LONG_PRESS_TIME 750 - -// How long between polling the gamepad to send virtual mouse input -#define MOUSE_EMULATION_POLLING_INTERVAL 50 - -// Determines how fast the mouse will move each interval -#define MOUSE_EMULATION_MOTION_MULTIPLIER 4 - -// Determines the maximum motion amount before allowing movement -#define MOUSE_EMULATION_DEADZONE 2 - -// Haptic capabilities (in addition to those from SDL_HapticQuery()) -#define ML_HAPTIC_GC_RUMBLE (1U << 16) -#define ML_HAPTIC_SIMPLE_RUMBLE (1U << 17) - -const int SdlInputHandler::k_ButtonMap[] = { - A_FLAG, B_FLAG, X_FLAG, Y_FLAG, - BACK_FLAG, SPECIAL_FLAG, PLAY_FLAG, - LS_CLK_FLAG, RS_CLK_FLAG, - LB_FLAG, RB_FLAG, - UP_FLAG, DOWN_FLAG, LEFT_FLAG, RIGHT_FLAG -}; - SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int streamWidth, int streamHeight) : m_MultiController(prefs.multiController), m_GamepadMouse(prefs.gamepadMouse), @@ -212,1136 +159,6 @@ void SdlInputHandler::setWindow(SDL_Window *window) m_Window = window; } -void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event) -{ - short keyCode; - char modifiers; - - // Check for our special key combos - if ((event->state == SDL_PRESSED) && - (event->keysym.mod & KMOD_CTRL) && - (event->keysym.mod & KMOD_ALT) && - (event->keysym.mod & KMOD_SHIFT)) { - // First we test the SDLK combos for matches, - // that way we ensure that latin keyboard users - // can match to the key they see on their keyboards. - // If nothing matches that, we'll then go on to - // checking scancodes so non-latin keyboard users - // can have working hotkeys (though possibly in - // odd positions). We must do all SDLK tests before - // any scancode tests to avoid issues in cases - // where the SDLK for one shortcut collides with - // the scancode of another. - - // Check for quit combo (Ctrl+Alt+Shift+Q) - if (event->keysym.sym == SDLK_q) { - SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, - "Detected quit key combo (SDLK)"); - - // Push a quit event to the main loop - SDL_Event event; - event.type = SDL_QUIT; - event.quit.timestamp = SDL_GetTicks(); - SDL_PushEvent(&event); - return; - } - // Check for the unbind combo (Ctrl+Alt+Shift+Z) unless on EGLFS which has no window manager - else if (event->keysym.sym == SDLK_z && QGuiApplication::platformName() != "eglfs") { - SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, - "Detected mouse capture toggle combo (SDLK)"); - - // Stop handling future input - setCaptureActive(!isCaptureActive()); - - // Force raise all keys to ensure they aren't stuck, - // since we won't get their key up events. - raiseAllKeys(); - return; - } - // Check for the mouse mode combo (Ctrl+Alt+Shift+M) unless on EGLFS which has no window manager - else if (event->keysym.sym == SDLK_m && QGuiApplication::platformName() != "eglfs") { - SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, - "Detected mouse mode toggle combo (SDLK)"); - - // Uncapture input - setCaptureActive(false); - - // Toggle mouse mode - m_AbsoluteMouseMode = !m_AbsoluteMouseMode; - - // Recapture input - setCaptureActive(true); - return; - } - else if (event->keysym.sym == SDLK_x && QGuiApplication::platformName() != "eglfs") { - SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, - "Detected full-screen toggle combo (SDLK)"); - Session::s_ActiveSession->toggleFullscreen(); - - // Force raise all keys just be safe across this full-screen/windowed - // transition just in case key events get lost. - raiseAllKeys(); - return; - } - // Check for overlay combo (Ctrl+Alt+Shift+S) - else if (event->keysym.sym == SDLK_s) { - SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, - "Detected stats toggle combo (SDLK)"); - - // Toggle the stats overlay - Session::get()->getOverlayManager().setOverlayState(Overlay::OverlayDebug, - !Session::get()->getOverlayManager().isOverlayEnabled(Overlay::OverlayDebug)); - - // Force raise all keys just be safe across this full-screen/windowed - // transition just in case key events get lost. - raiseAllKeys(); - return; - } - // Check for quit combo (Ctrl+Alt+Shift+Q) - else if (event->keysym.scancode == SDL_SCANCODE_Q) { - SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, - "Detected quit key combo (scancode)"); - - // Push a quit event to the main loop - SDL_Event event; - event.type = SDL_QUIT; - event.quit.timestamp = SDL_GetTicks(); - SDL_PushEvent(&event); - return; - } - // Check for the unbind combo (Ctrl+Alt+Shift+Z) - else if (event->keysym.scancode == SDL_SCANCODE_Z && QGuiApplication::platformName() != "eglfs") { - SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, - "Detected mouse capture toggle combo (scancode)"); - - // Stop handling future input - setCaptureActive(!isCaptureActive()); - - // Force raise all keys to ensure they aren't stuck, - // since we won't get their key up events. - raiseAllKeys(); - return; - } - // Check for the full-screen combo (Ctrl+Alt+Shift+X) unless on EGLFS which has no window manager - else if (event->keysym.scancode == SDL_SCANCODE_X && QGuiApplication::platformName() != "eglfs") { - SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, - "Detected full-screen toggle combo (scancode)"); - Session::s_ActiveSession->toggleFullscreen(); - - // Force raise all keys just be safe across this full-screen/windowed - // transition just in case key events get lost. - raiseAllKeys(); - return; - } - // Check for the mouse mode toggle combo (Ctrl+Alt+Shift+M) unless on EGLFS which has no window manager - else if (event->keysym.scancode == SDL_SCANCODE_M && QGuiApplication::platformName() != "eglfs") { - SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, - "Detected mouse mode toggle combo (scancode)"); - - // Uncapture input - setCaptureActive(false); - - // Toggle mouse mode - m_AbsoluteMouseMode = !m_AbsoluteMouseMode; - - // Recapture input - setCaptureActive(true); - return; - } - else if (event->keysym.scancode == SDL_SCANCODE_S) { - SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, - "Detected stats toggle combo (scancode)"); - - // Toggle the stats overlay - Session::get()->getOverlayManager().setOverlayState(Overlay::OverlayDebug, - !Session::get()->getOverlayManager().isOverlayEnabled(Overlay::OverlayDebug)); - - // Force raise all keys just be safe across this full-screen/windowed - // transition just in case key events get lost. - raiseAllKeys(); - return; - } - } - - if (event->repeat) { - // Ignore repeat key down events - SDL_assert(event->state == SDL_PRESSED); - return; - } - - // Set modifier flags - modifiers = 0; - if (event->keysym.mod & KMOD_CTRL) { - modifiers |= MODIFIER_CTRL; - } - if (event->keysym.mod & KMOD_ALT) { - modifiers |= MODIFIER_ALT; - } - if (event->keysym.mod & KMOD_SHIFT) { - modifiers |= MODIFIER_SHIFT; - } -#ifdef ENABLE_META - if (event->keysym.mod & KMOD_GUI) { - modifiers |= MODIFIER_META; - } -#endif - - // Set keycode. We explicitly use scancode here because GFE will try to correct - // for AZERTY layouts on the host but it depends on receiving VK_ values matching - // a QWERTY layout to work. - if (event->keysym.scancode >= SDL_SCANCODE_1 && event->keysym.scancode <= SDL_SCANCODE_9) { - // SDL defines SDL_SCANCODE_0 > SDL_SCANCODE_9, so we need to handle that manually - keyCode = (event->keysym.scancode - SDL_SCANCODE_1) + VK_0 + 1; - } - else if (event->keysym.scancode >= SDL_SCANCODE_A && event->keysym.scancode <= SDL_SCANCODE_Z) { - keyCode = (event->keysym.scancode - SDL_SCANCODE_A) + VK_A; - } - else if (event->keysym.scancode >= SDL_SCANCODE_F1 && event->keysym.scancode <= SDL_SCANCODE_F12) { - keyCode = (event->keysym.scancode - SDL_SCANCODE_F1) + VK_F1; - } - else if (event->keysym.scancode >= SDL_SCANCODE_F13 && event->keysym.scancode <= SDL_SCANCODE_F24) { - keyCode = (event->keysym.scancode - SDL_SCANCODE_F13) + VK_F13; - } - else if (event->keysym.scancode >= SDL_SCANCODE_KP_1 && event->keysym.scancode <= SDL_SCANCODE_KP_9) { - // SDL defines SDL_SCANCODE_KP_0 > SDL_SCANCODE_KP_9, so we need to handle that manually - keyCode = (event->keysym.scancode - SDL_SCANCODE_KP_1) + VK_NUMPAD0 + 1; - } - else { - switch (event->keysym.scancode) { - case SDL_SCANCODE_BACKSPACE: - keyCode = 0x08; - break; - case SDL_SCANCODE_TAB: - keyCode = 0x09; - break; - case SDL_SCANCODE_CLEAR: - keyCode = 0x0C; - break; - case SDL_SCANCODE_KP_ENTER: // FIXME: Is this correct? - case SDL_SCANCODE_RETURN: - keyCode = 0x0D; - break; - case SDL_SCANCODE_PAUSE: - keyCode = 0x13; - break; - case SDL_SCANCODE_CAPSLOCK: - keyCode = 0x14; - break; - case SDL_SCANCODE_ESCAPE: - keyCode = 0x1B; - break; - case SDL_SCANCODE_SPACE: - keyCode = 0x20; - break; - case SDL_SCANCODE_PAGEUP: - keyCode = 0x21; - break; - case SDL_SCANCODE_PAGEDOWN: - keyCode = 0x22; - break; - case SDL_SCANCODE_END: - keyCode = 0x23; - break; - case SDL_SCANCODE_HOME: - keyCode = 0x24; - break; - case SDL_SCANCODE_LEFT: - keyCode = 0x25; - break; - case SDL_SCANCODE_UP: - keyCode = 0x26; - break; - case SDL_SCANCODE_RIGHT: - keyCode = 0x27; - break; - case SDL_SCANCODE_DOWN: - keyCode = 0x28; - break; - case SDL_SCANCODE_SELECT: - keyCode = 0x29; - break; - case SDL_SCANCODE_EXECUTE: - keyCode = 0x2B; - break; - case SDL_SCANCODE_PRINTSCREEN: - keyCode = 0x2C; - break; - case SDL_SCANCODE_INSERT: - keyCode = 0x2D; - break; - case SDL_SCANCODE_DELETE: - keyCode = 0x2E; - break; - case SDL_SCANCODE_HELP: - keyCode = 0x2F; - break; - case SDL_SCANCODE_KP_0: - // See comment above about why we only handle SDL_SCANCODE_KP_0 here - keyCode = VK_NUMPAD0; - break; - case SDL_SCANCODE_0: - // See comment above about why we only handle SDL_SCANCODE_0 here - keyCode = VK_0; - break; - case SDL_SCANCODE_KP_MULTIPLY: - keyCode = 0x6A; - break; - case SDL_SCANCODE_KP_PLUS: - keyCode = 0x6B; - break; - case SDL_SCANCODE_KP_COMMA: - keyCode = 0x6C; - break; - case SDL_SCANCODE_KP_MINUS: - keyCode = 0x6D; - break; - case SDL_SCANCODE_KP_PERIOD: - keyCode = 0x6E; - break; - case SDL_SCANCODE_KP_DIVIDE: - keyCode = 0x6F; - break; - case SDL_SCANCODE_NUMLOCKCLEAR: - keyCode = 0x90; - break; - case SDL_SCANCODE_SCROLLLOCK: - keyCode = 0x91; - break; - case SDL_SCANCODE_LSHIFT: - keyCode = 0xA0; - break; - case SDL_SCANCODE_RSHIFT: - keyCode = 0xA1; - break; - case SDL_SCANCODE_LCTRL: - keyCode = 0xA2; - break; - case SDL_SCANCODE_RCTRL: - keyCode = 0xA3; - break; - case SDL_SCANCODE_LALT: - keyCode = 0xA4; - break; - case SDL_SCANCODE_RALT: - keyCode = 0xA5; - break; - #ifdef ENABLE_META - case SDL_SCANCODE_LGUI: - keyCode = 0x5B; - break; - case SDL_SCANCODE_RGUI: - keyCode = 0x5C; - break; - #endif - case SDL_SCANCODE_AC_BACK: - keyCode = 0xA6; - break; - case SDL_SCANCODE_AC_FORWARD: - keyCode = 0xA7; - break; - case SDL_SCANCODE_AC_REFRESH: - keyCode = 0xA8; - break; - case SDL_SCANCODE_AC_STOP: - keyCode = 0xA9; - break; - case SDL_SCANCODE_AC_SEARCH: - keyCode = 0xAA; - break; - case SDL_SCANCODE_AC_BOOKMARKS: - keyCode = 0xAB; - break; - case SDL_SCANCODE_AC_HOME: - keyCode = 0xAC; - break; - case SDL_SCANCODE_SEMICOLON: - keyCode = 0xBA; - break; - case SDL_SCANCODE_EQUALS: - keyCode = 0xBB; - break; - case SDL_SCANCODE_COMMA: - keyCode = 0xBC; - break; - case SDL_SCANCODE_MINUS: - keyCode = 0xBD; - break; - case SDL_SCANCODE_PERIOD: - keyCode = 0xBE; - break; - case SDL_SCANCODE_SLASH: - keyCode = 0xBF; - break; - case SDL_SCANCODE_GRAVE: - keyCode = 0xC0; - break; - case SDL_SCANCODE_LEFTBRACKET: - keyCode = 0xDB; - break; - case SDL_SCANCODE_BACKSLASH: - keyCode = 0xDC; - break; - case SDL_SCANCODE_RIGHTBRACKET: - keyCode = 0xDD; - break; - case SDL_SCANCODE_APOSTROPHE: - keyCode = 0xDE; - break; - case SDL_SCANCODE_NONUSBACKSLASH: - keyCode = 0xE2; - break; - default: - SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, - "Unhandled button event: %d", - event->keysym.scancode); - return; - } - } - - // Track the key state so we always know which keys are down - if (event->state == SDL_PRESSED) { - m_KeysDown.insert(keyCode); - } - else { - m_KeysDown.remove(keyCode); - } - - LiSendKeyboardEvent(keyCode, - event->state == SDL_PRESSED ? - KEY_ACTION_DOWN : KEY_ACTION_UP, - modifiers); -} - -void SdlInputHandler::handleMouseButtonEvent(SDL_MouseButtonEvent* event) -{ - int button; - - if (event->which == SDL_TOUCH_MOUSEID) { - // Ignore synthetic mouse events - return; - } - else if (!isCaptureActive()) { - if (event->button == SDL_BUTTON_LEFT && event->state == SDL_RELEASED) { - // Capture the mouse again if clicked when unbound. - // We start capture on left button released instead of - // pressed to avoid sending an errant mouse button released - // event to the host when clicking into our window (since - // the pressed event was consumed by this code). - setCaptureActive(true); - } - - // Not capturing - return; - } - - switch (event->button) - { - case SDL_BUTTON_LEFT: - button = BUTTON_LEFT; - break; - case SDL_BUTTON_MIDDLE: - button = BUTTON_MIDDLE; - break; - case SDL_BUTTON_RIGHT: - button = BUTTON_RIGHT; - break; - case SDL_BUTTON_X1: - button = BUTTON_X1; - break; - case SDL_BUTTON_X2: - button = BUTTON_X2; - break; - default: - SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, - "Unhandled button event: %d", - event->button); - return; - } - - LiSendMouseButtonEvent(event->state == SDL_PRESSED ? - BUTTON_ACTION_PRESS : - BUTTON_ACTION_RELEASE, - button); -} - -void SdlInputHandler::handleMouseMotionEvent(SDL_MouseMotionEvent* event) -{ - if (!isCaptureActive()) { - // Not capturing - return; - } - else if (event->which == SDL_TOUCH_MOUSEID) { - // Ignore synthetic mouse events - return; - } - - if (m_AbsoluteMouseMode) { - SDL_Rect src, dst; - - src.x = src.y = 0; - src.w = m_StreamWidth; - src.h = m_StreamHeight; - - dst.x = dst.y = 0; - SDL_GetWindowSize(m_Window, &dst.w, &dst.h); - - // Use the stream and window sizes to determine the video region - StreamUtils::scaleSourceToDestinationSurface(&src, &dst); - - // Clamp motion to the video region - short x = qMin(qMax(event->x - dst.x, 0), dst.w); - short y = qMin(qMax(event->y - dst.y, 0), dst.h); - - // Send the mouse position update - LiSendMousePositionEvent(x, y, dst.w, dst.h); - } - else { - // Batch until the next mouse polling window or we'll get awful - // input lag everything except GFE 3.14 and 3.15. - SDL_AtomicAdd(&m_MouseDeltaX, event->xrel); - SDL_AtomicAdd(&m_MouseDeltaY, event->yrel); - } -} - -void SdlInputHandler::handleMouseWheelEvent(SDL_MouseWheelEvent* event) -{ - if (!isCaptureActive()) { - // Not capturing - return; - } - else if (event->which == SDL_TOUCH_MOUSEID) { - // Ignore synthetic mouse events - return; - } - - if (event->y != 0) { - LiSendScrollEvent((signed char)event->y); - } -} - -GamepadState* -SdlInputHandler::findStateForGamepad(SDL_JoystickID id) -{ - int i; - - for (i = 0; i < MAX_GAMEPADS; i++) { - if (m_GamepadState[i].jsId == id) { - SDL_assert(!m_MultiController || m_GamepadState[i].index == i); - return &m_GamepadState[i]; - } - } - - // This should only happen with > 4 gamepads - SDL_assert(SDL_NumJoysticks() > 4); - return nullptr; -} - -void SdlInputHandler::sendGamepadState(GamepadState* state) -{ - SDL_assert(m_GamepadMask == 0x1 || m_MultiController); - LiSendMultiControllerEvent(state->index, - m_GamepadMask, - state->buttons, - state->lt, - state->rt, - state->lsX, - state->lsY, - state->rsX, - state->rsY); -} - -Uint32 SdlInputHandler::longPressTimerCallback(Uint32, void*) -{ - // Raise the left click and start a right click - LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT); - LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT); - - return 0; -} - -Uint32 SdlInputHandler::mouseMoveTimerCallback(Uint32 interval, void *param) -{ - auto me = reinterpret_cast(param); - - short deltaX = (short)SDL_AtomicSet(&me->m_MouseDeltaX, 0); - short deltaY = (short)SDL_AtomicSet(&me->m_MouseDeltaY, 0); - - if (deltaX != 0 || deltaY != 0) { - LiSendMouseMoveEvent(deltaX, deltaY); - } - - return interval; -} - -Uint32 SdlInputHandler::mouseEmulationTimerCallback(Uint32 interval, void *param) -{ - auto gamepad = reinterpret_cast(param); - - short rawX; - short rawY; - - // Determine which analog stick is currently receiving the strongest input - if ((uint32_t)qAbs(gamepad->lsX) + qAbs(gamepad->lsY) > (uint32_t)qAbs(gamepad->rsX) + qAbs(gamepad->rsY)) { - rawX = gamepad->lsX; - rawY = -gamepad->lsY; - } - else { - rawX = gamepad->rsX; - rawY = -gamepad->rsY; - } - - float deltaX; - float deltaY; - - // Produce a base vector for mouse movement with increased speed as we deviate further from center - deltaX = qPow(rawX / 32766.0f * MOUSE_EMULATION_MOTION_MULTIPLIER, 3); - deltaY = qPow(rawY / 32766.0f * MOUSE_EMULATION_MOTION_MULTIPLIER, 3); - - // Enforce deadzones - deltaX = qAbs(deltaX) > MOUSE_EMULATION_DEADZONE ? deltaX - MOUSE_EMULATION_DEADZONE : 0; - deltaY = qAbs(deltaY) > MOUSE_EMULATION_DEADZONE ? deltaY - MOUSE_EMULATION_DEADZONE : 0; - - if (deltaX != 0 || deltaY != 0) { - LiSendMouseMoveEvent((short)deltaX, (short)deltaY); - } - - return interval; -} - -void SdlInputHandler::handleControllerAxisEvent(SDL_ControllerAxisEvent* event) -{ - SDL_JoystickID gameControllerId = event->which; - GamepadState* state = findStateForGamepad(gameControllerId); - if (state == NULL) { - return; - } - - // Batch all pending axis motion events for this gamepad to save CPU time - SDL_Event nextEvent; - for (;;) { - switch (event->axis) - { - case SDL_CONTROLLER_AXIS_LEFTX: - state->lsX = event->value; - break; - case SDL_CONTROLLER_AXIS_LEFTY: - // Signed values have one more negative value than - // positive value, so inverting the sign on -32768 - // could actually cause the value to overflow and - // wrap around to be negative again. Avoid that by - // capping the value at 32767. - state->lsY = -qMax(event->value, (short)-32767); - break; - case SDL_CONTROLLER_AXIS_RIGHTX: - state->rsX = event->value; - break; - case SDL_CONTROLLER_AXIS_RIGHTY: - state->rsY = -qMax(event->value, (short)-32767); - break; - case SDL_CONTROLLER_AXIS_TRIGGERLEFT: - state->lt = (unsigned char)(event->value * 255UL / 32767); - break; - case SDL_CONTROLLER_AXIS_TRIGGERRIGHT: - state->rt = (unsigned char)(event->value * 255UL / 32767); - break; - default: - SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, - "Unhandled controller axis: %d", - event->axis); - return; - } - - // Check for another event to batch with - if (SDL_PeepEvents(&nextEvent, 1, SDL_PEEKEVENT, SDL_CONTROLLERAXISMOTION, SDL_CONTROLLERAXISMOTION) <= 0) { - break; - } - - event = &nextEvent.caxis; - if (event->which != gameControllerId) { - // Stop batching if a different gamepad interrupts us - break; - } - - // Remove the next event to batch - SDL_PeepEvents(&nextEvent, 1, SDL_GETEVENT, SDL_CONTROLLERAXISMOTION, SDL_CONTROLLERAXISMOTION); - } - - // Only send the gamepad state to the host if it's not in mouse emulation mode - if (state->mouseEmulationTimer == 0) { - sendGamepadState(state); - } -} - -void SdlInputHandler::handleControllerButtonEvent(SDL_ControllerButtonEvent* event) -{ - GamepadState* state = findStateForGamepad(event->which); - if (state == NULL) { - return; - } - - if (event->state == SDL_PRESSED) { - state->buttons |= k_ButtonMap[event->button]; - - if (event->button == SDL_CONTROLLER_BUTTON_START) { - state->lastStartDownTime = SDL_GetTicks(); - } - else if (state->mouseEmulationTimer != 0) { - if (event->button == SDL_CONTROLLER_BUTTON_A) { - LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT); - } - else if (event->button == SDL_CONTROLLER_BUTTON_B) { - LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT); - } - else if (event->button == SDL_CONTROLLER_BUTTON_X) { - LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_MIDDLE); - } - else if (event->button == SDL_CONTROLLER_BUTTON_LEFTSHOULDER) { - LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_X1); - } - else if (event->button == SDL_CONTROLLER_BUTTON_RIGHTSHOULDER) { - LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_X2); - } - else if (event->button == SDL_CONTROLLER_BUTTON_DPAD_UP) { - LiSendScrollEvent(1); - } - else if (event->button == SDL_CONTROLLER_BUTTON_DPAD_DOWN) { - LiSendScrollEvent(-1); - } - } - } - else { - state->buttons &= ~k_ButtonMap[event->button]; - - if (event->button == SDL_CONTROLLER_BUTTON_START) { - if (SDL_GetTicks() - state->lastStartDownTime > MOUSE_EMULATION_LONG_PRESS_TIME) { - if (state->mouseEmulationTimer != 0) { - SDL_RemoveTimer(state->mouseEmulationTimer); - state->mouseEmulationTimer = 0; - - SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, - "Mouse emulation deactivated"); - Session::get()->notifyMouseEmulationMode(false); - } - else if (m_GamepadMouse) { - // Send the start button up event to the host, since we won't do it below - sendGamepadState(state); - - state->mouseEmulationTimer = SDL_AddTimer(MOUSE_EMULATION_POLLING_INTERVAL, SdlInputHandler::mouseEmulationTimerCallback, state); - - SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, - "Mouse emulation active"); - Session::get()->notifyMouseEmulationMode(true); - } - } - } - else if (state->mouseEmulationTimer != 0) { - if (event->button == SDL_CONTROLLER_BUTTON_A) { - LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT); - } - else if (event->button == SDL_CONTROLLER_BUTTON_B) { - LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT); - } - else if (event->button == SDL_CONTROLLER_BUTTON_X) { - LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_MIDDLE); - } - else if (event->button == SDL_CONTROLLER_BUTTON_LEFTSHOULDER) { - LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_X1); - } - else if (event->button == SDL_CONTROLLER_BUTTON_RIGHTSHOULDER) { - LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_X2); - } - } - } - - // Handle Start+Select+L1+R1 as a gamepad quit combo - if (state->buttons == (PLAY_FLAG | BACK_FLAG | LB_FLAG | RB_FLAG)) { - SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, - "Detected quit gamepad button combo"); - - // Push a quit event to the main loop - SDL_Event event; - event.type = SDL_QUIT; - event.quit.timestamp = SDL_GetTicks(); - SDL_PushEvent(&event); - - // Clear buttons down on this gameapd - LiSendMultiControllerEvent(state->index, m_GamepadMask, - 0, 0, 0, 0, 0, 0, 0); - return; - } - - // Only send the gamepad state to the host if it's not in mouse emulation mode - if (state->mouseEmulationTimer == 0) { - sendGamepadState(state); - } -} - -void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* event) -{ - GamepadState* state; - - if (event->type == SDL_CONTROLLERDEVICEADDED) { - int i; - const char* name; - SDL_GameController* controller; - const char* mapping; - char guidStr[33]; - uint32_t hapticCaps; - - controller = SDL_GameControllerOpen(event->which); - if (controller == NULL) { - SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, - "Failed to open gamepad: %s", - SDL_GetError()); - return; - } - - // We used to use SDL_GameControllerGetPlayerIndex() here but that - // can lead to strange issues due to bugs in Windows where an Xbox - // controller will join as player 2, even though no player 1 controller - // is connected at all. This pretty much screws any attempt to use - // the gamepad in single player games, so just assign them in order from 0. - i = 0; - - for (; i < MAX_GAMEPADS; i++) { - SDL_assert(m_GamepadState[i].controller != controller); - if (m_GamepadState[i].controller == NULL) { - // Found an empty slot - break; - } - } - - if (i == MAX_GAMEPADS) { - SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, - "No open gamepad slots found!"); - SDL_GameControllerClose(controller); - return; - } - - state = &m_GamepadState[i]; - if (m_MultiController) { - state->index = i; - -#if SDL_VERSION_ATLEAST(2, 0, 12) - // This will change indicators on the controller to show the assigned - // player index. For Xbox 360 controllers, that means updating the LED - // ring to light up the corresponding quadrant for this player. - SDL_GameControllerSetPlayerIndex(controller, state->index); -#endif - } - else { - // Always player 1 in single controller mode - state->index = 0; - } - - state->controller = controller; - state->jsId = SDL_JoystickInstanceID(SDL_GameControllerGetJoystick(state->controller)); - -#if SDL_VERSION_ATLEAST(2, 0, 9) - // Perform a tiny rumble to see if haptics are supported. - // NB: We cannot use zeros for rumble intensity or SDL will not actually call the JS driver - // and we'll get a (potentially false) success value returned. - hapticCaps = SDL_GameControllerRumble(controller, 1, 1, 1) == 0 ? - ML_HAPTIC_GC_RUMBLE : 0; -#else - state->haptic = SDL_HapticOpenFromJoystick(SDL_GameControllerGetJoystick(state->controller)); - state->hapticEffectId = -1; - state->hapticMethod = GAMEPAD_HAPTIC_METHOD_NONE; - if (state->haptic != nullptr) { - // Query for supported haptic effects - hapticCaps = SDL_HapticQuery(state->haptic); - hapticCaps |= SDL_HapticRumbleSupported(state->haptic) ? - ML_HAPTIC_SIMPLE_RUMBLE : 0; - - if ((SDL_HapticQuery(state->haptic) & SDL_HAPTIC_LEFTRIGHT) == 0) { - if (SDL_HapticRumbleSupported(state->haptic)) { - if (SDL_HapticRumbleInit(state->haptic) == 0) { - state->hapticMethod = GAMEPAD_HAPTIC_METHOD_SIMPLERUMBLE; - } - } - if (state->hapticMethod == GAMEPAD_HAPTIC_METHOD_NONE) { - SDL_HapticClose(state->haptic); - state->haptic = nullptr; - } - } else { - state->hapticMethod = GAMEPAD_HAPTIC_METHOD_LEFTRIGHT; - } - } - else { - hapticCaps = 0; - } -#endif - - SDL_JoystickGetGUIDString(SDL_JoystickGetGUID(SDL_GameControllerGetJoystick(state->controller)), - guidStr, sizeof(guidStr)); - mapping = SDL_GameControllerMapping(state->controller); - name = SDL_GameControllerName(state->controller); - SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, - "Gamepad %d (player %d) is: %s (haptic capabilities: 0x%x) (mapping: %s -> %s)", - i, - state->index, - name != nullptr ? name : "", - hapticCaps, - guidStr, - mapping != nullptr ? mapping : ""); - if (mapping != nullptr) { - SDL_free((void*)mapping); - } - - // Add this gamepad to the gamepad mask - if (m_MultiController) { - // NB: Don't assert that it's unset here because we will already - // have the mask set for initially attached gamepads to avoid confusing - // apps running on the host. - m_GamepadMask |= (1 << state->index); - } - else { - SDL_assert(m_GamepadMask == 0x1); - } - - // Send an empty event to tell the PC we've arrived - sendGamepadState(state); - } - else if (event->type == SDL_CONTROLLERDEVICEREMOVED) { - state = findStateForGamepad(event->which); - if (state != NULL) { - if (state->mouseEmulationTimer != 0) { - Session::get()->notifyMouseEmulationMode(false); - SDL_RemoveTimer(state->mouseEmulationTimer); - } - - SDL_GameControllerClose(state->controller); - -#if !SDL_VERSION_ATLEAST(2, 0, 9) - if (state->haptic != nullptr) { - SDL_HapticClose(state->haptic); - } -#endif - - // Remove this from the gamepad mask in MC-mode - if (m_MultiController) { - SDL_assert(m_GamepadMask & (1 << state->index)); - m_GamepadMask &= ~(1 << state->index); - } - else { - SDL_assert(m_GamepadMask == 0x1); - } - - SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, - "Gamepad %d is gone", - state->index); - - // Send a final event to let the PC know this gamepad is gone - LiSendMultiControllerEvent(state->index, m_GamepadMask, - 0, 0, 0, 0, 0, 0, 0); - - // Clear all remaining state from this slot - SDL_memset(state, 0, sizeof(*state)); - } - } -} - -void SdlInputHandler::handleJoystickArrivalEvent(SDL_JoyDeviceEvent* event) -{ - SDL_assert(event->type == SDL_JOYDEVICEADDED); - - if (!SDL_IsGameController(event->which)) { - char guidStr[33]; - SDL_JoystickGetGUIDString(SDL_JoystickGetDeviceGUID(event->which), - guidStr, sizeof(guidStr)); - const char* name = SDL_JoystickNameForIndex(event->which); - SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, - "Joystick discovered with no mapping: %s %s", - name ? name : "", - guidStr); - SDL_Joystick* joy = SDL_JoystickOpen(event->which); - if (joy != nullptr) { - SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, - "Number of axes: %d | Number of buttons: %d | Number of hats: %d", - SDL_JoystickNumAxes(joy), SDL_JoystickNumButtons(joy), - SDL_JoystickNumHats(joy)); - SDL_JoystickClose(joy); - } - else { - SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, - "Unable to open joystick for query: %s", - SDL_GetError()); - } - } -} - -void SdlInputHandler::rumble(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor) -{ - // Make sure the controller number is within our supported count - if (controllerNumber >= MAX_GAMEPADS) { - return; - } - -#if SDL_VERSION_ATLEAST(2, 0, 9) - if (m_GamepadState[controllerNumber].controller != nullptr) { - SDL_GameControllerRumble(m_GamepadState[controllerNumber].controller, lowFreqMotor, highFreqMotor, 30000); - } -#else - // Check if the controller supports haptics (and if the controller exists at all) - SDL_Haptic* haptic = m_GamepadState[controllerNumber].haptic; - if (haptic == nullptr) { - return; - } - - // Stop the last effect we played - if (m_GamepadState[controllerNumber].hapticMethod == GAMEPAD_HAPTIC_METHOD_LEFTRIGHT) { - if (m_GamepadState[controllerNumber].hapticEffectId >= 0) { - SDL_HapticDestroyEffect(haptic, m_GamepadState[controllerNumber].hapticEffectId); - } - } else if (m_GamepadState[controllerNumber].hapticMethod == GAMEPAD_HAPTIC_METHOD_SIMPLERUMBLE) { - SDL_HapticRumbleStop(haptic); - } - - // If this callback is telling us to stop both motors, don't bother queuing a new effect - if (lowFreqMotor == 0 && highFreqMotor == 0) { - return; - } - - if (m_GamepadState[controllerNumber].hapticMethod == GAMEPAD_HAPTIC_METHOD_LEFTRIGHT) { - SDL_HapticEffect effect; - SDL_memset(&effect, 0, sizeof(effect)); - effect.type = SDL_HAPTIC_LEFTRIGHT; - - // The effect should last until we are instructed to stop or change it - effect.leftright.length = SDL_HAPTIC_INFINITY; - - // SDL haptics range from 0-32767 but XInput uses 0-65535, so divide by 2 to correct for SDL's scaling - effect.leftright.large_magnitude = lowFreqMotor / 2; - effect.leftright.small_magnitude = highFreqMotor / 2; - - // Play the new effect - m_GamepadState[controllerNumber].hapticEffectId = SDL_HapticNewEffect(haptic, &effect); - if (m_GamepadState[controllerNumber].hapticEffectId >= 0) { - SDL_HapticRunEffect(haptic, m_GamepadState[controllerNumber].hapticEffectId, 1); - } - } else if (m_GamepadState[controllerNumber].hapticMethod == GAMEPAD_HAPTIC_METHOD_SIMPLERUMBLE) { - SDL_HapticRumblePlay(haptic, - std::min(1.0, (GAMEPAD_HAPTIC_SIMPLE_HIFREQ_MOTOR_WEIGHT*highFreqMotor + - GAMEPAD_HAPTIC_SIMPLE_LOWFREQ_MOTOR_WEIGHT*lowFreqMotor) / 65535.0), - SDL_HAPTIC_INFINITY); - } -#endif -} - -void SdlInputHandler::handleTouchFingerEvent(SDL_TouchFingerEvent* event) -{ -#if SDL_VERSION_ATLEAST(2, 0, 10) - if (SDL_GetTouchDeviceType(event->touchId) != SDL_TOUCH_DEVICE_DIRECT) { - // Ignore anything that isn't a touchscreen. We may get callbacks - // for trackpads, but we want to handle those in the mouse path. - return; - } -#elif defined(Q_OS_DARWIN) - // SDL2 sends touch events from trackpads by default on - // macOS. This totally screws our actual mouse handling, - // so we must explicitly ignore touch events on macOS - // until SDL 2.0.10 where we have SDL_GetTouchDeviceType() - // to tell them apart. - return; -#endif - - // Observations on Windows 10: x and y appear to be relative to 0,0 of the window client area. - // Although SDL documentation states they are 0.0 - 1.0 float values, they can actually be higher - // or lower than those values as touch events continue for touches started within the client area that - // leave the client area during a drag motion. - // dx and dy are deltas from the last touch event, not the first touch down. - - // Ignore touch down events with more than one finger - if (event->type == SDL_FINGERDOWN && SDL_GetNumTouchFingers(event->touchId) > 1) { - return; - } - - // Ignore touch move and touch up events from the non-primary finger - if (event->type != SDL_FINGERDOWN && event->fingerId != m_LastTouchDownEvent.fingerId) { - return; - } - - SDL_Rect src, dst; - int windowWidth, windowHeight; - - SDL_GetWindowSize(m_Window, &windowWidth, &windowHeight); - - src.x = src.y = 0; - src.w = m_StreamWidth; - src.h = m_StreamHeight; - - dst.x = dst.y = 0; - dst.w = windowWidth; - dst.h = windowHeight; - - // Use the stream and window sizes to determine the video region - StreamUtils::scaleSourceToDestinationSurface(&src, &dst); - - if (qSqrt(qPow(event->x - m_LastTouchDownEvent.x, 2) + qPow(event->y - m_LastTouchDownEvent.y, 2)) > LONG_PRESS_ACTIVATION_DELTA) { - // Moved too far since touch down. Cancel the long press timer. - SDL_RemoveTimer(m_LongPressTimer); - m_LongPressTimer = 0; - } - - // Don't reposition for finger down events within the deadzone. This makes double-clicking easier. - if (event->type != SDL_FINGERDOWN || - event->timestamp - m_LastTouchUpEvent.timestamp > DOUBLE_TAP_DEAD_ZONE_DELAY || - qSqrt(qPow(event->x - m_LastTouchUpEvent.x, 2) + qPow(event->y - m_LastTouchUpEvent.y, 2)) > DOUBLE_TAP_DEAD_ZONE_DELTA) { - // Scale window-relative events to be video-relative and clamp to video region - short x = qMin(qMax((int)(event->x * windowWidth), dst.x), dst.x + dst.w); - short y = qMin(qMax((int)(event->y * windowHeight), dst.y), dst.y + dst.h); - - // Update the cursor position relative to the video region - LiSendMousePositionEvent(x - dst.x, y - dst.y, dst.w, dst.h); - } - - if (event->type == SDL_FINGERDOWN) { - m_LastTouchDownEvent = *event; - - // Start/restart the long press timer - SDL_RemoveTimer(m_LongPressTimer); - m_LongPressTimer = SDL_AddTimer(LONG_PRESS_ACTIVATION_DELAY, - longPressTimerCallback, - nullptr); - - // Left button down on finger down - LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT); - } - else if (event->type == SDL_FINGERUP) { - m_LastTouchUpEvent = *event; - - // Cancel the long press timer - SDL_RemoveTimer(m_LongPressTimer); - m_LongPressTimer = 0; - - // Left button up on finger up - LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT); - - // Raise right button too in case we triggered a long press gesture - LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT); - } -} - -int SdlInputHandler::getAttachedGamepadMask() -{ - int count; - int mask; - - if (!m_MultiController) { - // Player 1 is always present in non-MC mode - return 0x1; - } - - count = mask = 0; - for (int i = 0; i < SDL_NumJoysticks(); i++) { - if (SDL_IsGameController(i)) { - mask |= (1 << count++); - } - } - - return mask; -} - void SdlInputHandler::raiseAllKeys() { if (m_KeysDown.isEmpty()) { @@ -1441,61 +258,3 @@ void SdlInputHandler::setCaptureActive(bool active) SDL_SetWindowGrab(m_Window, SDL_FALSE); } } - -QString SdlInputHandler::getUnmappedGamepads() -{ - QString ret; - - if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) != 0) { - SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, - "SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) failed: %s", - SDL_GetError()); - } - - MappingManager mappingManager; - mappingManager.applyMappings(); - - for (int i = 0; i < SDL_NumJoysticks(); i++) { - if (!SDL_IsGameController(i)) { - char guidStr[33]; - SDL_JoystickGetGUIDString(SDL_JoystickGetDeviceGUID(i), - guidStr, sizeof(guidStr)); - const char* name = SDL_JoystickNameForIndex(i); - SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, - "Unmapped joystick: %s %s", - name ? name : "", - guidStr); - SDL_Joystick* joy = SDL_JoystickOpen(i); - if (joy != nullptr) { - int numButtons = SDL_JoystickNumButtons(joy); - int numHats = SDL_JoystickNumHats(joy); - int numAxes = SDL_JoystickNumAxes(joy); - - SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, - "Number of axes: %d | Number of buttons: %d | Number of hats: %d", - numAxes, numButtons, numHats); - - if ((numAxes >= 4 && numAxes <= 8) && numButtons >= 8 && numHats <= 1) { - SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, - "Joystick likely to be an unmapped game controller"); - if (!ret.isEmpty()) { - ret += ", "; - } - - ret += name; - } - - SDL_JoystickClose(joy); - } - else { - SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, - "Unable to open joystick for query: %s", - SDL_GetError()); - } - } - } - - SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER); - - return ret; -} diff --git a/app/streaming/input/keyboard.cpp b/app/streaming/input/keyboard.cpp new file mode 100644 index 00000000..8a0d461d --- /dev/null +++ b/app/streaming/input/keyboard.cpp @@ -0,0 +1,420 @@ +#include "streaming/session.h" + +#include +#include + +#include + +// Until we can fully capture these on all platforms (without conflicting with +// OS-provided shortcuts), we should avoid passing them through to the host. +//#define ENABLE_META + +#define VK_0 0x30 +#define VK_A 0x41 + +// These are real Windows VK_* codes +#ifndef VK_F1 +#define VK_F1 0x70 +#define VK_F13 0x7C +#define VK_NUMPAD0 0x60 +#endif + +void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event) +{ + short keyCode; + char modifiers; + + // Check for our special key combos + if ((event->state == SDL_PRESSED) && + (event->keysym.mod & KMOD_CTRL) && + (event->keysym.mod & KMOD_ALT) && + (event->keysym.mod & KMOD_SHIFT)) { + // First we test the SDLK combos for matches, + // that way we ensure that latin keyboard users + // can match to the key they see on their keyboards. + // If nothing matches that, we'll then go on to + // checking scancodes so non-latin keyboard users + // can have working hotkeys (though possibly in + // odd positions). We must do all SDLK tests before + // any scancode tests to avoid issues in cases + // where the SDLK for one shortcut collides with + // the scancode of another. + + // Check for quit combo (Ctrl+Alt+Shift+Q) + if (event->keysym.sym == SDLK_q) { + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, + "Detected quit key combo (SDLK)"); + + // Push a quit event to the main loop + SDL_Event event; + event.type = SDL_QUIT; + event.quit.timestamp = SDL_GetTicks(); + SDL_PushEvent(&event); + return; + } + // Check for the unbind combo (Ctrl+Alt+Shift+Z) unless on EGLFS which has no window manager + else if (event->keysym.sym == SDLK_z && QGuiApplication::platformName() != "eglfs") { + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, + "Detected mouse capture toggle combo (SDLK)"); + + // Stop handling future input + setCaptureActive(!isCaptureActive()); + + // Force raise all keys to ensure they aren't stuck, + // since we won't get their key up events. + raiseAllKeys(); + return; + } + // Check for the mouse mode combo (Ctrl+Alt+Shift+M) unless on EGLFS which has no window manager + else if (event->keysym.sym == SDLK_m && QGuiApplication::platformName() != "eglfs") { + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, + "Detected mouse mode toggle combo (SDLK)"); + + // Uncapture input + setCaptureActive(false); + + // Toggle mouse mode + m_AbsoluteMouseMode = !m_AbsoluteMouseMode; + + // Recapture input + setCaptureActive(true); + return; + } + else if (event->keysym.sym == SDLK_x && QGuiApplication::platformName() != "eglfs") { + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, + "Detected full-screen toggle combo (SDLK)"); + Session::s_ActiveSession->toggleFullscreen(); + + // Force raise all keys just be safe across this full-screen/windowed + // transition just in case key events get lost. + raiseAllKeys(); + return; + } + // Check for overlay combo (Ctrl+Alt+Shift+S) + else if (event->keysym.sym == SDLK_s) { + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, + "Detected stats toggle combo (SDLK)"); + + // Toggle the stats overlay + Session::get()->getOverlayManager().setOverlayState(Overlay::OverlayDebug, + !Session::get()->getOverlayManager().isOverlayEnabled(Overlay::OverlayDebug)); + + // Force raise all keys just be safe across this full-screen/windowed + // transition just in case key events get lost. + raiseAllKeys(); + return; + } + // Check for quit combo (Ctrl+Alt+Shift+Q) + else if (event->keysym.scancode == SDL_SCANCODE_Q) { + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, + "Detected quit key combo (scancode)"); + + // Push a quit event to the main loop + SDL_Event event; + event.type = SDL_QUIT; + event.quit.timestamp = SDL_GetTicks(); + SDL_PushEvent(&event); + return; + } + // Check for the unbind combo (Ctrl+Alt+Shift+Z) + else if (event->keysym.scancode == SDL_SCANCODE_Z && QGuiApplication::platformName() != "eglfs") { + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, + "Detected mouse capture toggle combo (scancode)"); + + // Stop handling future input + setCaptureActive(!isCaptureActive()); + + // Force raise all keys to ensure they aren't stuck, + // since we won't get their key up events. + raiseAllKeys(); + return; + } + // Check for the full-screen combo (Ctrl+Alt+Shift+X) unless on EGLFS which has no window manager + else if (event->keysym.scancode == SDL_SCANCODE_X && QGuiApplication::platformName() != "eglfs") { + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, + "Detected full-screen toggle combo (scancode)"); + Session::s_ActiveSession->toggleFullscreen(); + + // Force raise all keys just be safe across this full-screen/windowed + // transition just in case key events get lost. + raiseAllKeys(); + return; + } + // Check for the mouse mode toggle combo (Ctrl+Alt+Shift+M) unless on EGLFS which has no window manager + else if (event->keysym.scancode == SDL_SCANCODE_M && QGuiApplication::platformName() != "eglfs") { + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, + "Detected mouse mode toggle combo (scancode)"); + + // Uncapture input + setCaptureActive(false); + + // Toggle mouse mode + m_AbsoluteMouseMode = !m_AbsoluteMouseMode; + + // Recapture input + setCaptureActive(true); + return; + } + else if (event->keysym.scancode == SDL_SCANCODE_S) { + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, + "Detected stats toggle combo (scancode)"); + + // Toggle the stats overlay + Session::get()->getOverlayManager().setOverlayState(Overlay::OverlayDebug, + !Session::get()->getOverlayManager().isOverlayEnabled(Overlay::OverlayDebug)); + + // Force raise all keys just be safe across this full-screen/windowed + // transition just in case key events get lost. + raiseAllKeys(); + return; + } + } + + if (event->repeat) { + // Ignore repeat key down events + SDL_assert(event->state == SDL_PRESSED); + return; + } + + // Set modifier flags + modifiers = 0; + if (event->keysym.mod & KMOD_CTRL) { + modifiers |= MODIFIER_CTRL; + } + if (event->keysym.mod & KMOD_ALT) { + modifiers |= MODIFIER_ALT; + } + if (event->keysym.mod & KMOD_SHIFT) { + modifiers |= MODIFIER_SHIFT; + } +#ifdef ENABLE_META + if (event->keysym.mod & KMOD_GUI) { + modifiers |= MODIFIER_META; + } +#endif + + // Set keycode. We explicitly use scancode here because GFE will try to correct + // for AZERTY layouts on the host but it depends on receiving VK_ values matching + // a QWERTY layout to work. + if (event->keysym.scancode >= SDL_SCANCODE_1 && event->keysym.scancode <= SDL_SCANCODE_9) { + // SDL defines SDL_SCANCODE_0 > SDL_SCANCODE_9, so we need to handle that manually + keyCode = (event->keysym.scancode - SDL_SCANCODE_1) + VK_0 + 1; + } + else if (event->keysym.scancode >= SDL_SCANCODE_A && event->keysym.scancode <= SDL_SCANCODE_Z) { + keyCode = (event->keysym.scancode - SDL_SCANCODE_A) + VK_A; + } + else if (event->keysym.scancode >= SDL_SCANCODE_F1 && event->keysym.scancode <= SDL_SCANCODE_F12) { + keyCode = (event->keysym.scancode - SDL_SCANCODE_F1) + VK_F1; + } + else if (event->keysym.scancode >= SDL_SCANCODE_F13 && event->keysym.scancode <= SDL_SCANCODE_F24) { + keyCode = (event->keysym.scancode - SDL_SCANCODE_F13) + VK_F13; + } + else if (event->keysym.scancode >= SDL_SCANCODE_KP_1 && event->keysym.scancode <= SDL_SCANCODE_KP_9) { + // SDL defines SDL_SCANCODE_KP_0 > SDL_SCANCODE_KP_9, so we need to handle that manually + keyCode = (event->keysym.scancode - SDL_SCANCODE_KP_1) + VK_NUMPAD0 + 1; + } + else { + switch (event->keysym.scancode) { + case SDL_SCANCODE_BACKSPACE: + keyCode = 0x08; + break; + case SDL_SCANCODE_TAB: + keyCode = 0x09; + break; + case SDL_SCANCODE_CLEAR: + keyCode = 0x0C; + break; + case SDL_SCANCODE_KP_ENTER: // FIXME: Is this correct? + case SDL_SCANCODE_RETURN: + keyCode = 0x0D; + break; + case SDL_SCANCODE_PAUSE: + keyCode = 0x13; + break; + case SDL_SCANCODE_CAPSLOCK: + keyCode = 0x14; + break; + case SDL_SCANCODE_ESCAPE: + keyCode = 0x1B; + break; + case SDL_SCANCODE_SPACE: + keyCode = 0x20; + break; + case SDL_SCANCODE_PAGEUP: + keyCode = 0x21; + break; + case SDL_SCANCODE_PAGEDOWN: + keyCode = 0x22; + break; + case SDL_SCANCODE_END: + keyCode = 0x23; + break; + case SDL_SCANCODE_HOME: + keyCode = 0x24; + break; + case SDL_SCANCODE_LEFT: + keyCode = 0x25; + break; + case SDL_SCANCODE_UP: + keyCode = 0x26; + break; + case SDL_SCANCODE_RIGHT: + keyCode = 0x27; + break; + case SDL_SCANCODE_DOWN: + keyCode = 0x28; + break; + case SDL_SCANCODE_SELECT: + keyCode = 0x29; + break; + case SDL_SCANCODE_EXECUTE: + keyCode = 0x2B; + break; + case SDL_SCANCODE_PRINTSCREEN: + keyCode = 0x2C; + break; + case SDL_SCANCODE_INSERT: + keyCode = 0x2D; + break; + case SDL_SCANCODE_DELETE: + keyCode = 0x2E; + break; + case SDL_SCANCODE_HELP: + keyCode = 0x2F; + break; + case SDL_SCANCODE_KP_0: + // See comment above about why we only handle SDL_SCANCODE_KP_0 here + keyCode = VK_NUMPAD0; + break; + case SDL_SCANCODE_0: + // See comment above about why we only handle SDL_SCANCODE_0 here + keyCode = VK_0; + break; + case SDL_SCANCODE_KP_MULTIPLY: + keyCode = 0x6A; + break; + case SDL_SCANCODE_KP_PLUS: + keyCode = 0x6B; + break; + case SDL_SCANCODE_KP_COMMA: + keyCode = 0x6C; + break; + case SDL_SCANCODE_KP_MINUS: + keyCode = 0x6D; + break; + case SDL_SCANCODE_KP_PERIOD: + keyCode = 0x6E; + break; + case SDL_SCANCODE_KP_DIVIDE: + keyCode = 0x6F; + break; + case SDL_SCANCODE_NUMLOCKCLEAR: + keyCode = 0x90; + break; + case SDL_SCANCODE_SCROLLLOCK: + keyCode = 0x91; + break; + case SDL_SCANCODE_LSHIFT: + keyCode = 0xA0; + break; + case SDL_SCANCODE_RSHIFT: + keyCode = 0xA1; + break; + case SDL_SCANCODE_LCTRL: + keyCode = 0xA2; + break; + case SDL_SCANCODE_RCTRL: + keyCode = 0xA3; + break; + case SDL_SCANCODE_LALT: + keyCode = 0xA4; + break; + case SDL_SCANCODE_RALT: + keyCode = 0xA5; + break; + #ifdef ENABLE_META + case SDL_SCANCODE_LGUI: + keyCode = 0x5B; + break; + case SDL_SCANCODE_RGUI: + keyCode = 0x5C; + break; + #endif + case SDL_SCANCODE_AC_BACK: + keyCode = 0xA6; + break; + case SDL_SCANCODE_AC_FORWARD: + keyCode = 0xA7; + break; + case SDL_SCANCODE_AC_REFRESH: + keyCode = 0xA8; + break; + case SDL_SCANCODE_AC_STOP: + keyCode = 0xA9; + break; + case SDL_SCANCODE_AC_SEARCH: + keyCode = 0xAA; + break; + case SDL_SCANCODE_AC_BOOKMARKS: + keyCode = 0xAB; + break; + case SDL_SCANCODE_AC_HOME: + keyCode = 0xAC; + break; + case SDL_SCANCODE_SEMICOLON: + keyCode = 0xBA; + break; + case SDL_SCANCODE_EQUALS: + keyCode = 0xBB; + break; + case SDL_SCANCODE_COMMA: + keyCode = 0xBC; + break; + case SDL_SCANCODE_MINUS: + keyCode = 0xBD; + break; + case SDL_SCANCODE_PERIOD: + keyCode = 0xBE; + break; + case SDL_SCANCODE_SLASH: + keyCode = 0xBF; + break; + case SDL_SCANCODE_GRAVE: + keyCode = 0xC0; + break; + case SDL_SCANCODE_LEFTBRACKET: + keyCode = 0xDB; + break; + case SDL_SCANCODE_BACKSLASH: + keyCode = 0xDC; + break; + case SDL_SCANCODE_RIGHTBRACKET: + keyCode = 0xDD; + break; + case SDL_SCANCODE_APOSTROPHE: + keyCode = 0xDE; + break; + case SDL_SCANCODE_NONUSBACKSLASH: + keyCode = 0xE2; + break; + default: + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, + "Unhandled button event: %d", + event->keysym.scancode); + return; + } + } + + // Track the key state so we always know which keys are down + if (event->state == SDL_PRESSED) { + m_KeysDown.insert(keyCode); + } + else { + m_KeysDown.remove(keyCode); + } + + LiSendKeyboardEvent(keyCode, + event->state == SDL_PRESSED ? + KEY_ACTION_DOWN : KEY_ACTION_UP, + modifiers); +} diff --git a/app/streaming/input/mouse.cpp b/app/streaming/input/mouse.cpp new file mode 100644 index 00000000..923aa302 --- /dev/null +++ b/app/streaming/input/mouse.cpp @@ -0,0 +1,126 @@ +#include "input.h" + +#include +#include +#include "streaming/streamutils.h" + +void SdlInputHandler::handleMouseButtonEvent(SDL_MouseButtonEvent* event) +{ + int button; + + if (event->which == SDL_TOUCH_MOUSEID) { + // Ignore synthetic mouse events + return; + } + else if (!isCaptureActive()) { + if (event->button == SDL_BUTTON_LEFT && event->state == SDL_RELEASED) { + // Capture the mouse again if clicked when unbound. + // We start capture on left button released instead of + // pressed to avoid sending an errant mouse button released + // event to the host when clicking into our window (since + // the pressed event was consumed by this code). + setCaptureActive(true); + } + + // Not capturing + return; + } + + switch (event->button) + { + case SDL_BUTTON_LEFT: + button = BUTTON_LEFT; + break; + case SDL_BUTTON_MIDDLE: + button = BUTTON_MIDDLE; + break; + case SDL_BUTTON_RIGHT: + button = BUTTON_RIGHT; + break; + case SDL_BUTTON_X1: + button = BUTTON_X1; + break; + case SDL_BUTTON_X2: + button = BUTTON_X2; + break; + default: + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, + "Unhandled button event: %d", + event->button); + return; + } + + LiSendMouseButtonEvent(event->state == SDL_PRESSED ? + BUTTON_ACTION_PRESS : + BUTTON_ACTION_RELEASE, + button); +} + +void SdlInputHandler::handleMouseMotionEvent(SDL_MouseMotionEvent* event) +{ + if (!isCaptureActive()) { + // Not capturing + return; + } + else if (event->which == SDL_TOUCH_MOUSEID) { + // Ignore synthetic mouse events + return; + } + + if (m_AbsoluteMouseMode) { + SDL_Rect src, dst; + + src.x = src.y = 0; + src.w = m_StreamWidth; + src.h = m_StreamHeight; + + dst.x = dst.y = 0; + SDL_GetWindowSize(m_Window, &dst.w, &dst.h); + + // Use the stream and window sizes to determine the video region + StreamUtils::scaleSourceToDestinationSurface(&src, &dst); + + // Clamp motion to the video region + short x = qMin(qMax(event->x - dst.x, 0), dst.w); + short y = qMin(qMax(event->y - dst.y, 0), dst.h); + + // Send the mouse position update + LiSendMousePositionEvent(x, y, dst.w, dst.h); + } + else { + // Batch until the next mouse polling window or we'll get awful + // input lag everything except GFE 3.14 and 3.15. + SDL_AtomicAdd(&m_MouseDeltaX, event->xrel); + SDL_AtomicAdd(&m_MouseDeltaY, event->yrel); + } +} + +void SdlInputHandler::handleMouseWheelEvent(SDL_MouseWheelEvent* event) +{ + if (!isCaptureActive()) { + // Not capturing + return; + } + else if (event->which == SDL_TOUCH_MOUSEID) { + // Ignore synthetic mouse events + return; + } + + if (event->y != 0) { + LiSendScrollEvent((signed char)event->y); + } +} + +Uint32 SdlInputHandler::mouseMoveTimerCallback(Uint32 interval, void *param) +{ + auto me = reinterpret_cast(param); + + short deltaX = (short)SDL_AtomicSet(&me->m_MouseDeltaX, 0); + short deltaY = (short)SDL_AtomicSet(&me->m_MouseDeltaY, 0); + + if (deltaX != 0 || deltaY != 0) { + LiSendMouseMoveEvent(deltaX, deltaY); + } + + return interval; +}