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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ra4my61t8q1FN5n59wB16D
This commit is contained in:
rustdesk
2026-09-23 20:01:12 +08:00
co-authored by Claude Fable 5.1
parent 001bdbcdd9
commit 63654263ed
6 changed files with 5 additions and 56 deletions
@@ -148,8 +148,6 @@ final List<KeyboardShortcutActionGroup> kKeyboardShortcutActionGroups = [
KeyboardShortcutActionEntry(
kShortcutActionKeyboardModeTranslate, 'Translate mode'),
]),
KeyboardShortcutActionEntry(
kShortcutActionToggleInputSource, 'Toggle input source'),
KeyboardShortcutActionEntry(kShortcutActionToggleViewOnly, 'View Mode'),
KeyboardShortcutActionEntry(
kShortcutActionToggleShowMyCursor, 'Show my cursor'),
@@ -258,10 +256,6 @@ List<KeyboardShortcutActionGroup> filterKeyboardShortcutActionGroupsForPlatform(
id == kShortcutActionViewModeCustom)) {
return false;
}
if (!cap.includeInputSourceShortcut &&
id == kShortcutActionToggleInputSource) {
return false;
}
if (!cap.includeVoiceCallShortcut && id == kShortcutActionToggleVoiceCall) {
return false;
}
@@ -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';
@@ -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<Map<String, dynamic>> filterDefaultBindingsForPlatform(
action == kShortcutActionViewModeCustom)) {
continue;
}
if (!cap.includeInputSourceShortcut &&
action == kShortcutActionToggleInputSource) {
continue;
}
if (!cap.includeVoiceCallShortcut &&
action == kShortcutActionToggleVoiceCall) {
continue;
-36
View File
@@ -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<dynamic> list;
try {
list = jsonDecode(raw) as List<dynamic>;
} 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();
});
}
}
+5 -6
View File
@@ -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',
-1
View File
@@ -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";