diff --git a/app/app.pro b/app/app.pro index a91c4fd2..e76562c0 100644 --- a/app/app.pro +++ b/app/app.pro @@ -134,6 +134,7 @@ SOURCES += \ settings/mappingfetcher.cpp \ settings/streamingpreferences.cpp \ streaming/input/abstouch.cpp \ + streaming/input/clipboard.cpp \ streaming/input/gamepad.cpp \ streaming/input/input.cpp \ streaming/input/keyboard.cpp \ diff --git a/app/streaming/input/clipboard.cpp b/app/streaming/input/clipboard.cpp new file mode 100644 index 00000000..ff87c2dd --- /dev/null +++ b/app/streaming/input/clipboard.cpp @@ -0,0 +1,101 @@ +#include "input.h" + +#define MAP_KEY(c, sc) \ + case c: \ + event.key.keysym.scancode = sc; \ + break + +#define MAP_KEY_SHIFT(c, sc) \ + case c: \ + event.key.keysym.scancode = sc; \ + event.key.keysym.mod = KMOD_SHIFT; \ + break + +void SdlInputHandler::sendText(const char* text) +{ + for (const char* c = text; *c != 0; c++) { + SDL_Event event = {}; + + if (*c >= 'A' && *c <= 'Z') { + event.key.keysym.scancode = (SDL_Scancode)((*c - 'A') + SDL_SCANCODE_A); + event.key.keysym.mod = KMOD_SHIFT; + } + else if (*c >= 'a' && *c <= 'z') { + event.key.keysym.scancode = (SDL_Scancode)((*c - 'a') + SDL_SCANCODE_A); + } + else if (*c >= '1' && *c <= '9') { + event.key.keysym.scancode = (SDL_Scancode)((*c - '1') + SDL_SCANCODE_1); + } + else { + // TODO: Smartquotes + switch (*c) { + + // Handle CRLF separately to avoid duplicate newlines + case '\r': + if (*(c + 1) == '\n') { + c++; + } + event.key.keysym.scancode = SDL_SCANCODE_RETURN; + break; + + MAP_KEY('\b', SDL_SCANCODE_BACKSPACE); + MAP_KEY('\n', SDL_SCANCODE_RETURN); + MAP_KEY('\t', SDL_SCANCODE_TAB); + + MAP_KEY(' ', SDL_SCANCODE_SPACE); + MAP_KEY_SHIFT('!', SDL_SCANCODE_1); + MAP_KEY_SHIFT('"', SDL_SCANCODE_APOSTROPHE); + MAP_KEY_SHIFT('#', SDL_SCANCODE_3); + MAP_KEY_SHIFT('$', SDL_SCANCODE_4); + MAP_KEY_SHIFT('%', SDL_SCANCODE_5); + MAP_KEY_SHIFT('&', SDL_SCANCODE_7); + MAP_KEY('\'', SDL_SCANCODE_APOSTROPHE); + MAP_KEY_SHIFT('(', SDL_SCANCODE_9); + MAP_KEY_SHIFT(')', SDL_SCANCODE_0); + MAP_KEY_SHIFT('*', SDL_SCANCODE_8); + MAP_KEY_SHIFT('+', SDL_SCANCODE_EQUALS); + MAP_KEY(',', SDL_SCANCODE_COMMA); + MAP_KEY('-', SDL_SCANCODE_MINUS); + MAP_KEY('.', SDL_SCANCODE_PERIOD); + MAP_KEY('/', SDL_SCANCODE_SLASH); + MAP_KEY('0', SDL_SCANCODE_0); + + MAP_KEY_SHIFT(':', SDL_SCANCODE_SEMICOLON); + MAP_KEY(';', SDL_SCANCODE_SEMICOLON); + MAP_KEY_SHIFT('<', SDL_SCANCODE_COMMA); + MAP_KEY('=', SDL_SCANCODE_EQUALS); + MAP_KEY_SHIFT('>', SDL_SCANCODE_PERIOD); + MAP_KEY_SHIFT('?', SDL_SCANCODE_SLASH); + MAP_KEY_SHIFT('@', SDL_SCANCODE_2); + + MAP_KEY('[', SDL_SCANCODE_LEFTBRACKET); + MAP_KEY('\\', SDL_SCANCODE_BACKSLASH); + MAP_KEY(']', SDL_SCANCODE_RIGHTBRACKET); + MAP_KEY_SHIFT('^', SDL_SCANCODE_6); + MAP_KEY_SHIFT('_', SDL_SCANCODE_MINUS); + MAP_KEY('`', SDL_SCANCODE_GRAVE); + + MAP_KEY_SHIFT('{', SDL_SCANCODE_LEFTBRACKET); + MAP_KEY_SHIFT('|', SDL_SCANCODE_BACKSLASH); + MAP_KEY_SHIFT('}', SDL_SCANCODE_RIGHTBRACKET); + MAP_KEY_SHIFT('~', SDL_SCANCODE_GRAVE); + + default: + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "Pasting text - non-ASCII character '%c' ignored", + *c); + continue; + } + } + + event.type = SDL_KEYDOWN; + event.key.state = SDL_PRESSED; + handleKeyEvent(&event.key); + + SDL_Delay(10); + + event.type = SDL_KEYUP; + event.key.state = SDL_RELEASED; + handleKeyEvent(&event.key); + } +} diff --git a/app/streaming/input/input.cpp b/app/streaming/input/input.cpp index 28afd798..19018770 100644 --- a/app/streaming/input/input.cpp +++ b/app/streaming/input/input.cpp @@ -109,6 +109,11 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int s m_SpecialKeyCombos[KeyComboToggleMinimize].scanCode = SDL_SCANCODE_D; m_SpecialKeyCombos[KeyComboToggleMinimize].enabled = QGuiApplication::platformName() != "eglfs"; + m_SpecialKeyCombos[KeyComboPasteText].keyCombo = KeyComboPasteText; + m_SpecialKeyCombos[KeyComboPasteText].keyCode = SDLK_v; + m_SpecialKeyCombos[KeyComboPasteText].scanCode = SDL_SCANCODE_V; + m_SpecialKeyCombos[KeyComboPasteText].enabled = true; + m_OldIgnoreDevices = SDL_GetHint(SDL_HINT_GAMECONTROLLER_IGNORE_DEVICES); m_OldIgnoreDevicesExcept = SDL_GetHint(SDL_HINT_GAMECONTROLLER_IGNORE_DEVICES_EXCEPT); diff --git a/app/streaming/input/input.h b/app/streaming/input/input.h index 6ca660cb..f0d13fde 100644 --- a/app/streaming/input/input.h +++ b/app/streaming/input/input.h @@ -65,6 +65,8 @@ public: void handleJoystickArrivalEvent(SDL_JoyDeviceEvent* event); + void sendText(const char* text); + void rumble(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor); void handleTouchFingerEvent(SDL_TouchFingerEvent* event); @@ -101,6 +103,7 @@ private: KeyComboToggleMouseMode, KeyComboToggleCursorHide, KeyComboToggleMinimize, + KeyComboPasteText, KeyComboMax }; diff --git a/app/streaming/input/keyboard.cpp b/app/streaming/input/keyboard.cpp index d95756f3..9575e6bd 100644 --- a/app/streaming/input/keyboard.cpp +++ b/app/streaming/input/keyboard.cpp @@ -87,6 +87,30 @@ void SdlInputHandler::performPendingSpecialKeyCombo() SDL_MinimizeWindow(m_Window); break; + + case KeyComboPasteText: + { + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, + "Detected paste text combo"); + const char* text = SDL_GetClipboardText(); + if (text != nullptr) { + // Reset pending key combo before pasting, + // otherwise it will ignore our keypresses. + m_PendingKeyCombo = KeyComboMax; + + // Send the text and free it as required by SDL + sendText(text); + SDL_free((void*)text); + } + else { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "No text in clipboard to paste!"); + } + break; + } + + default: + Q_UNREACHABLE(); } // Reset pending key combo