From 63654263ed5d339ef4fab9c2c64bb5d7261fdd79 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 23 Sep 2026 20:01:12 +0800 Subject: [PATCH] fix(keyboard): shortcuts, drop the Toggle Input Source action The action switches the key capture backend between key down and key up. Fired-key ownership lives in whichever matcher saw the press, the Flutter dispatcher or the Rust set, and there is no handoff between them: after the switch the repeats and the release of the held key reach the other backend, which forwards them to the remote and, while the chord is held, matches the action again and toggles the source back. Remove the shortcut instead of building a handoff for one action. The toolbar radio menu is unchanged. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Ra4my61t8q1FN5n59wB16D --- .../keyboard_shortcuts/shortcut_actions.dart | 6 ---- .../shortcut_constants.dart | 1 - .../keyboard_shortcuts/shortcut_utils.dart | 6 ---- flutter/lib/models/shortcut_model.dart | 36 ------------------- flutter/test/keyboard_shortcuts_test.dart | 11 +++--- src/keyboard/shortcuts.rs | 1 - 6 files changed, 5 insertions(+), 56 deletions(-) diff --git a/flutter/lib/common/widgets/keyboard_shortcuts/shortcut_actions.dart b/flutter/lib/common/widgets/keyboard_shortcuts/shortcut_actions.dart index bca8ffa7a..1da38cce0 100644 --- a/flutter/lib/common/widgets/keyboard_shortcuts/shortcut_actions.dart +++ b/flutter/lib/common/widgets/keyboard_shortcuts/shortcut_actions.dart @@ -148,8 +148,6 @@ final List kKeyboardShortcutActionGroups = [ KeyboardShortcutActionEntry( kShortcutActionKeyboardModeTranslate, 'Translate mode'), ]), - KeyboardShortcutActionEntry( - kShortcutActionToggleInputSource, 'Toggle input source'), KeyboardShortcutActionEntry(kShortcutActionToggleViewOnly, 'View Mode'), KeyboardShortcutActionEntry( kShortcutActionToggleShowMyCursor, 'Show my cursor'), @@ -258,10 +256,6 @@ List filterKeyboardShortcutActionGroupsForPlatform( id == kShortcutActionViewModeCustom)) { return false; } - if (!cap.includeInputSourceShortcut && - id == kShortcutActionToggleInputSource) { - return false; - } if (!cap.includeVoiceCallShortcut && id == kShortcutActionToggleVoiceCall) { return false; } diff --git a/flutter/lib/common/widgets/keyboard_shortcuts/shortcut_constants.dart b/flutter/lib/common/widgets/keyboard_shortcuts/shortcut_constants.dart index 05c2fc174..3d265199d 100644 --- a/flutter/lib/common/widgets/keyboard_shortcuts/shortcut_constants.dart +++ b/flutter/lib/common/widgets/keyboard_shortcuts/shortcut_constants.dart @@ -61,7 +61,6 @@ const kShortcutActionImageQualityBest = 'image_quality_best'; const kShortcutActionImageQualityBalanced = 'image_quality_balanced'; const kShortcutActionImageQualityLow = 'image_quality_low'; const kShortcutActionSendClipboardKeystrokes = 'send_clipboard_keystrokes'; -const kShortcutActionToggleInputSource = 'toggle_input_source'; const kShortcutActionToggleVoiceCall = 'toggle_voice_call'; const kShortcutActionToggleViewOnly = 'toggle_view_only'; diff --git a/flutter/lib/common/widgets/keyboard_shortcuts/shortcut_utils.dart b/flutter/lib/common/widgets/keyboard_shortcuts/shortcut_utils.dart index 965d4e985..58f85fbdd 100644 --- a/flutter/lib/common/widgets/keyboard_shortcuts/shortcut_utils.dart +++ b/flutter/lib/common/widgets/keyboard_shortcuts/shortcut_utils.dart @@ -117,7 +117,6 @@ class ShortcutPlatformCapabilities { final bool includeResetCanvasShortcut; final bool includePinToolbarShortcut; final bool includeViewModeShortcut; - final bool includeInputSourceShortcut; final bool includeVoiceCallShortcut; const ShortcutPlatformCapabilities({ @@ -131,7 +130,6 @@ class ShortcutPlatformCapabilities { required this.includeResetCanvasShortcut, required this.includePinToolbarShortcut, required this.includeViewModeShortcut, - required this.includeInputSourceShortcut, required this.includeVoiceCallShortcut, }); } @@ -185,10 +183,6 @@ List> filterDefaultBindingsForPlatform( action == kShortcutActionViewModeCustom)) { continue; } - if (!cap.includeInputSourceShortcut && - action == kShortcutActionToggleInputSource) { - continue; - } if (!cap.includeVoiceCallShortcut && action == kShortcutActionToggleVoiceCall) { continue; diff --git a/flutter/lib/models/shortcut_model.dart b/flutter/lib/models/shortcut_model.dart index 128ed6a2e..7745c4d0c 100644 --- a/flutter/lib/models/shortcut_model.dart +++ b/flutter/lib/models/shortcut_model.dart @@ -203,9 +203,6 @@ class ShortcutModel { /// `recordingModel.toggle()`; Web has no implementation. /// * Reset Canvas: only the mobile toolbar builds the menu entry /// (`isDefaultConn && isMobile` in `toolbarControls`). - /// * Input Source: Web only ships a single source so toggling is a - /// no-op; the toolbar menu hides itself when fewer than 2 sources are - /// advertised. /// * Voice Call: Web bridge throws `UnimplementedError` for both /// `sessionRequestVoiceCall` and `sessionCloseVoiceCall`. static ShortcutPlatformCapabilities currentPlatformCapabilities() { @@ -221,7 +218,6 @@ class ShortcutModel { includeResetCanvasShortcut: isMobile, includePinToolbarShortcut: desktopLayout, includeViewModeShortcut: desktopLayout, - includeInputSourceShortcut: !isWeb, includeVoiceCallShortcut: !isWeb, ); } @@ -605,36 +601,4 @@ void registerSessionShortcutActions( ffi.inputModel.toggleRelativeMouseMode(); }); } - - // Toggle Input Source — flips between the available keyboard-event capture - // backends (e.g. JS vs Flutter on desktop). Mirrors the radio menu in - // remote_toolbar.dart::inputSource(); when fewer than 2 sources are - // available the menu hides itself, so this handler is a no-op too. - // Useful for accessibility: screen-reader users sometimes need to swap - // sources to regain control of the local keyboard (discussion #1933). - // Web only ships a single source, so we don't register on web. - if (!isWeb) { - ffi.shortcutModel.register(kShortcutActionToggleInputSource, () async { - final raw = bind.mainSupportedInputSource(); - if (raw.isEmpty) return; - final List list; - try { - list = jsonDecode(raw) as List; - } catch (_) { - return; - } - if (list.length < 2) return; - final ids = list - .map((e) => (e is List && e.isNotEmpty) ? e[0] as String : '') - .where((s) => s.isNotEmpty) - .toList(); - if (ids.length < 2) return; - final current = stateGlobal.getInputSource(); - final idx = ids.indexOf(current); - final next = ids[(idx < 0 ? 0 : idx + 1) % ids.length]; - await stateGlobal.setInputSource(sessionId, next); - await ffi.ffiModel.checkDesktopKeyboardMode(); - await ffi.inputModel.updateKeyboardMode(); - }); - } } diff --git a/flutter/test/keyboard_shortcuts_test.dart b/flutter/test/keyboard_shortcuts_test.dart index 57ddce6e3..373a76a2e 100644 --- a/flutter/test/keyboard_shortcuts_test.dart +++ b/flutter/test/keyboard_shortcuts_test.dart @@ -18,7 +18,6 @@ ShortcutPlatformCapabilities capabilities({ bool includeResetCanvasShortcut = true, bool includePinToolbarShortcut = true, bool includeViewModeShortcut = true, - bool includeInputSourceShortcut = true, bool includeVoiceCallShortcut = true, }) { return ShortcutPlatformCapabilities( @@ -32,7 +31,6 @@ ShortcutPlatformCapabilities capabilities({ includeResetCanvasShortcut: includeResetCanvasShortcut, includePinToolbarShortcut: includePinToolbarShortcut, includeViewModeShortcut: includeViewModeShortcut, - includeInputSourceShortcut: includeInputSourceShortcut, includeVoiceCallShortcut: includeVoiceCallShortcut, ); } @@ -197,7 +195,6 @@ void main() { includeResetCanvasShortcut: false, includePinToolbarShortcut: false, includeViewModeShortcut: false, - includeInputSourceShortcut: false, includeVoiceCallShortcut: false, ), ); @@ -248,7 +245,6 @@ void main() { includeResetCanvasShortcut: true, includePinToolbarShortcut: false, includeViewModeShortcut: false, - includeInputSourceShortcut: false, includeVoiceCallShortcut: false, ), ); @@ -358,7 +354,6 @@ void main() { ' $kShortcutActionKeyboardModeLegacy', ' $kShortcutActionKeyboardModeMap', ' $kShortcutActionKeyboardModeTranslate', - kShortcutActionToggleInputSource, kShortcutActionToggleViewOnly, kShortcutActionToggleShowMyCursor, kShortcutActionToggleSwapCtrlCmd, @@ -394,7 +389,6 @@ void main() { includeResetCanvasShortcut: false, includePinToolbarShortcut: false, includeViewModeShortcut: false, - includeInputSourceShortcut: false, includeVoiceCallShortcut: false, ), ); @@ -503,8 +497,13 @@ void main() { // legacy IDs (toggle_audio, view_mode_shrink/stretch, view_mode_1_to_1) // were renamed: their replacements are kShortcutActionToggleMute and // kShortcutActionViewModeOriginal/Adaptive/Custom. + // + // `toggle_input_source` was removed on purpose: it switches the key + // capture backend between key down and key up, so the matcher that + // consumed the press never sees the repeats and the release. const knownRemoved = [ 'toggle_audio', + 'toggle_input_source', 'view_mode_1_to_1', 'view_mode_shrink', 'view_mode_stretch', diff --git a/src/keyboard/shortcuts.rs b/src/keyboard/shortcuts.rs index 67c390a3c..46919be94 100644 --- a/src/keyboard/shortcuts.rs +++ b/src/keyboard/shortcuts.rs @@ -68,7 +68,6 @@ pub mod action_id { pub const IMAGE_QUALITY_BALANCED: &str = "image_quality_balanced"; pub const IMAGE_QUALITY_LOW: &str = "image_quality_low"; pub const SEND_CLIPBOARD_KEYSTROKES: &str = "send_clipboard_keystrokes"; - pub const TOGGLE_INPUT_SOURCE: &str = "toggle_input_source"; pub const SWITCH_TAB_NEXT: &str = "switch_tab_next"; pub const SWITCH_TAB_PREV: &str = "switch_tab_prev"; pub const TOGGLE_VOICE_CALL: &str = "toggle_voice_call";