diff --git a/app/cli/commandlineparser.cpp b/app/cli/commandlineparser.cpp index fbfcdbf5..220858c0 100644 --- a/app/cli/commandlineparser.cpp +++ b/app/cli/commandlineparser.cpp @@ -264,6 +264,11 @@ StreamCommandLineParser::StreamCommandLineParser() {"software", StreamingPreferences::VDS_FORCE_SOFTWARE}, {"hardware", StreamingPreferences::VDS_FORCE_HARDWARE}, }; + m_CaptureSysKeysModeMap = { + {"never", StreamingPreferences::CSK_OFF}, + {"fullscreen", StreamingPreferences::CSK_FULLSCREEN}, + {"always", StreamingPreferences::CSK_ALWAYS}, + }; } StreamCommandLineParser::~StreamCommandLineParser() @@ -307,7 +312,7 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference parser.addToggleOption("background-gamepad", "background gamepad input"); parser.addToggleOption("reverse-scroll-direction", "inverted scroll direction"); parser.addToggleOption("swap-gamepad-buttons", "swap A/B and X/Y gamepad buttons (Nintendo-style)"); - parser.addToggleOption("capture-system-keys", "capture system key combos in fullscreen mode"); + parser.addChoiceOption("capture-system-keys", "capture system key combos", m_CaptureSysKeysModeMap.keys()); parser.addChoiceOption("video-codec", "video codec", m_VideoCodecMap.keys()); parser.addChoiceOption("video-decoder", "video decoder", m_VideoDecoderMap.keys()); @@ -422,8 +427,10 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference // Resolve --swap-gamepad-buttons and --no-swap-gamepad-buttons options preferences->swapFaceButtons = parser.getToggleOptionValue("swap-gamepad-buttons", preferences->swapFaceButtons); - // Resolve --capture-system-keys and --no-capture-system-keys options - preferences->captureSysKeys = parser.getToggleOptionValue("capture-system-keys", preferences->captureSysKeys); + // Resolve --capture-system-keys option + if (parser.isSet("capture-system-keys")) { + preferences->captureSysKeysMode = mapValue(m_CaptureSysKeysModeMap, parser.getChoiceOptionValue("capture-system-keys")); + } // Resolve --video-codec option if (parser.isSet("video-codec")) { diff --git a/app/cli/commandlineparser.h b/app/cli/commandlineparser.h index 4f76d2cf..5844204b 100644 --- a/app/cli/commandlineparser.h +++ b/app/cli/commandlineparser.h @@ -53,4 +53,5 @@ private: QMap m_AudioConfigMap; QMap m_VideoCodecMap; QMap m_VideoDecoderMap; + QMap m_CaptureSysKeysModeMap; }; diff --git a/app/gui/SettingsView.qml b/app/gui/SettingsView.qml index ec0fd28f..193486d6 100644 --- a/app/gui/SettingsView.qml +++ b/app/gui/SettingsView.qml @@ -907,23 +907,79 @@ Flickable { qsTr("NOTE: Due to a bug in GeForce Experience, this option may not work properly if your host PC has multiple monitors.") } - CheckBox { - id: captureSysKeysCheck - hoverEnabled: true + Row { + spacing: 5 width: parent.width - text: qsTr("Capture system keyboard shortcuts") - font.pointSize: 12 - enabled: SystemProperties.hasWindowManager - checked: StreamingPreferences.captureSysKeys && SystemProperties.hasWindowManager - onCheckedChanged: { - StreamingPreferences.captureSysKeys = checked + + CheckBox { + id: captureSysKeysCheck + hoverEnabled: true + text: qsTr("Capture system keyboard shortcuts") + font.pointSize: 12 + enabled: SystemProperties.hasWindowManager + checked: StreamingPreferences.captureSysKeysMode !== StreamingPreferences.CSK_OFF || !SystemProperties.hasWindowManager + + ToolTip.delay: 1000 + ToolTip.timeout: 10000 + ToolTip.visible: hovered + ToolTip.text: qsTr("This enables the capture of system-wide keyboard shortcuts like Alt+Tab that would normally be handled by the client OS while streaming.") + "\n\n" + + qsTr("NOTE: Certain keyboard shortcuts like Ctrl+Alt+Del on Windows cannot be intercepted by any application, including Moonlight.") } - ToolTip.delay: 1000 - ToolTip.timeout: 10000 - ToolTip.visible: hovered - ToolTip.text: qsTr("This enables the capture of system-wide keyboard shortcuts like Alt+Tab that would normally be handled by the client OS while streaming.") + "\n\n" + - qsTr("NOTE: Certain keyboard shortcuts like Ctrl+Alt+Del on Windows cannot be intercepted by any application, including Moonlight.") + AutoResizingComboBox { + // ignore setting the index at first, and actually set it when the component is loaded + Component.onCompleted: { + if (!visible) { + // Do nothing if the control won't even be visible + return + } + + var saved_syskeysmode = StreamingPreferences.captureSysKeysMode + currentIndex = 0 + for (var i = 0; i < captureSysKeysModeListModel.count; i++) { + var el_syskeysmode = captureSysKeysModeListModel.get(i).val; + if (saved_syskeysmode === el_syskeysmode) { + currentIndex = i + break + } + } + + activated(currentIndex) + } + + enabled: captureSysKeysCheck.checked + textRole: "text" + model: ListModel { + id: captureSysKeysModeListModel + ListElement { + text: qsTr("in fullscreen") + val: StreamingPreferences.CSK_FULLSCREEN + } + ListElement { + text: qsTr("always") + val: StreamingPreferences.CSK_ALWAYS + } + } + + function updatePref() { + if (!enabled) { + StreamingPreferences.captureSysKeysMode = StreamingPreferences.CSK_OFF + } + else { + StreamingPreferences.captureSysKeysMode = captureSysKeysModeListModel.get(currentIndex).val + } + } + + // ::onActivated must be used, as it only listens for when the index is changed by a human + onActivated: { + updatePref() + } + + // This handles transition of the checkbox state + onEnabledChanged: { + updatePref() + } + } } CheckBox { diff --git a/app/settings/streamingpreferences.cpp b/app/settings/streamingpreferences.cpp index e036a1aa..22372456 100644 --- a/app/settings/streamingpreferences.cpp +++ b/app/settings/streamingpreferences.cpp @@ -95,7 +95,8 @@ void StreamingPreferences::reload() backgroundGamepad = settings.value(SER_BACKGROUNDGAMEPAD, false).toBool(); reverseScrollDirection = settings.value(SER_REVERSESCROLL, false).toBool(); swapFaceButtons = settings.value(SER_SWAPFACEBUTTONS, false).toBool(); - captureSysKeys = settings.value(SER_CAPTURESYSKEYS, false).toBool(); + captureSysKeysMode = static_cast(settings.value(SER_CAPTURESYSKEYS, + static_cast(CaptureSysKeysMode::CSK_OFF)).toInt()); audioConfig = static_cast(settings.value(SER_AUDIOCFG, static_cast(AudioConfig::AC_STEREO)).toInt()); videoCodecConfig = static_cast(settings.value(SER_VIDEOCFG, @@ -227,7 +228,7 @@ void StreamingPreferences::save() settings.setValue(SER_BACKGROUNDGAMEPAD, backgroundGamepad); settings.setValue(SER_REVERSESCROLL, reverseScrollDirection); settings.setValue(SER_SWAPFACEBUTTONS, swapFaceButtons); - settings.setValue(SER_CAPTURESYSKEYS, captureSysKeys); + settings.setValue(SER_CAPTURESYSKEYS, captureSysKeysMode); } int StreamingPreferences::getDefaultBitrate(int width, int height, int fps) diff --git a/app/settings/streamingpreferences.h b/app/settings/streamingpreferences.h index 80b04905..e9874a15 100644 --- a/app/settings/streamingpreferences.h +++ b/app/settings/streamingpreferences.h @@ -69,6 +69,14 @@ public: }; Q_ENUM(Language); + enum CaptureSysKeysMode + { + CSK_OFF, + CSK_FULLSCREEN, + CSK_ALWAYS, + }; + Q_ENUM(CaptureSysKeysMode); + Q_PROPERTY(int width MEMBER width NOTIFY displayModeChanged) Q_PROPERTY(int height MEMBER height NOTIFY displayModeChanged) Q_PROPERTY(int fps MEMBER fps NOTIFY displayModeChanged) @@ -98,7 +106,7 @@ public: Q_PROPERTY(bool backgroundGamepad MEMBER backgroundGamepad NOTIFY backgroundGamepadChanged) Q_PROPERTY(bool reverseScrollDirection MEMBER reverseScrollDirection NOTIFY reverseScrollDirectionChanged) Q_PROPERTY(bool swapFaceButtons MEMBER swapFaceButtons NOTIFY swapFaceButtonsChanged) - Q_PROPERTY(bool captureSysKeys MEMBER captureSysKeys NOTIFY captureSysKeysChanged) + Q_PROPERTY(CaptureSysKeysMode captureSysKeysMode MEMBER captureSysKeysMode NOTIFY captureSysKeysModeChanged) Q_PROPERTY(Language language MEMBER language NOTIFY languageChanged); Q_INVOKABLE bool retranslate(); @@ -127,7 +135,6 @@ public: bool backgroundGamepad; bool reverseScrollDirection; bool swapFaceButtons; - bool captureSysKeys; int packetSize; AudioConfig audioConfig; VideoCodecConfig videoCodecConfig; @@ -136,6 +143,7 @@ public: WindowMode recommendedFullScreenMode; UIDisplayMode uiDisplayMode; Language language; + CaptureSysKeysMode captureSysKeysMode; signals: void displayModeChanged(); @@ -164,7 +172,7 @@ signals: void backgroundGamepadChanged(); void reverseScrollDirectionChanged(); void swapFaceButtonsChanged(); - void captureSysKeysChanged(); + void captureSysKeysModeChanged(); void languageChanged(); private: diff --git a/app/streaming/input/input.cpp b/app/streaming/input/input.cpp index 38c4c602..921c950c 100644 --- a/app/streaming/input/input.cpp +++ b/app/streaming/input/input.cpp @@ -22,7 +22,7 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int s m_MouseWasInVideoRegion(false), m_PendingMouseButtonsAllUpOnVideoRegionLeave(false), m_FakeCaptureActive(false), - m_CaptureSystemKeysEnabled(prefs.captureSysKeys || !WMUtils::isRunningWindowManager()), + m_CaptureSystemKeysMode(prefs.captureSysKeysMode), m_MouseCursorCapturedVisibilityState(SDL_DISABLE), m_PendingKeyCombo(KeyComboMax), m_LongPressTimer(0), @@ -37,6 +37,11 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int s m_NumFingersDown(0), m_ClipboardData() { + // System keys are always captured when running without a WM + if (!WMUtils::isRunningWindowManager()) { + m_CaptureSystemKeysMode = StreamingPreferences::CSK_ALWAYS; + } + // Allow gamepad input when the app doesn't have focus if requested SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, prefs.backgroundGamepad ? "1" : "0"); @@ -51,16 +56,13 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int s #if !SDL_VERSION_ATLEAST(2, 0, 15) // For older versions of SDL (2.0.14 and earlier), use SDL_HINT_GRAB_KEYBOARD SDL_SetHintWithPriority(SDL_HINT_GRAB_KEYBOARD, - m_CaptureSystemKeysEnabled ? "1" : "0", + m_CaptureSystemKeysMode != StreamingPreferences::CSK_OFF ? "1" : "0", SDL_HINT_OVERRIDE); #endif // Opt-out of SDL's built-in Alt+Tab handling while keyboard grab is enabled SDL_SetHint("SDL_ALLOW_ALT_TAB_WHILE_GRABBED", "0"); - // Don't close the window on Alt+F4 when keyboard grab is enabled - SDL_SetHint(SDL_HINT_WINDOWS_NO_CLOSE_ON_ALT_F4, m_CaptureSystemKeysEnabled ? "1" : "0"); - // Allow clicks to pass through to us when focusing the window. If we're in // absolute mouse mode, this will avoid the user having to click twice to // trigger a click on the host if the Moonlight window is not focused. In @@ -328,10 +330,8 @@ void SdlInputHandler::notifyFocusLost() } #ifdef Q_OS_DARWIN - if (m_CaptureSystemKeysEnabled) { - // Stop capturing system keys on focus loss - CGSSetGlobalHotKeyOperatingMode(_CGSDefaultConnection(), m_OldHotKeyMode); - } + // Ungrab the keyboard + updateKeyboardGrabState(); #endif // Raise all keys that are currently pressed. If we don't do this, certain keys @@ -342,10 +342,11 @@ void SdlInputHandler::notifyFocusLost() void SdlInputHandler::notifyFocusGained() { #ifdef Q_OS_DARWIN - if (m_CaptureSystemKeysEnabled) { - // Start capturing system keys again on focus gain - CGSSetGlobalHotKeyOperatingMode(_CGSDefaultConnection(), CGSGlobalHotKeyDisable); - } + // Re-grab the keyboard if it was grabbed before focus loss + // FIXME: We only do this on macOS because we get a spurious + // focus gain when in SDL_WINDOW_FULLSCREEN_DESKTOP on Windows + // immediately after losing focus by clicking on another window. + updateKeyboardGrabState(); #endif } @@ -359,9 +360,62 @@ bool SdlInputHandler::isCaptureActive() return m_FakeCaptureActive; } +void SdlInputHandler::updateKeyboardGrabState() +{ + if (m_CaptureSystemKeysMode == StreamingPreferences::CSK_OFF) { + return; + } + + bool shouldGrab = isCaptureActive(); + Uint32 windowFlags = SDL_GetWindowFlags(m_Window); + if (m_CaptureSystemKeysMode == StreamingPreferences::CSK_FULLSCREEN && + !(windowFlags & SDL_WINDOW_FULLSCREEN)) { + // Ungrab if it's fullscreen only and we left fullscreen + shouldGrab = false; + } + else if (!(windowFlags & SDL_WINDOW_INPUT_FOCUS)) { + // Ungrab if we lose input focus (SDL will do this internally, but + // not for macOS where SDL is not handling the grab logic). + shouldGrab = false; + } + + // Don't close the window on Alt+F4 when keyboard grab is enabled + SDL_SetHint(SDL_HINT_WINDOWS_NO_CLOSE_ON_ALT_F4, shouldGrab ? "1" : "0"); + + if (shouldGrab) { +#if SDL_VERSION_ATLEAST(2, 0, 15) + // On SDL 2.0.15, we can get keyboard-only grab on Win32, X11, and Wayland. + // This does nothing on macOS but it sets the SDL_WINDOW_KEYBOARD_GRABBED flag + // that we look for to see if keyboard capture is enabled. We'll handle macOS + // ourselves below using the private CGSSetGlobalHotKeyOperatingMode() API. + SDL_SetWindowKeyboardGrab(m_Window, SDL_TRUE); +#else + // If we're in full-screen desktop mode and SDL doesn't have keyboard grab yet, + // grab the cursor (will grab the keyboard too on X11). + if (SDL_GetWindowFlags(m_Window) & SDL_WINDOW_FULLSCREEN) { + SDL_SetWindowGrab(m_Window, SDL_TRUE); + } +#endif +#ifdef Q_OS_DARWIN + // SDL doesn't support this private macOS API + CGSSetGlobalHotKeyOperatingMode(_CGSDefaultConnection(), CGSGlobalHotKeyDisable); +#endif + } + else { +#if SDL_VERSION_ATLEAST(2, 0, 15) + // Allow the keyboard to leave the window + SDL_SetWindowKeyboardGrab(m_Window, SDL_FALSE); +#endif +#ifdef Q_OS_DARWIN + // SDL doesn't support this private macOS API + CGSSetGlobalHotKeyOperatingMode(_CGSDefaultConnection(), m_OldHotKeyMode); +#endif + } +} + bool SdlInputHandler::isSystemKeyCaptureActive() { - if (!m_CaptureSystemKeysEnabled) { + if (m_CaptureSystemKeysMode == StreamingPreferences::CSK_OFF) { return false; } @@ -370,13 +424,23 @@ bool SdlInputHandler::isSystemKeyCaptureActive() } Uint32 windowFlags = SDL_GetWindowFlags(m_Window); - return (windowFlags & SDL_WINDOW_INPUT_FOCUS) + if (!(windowFlags & SDL_WINDOW_INPUT_FOCUS) #if SDL_VERSION_ATLEAST(2, 0, 15) - && (windowFlags & SDL_WINDOW_KEYBOARD_GRABBED) + || !(windowFlags & SDL_WINDOW_KEYBOARD_GRABBED) #else - && (windowFlags & SDL_WINDOW_INPUT_GRABBED) + || !(windowFlags & SDL_WINDOW_INPUT_GRABBED) #endif - ; + ) + { + return false; + } + + if (m_CaptureSystemKeysMode == StreamingPreferences::CSK_FULLSCREEN && + !(windowFlags & SDL_WINDOW_FULLSCREEN)) { + return false; + } + + return true; } void SdlInputHandler::setCaptureActive(bool active) @@ -391,26 +455,6 @@ void SdlInputHandler::setCaptureActive(bool active) #endif } - // Grab the keyboard too if system key capture is enabled - if (m_CaptureSystemKeysEnabled) { -#if SDL_VERSION_ATLEAST(2, 0, 15) - // On SDL 2.0.15, we can get keyboard-only grab on Win32, X11, and Wayland. - // This does nothing on macOS but it sets the SDL_WINDOW_KEYBOARD_GRABBED flag - // that we look for to see if keyboard capture is enabled. - SDL_SetWindowKeyboardGrab(m_Window, SDL_TRUE); -#else - // If we're in full-screen desktop mode and SDL doesn't have keyboard grab yet, - // grab the cursor (will grab the keyboard too on X11). - if (SDL_GetWindowFlags(m_Window) & SDL_WINDOW_FULLSCREEN) { - SDL_SetWindowGrab(m_Window, SDL_TRUE); - } -#endif -#ifdef Q_OS_DARWIN - // SDL doesn't support this private macOS API - CGSSetGlobalHotKeyOperatingMode(_CGSDefaultConnection(), CGSGlobalHotKeyDisable); -#endif - } - if (!m_AbsoluteMouseMode) { // If our window is occluded when mouse is captured, the mouse may // get stuck on top of the occluding window and not be properly @@ -458,19 +502,14 @@ void SdlInputHandler::setCaptureActive(bool active) #if SDL_VERSION_ATLEAST(2, 0, 15) // Allow the cursor to leave the bounds of our window again. SDL_SetWindowMouseGrab(m_Window, SDL_FALSE); - - // Allow the keyboard to leave the window - SDL_SetWindowKeyboardGrab(m_Window, SDL_FALSE); #else // Allow the cursor to leave the bounds of our window again. SDL_SetWindowGrab(m_Window, SDL_FALSE); #endif - -#ifdef Q_OS_DARWIN - // SDL doesn't support this private macOS API - CGSSetGlobalHotKeyOperatingMode(_CGSDefaultConnection(), m_OldHotKeyMode); -#endif } + + // Now update the keyboard grab + updateKeyboardGrabState(); } void SdlInputHandler::handleTouchFingerEvent(SDL_TouchFingerEvent* event) diff --git a/app/streaming/input/input.h b/app/streaming/input/input.h index 823f502d..d9d81d4c 100644 --- a/app/streaming/input/input.h +++ b/app/streaming/input/input.h @@ -112,6 +112,8 @@ public: void flushMousePositionUpdate(); + void updateKeyboardGrabState(); + static QString getUnmappedGamepads(); @@ -185,7 +187,7 @@ private: bool m_FakeCaptureActive; QString m_OldIgnoreDevices; QString m_OldIgnoreDevicesExcept; - bool m_CaptureSystemKeysEnabled; + StreamingPreferences::CaptureSysKeysMode m_CaptureSystemKeysMode; int m_MouseCursorCapturedVisibilityState; #ifdef Q_OS_DARWIN diff --git a/app/streaming/session.cpp b/app/streaming/session.cpp index fc0013c0..7f95d3ff 100644 --- a/app/streaming/session.cpp +++ b/app/streaming/session.cpp @@ -926,10 +926,8 @@ void Session::toggleFullscreen() bool fullScreen = !(SDL_GetWindowFlags(m_Window) & m_FullScreenFlag); if (fullScreen) { - if ((m_FullScreenFlag == SDL_WINDOW_FULLSCREEN || m_Preferences->captureSysKeys) && m_InputHandler->isCaptureActive()) { + if (m_FullScreenFlag == SDL_WINDOW_FULLSCREEN && m_InputHandler->isCaptureActive()) { // Confine the cursor to the window if we're capturing input while transitioning to full screen. - // We also need to grab if we're capturing system keys, because SDL requires window grab to - // capture the keyboard on X11. SDL_SetWindowGrab(m_Window, SDL_TRUE); } @@ -946,6 +944,9 @@ void Session::toggleFullscreen() // Reposition the window when the resize is complete m_PendingWindowedTransition = true; } + + // Input handler might need to start/stop keyboard grab after changing modes + m_InputHandler->updateKeyboardGrabState(); } void Session::notifyMouseEmulationMode(bool enabled)