Compare commits

..

1 Commits

Author SHA1 Message Date
rustdesk
04faf21c78 feat: keyboard shortcuts in remote sessions
Add an opt-in keyboard-shortcut system that triggers session
actions (Send Ctrl+Alt+Del, Toggle Fullscreen, Switch Display,
Screenshot, Switch Tab, etc.) via three-modifier combinations
during a remote session.

Architecture
- Native: src/keyboard/shortcuts.rs intercepts at the encoder
  layer (process_event and process_event_with_session), so the
  feature is input-source-independent. Bindings persist as a
  single JSON blob in LocalConfig.
- Web: matching + keydown intercept live in the separate hand-
  written TS client at flutter/web/js/ (gitignored, not in this
  repo). flutter/lib/web/bridge.dart::mainInit registers
  window.onShortcutTriggered so the JS matcher can dispatch
  back into the active session's ShortcutModel; the bridge's
  mainReloadKeyboardShortcuts forwards to a JS reloadShortcuts
  on settings writes.
- Three-modifier prefix (Ctrl+Alt+Shift; Cmd+Option+Shift on
  macOS/iOS) sidesteps the need for a pass-through toggle.
- Flutter native path threads the explicit per-call SessionID
  for tab-precise routing; rdev path uses globally-current
  session.

UI
- Settings -> General -> Keyboard Shortcuts opens a dedicated
  configuration page; desktop and mobile share a body widget.
- Recording dialog with live capture, prefix validation, and a
  conflict-replace flow.
- Toolbar menu items display the bound shortcut inline.
- Default bindings (adapted from AnyDesk):
    +Del    Send Ctrl+Alt+Del
    +Enter  Toggle Fullscreen
    +Left/Right  Switch Display Prev/Next
    +P      Screenshot
    +1..9   Switch Session Tab

Other
- AGENTS.md: documented (a) flutter_rust_bridge_codegen needs
  a pinned version + Dart bridge wrappers should be hand-
  written, and (b) the Web-target split where flutter/web/js/
  is the runtime owner on Web rather than wasm-compiled Rust.
- 38 new i18n strings in src/lang/en.rs with Chinese
  translations in src/lang/cn.rs.

Refs discussion #1933.
2026-04-28 15:48:12 +08:00
76 changed files with 663 additions and 4458 deletions

View File

@@ -3,49 +3,21 @@ import 'dart:convert';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import '../../../consts.dart'; import '../../../consts.dart';
import '../../../models/platform_model.dart'; import '../../../models/platform_model.dart';
import 'shortcut_utils.dart';
/// Read the bindings JSON and produce a human-readable shortcut string for /// Read the bindings JSON and produce a human-readable shortcut string for
/// `actionId`, formatted for the current OS. Returns null if unbound, or — /// `actionId`, formatted for the current OS. Returns null if unbound.
/// when [requireEnabled] is true (the default) — when the master toggle is
/// off. The configuration page passes `requireEnabled: false` so users still
/// see what they have bound while the feature is disabled.
class ShortcutDisplay { class ShortcutDisplay {
// Cache parsed JSON keyed by the raw string — called per visible action on static String? formatFor(String actionId) {
// every menu rebuild, so the jsonDecode is the real cost. Invalidation is
// automatic: a write changes the raw and we re-parse.
static String? _cachedRaw;
static Map<String, dynamic>? _cachedParsed;
@visibleForTesting
static void resetCache() {
_cachedRaw = null;
_cachedParsed = null;
}
static String? formatFor(String actionId, {bool requireEnabled = true}) {
final raw = bind.mainGetLocalOption(key: kShortcutLocalConfigKey); final raw = bind.mainGetLocalOption(key: kShortcutLocalConfigKey);
if (raw.isEmpty) return null; if (raw.isEmpty) return null;
Map<String, dynamic>? parsed; final Map<String, dynamic> parsed;
if (raw == _cachedRaw) { try {
parsed = _cachedParsed; parsed = jsonDecode(raw) as Map<String, dynamic>;
} else { } catch (_) {
try { return null;
parsed = jsonDecode(raw) as Map<String, dynamic>;
} catch (_) {
parsed = null;
}
_cachedRaw = raw;
_cachedParsed = parsed;
} }
if (parsed == null) return null; if (parsed['enabled'] != true) return null;
if (requireEnabled && parsed['enabled'] != true) return null; final list = (parsed['bindings'] as List? ?? []).cast<Map<String, dynamic>>();
// When pass-through is on, the matcher returns early on every keystroke.
// Showing the bound combo next to a menu item would lie to the user — they
// would press it expecting the local action and instead the keys would go
// to the remote. Treat as unbound for display purposes.
if (requireEnabled && parsed['pass_through'] == true) return null;
final list = shortcutBindingMapsFrom(parsed['bindings']);
final found = list.firstWhere( final found = list.firstWhere(
(b) => b['action'] == actionId, (b) => b['action'] == actionId,
orElse: () => {}, orElse: () => {},
@@ -65,47 +37,29 @@ class ShortcutDisplay {
final mods = modsRaw is List final mods = modsRaw is List
? modsRaw.whereType<String>().toList() ? modsRaw.whereType<String>().toList()
: const <String>[]; : const <String>[];
// Plain-text labels (Cmd / Ctrl / Alt / Shift) instead of Unicode glyphs
// (⌘ ⌃ ⌥ ⇧). Flutter Web's CanvasKit bundled fonts don't always carry the
// macOS modifier symbols, which renders as garbled boxes on Mac browsers;
// text is portable and readable on every platform.
//
// Order matches the canonical macOS order (Cmd, Control, Option, Shift)
// so the rendered hint reads naturally. `ctrl` only ever appears in
// saved bindings on macOS — Win/Linux collapses Ctrl into `primary`.
final parts = <String>[]; final parts = <String>[];
for (final m in ['primary', 'ctrl', 'alt', 'shift']) { for (final m in ['primary', 'alt', 'shift']) {
if (!mods.contains(m)) continue; if (!mods.contains(m)) continue;
switch (m) { switch (m) {
case 'primary': parts.add(isMac ? 'Cmd' : 'Ctrl'); break; case 'primary': parts.add(isMac ? '' : 'Ctrl'); break;
case 'ctrl': parts.add(isMac ? 'Control' : 'Ctrl'); break; case 'alt': parts.add(isMac ? '' : 'Alt'); break;
case 'alt': parts.add(isMac ? 'Option' : 'Alt'); break; case 'shift': parts.add(isMac ? '' : 'Shift'); break;
case 'shift': parts.add('Shift'); break;
} }
} }
parts.add(_keyDisplay(keyValue)); parts.add(_keyDisplay(keyValue, isMac));
return parts.join('+'); return isMac ? parts.join('') : parts.join('+');
} }
static String _keyDisplay(String key) { static String _keyDisplay(String key, bool isMac) {
switch (key) { switch (key) {
case 'delete': return 'Del'; case 'delete': return isMac ? '' : 'Del';
case 'backspace': return 'Backspace'; case 'enter': return isMac ? '' : 'Enter';
case 'enter': return 'Enter'; case 'arrow_left': return '';
case 'tab': return 'Tab'; case 'arrow_right':return '';
case 'space': return 'Space'; case 'arrow_up': return '';
case 'arrow_left': return 'Left'; case 'arrow_down': return '';
case 'arrow_right':return 'Right';
case 'arrow_up': return 'Up';
case 'arrow_down': return 'Down';
case 'home': return 'Home';
case 'end': return 'End';
case 'page_up': return 'PgUp';
case 'page_down': return 'PgDn';
case 'insert': return 'Ins';
} }
if (key.startsWith('digit')) return key.substring(5); if (key.startsWith('digit')) return key.substring(5);
// F-keys ("f1".."f12") and single letters fall through to uppercase.
return key.toUpperCase(); return key.toUpperCase();
} }
} }

View File

@@ -30,10 +30,61 @@ import '../../../common.dart';
import '../../../consts.dart'; import '../../../consts.dart';
import '../../../models/platform_model.dart'; import '../../../models/platform_model.dart';
import '../../../models/shortcut_model.dart'; import '../../../models/shortcut_model.dart';
import 'display.dart';
import 'recording_dialog.dart'; import 'recording_dialog.dart';
import 'shortcut_actions.dart';
import 'shortcut_utils.dart'; /// One configurable action — id + i18n key for its label.
class KeyboardShortcutActionEntry {
final String id;
final String labelKey;
const KeyboardShortcutActionEntry(this.id, this.labelKey);
}
/// A named group of actions (e.g. "Session Control").
class KeyboardShortcutActionGroup {
final String titleKey;
final List<KeyboardShortcutActionEntry> actions;
const KeyboardShortcutActionGroup(this.titleKey, this.actions);
}
/// Canonical action group definitions used by both the desktop and mobile
/// configuration pages. The order of groups and entries here is the order
/// the user sees in the UI. (Not `const` because the per-tab ids come from
/// the `kShortcutActionSwitchTab(n)` helper in `consts.dart`.)
final List<KeyboardShortcutActionGroup> kKeyboardShortcutActionGroups = [
KeyboardShortcutActionGroup('Session Control', [
KeyboardShortcutActionEntry(
kShortcutActionSendCtrlAltDel, 'Insert Ctrl + Alt + Del'),
KeyboardShortcutActionEntry(kShortcutActionInsertLock, 'Insert Lock'),
KeyboardShortcutActionEntry(kShortcutActionRefresh, 'Refresh'),
KeyboardShortcutActionEntry(kShortcutActionSwitchSides, 'Switch Sides'),
KeyboardShortcutActionEntry(
kShortcutActionToggleRecording, 'Toggle Recording'),
KeyboardShortcutActionEntry(
kShortcutActionToggleBlockInput, 'Toggle Block User Input'),
]),
KeyboardShortcutActionGroup('Display', [
KeyboardShortcutActionEntry(
kShortcutActionToggleFullscreen, 'Toggle Fullscreen'),
KeyboardShortcutActionEntry(
kShortcutActionSwitchDisplayNext, 'Switch to next display'),
KeyboardShortcutActionEntry(
kShortcutActionSwitchDisplayPrev, 'Switch to previous display'),
KeyboardShortcutActionEntry(kShortcutActionViewMode1to1, 'View Mode 1:1'),
KeyboardShortcutActionEntry(
kShortcutActionViewModeShrink, 'View Mode Shrink'),
KeyboardShortcutActionEntry(
kShortcutActionViewModeStretch, 'View Mode Stretch'),
]),
KeyboardShortcutActionGroup('Other', [
KeyboardShortcutActionEntry(kShortcutActionScreenshot, 'Take Screenshot'),
KeyboardShortcutActionEntry(kShortcutActionToggleAudio, 'Toggle Audio'),
KeyboardShortcutActionEntry(
kShortcutActionTogglePrivacyMode, 'Toggle Privacy Mode'),
for (var n = 1; n <= 9; n++)
KeyboardShortcutActionEntry(
kShortcutActionSwitchTab(n), 'Switch Tab $n'),
]),
];
/// The shared body widget. Render this inside a platform-styled Scaffold. /// The shared body widget. Render this inside a platform-styled Scaffold.
/// ///
@@ -50,20 +101,11 @@ class KeyboardShortcutsPageBody extends StatefulWidget {
final String? editButtonHint; final String? editButtonHint;
final Widget? headerBanner; final Widget? headerBanner;
/// Whether to render the master Enable + Pass-through toggles inside the
/// body. Desktop shells set this to false because the General settings tab
/// already exposes both checkboxes (and is the only entry point to this
/// page on desktop). Mobile defaults to true: its entry point is a plain
/// nav tile in Settings, so this page is the only place the user can
/// flip the master switches.
final bool showMasterToggles;
const KeyboardShortcutsPageBody({ const KeyboardShortcutsPageBody({
Key? key, Key? key,
this.compact = true, this.compact = true,
this.editButtonHint, this.editButtonHint,
this.headerBanner, this.headerBanner,
this.showMasterToggles = true,
}) : super(key: key); }) : super(key: key);
@override @override
@@ -109,7 +151,9 @@ class KeyboardShortcutsPageBodyState extends State<KeyboardShortcutsPageBody> {
String? clearActionId, String? clearActionId,
}) async { }) async {
final json = _readJson(); final json = _readJson();
final list = shortcutBindingMapsFrom(json['bindings']); final list = ((json['bindings'] as List?) ?? <dynamic>[])
.cast<Map<String, dynamic>>()
.toList();
list.removeWhere((b) { list.removeWhere((b) {
final a = b['action']; final a = b['action'];
return a == actionId || (clearActionId != null && a == clearActionId); return a == actionId || (clearActionId != null && a == clearActionId);
@@ -122,55 +166,37 @@ class KeyboardShortcutsPageBodyState extends State<KeyboardShortcutsPageBody> {
} }
Future<void> _setEnabled(bool v) async { Future<void> _setEnabled(bool v) async {
await ShortcutModel.setEnabled(v); final json = _readJson();
if (mounted) setState(() {}); json['enabled'] = v;
} // First-time enable: seed defaults if the user has never bound anything.
final list = (json['bindings'] as List?) ?? const [];
Future<void> _setPassThrough(bool v) async { if (v && list.isEmpty) {
await ShortcutModel.setPassThrough(v); json['bindings'] = jsonDecode(bind.mainGetDefaultKeyboardShortcuts());
if (mounted) setState(() {}); }
await _writeJson(json);
} }
Future<void> _resetToDefaults() async { Future<void> _resetToDefaults() async {
final json = _readJson(); final json = _readJson();
// Single source of truth lives in `ShortcutModel.currentPlatformCapabilities` json['bindings'] = jsonDecode(bind.mainGetDefaultKeyboardShortcuts());
// — the same helper feeds the first-enable seed pass, this Reset action,
// and the action-list filter below, so the three can never disagree on
// which actions belong on this platform.
json['bindings'] = filterDefaultBindingsForPlatform(
jsonDecode(bind.mainGetDefaultKeyboardShortcuts()) as List,
ShortcutModel.currentPlatformCapabilities(),
);
await _writeJson(json); await _writeJson(json);
} }
String _labelFor(String actionId) { String _labelFor(String actionId) {
// Intentionally walks the unfiltered list (via the recursive helper, so for (final g in kKeyboardShortcutActionGroups) {
// both direct entries and subgroup entries are covered) — a stale for (final a in g.actions) {
// cross-platform binding (e.g. Toggle Toolbar carried over from if (a.id == actionId) return translate(a.labelKey);
// desktop) should still resolve to its human-readable label in conflict }
// warnings.
for (final entry in allActionEntries(kKeyboardShortcutActionGroups)) {
if (entry.id == actionId) return translate(entry.labelKey);
} }
return actionId; return actionId;
} }
/// Action groups visible on the current platform. Reads the same
/// capability set as the seed-defaults / reset-to-defaults paths from
/// `ShortcutModel.currentPlatformCapabilities`, so the UI lists exactly
/// the actions whose handlers the matcher can dispatch here.
List<KeyboardShortcutActionGroup> _groupsForCurrentPlatform() {
return filterKeyboardShortcutActionGroupsForPlatform(
ShortcutModel.currentPlatformCapabilities(),
);
}
// ----- UI handlers ----- // ----- UI handlers -----
Future<void> _onEdit(KeyboardShortcutActionEntry entry) async { Future<void> _onEdit(KeyboardShortcutActionEntry entry) async {
final json = _readJson(); final json = _readJson();
final bindings = shortcutBindingMapsFrom(json['bindings']); final bindings = ((json['bindings'] as List?) ?? <dynamic>[])
.cast<Map<String, dynamic>>();
final result = await showRecordingDialog( final result = await showRecordingDialog(
context: context, context: context,
actionId: entry.id, actionId: entry.id,
@@ -199,7 +225,8 @@ class KeyboardShortcutsPageBodyState extends State<KeyboardShortcutsPageBody> {
content: Text(translate('shortcut-reset-confirm-tip')), content: Text(translate('shortcut-reset-confirm-tip')),
actions: [ actions: [
dialogButton('Cancel', dialogButton('Cancel',
onPressed: () => Navigator.of(ctx).pop(false), isOutline: true), onPressed: () => Navigator.of(ctx).pop(false),
isOutline: true),
dialogButton('OK', onPressed: () => Navigator.of(ctx).pop(true)), dialogButton('OK', onPressed: () => Navigator.of(ctx).pop(true)),
], ],
), ),
@@ -223,19 +250,28 @@ class KeyboardShortcutsPageBodyState extends State<KeyboardShortcutsPageBody> {
widget.headerBanner!, widget.headerBanner!,
const SizedBox(height: 12), const SizedBox(height: 12),
], ],
if (widget.showMasterToggles) ...[ // Top toggle — mirrors the General-tab _OptionCheckBox semantics.
_toggleRow( Row(
enabled, children: [
'Enable keyboard shortcuts in remote session', Checkbox(
(v) => _setEnabled(v), value: enabled,
), onChanged: (v) async {
if (enabled) if (v == null) return;
_toggleRow( await _setEnabled(v);
ShortcutModel.isPassThrough(), },
'Pass-through to remote',
(v) => _setPassThrough(v),
), ),
], const SizedBox(width: 4),
Expanded(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => _setEnabled(!enabled),
child: Text(
translate('Enable keyboard shortcuts in remote session'),
),
),
),
],
),
const SizedBox(height: 8), const SizedBox(height: 8),
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 8), padding: const EdgeInsets.symmetric(horizontal: 8),
@@ -245,133 +281,60 @@ class KeyboardShortcutsPageBodyState extends State<KeyboardShortcutsPageBody> {
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
// Bindings list and configuration entry only show when shortcuts are // Disabled visual state when toggle is off — but still scrollable.
// enabled — there is nothing to configure while the matcher is off. Opacity(
if (enabled) opacity: enabled ? 1.0 : 0.5,
Column( child: AbsorbPointer(
crossAxisAlignment: CrossAxisAlignment.start, absorbing: !enabled,
children: [ child: Column(
for (final group in _groupsForCurrentPlatform()) crossAxisAlignment: CrossAxisAlignment.start,
_buildGroup(context, group), children: [
], for (final group in kKeyboardShortcutActionGroups)
_buildGroup(context, group),
],
),
), ),
),
], ],
); );
} }
Widget _toggleRow(
bool value, String labelKey, Future<void> Function(bool) onChanged,
{String? tooltipKey}) {
return Row(
children: [
Checkbox(
value: value,
onChanged: (v) async {
if (v == null) return;
await onChanged(v);
},
),
const SizedBox(width: 4),
Expanded(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => onChanged(!value),
child: Text(translate(labelKey)),
),
),
if (tooltipKey != null) InfoTooltipIcon(tipKey: tooltipKey),
],
);
}
// One indent unit per nesting level. Both "top item under top heading"
// and "subgroup heading under top group" are *one* level deeper than the
// top heading, so they share this indent — meaning a top-level direct
// item and a sibling subgroup heading line up at exactly the same x.
// Subgroup items are *two* levels deeper.
static const double _kIndentStep = 16.0;
/// Top-level group: heading at zero indent, then walk `children` in
/// declaration order. Direct entries get [_kIndentStep] of indent so
/// they read as "items under this heading"; subgroup headings sit at
/// the same indent (a subgroup is a sibling of the direct items, just
/// with its own nested entries below).
Widget _buildGroup(BuildContext context, KeyboardShortcutActionGroup group) { Widget _buildGroup(BuildContext context, KeyboardShortcutActionGroup group) {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const SizedBox(height: 12), const SizedBox(height: 12),
_buildHeading(context, group.titleKey, isSub: false), Padding(
const SizedBox(height: 4), padding: const EdgeInsets.symmetric(horizontal: 8),
for (final child in group.children) child: Row(
switch (child) { children: [
KeyboardShortcutActionEntry() => Padding( Text(
padding: const EdgeInsets.only(left: _kIndentStep), translate(group.titleKey),
child: _buildEntryRow(context, child), style: TextStyle(
fontWeight: FontWeight.w600,
color: Theme.of(context).colorScheme.primary,
),
), ),
KeyboardShortcutActionSubgroup() => const SizedBox(width: 8),
_buildSubgroup(context, child), const Expanded(
}, child: Divider(thickness: 1),
], ),
); ],
} ),
),
Widget _buildSubgroup(
BuildContext context, KeyboardShortcutActionSubgroup subgroup) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 8),
_buildHeading(context, subgroup.titleKey, isSub: true),
const SizedBox(height: 4), const SizedBox(height: 4),
for (final entry in subgroup.entries) for (final action in group.actions)
Padding( widget.compact
// Two indent steps: one for "subgroup heading is nested under ? _buildCompactRow(context, action)
// top heading" (matches the heading's own indent) and one for : _buildTouchRow(context, action),
// "this entry is under the subgroup heading".
padding: const EdgeInsets.only(left: _kIndentStep * 2),
child: _buildEntryRow(context, entry),
),
], ],
); );
} }
Widget _buildHeading(BuildContext context, String titleKey,
{required bool isSub}) {
// Subgroup heading nests one step under the top heading — same indent
// as a top-level direct item, so the two line up at the same x.
final indent = isSub ? _kIndentStep : 0.0;
return Padding(
padding: EdgeInsets.only(left: 8 + indent, right: 8),
child: Row(
children: [
Text(
translate(titleKey),
style: TextStyle(
fontWeight: isSub ? FontWeight.w500 : FontWeight.w600,
color: isSub
? Theme.of(context).hintColor
: Theme.of(context).colorScheme.primary,
),
),
const SizedBox(width: 8),
Expanded(child: Divider(thickness: isSub ? 0.5 : 1)),
],
),
);
}
Widget _buildEntryRow(
BuildContext context, KeyboardShortcutActionEntry entry) {
return widget.compact
? _buildCompactRow(context, entry)
: _buildTouchRow(context, entry);
}
/// Desktop dense row: label | shortcut | edit | clear, all in one Row. /// Desktop dense row: label | shortcut | edit | clear, all in one Row.
Widget _buildCompactRow( Widget _buildCompactRow(
BuildContext context, KeyboardShortcutActionEntry entry) { BuildContext context, KeyboardShortcutActionEntry entry) {
final shortcut = ShortcutDisplay.formatFor(entry.id, requireEnabled: false); final shortcut = ShortcutDisplayForActionId.format(entry.id);
final hasBinding = shortcut != null; final hasBinding = shortcut != null;
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
@@ -416,7 +379,7 @@ class KeyboardShortcutsPageBodyState extends State<KeyboardShortcutsPageBody> {
/// Mobile touch row: ListTile with title + subtitle + trailing icons. /// Mobile touch row: ListTile with title + subtitle + trailing icons.
Widget _buildTouchRow( Widget _buildTouchRow(
BuildContext context, KeyboardShortcutActionEntry entry) { BuildContext context, KeyboardShortcutActionEntry entry) {
final shortcut = ShortcutDisplay.formatFor(entry.id, requireEnabled: false); final shortcut = ShortcutDisplayForActionId.format(entry.id);
final hasBinding = shortcut != null; final hasBinding = shortcut != null;
return ListTile( return ListTile(
dense: false, dense: false,
@@ -453,29 +416,75 @@ class KeyboardShortcutsPageBodyState extends State<KeyboardShortcutsPageBody> {
} }
} }
/// Small help-icon tooltip used for inline explanations next to a checkbox / /// Thin wrapper around [ShortcutDisplay.formatFor] that ignores the
/// row. Triggers on hover (desktop) and tap (mobile). Public so the desktop /// `enabled` flag so the configuration page can always show the user what
/// General settings tab can reuse it. /// they have bound, even when the feature is currently disabled.
class InfoTooltipIcon extends StatelessWidget { class ShortcutDisplayForActionId {
final String tipKey; static String? format(String actionId) {
const InfoTooltipIcon({Key? key, required this.tipKey}) : super(key: key); final raw = bind.mainGetLocalOption(key: kShortcutLocalConfigKey);
if (raw.isEmpty) return null;
@override final Map<String, dynamic> parsed;
Widget build(BuildContext context) { try {
return Tooltip( parsed = jsonDecode(raw) as Map<String, dynamic>;
message: translate(tipKey), } catch (_) {
triggerMode: TooltipTriggerMode.tap, return null;
preferBelow: false, }
waitDuration: const Duration(milliseconds: 250), final list = (parsed['bindings'] as List? ?? const [])
showDuration: const Duration(seconds: 6), .cast<Map<String, dynamic>>();
child: Padding( final found = list.firstWhere(
padding: const EdgeInsets.symmetric(horizontal: 6), (b) => b['action'] == actionId,
child: Icon( orElse: () => {},
Icons.help_outline,
size: 16,
color: Theme.of(context).hintColor,
),
),
); );
if (found.isEmpty) return null;
// Guard against a hand-edited / corrupt config where `key` is missing or
// not a string — render the row as unbound instead of crashing the
// settings page.
final keyValue = found['key'];
if (keyValue is! String) return null;
final isMac = defaultTargetPlatform == TargetPlatform.macOS ||
defaultTargetPlatform == TargetPlatform.iOS;
// `mods` similarly may be malformed; treat a non-list as no modifiers.
final modsRaw = found['mods'];
final mods = modsRaw is List
? modsRaw.whereType<String>().toList()
: const <String>[];
final parts = <String>[];
for (final m in ['primary', 'alt', 'shift']) {
if (!mods.contains(m)) continue;
switch (m) {
case 'primary':
parts.add(isMac ? '' : 'Ctrl');
break;
case 'alt':
parts.add(isMac ? '' : 'Alt');
break;
case 'shift':
parts.add(isMac ? '' : 'Shift');
break;
}
}
parts.add(_keyDisplay(keyValue, isMac));
return isMac ? parts.join('') : parts.join('+');
}
static String _keyDisplay(String key, bool isMac) {
switch (key) {
case 'delete':
return isMac ? '' : 'Del';
case 'enter':
return isMac ? '' : 'Enter';
case 'arrow_left':
return '';
case 'arrow_right':
return '';
case 'arrow_up':
return '';
case 'arrow_down':
return '';
}
if (key.startsWith('digit')) return key.substring(5);
return key.toUpperCase();
} }
} }

View File

@@ -2,9 +2,9 @@
// //
// Modal dialog used by the Keyboard Shortcuts settings page to capture a new // Modal dialog used by the Keyboard Shortcuts settings page to capture a new
// key combination for a given action. The dialog listens for KeyDown events, // key combination for a given action. The dialog listens for KeyDown events,
// extracts the modifier set + non-modifier key, validates that at least one // extracts the modifier set + non-modifier key, validates against the
// modifier is present, and reports any conflict with another already-bound // "must include Ctrl+Alt+Shift (Cmd+Option+Shift on macOS)" rule, and reports
// action. // any conflict with another already-bound action.
// //
// On Save, returns the new binding map ({action, mods, key}) plus the // On Save, returns the new binding map ({action, mods, key}) plus the
// optional id of the action whose binding should be cleared (the conflict // optional id of the action whose binding should be cleared (the conflict
@@ -15,7 +15,6 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import '../../../common.dart'; import '../../../common.dart';
import 'shortcut_utils.dart';
/// Result of the recording dialog. /// Result of the recording dialog.
class RecordingResult { class RecordingResult {
@@ -81,39 +80,6 @@ class _RecordingDialogState extends State<_RecordingDialog> {
Set<String> _mods = {}; Set<String> _mods = {};
String? _key; String? _key;
// Human-readable label for the most recent press that we couldn't bind to
// (e.g. F13, media keys). null when the last press was either supported or
// a modifier-only press. Cleared whenever a supported key arrives, so a
// user who hits an unsupported key after a valid capture sees the warning
// until they press something else. Distinct from `_key == null` so the
// status line can tell the user *why* their press was ignored instead of
// silently doing nothing.
String? _unsupportedKey;
// Modifier LogicalKeyboardKeys we should *not* treat as "unsupported" when
// they fail to map to a key name. A modifier-only press is normal during
// combo capture (the user is building up their combo) — only non-modifier
// unmapped keys deserve the warning.
static final _modifierKeys = <LogicalKeyboardKey>{
LogicalKeyboardKey.shift,
LogicalKeyboardKey.shiftLeft,
LogicalKeyboardKey.shiftRight,
LogicalKeyboardKey.control,
LogicalKeyboardKey.controlLeft,
LogicalKeyboardKey.controlRight,
LogicalKeyboardKey.alt,
LogicalKeyboardKey.altLeft,
LogicalKeyboardKey.altRight,
LogicalKeyboardKey.meta,
LogicalKeyboardKey.metaLeft,
LogicalKeyboardKey.metaRight,
LogicalKeyboardKey.capsLock,
LogicalKeyboardKey.numLock,
LogicalKeyboardKey.scrollLock,
LogicalKeyboardKey.fn,
LogicalKeyboardKey.fnLock,
};
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@@ -132,12 +98,12 @@ class _RecordingDialogState extends State<_RecordingDialog> {
defaultTargetPlatform == TargetPlatform.macOS || defaultTargetPlatform == TargetPlatform.macOS ||
defaultTargetPlatform == TargetPlatform.iOS; defaultTargetPlatform == TargetPlatform.iOS;
/// True when the captured combo includes at least one modifier. Lower bound /// True when the captured combo includes the required Ctrl+Alt+Shift
/// for any sensible binding — pure single-key bindings would swallow normal /// (Cmd+Option+Shift on macOS) prefix and a non-modifier key.
/// typing the moment shortcuts are enabled. Beyond one mod the user is on bool get _hasRequiredPrefix =>
/// their own; the in-session pass-through toggle is the escape hatch when _mods.contains('primary') &&
/// a chosen combo collides with something needed on the remote. _mods.contains('alt') &&
bool get _hasRequiredPrefix => _mods.isNotEmpty; _mods.contains('shift');
/// Return the actionId that this combo currently conflicts with, or null. /// Return the actionId that this combo currently conflicts with, or null.
/// The action being edited is not a conflict with itself. /// The action being edited is not a conflict with itself.
@@ -147,7 +113,8 @@ class _RecordingDialogState extends State<_RecordingDialog> {
final otherAction = b['action'] as String?; final otherAction = b['action'] as String?;
if (otherAction == null || otherAction == widget.actionId) continue; if (otherAction == null || otherAction == widget.actionId) continue;
final otherKey = b['key'] as String?; final otherKey = b['key'] as String?;
final otherMods = shortcutModSetFrom(b['mods']); final otherMods =
((b['mods'] as List?) ?? const []).cast<String>().toSet();
if (otherKey == _key && if (otherKey == _key &&
otherMods.length == _mods.length && otherMods.length == _mods.length &&
otherMods.containsAll(_mods)) { otherMods.containsAll(_mods)) {
@@ -158,8 +125,7 @@ class _RecordingDialogState extends State<_RecordingDialog> {
} }
KeyEventResult _onKeyEvent(FocusNode node, KeyEvent event) { KeyEventResult _onKeyEvent(FocusNode node, KeyEvent event) {
if (event is KeyDownEvent && if (event is KeyDownEvent && event.logicalKey == LogicalKeyboardKey.escape) {
event.logicalKey == LogicalKeyboardKey.escape) {
Navigator.of(context).pop(); Navigator.of(context).pop();
return KeyEventResult.handled; return KeyEventResult.handled;
} }
@@ -167,22 +133,15 @@ class _RecordingDialogState extends State<_RecordingDialog> {
// Ignore modifier-only KeyDowns: don't lock in a partial combo. // Ignore modifier-only KeyDowns: don't lock in a partial combo.
final logical = event.logicalKey; final logical = event.logicalKey;
final keyName = logicalKeyName(logical); final keyName = _logicalToKeyName(logical);
// Mirror of `normalize_modifiers` in src/keyboard/shortcuts.rs:
// * macOS: Cmd → primary, Ctrl → ctrl (distinct).
// * Win/Linux: Ctrl → primary, no separate Ctrl modifier.
// The two halves must agree on labels, otherwise saved bindings will not
// match the events the matcher sees at runtime.
final mods = <String>{}; final mods = <String>{};
if (HardwareKeyboard.instance.isAltPressed) mods.add('alt'); if (HardwareKeyboard.instance.isAltPressed) mods.add('alt');
if (HardwareKeyboard.instance.isShiftPressed) mods.add('shift'); if (HardwareKeyboard.instance.isShiftPressed) mods.add('shift');
if (_isMac) { final primary = _isMac
if (HardwareKeyboard.instance.isMetaPressed) mods.add('primary'); ? HardwareKeyboard.instance.isMetaPressed
if (HardwareKeyboard.instance.isControlPressed) mods.add('ctrl'); : HardwareKeyboard.instance.isControlPressed;
} else { if (primary) mods.add('primary');
if (HardwareKeyboard.instance.isControlPressed) mods.add('primary');
}
setState(() { setState(() {
_mods = mods; _mods = mods;
@@ -191,15 +150,6 @@ class _RecordingDialogState extends State<_RecordingDialog> {
// untouched, so the user can adjust modifiers after the fact. // untouched, so the user can adjust modifiers after the fact.
if (keyName != null) { if (keyName != null) {
_key = keyName; _key = keyName;
_unsupportedKey = null;
} else if (!_modifierKeys.contains(logical)) {
// Non-modifier key we don't recognize (e.g. F13, media keys, IME
// compose keys). Surface a warning instead of silently dropping the
// press — the dialog otherwise looks unresponsive.
final label = logical.keyLabel.isNotEmpty
? logical.keyLabel
: (logical.debugName ?? 'this key');
_unsupportedKey = label;
} }
}); });
return KeyEventResult.handled; return KeyEventResult.handled;
@@ -207,7 +157,13 @@ class _RecordingDialogState extends State<_RecordingDialog> {
void _onSave() { void _onSave() {
if (_key == null || !_hasRequiredPrefix) return; if (_key == null || !_hasRequiredPrefix) return;
final ordered = canonicalShortcutModsForSave(_mods); // Sort mods to match the canonical order used by Rust default_bindings:
// primary, alt, shift.
final ordered = <String>[
if (_mods.contains('primary')) 'primary',
if (_mods.contains('alt')) 'alt',
if (_mods.contains('shift')) 'shift',
];
final binding = <String, dynamic>{ final binding = <String, dynamic>{
'action': widget.actionId, 'action': widget.actionId,
'mods': ordered, 'mods': ordered,
@@ -217,30 +173,23 @@ class _RecordingDialogState extends State<_RecordingDialog> {
} }
String _formatPrefix() { String _formatPrefix() {
// Used in the "must include..." validation row; lists the modifier set if (_isMac) return 'Cmd+Option+Shift';
// a binding can pick from. Localised modifier glyphs aren't used here so return 'Ctrl+Alt+Shift';
// the names stay greppable for users searching for "Option" / "Cmd".
if (_isMac) return 'Cmd / Control / Option / Shift';
return 'Ctrl / Alt / Shift';
} }
String _formatCombo() { String _formatCombo() {
// Plain-text labels (see same rationale in display.dart::_keyDisplay).
final parts = <String>[]; final parts = <String>[];
for (final m in ['primary', 'ctrl', 'alt', 'shift']) { for (final m in ['primary', 'alt', 'shift']) {
if (!_mods.contains(m)) continue; if (!_mods.contains(m)) continue;
switch (m) { switch (m) {
case 'primary': case 'primary':
parts.add(_isMac ? 'Cmd' : 'Ctrl'); parts.add(_isMac ? '' : 'Ctrl');
break;
case 'ctrl':
parts.add(_isMac ? 'Control' : 'Ctrl');
break; break;
case 'alt': case 'alt':
parts.add(_isMac ? 'Option' : 'Alt'); parts.add(_isMac ? '' : 'Alt');
break; break;
case 'shift': case 'shift':
parts.add('Shift'); parts.add(_isMac ? '' : 'Shift');
break; break;
} }
} }
@@ -248,25 +197,23 @@ class _RecordingDialogState extends State<_RecordingDialog> {
parts.add(_keyDisplay(_key!)); parts.add(_keyDisplay(_key!));
} }
if (parts.isEmpty) return translate('shortcut-recording-press-keys-tip'); if (parts.isEmpty) return translate('shortcut-recording-press-keys-tip');
return parts.join('+'); return _isMac ? parts.join('') : parts.join('+');
} }
String _keyDisplay(String key) { String _keyDisplay(String key) {
switch (key) { switch (key) {
case 'delete': return 'Del'; case 'delete':
case 'backspace': return 'Backspace'; return _isMac ? '' : 'Del';
case 'enter': return 'Enter'; case 'enter':
case 'tab': return 'Tab'; return _isMac ? '' : 'Enter';
case 'space': return 'Space'; case 'arrow_left':
case 'arrow_left': return 'Left'; return '';
case 'arrow_right':return 'Right'; case 'arrow_right':
case 'arrow_up': return 'Up'; return '';
case 'arrow_down': return 'Down'; case 'arrow_up':
case 'home': return 'Home'; return '';
case 'end': return 'End'; case 'arrow_down':
case 'page_up': return 'PgUp'; return '';
case 'page_down': return 'PgDn';
case 'insert': return 'Ins';
} }
if (key.startsWith('digit')) return key.substring(5); if (key.startsWith('digit')) return key.substring(5);
return key.toUpperCase(); return key.toUpperCase();
@@ -277,31 +224,10 @@ class _RecordingDialogState extends State<_RecordingDialog> {
final hasKey = _key != null; final hasKey = _key != null;
final conflictId = _conflictActionId; final conflictId = _conflictActionId;
final hasConflict = conflictId != null; final hasConflict = conflictId != null;
// The Save button still fires for the previously-captured combo even if
// the user just hit an unsupported key — the captured state is what gets
// saved, the warning is just feedback that the latest press was rejected.
final canSave = hasKey && _hasRequiredPrefix; final canSave = hasKey && _hasRequiredPrefix;
Widget statusLine; Widget statusLine;
if (_unsupportedKey != null) { if (!hasKey) {
// Most recent press was unsupported. Take precedence over the
// captured-combo states so the user gets explicit feedback that their
// last keystroke was ignored, regardless of whether a previous combo
// is still captured.
statusLine = Row(
children: [
const Icon(Icons.close, size: 16, color: Colors.red),
const SizedBox(width: 6),
Flexible(
child: Text(
translate('shortcut-key-not-supported')
.replaceAll('{}', _unsupportedKey!),
style: const TextStyle(color: Colors.red),
),
),
],
);
} else if (!hasKey) {
statusLine = Text( statusLine = Text(
translate('shortcut-recording-press-keys-tip'), translate('shortcut-recording-press-keys-tip'),
style: TextStyle(color: Theme.of(context).hintColor), style: TextStyle(color: Theme.of(context).hintColor),
@@ -313,8 +239,7 @@ class _RecordingDialogState extends State<_RecordingDialog> {
const SizedBox(width: 6), const SizedBox(width: 6),
Flexible( Flexible(
child: Text( child: Text(
translate('shortcut-must-include-modifiers') '${translate('shortcut-must-include-prefix')} ${_formatPrefix()}',
.replaceAll('{}', _formatPrefix()),
style: const TextStyle(color: Colors.red), style: const TextStyle(color: Colors.red),
), ),
), ),
@@ -340,7 +265,8 @@ class _RecordingDialogState extends State<_RecordingDialog> {
children: [ children: [
const Icon(Icons.check, size: 16, color: Colors.green), const Icon(Icons.check, size: 16, color: Colors.green),
const SizedBox(width: 6), const SizedBox(width: 6),
Text(translate('Valid'), style: const TextStyle(color: Colors.green)), Text(translate('Valid'),
style: const TextStyle(color: Colors.green)),
], ],
); );
} }
@@ -365,8 +291,8 @@ class _RecordingDialogState extends State<_RecordingDialog> {
const SizedBox(height: 12), const SizedBox(height: 12),
Container( Container(
width: double.infinity, width: double.infinity,
padding: padding: const EdgeInsets.symmetric(
const EdgeInsets.symmetric(vertical: 18, horizontal: 12), vertical: 18, horizontal: 12),
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.all(color: Theme.of(context).dividerColor), border: Border.all(color: Theme.of(context).dividerColor),
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
@@ -391,9 +317,55 @@ class _RecordingDialogState extends State<_RecordingDialog> {
), ),
actions: [ actions: [
dialogButton('Cancel', dialogButton('Cancel',
onPressed: () => Navigator.of(context).pop(), isOutline: true), onPressed: () => Navigator.of(context).pop(),
isOutline: true),
dialogButton(saveLabel, onPressed: canSave ? _onSave : null), dialogButton(saveLabel, onPressed: canSave ? _onSave : null),
], ],
); );
} }
/// Mirror of `event_to_key_name` in `src/keyboard/shortcuts.rs` and
/// `logicalToKeyName` in `flutter/web/js/src/shortcut_matcher.ts` — keep
/// the three in lockstep. Returns null for modifier-only or unsupported keys.
static String? _logicalToKeyName(LogicalKeyboardKey k) {
if (k == LogicalKeyboardKey.delete) return 'delete';
if (k == LogicalKeyboardKey.enter ||
k == LogicalKeyboardKey.numpadEnter) return 'enter';
if (k == LogicalKeyboardKey.arrowLeft) return 'arrow_left';
if (k == LogicalKeyboardKey.arrowRight) return 'arrow_right';
if (k == LogicalKeyboardKey.arrowUp) return 'arrow_up';
if (k == LogicalKeyboardKey.arrowDown) return 'arrow_down';
final letters = <LogicalKeyboardKey, String>{
LogicalKeyboardKey.keyA: 'a', LogicalKeyboardKey.keyB: 'b',
LogicalKeyboardKey.keyC: 'c', LogicalKeyboardKey.keyD: 'd',
LogicalKeyboardKey.keyE: 'e', LogicalKeyboardKey.keyF: 'f',
LogicalKeyboardKey.keyG: 'g', LogicalKeyboardKey.keyH: 'h',
LogicalKeyboardKey.keyI: 'i', LogicalKeyboardKey.keyJ: 'j',
LogicalKeyboardKey.keyK: 'k', LogicalKeyboardKey.keyL: 'l',
LogicalKeyboardKey.keyM: 'm', LogicalKeyboardKey.keyN: 'n',
LogicalKeyboardKey.keyO: 'o', LogicalKeyboardKey.keyP: 'p',
LogicalKeyboardKey.keyQ: 'q', LogicalKeyboardKey.keyR: 'r',
LogicalKeyboardKey.keyS: 's', LogicalKeyboardKey.keyT: 't',
LogicalKeyboardKey.keyU: 'u', LogicalKeyboardKey.keyV: 'v',
LogicalKeyboardKey.keyW: 'w', LogicalKeyboardKey.keyX: 'x',
LogicalKeyboardKey.keyY: 'y', LogicalKeyboardKey.keyZ: 'z',
};
if (letters.containsKey(k)) return letters[k];
final digits = <LogicalKeyboardKey, String>{
LogicalKeyboardKey.digit1: 'digit1',
LogicalKeyboardKey.digit2: 'digit2',
LogicalKeyboardKey.digit3: 'digit3',
LogicalKeyboardKey.digit4: 'digit4',
LogicalKeyboardKey.digit5: 'digit5',
LogicalKeyboardKey.digit6: 'digit6',
LogicalKeyboardKey.digit7: 'digit7',
LogicalKeyboardKey.digit8: 'digit8',
LogicalKeyboardKey.digit9: 'digit9',
};
if (digits.containsKey(k)) return digits[k];
return null;
}
} }

View File

@@ -1,292 +0,0 @@
import 'shortcut_constants.dart';
import 'shortcut_utils.dart';
/// Marker for the union of [KeyboardShortcutActionEntry] /
/// [KeyboardShortcutActionSubgroup] — anything a top-level
/// [KeyboardShortcutActionGroup] can directly contain. Sealed so renderers
/// and filters can `switch` on it without a default branch.
sealed class KeyboardShortcutActionGroupChild {
const KeyboardShortcutActionGroupChild();
}
/// One configurable action — id + i18n key for its label.
class KeyboardShortcutActionEntry extends KeyboardShortcutActionGroupChild {
final String id;
final String labelKey;
const KeyboardShortcutActionEntry(this.id, this.labelKey);
}
/// A nested subgroup (e.g. "View Mode" under "Display"). Renders with extra
/// indent so its items are visually distinguished from the parent group's
/// direct items.
class KeyboardShortcutActionSubgroup extends KeyboardShortcutActionGroupChild {
final String titleKey;
final List<KeyboardShortcutActionEntry> entries;
const KeyboardShortcutActionSubgroup(this.titleKey, this.entries);
}
/// A top-level group ("Display", "Keyboard", "Chat", …). `children` is an
/// *ordered* mix of direct entries and subgroups, so layouts like
/// "subgroups first → direct items → trailing subgroup" — exactly the
/// shape `_DisplayMenu` uses (Privacy mode lives after the cursor / display
/// toggles direct items) — are first-class instead of needing a wrapper
/// "Display Settings" subgroup just to insert the items.
class KeyboardShortcutActionGroup {
final String titleKey;
final List<KeyboardShortcutActionGroupChild> children;
const KeyboardShortcutActionGroup(this.titleKey, this.children);
}
/// Canonical action group definitions used by both the desktop and mobile
/// configuration pages. The order of groups, subgroups, and entries here
/// is the order the user sees in the UI, and mirrors the corresponding
/// toolbar submenu (`_DisplayMenu` / `_KeyboardMenu` in
/// `desktop/widgets/remote_toolbar.dart`) child order — modulo entries
/// without shortcut counterparts (e.g. `_screenAdjustor.adjustWindow`,
/// `scrollStyle`, `_ResolutionsMenu`, `localKeyboardType`).
final List<KeyboardShortcutActionGroup> kKeyboardShortcutActionGroups = [
KeyboardShortcutActionGroup('Monitor', [
KeyboardShortcutActionEntry(
kShortcutActionSwitchDisplayNext, 'Switch to next display'),
KeyboardShortcutActionEntry(
kShortcutActionSwitchDisplayPrev, 'Switch to previous display'),
KeyboardShortcutActionEntry(
kShortcutActionSwitchDisplayAll, 'All monitors'),
]),
KeyboardShortcutActionGroup('Control Actions', [
KeyboardShortcutActionEntry(
kShortcutActionSendClipboardKeystrokes, 'Send clipboard keystrokes'),
KeyboardShortcutActionEntry(kShortcutActionResetCanvas, 'Reset canvas'),
KeyboardShortcutActionEntry(
kShortcutActionSendCtrlAltDel, 'Insert Ctrl + Alt + Del'),
KeyboardShortcutActionEntry(
kShortcutActionRestartRemote, 'Restart remote device'),
KeyboardShortcutActionEntry(kShortcutActionInsertLock, 'Insert Lock'),
KeyboardShortcutActionEntry(
kShortcutActionToggleBlockInput, 'Block user input'),
KeyboardShortcutActionEntry(kShortcutActionSwitchSides, 'Switch Sides'),
KeyboardShortcutActionEntry(kShortcutActionRefresh, 'Refresh'),
KeyboardShortcutActionEntry(
kShortcutActionToggleRecording, 'Toggle session recording'),
KeyboardShortcutActionEntry(kShortcutActionScreenshot, 'Take screenshot'),
]),
// Display: subgroups (View Mode → Image Quality → Codec → Virtual display)
// first, then the direct items (cursor toggles + display toggles), then
// Privacy mode subgroup last — matching `_DisplayMenu.menuChildrenGetter`
// exactly. Rebalancing this order should also rebalance the toolbar.
KeyboardShortcutActionGroup('Display', [
KeyboardShortcutActionSubgroup('View Mode', [
KeyboardShortcutActionEntry(
kShortcutActionViewModeOriginal, 'Scale original'),
KeyboardShortcutActionEntry(
kShortcutActionViewModeAdaptive, 'Scale adaptive'),
KeyboardShortcutActionEntry(
kShortcutActionViewModeCustom, 'Scale custom'),
]),
KeyboardShortcutActionSubgroup('Image Quality', [
KeyboardShortcutActionEntry(
kShortcutActionImageQualityBest, 'Good image quality'),
KeyboardShortcutActionEntry(
kShortcutActionImageQualityBalanced, 'Balanced'),
KeyboardShortcutActionEntry(
kShortcutActionImageQualityLow, 'Optimize reaction time'),
]),
KeyboardShortcutActionSubgroup('Codec', [
KeyboardShortcutActionEntry(kShortcutActionCodecAuto, 'Auto'),
KeyboardShortcutActionEntry(kShortcutActionCodecVp8, 'VP8'),
KeyboardShortcutActionEntry(kShortcutActionCodecVp9, 'VP9'),
KeyboardShortcutActionEntry(kShortcutActionCodecAv1, 'AV1'),
KeyboardShortcutActionEntry(kShortcutActionCodecH264, 'H264'),
KeyboardShortcutActionEntry(kShortcutActionCodecH265, 'H265'),
]),
KeyboardShortcutActionSubgroup('Virtual display', [
KeyboardShortcutActionEntry(
kShortcutActionPlugOutAllVirtualDisplays, 'Plug out all'),
]),
// Direct items: cursorToggles + display toggles, in toolbar order.
KeyboardShortcutActionEntry(
kShortcutActionToggleShowRemoteCursor, 'Show remote cursor'),
KeyboardShortcutActionEntry(
kShortcutActionToggleFollowRemoteCursor, 'Follow remote cursor'),
KeyboardShortcutActionEntry(
kShortcutActionToggleFollowRemoteWindow, 'Follow remote window focus'),
KeyboardShortcutActionEntry(
kShortcutActionToggleZoomCursor, 'Zoom cursor'),
KeyboardShortcutActionEntry(
kShortcutActionToggleQualityMonitor, 'Show quality monitor'),
KeyboardShortcutActionEntry(kShortcutActionToggleMute, 'Mute'),
KeyboardShortcutActionEntry(
kShortcutActionToggleEnableFileCopyPaste, 'Enable file copy and paste'),
KeyboardShortcutActionEntry(
kShortcutActionToggleDisableClipboard, 'Disable clipboard'),
KeyboardShortcutActionEntry(
kShortcutActionToggleLockAfterSessionEnd, 'Lock after session end'),
KeyboardShortcutActionEntry(
kShortcutActionToggleTrueColor, 'True color (4:4:4)'),
// Privacy mode at the bottom — mirrors `_DisplayMenu` where it's the
// last submenu added (line ~1023 of remote_toolbar.dart, after toggles).
KeyboardShortcutActionSubgroup('Privacy mode', [
// Reuse toolbar's existing impl-name i18n keys. The handler at
// runtime matches `privacy_mode_impl_mag_tip` /
// `privacy_mode_impl_virtual_display_tip` against the peer's
// advertised impls — same logic the toolbar's `toolbarPrivacyMode`
// submenu uses.
KeyboardShortcutActionEntry(
kShortcutActionPrivacyMode1, 'privacy_mode_impl_mag_tip'),
KeyboardShortcutActionEntry(
kShortcutActionPrivacyMode2, 'privacy_mode_impl_virtual_display_tip'),
]),
]),
// Keyboard: Keyboard mode subgroup first, then direct items
// (inputSource → viewMode → showMyCursor → toolbarKeyboardToggles),
// matching `_KeyboardMenu.menuChildrenGetter`.
KeyboardShortcutActionGroup('Keyboard', [
KeyboardShortcutActionSubgroup('Keyboard mode', [
KeyboardShortcutActionEntry(
kShortcutActionKeyboardModeLegacy, 'Legacy mode'),
KeyboardShortcutActionEntry(kShortcutActionKeyboardModeMap, 'Map mode'),
KeyboardShortcutActionEntry(
kShortcutActionKeyboardModeTranslate, 'Translate mode'),
]),
KeyboardShortcutActionEntry(
kShortcutActionToggleInputSource, 'Toggle input source'),
KeyboardShortcutActionEntry(kShortcutActionToggleViewOnly, 'View Mode'),
KeyboardShortcutActionEntry(
kShortcutActionToggleShowMyCursor, 'Show my cursor'),
KeyboardShortcutActionEntry(
kShortcutActionToggleSwapCtrlCmd, 'Swap control-command key'),
KeyboardShortcutActionEntry(
kShortcutActionToggleRelativeMouseMode, 'Relative mouse mode'),
KeyboardShortcutActionEntry(
kShortcutActionToggleReverseMouseWheel, 'Reverse mouse wheel'),
KeyboardShortcutActionEntry(
kShortcutActionToggleSwapLeftRightMouse, 'swap-left-right-mouse'),
]),
KeyboardShortcutActionGroup('Chat', [
KeyboardShortcutActionEntry(kShortcutActionToggleChat, 'Text chat'),
KeyboardShortcutActionEntry(kShortcutActionToggleVoiceCall, 'Voice call'),
]),
// "Other" collects single-icon toolbar buttons that have no dropdown
// (Pin, Close), plus actions with no toolbar entry at all (Fullscreen —
// driven by callback, not menu; Toggle Toolbar / tab navigation — tab
// right-click menu, not toolbar). Combined into one group rather than
// several 1-item groups for cleaner visual hierarchy.
KeyboardShortcutActionGroup('Other', [
KeyboardShortcutActionEntry(kShortcutActionPinToolbar, 'Pin Toolbar'),
KeyboardShortcutActionEntry(
kShortcutActionToggleFullscreen, 'Toggle fullscreen'),
KeyboardShortcutActionEntry(kShortcutActionToggleToolbar, 'Toggle toolbar'),
KeyboardShortcutActionEntry(kShortcutActionCloseTab, 'Close tab'),
KeyboardShortcutActionEntry(
kShortcutActionSwitchTabNext, 'Switch to next tab'),
KeyboardShortcutActionEntry(
kShortcutActionSwitchTabPrev, 'Switch to previous tab'),
]),
];
/// Walk the (filtered or unfiltered) group tree and yield every
/// [KeyboardShortcutActionEntry], regardless of whether it sits as a direct
/// child of a top-level group or inside a subgroup. Useful for label
/// lookups, ghost-action tests, and any consumer that just wants the flat
/// list of action ids.
Iterable<KeyboardShortcutActionEntry> allActionEntries(
Iterable<KeyboardShortcutActionGroup> groups,
) sync* {
for (final group in groups) {
for (final child in group.children) {
switch (child) {
case KeyboardShortcutActionEntry():
yield child;
case KeyboardShortcutActionSubgroup():
yield* child.entries;
}
}
}
}
/// Return [kKeyboardShortcutActionGroups] with actions that aren't supported
/// on the current platform stripped out. Subgroups whose every entry was
/// filtered are dropped; top-level groups whose every child (direct entry
/// or subgroup) was dropped are themselves dropped.
///
/// Mirrors the capability flags used by [filterDefaultBindingsForPlatform]
/// so the configuration UI shows only what the matcher can actually
/// dispatch on this platform.
///
/// Note: callers should still walk the unfiltered
/// [kKeyboardShortcutActionGroups] for label lookups (e.g. conflict
/// warnings about a stale cross-platform binding), so an action bound on
/// desktop and carried over to mobile still has a human-readable name in
/// dialogs.
List<KeyboardShortcutActionGroup> filterKeyboardShortcutActionGroupsForPlatform(
ShortcutPlatformCapabilities cap,
) {
bool allowed(String id) {
if (!cap.includeFullscreenShortcut &&
id == kShortcutActionToggleFullscreen) {
return false;
}
if (!cap.includeScreenshotShortcut && id == kShortcutActionScreenshot) {
return false;
}
if (!cap.includeScreenshotShortcut &&
id == kShortcutActionToggleRelativeMouseMode) {
return false;
}
if (!cap.includeTabShortcuts && isSwitchTabShortcutAction(id)) return false;
if (!cap.includeToolbarShortcut && id == kShortcutActionToggleToolbar) {
return false;
}
if (!cap.includeCloseTabShortcut && id == kShortcutActionCloseTab) {
return false;
}
if (!cap.includeSwitchSidesShortcut && id == kShortcutActionSwitchSides) {
return false;
}
if (!cap.includeRecordingShortcut && id == kShortcutActionToggleRecording) {
return false;
}
if (!cap.includeResetCanvasShortcut && id == kShortcutActionResetCanvas) {
return false;
}
if (!cap.includePinToolbarShortcut && id == kShortcutActionPinToolbar) {
return false;
}
if (!cap.includeViewModeShortcut &&
(id == kShortcutActionViewModeOriginal ||
id == kShortcutActionViewModeAdaptive ||
id == kShortcutActionViewModeCustom)) {
return false;
}
if (!cap.includeInputSourceShortcut &&
id == kShortcutActionToggleInputSource) {
return false;
}
if (!cap.includeVoiceCallShortcut && id == kShortcutActionToggleVoiceCall) {
return false;
}
return true;
}
final out = <KeyboardShortcutActionGroup>[];
for (final group in kKeyboardShortcutActionGroups) {
final filteredChildren = <KeyboardShortcutActionGroupChild>[];
for (final child in group.children) {
switch (child) {
case KeyboardShortcutActionEntry():
if (allowed(child.id)) filteredChildren.add(child);
case KeyboardShortcutActionSubgroup():
final entries =
child.entries.where((e) => allowed(e.id)).toList();
if (entries.isNotEmpty) {
filteredChildren.add(
KeyboardShortcutActionSubgroup(child.titleKey, entries));
}
}
}
if (filteredChildren.isNotEmpty) {
out.add(KeyboardShortcutActionGroup(group.titleKey, filteredChildren));
}
}
return out;
}

View File

@@ -1,104 +0,0 @@
/// Keyboard shortcut action IDs - must match
/// src/keyboard/shortcuts.rs::action_id.
const kShortcutActionSendCtrlAltDel = 'send_ctrl_alt_del';
const kShortcutActionToggleFullscreen = 'toggle_fullscreen';
const kShortcutActionSwitchDisplayNext = 'switch_display_next';
const kShortcutActionSwitchDisplayPrev = 'switch_display_prev';
const kShortcutActionSwitchDisplayAll = 'switch_display_all';
const kShortcutActionScreenshot = 'screenshot';
const kShortcutActionInsertLock = 'insert_lock';
const kShortcutActionRefresh = 'refresh';
const kShortcutActionToggleBlockInput = 'toggle_block_input';
const kShortcutActionToggleRecording = 'toggle_recording';
const kShortcutActionSwitchSides = 'switch_sides';
const kShortcutActionCloseTab = 'close_tab';
const kShortcutActionToggleToolbar = 'toggle_toolbar';
const kShortcutActionRestartRemote = 'restart_remote';
const kShortcutActionResetCanvas = 'reset_canvas';
const kShortcutActionSwitchTabNext = 'switch_tab_next';
const kShortcutActionSwitchTabPrev = 'switch_tab_prev';
const kShortcutActionToggleMute = 'toggle_mute';
const kShortcutActionPinToolbar = 'pin_toolbar';
const kShortcutActionViewModeOriginal = 'view_mode_original';
const kShortcutActionViewModeAdaptive = 'view_mode_adaptive';
const kShortcutActionToggleChat = 'toggle_chat';
const kShortcutActionToggleQualityMonitor = 'toggle_quality_monitor';
const kShortcutActionToggleShowRemoteCursor = 'toggle_show_remote_cursor';
const kShortcutActionToggleShowMyCursor = 'toggle_show_my_cursor';
const kShortcutActionToggleDisableClipboard = 'toggle_disable_clipboard';
const kShortcutActionPrivacyMode1 = 'privacy_mode_1';
const kShortcutActionPrivacyMode2 = 'privacy_mode_2';
// Keyboard mode (Map / Translate / Legacy).
const kShortcutActionKeyboardModeMap = 'keyboard_mode_map';
const kShortcutActionKeyboardModeTranslate = 'keyboard_mode_translate';
const kShortcutActionKeyboardModeLegacy = 'keyboard_mode_legacy';
// Codec preference (Auto + the four optional codecs the toolbar surfaces).
const kShortcutActionCodecAuto = 'codec_auto';
const kShortcutActionCodecVp8 = 'codec_vp8';
const kShortcutActionCodecVp9 = 'codec_vp9';
const kShortcutActionCodecAv1 = 'codec_av1';
const kShortcutActionCodecH264 = 'codec_h264';
const kShortcutActionCodecH265 = 'codec_h265';
// Plug out every virtual display in one shot — toolbar exposes this in
// both IDD modes (RustDesk and Amyuni). Per-index virtual-display toggles
// (RustDesk IDD's 4 checkboxes) and the +/- count buttons (Amyuni-only)
// are NOT exposed as shortcuts: per-index is too granular, and +/- has
// no toolbar counterpart on RustDesk IDD peers.
const kShortcutActionPlugOutAllVirtualDisplays =
'plug_out_all_virtual_displays';
const kShortcutActionToggleRelativeMouseMode = 'toggle_relative_mouse_mode';
const kShortcutActionToggleFollowRemoteCursor = 'toggle_follow_remote_cursor';
const kShortcutActionToggleFollowRemoteWindow = 'toggle_follow_remote_window';
const kShortcutActionToggleZoomCursor = 'toggle_zoom_cursor';
const kShortcutActionToggleReverseMouseWheel = 'toggle_reverse_mouse_wheel';
const kShortcutActionToggleSwapLeftRightMouse = 'toggle_swap_left_right_mouse';
const kShortcutActionToggleLockAfterSessionEnd = 'toggle_lock_after_session_end';
const kShortcutActionToggleTrueColor = 'toggle_true_color';
const kShortcutActionToggleSwapCtrlCmd = 'toggle_swap_ctrl_cmd';
const kShortcutActionToggleEnableFileCopyPaste = 'toggle_enable_file_copy_paste';
const kShortcutActionViewModeCustom = 'view_mode_custom';
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';
const kShortcutLocalConfigKey = 'keyboard-shortcuts';
const kShortcutEventName = 'shortcut_triggered';
/// Canonical default keyboard-shortcut bindings, mirroring Rust's
/// `default_bindings()` in `src/keyboard/shortcuts.rs`. Used by:
/// * the Web bridge (`flutter/lib/web/bridge.dart::mainGetDefaultKeyboardShortcuts`)
/// — Web has no Rust at runtime, so the seed list is read from this Dart
/// constant instead of going through FFI.
/// * the configuration page when seeding defaults on first enable, after
/// [filterDefaultBindingsForPlatform] has trimmed platform-specific
/// entries.
///
/// Parity with Rust is unit-tested on both sides against
/// `flutter/test/fixtures/default_keyboard_shortcuts.json` — see the
/// `kDefaultShortcutBindings matches fixture` test in
/// `flutter/test/keyboard_shortcuts_test.dart` and
/// `default_bindings_match_fixture_json` in `src/keyboard/shortcuts.rs`.
/// Any change here MUST also update the fixture and the Rust source, or CI
/// will fail in the side that drifted.
final List<Map<String, Object>> kDefaultShortcutBindings = [
for (final entry in <List<Object>>[
[kShortcutActionSendCtrlAltDel, 'delete'],
[kShortcutActionToggleFullscreen, 'enter'],
[kShortcutActionSwitchDisplayNext, 'arrow_right'],
[kShortcutActionSwitchDisplayPrev, 'arrow_left'],
[kShortcutActionScreenshot, 'p'],
[kShortcutActionToggleShowRemoteCursor, 'm'],
[kShortcutActionToggleMute, 's'],
[kShortcutActionToggleBlockInput, 'i'],
[kShortcutActionToggleChat, 'c'],
])
{
'action': entry[0],
'mods': const ['primary', 'alt', 'shift'],
'key': entry[1],
},
];

View File

@@ -1,226 +0,0 @@
import 'package:flutter/services.dart';
import 'shortcut_constants.dart';
List<String> canonicalShortcutModsForSave(Set<String> mods) {
return <String>[
if (mods.contains('primary')) 'primary',
if (mods.contains('ctrl')) 'ctrl',
if (mods.contains('alt')) 'alt',
if (mods.contains('shift')) 'shift',
];
}
List<Map<String, dynamic>> shortcutBindingMapsFrom(dynamic rawBindings) {
if (rawBindings is! Iterable) return <Map<String, dynamic>>[];
final bindings = <Map<String, dynamic>>[];
for (final raw in rawBindings) {
if (raw is! Map) continue;
final binding = <String, dynamic>{};
for (final entry in raw.entries) {
final key = entry.key;
if (key is String) {
binding[key] = entry.value;
}
}
if (binding.isNotEmpty) {
bindings.add(binding);
}
}
return bindings;
}
Set<String> shortcutModSetFrom(dynamic rawMods) {
if (rawMods is! Iterable) return <String>{};
return rawMods.whereType<String>().toSet();
}
bool isSwitchTabShortcutAction(String? actionId) {
return actionId == kShortcutActionSwitchTabNext ||
actionId == kShortcutActionSwitchTabPrev;
}
/// Map a [LogicalKeyboardKey] to the canonical key name used in saved
/// bindings, or `null` for keys we don't accept as shortcuts.
///
/// Mirror of `event_to_key_name` in `src/keyboard/shortcuts.rs` and
/// `logicalToKeyName` in `flutter/web/js/src/shortcut_matcher.ts` — keep
/// the three in lockstep. Cross-language parity is enforced by:
/// * `flutter/test/fixtures/supported_shortcut_keys.json` — the
/// authoritative list of names this function must produce.
/// * Dart `supported keys` test in `keyboard_shortcuts_test.dart` —
/// asserts the (LogicalKeyboardKey → name) mapping covers the fixture.
/// * Rust `supported_keys_match_fixture` test in `shortcuts.rs` — the
/// Rust-side mirror against the same fixture.
/// A drift in any of the three breaks one of the two tests.
String? logicalKeyName(LogicalKeyboardKey k) {
// Singletons that map 1:1.
if (k == LogicalKeyboardKey.delete) return 'delete';
if (k == LogicalKeyboardKey.backspace) return 'backspace';
// Numpad Enter shares the "enter" name with the main Return key — matches
// the Rust matcher (`Return | KpReturn` → "enter") and matches user
// expectation that the two physical Enters are interchangeable.
if (k == LogicalKeyboardKey.enter || k == LogicalKeyboardKey.numpadEnter) {
return 'enter';
}
if (k == LogicalKeyboardKey.tab) return 'tab';
if (k == LogicalKeyboardKey.space) return 'space';
if (k == LogicalKeyboardKey.arrowLeft) return 'arrow_left';
if (k == LogicalKeyboardKey.arrowRight) return 'arrow_right';
if (k == LogicalKeyboardKey.arrowUp) return 'arrow_up';
if (k == LogicalKeyboardKey.arrowDown) return 'arrow_down';
if (k == LogicalKeyboardKey.home) return 'home';
if (k == LogicalKeyboardKey.end) return 'end';
if (k == LogicalKeyboardKey.pageUp) return 'page_up';
if (k == LogicalKeyboardKey.pageDown) return 'page_down';
if (k == LogicalKeyboardKey.insert) return 'insert';
// Letter / digit / F-key tables. `LogicalKeyboardKey` constants are
// `static final` (not `const`), so the maps can't be `const` — but they
// initialize once per process and the lookup is O(1).
final letters = <LogicalKeyboardKey, String>{
LogicalKeyboardKey.keyA: 'a', LogicalKeyboardKey.keyB: 'b',
LogicalKeyboardKey.keyC: 'c', LogicalKeyboardKey.keyD: 'd',
LogicalKeyboardKey.keyE: 'e', LogicalKeyboardKey.keyF: 'f',
LogicalKeyboardKey.keyG: 'g', LogicalKeyboardKey.keyH: 'h',
LogicalKeyboardKey.keyI: 'i', LogicalKeyboardKey.keyJ: 'j',
LogicalKeyboardKey.keyK: 'k', LogicalKeyboardKey.keyL: 'l',
LogicalKeyboardKey.keyM: 'm', LogicalKeyboardKey.keyN: 'n',
LogicalKeyboardKey.keyO: 'o', LogicalKeyboardKey.keyP: 'p',
LogicalKeyboardKey.keyQ: 'q', LogicalKeyboardKey.keyR: 'r',
LogicalKeyboardKey.keyS: 's', LogicalKeyboardKey.keyT: 't',
LogicalKeyboardKey.keyU: 'u', LogicalKeyboardKey.keyV: 'v',
LogicalKeyboardKey.keyW: 'w', LogicalKeyboardKey.keyX: 'x',
LogicalKeyboardKey.keyY: 'y', LogicalKeyboardKey.keyZ: 'z',
};
final letter = letters[k];
if (letter != null) return letter;
final digits = <LogicalKeyboardKey, String>{
LogicalKeyboardKey.digit0: 'digit0',
LogicalKeyboardKey.digit1: 'digit1',
LogicalKeyboardKey.digit2: 'digit2',
LogicalKeyboardKey.digit3: 'digit3',
LogicalKeyboardKey.digit4: 'digit4',
LogicalKeyboardKey.digit5: 'digit5',
LogicalKeyboardKey.digit6: 'digit6',
LogicalKeyboardKey.digit7: 'digit7',
LogicalKeyboardKey.digit8: 'digit8',
LogicalKeyboardKey.digit9: 'digit9',
};
final digit = digits[k];
if (digit != null) return digit;
final fkeys = <LogicalKeyboardKey, String>{
LogicalKeyboardKey.f1: 'f1', LogicalKeyboardKey.f2: 'f2',
LogicalKeyboardKey.f3: 'f3', LogicalKeyboardKey.f4: 'f4',
LogicalKeyboardKey.f5: 'f5', LogicalKeyboardKey.f6: 'f6',
LogicalKeyboardKey.f7: 'f7', LogicalKeyboardKey.f8: 'f8',
LogicalKeyboardKey.f9: 'f9', LogicalKeyboardKey.f10: 'f10',
LogicalKeyboardKey.f11: 'f11', LogicalKeyboardKey.f12: 'f12',
};
return fkeys[k];
}
/// Bundle of "is this shortcut available on the current platform" flags.
///
/// Production code reaches a single source of truth via
/// [ShortcutModel.currentPlatformCapabilities] (which encodes the per-runtime
/// rules in one place); tests construct one directly with whichever flags
/// they want to exercise. Two filter functions consume this:
/// [filterDefaultBindingsForPlatform] (for trimming default-binding JSON
/// before it hits LocalConfig) and [filterKeyboardShortcutActionGroupsForPlatform]
/// (for trimming the configuration UI's action list). Both must agree on the
/// same capability set, otherwise a default binding could be seeded for an
/// action the user has no UI to manage.
class ShortcutPlatformCapabilities {
final bool includeFullscreenShortcut;
final bool includeScreenshotShortcut;
final bool includeTabShortcuts;
final bool includeToolbarShortcut;
final bool includeCloseTabShortcut;
final bool includeSwitchSidesShortcut;
final bool includeRecordingShortcut;
final bool includeResetCanvasShortcut;
final bool includePinToolbarShortcut;
final bool includeViewModeShortcut;
final bool includeInputSourceShortcut;
final bool includeVoiceCallShortcut;
const ShortcutPlatformCapabilities({
required this.includeFullscreenShortcut,
required this.includeScreenshotShortcut,
required this.includeTabShortcuts,
required this.includeToolbarShortcut,
required this.includeCloseTabShortcut,
required this.includeSwitchSidesShortcut,
required this.includeRecordingShortcut,
required this.includeResetCanvasShortcut,
required this.includePinToolbarShortcut,
required this.includeViewModeShortcut,
required this.includeInputSourceShortcut,
required this.includeVoiceCallShortcut,
});
}
List<Map<String, dynamic>> filterDefaultBindingsForPlatform(
Iterable<dynamic> bindings,
ShortcutPlatformCapabilities cap,
) {
final filtered = <Map<String, dynamic>>[];
for (final binding in shortcutBindingMapsFrom(bindings)) {
final action = binding['action'] as String?;
if (!cap.includeFullscreenShortcut &&
action == kShortcutActionToggleFullscreen) {
continue;
}
if (!cap.includeScreenshotShortcut && action == kShortcutActionScreenshot) {
continue;
}
if (!cap.includeScreenshotShortcut &&
action == kShortcutActionToggleRelativeMouseMode) {
continue;
}
if (!cap.includeTabShortcuts && isSwitchTabShortcutAction(action)) {
continue;
}
if (!cap.includeToolbarShortcut &&
action == kShortcutActionToggleToolbar) {
continue;
}
if (!cap.includeCloseTabShortcut && action == kShortcutActionCloseTab) {
continue;
}
if (!cap.includeSwitchSidesShortcut &&
action == kShortcutActionSwitchSides) {
continue;
}
if (!cap.includeRecordingShortcut &&
action == kShortcutActionToggleRecording) {
continue;
}
if (!cap.includeResetCanvasShortcut &&
action == kShortcutActionResetCanvas) {
continue;
}
if (!cap.includePinToolbarShortcut && action == kShortcutActionPinToolbar) {
continue;
}
if (!cap.includeViewModeShortcut &&
(action == kShortcutActionViewModeOriginal ||
action == kShortcutActionViewModeAdaptive ||
action == kShortcutActionViewModeCustom)) {
continue;
}
if (!cap.includeInputSourceShortcut &&
action == kShortcutActionToggleInputSource) {
continue;
}
if (!cap.includeVoiceCallShortcut &&
action == kShortcutActionToggleVoiceCall) {
continue;
}
filtered.add(binding);
}
return filtered;
}

View File

@@ -11,71 +11,11 @@ import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart'; import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart';
import 'package:flutter_hbb/models/model.dart'; import 'package:flutter_hbb/models/model.dart';
import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/platform_model.dart';
import 'package:flutter_hbb/models/shortcut_model.dart';
import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
bool isEditOsPassword = false; bool isEditOsPassword = false;
/// Action IDs that `toolbarControls` is the sole registrar for. Wiped on
/// every call so stale closures don't outlive the menu entry that owned
/// them. Actions registered by `registerSessionShortcutActions` MUST NOT
/// appear here. `kShortcutActionToggleRecording` is platform-conditional
/// and handled separately in the unregister pass below.
const _kToolbarOwnedActionIds = <String>[
kShortcutActionSendCtrlAltDel,
kShortcutActionRestartRemote,
kShortcutActionInsertLock,
kShortcutActionToggleBlockInput,
kShortcutActionSwitchSides,
kShortcutActionRefresh,
kShortcutActionScreenshot,
kShortcutActionResetCanvas,
kShortcutActionSendClipboardKeystrokes,
];
const _kToolbarViewStyleActionIds = <String>[
kShortcutActionViewModeOriginal,
kShortcutActionViewModeAdaptive,
kShortcutActionViewModeCustom,
];
const _kToolbarImageQualityActionIds = <String>[
kShortcutActionImageQualityBest,
kShortcutActionImageQualityBalanced,
kShortcutActionImageQualityLow,
];
const _kToolbarCodecActionIds = <String>[
kShortcutActionCodecAuto,
kShortcutActionCodecVp8,
kShortcutActionCodecVp9,
kShortcutActionCodecAv1,
kShortcutActionCodecH264,
kShortcutActionCodecH265,
];
const _kToolbarCursorActionIds = <String>[
kShortcutActionToggleShowRemoteCursor,
kShortcutActionToggleFollowRemoteCursor,
kShortcutActionToggleFollowRemoteWindow,
kShortcutActionToggleZoomCursor,
];
const _kToolbarDisplayToggleActionIds = <String>[
kShortcutActionToggleQualityMonitor,
kShortcutActionToggleMute,
kShortcutActionToggleEnableFileCopyPaste,
kShortcutActionToggleDisableClipboard,
kShortcutActionToggleLockAfterSessionEnd,
kShortcutActionToggleTrueColor,
];
const _kToolbarKeyboardToggleActionIds = <String>[
kShortcutActionToggleSwapCtrlCmd,
kShortcutActionToggleSwapLeftRightMouse,
];
class TTextMenu { class TTextMenu {
final Widget child; final Widget child;
final VoidCallback? onPressed; final VoidCallback? onPressed;
@@ -109,73 +49,20 @@ class TRadioMenu<T> {
final T value; final T value;
final T groupValue; final T groupValue;
final ValueChanged<T?>? onChanged; final ValueChanged<T?>? onChanged;
final String? actionId;
TRadioMenu( TRadioMenu(
{required this.child, {required this.child,
required this.value, required this.value,
required this.groupValue, required this.groupValue,
required this.onChanged, required this.onChanged});
this.actionId});
} }
class TToggleMenu { class TToggleMenu {
final Widget child; final Widget child;
final bool value; final bool value;
final ValueChanged<bool?>? onChanged; final ValueChanged<bool?>? onChanged;
final String? actionId;
TToggleMenu( TToggleMenu(
{required this.child, {required this.child, required this.value, required this.onChanged});
required this.value,
required this.onChanged,
this.actionId});
}
/// Register each tagged entry's `onChanged` with the session [ShortcutModel].
/// Passthrough — returns [menus] so a caller can wrap `return [...]` directly.
List<TToggleMenu> _registerToggleMenuShortcuts(
FFI ffi,
List<TToggleMenu> menus, {
List<String> ownedActionIds = const [],
}) {
for (final actionId in ownedActionIds) {
ffi.shortcutModel.unregister(actionId);
}
for (final menu in menus) {
final actionId = menu.actionId;
if (actionId == null) continue;
final onChanged = menu.onChanged;
if (onChanged == null) {
ffi.shortcutModel.unregister(actionId);
} else {
final value = menu.value;
ffi.shortcutModel.register(actionId, () => onChanged(!value));
}
}
return menus;
}
/// Radio variant of [_registerToggleMenuShortcuts].
List<TRadioMenu<T>> _registerRadioMenuShortcuts<T>(
FFI ffi,
List<TRadioMenu<T>> menus, {
List<String> ownedActionIds = const [],
}) {
for (final actionId in ownedActionIds) {
ffi.shortcutModel.unregister(actionId);
}
for (final menu in menus) {
final actionId = menu.actionId;
if (actionId == null) continue;
final onChanged = menu.onChanged;
if (onChanged == null) {
ffi.shortcutModel.unregister(actionId);
} else {
final value = menu.value;
ffi.shortcutModel.register(actionId, () => onChanged(value));
}
}
return menus;
} }
handleOsPasswordEditIcon( handleOsPasswordEditIcon(
@@ -209,17 +96,6 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
final sessionId = ffi.sessionId; final sessionId = ffi.sessionId;
final isDefaultConn = ffi.connType == ConnType.defaultConn; final isDefaultConn = ffi.connType == ConnType.defaultConn;
// Wipe stale registrations from previous menu builds before re-registering
// below; runs unconditionally so mid-session enable works without reconnect.
for (final actionId in _kToolbarOwnedActionIds) {
ffi.shortcutModel.unregister(actionId);
}
// toggle_recording is mobile-only here; desktop's registration is owned by
// `registerSessionShortcutActions` and must not be touched.
if (!(isDesktop || isWeb)) {
ffi.shortcutModel.unregister(kShortcutActionToggleRecording);
}
List<TTextMenu> v = []; List<TTextMenu> v = [];
// elevation // elevation
if (isDefaultConn && if (isDefaultConn &&
@@ -273,15 +149,13 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
bind.sessionInputString( bind.sessionInputString(
sessionId: sessionId, value: data.text ?? ""); sessionId: sessionId, value: data.text ?? "");
} }
}, }));
actionId: kShortcutActionSendClipboardKeystrokes));
} }
// reset canvas // reset canvas
if (isDefaultConn && isMobile) { if (isDefaultConn && isMobile) {
v.add(TTextMenu( v.add(TTextMenu(
child: Text(translate('Reset canvas')), child: Text(translate('Reset canvas')),
onPressed: () => ffi.cursorModel.reset(), onPressed: () => ffi.cursorModel.reset()));
actionId: kShortcutActionResetCanvas));
} }
// https://github.com/rustdesk/rustdesk/pull/9731 // https://github.com/rustdesk/rustdesk/pull/9731
@@ -371,8 +245,7 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
TTextMenu( TTextMenu(
child: Text(translate('Restart remote device')), child: Text(translate('Restart remote device')),
onPressed: () => onPressed: () =>
showRestartRemoteDevice(pi, id, sessionId, ffi.dialogManager), showRestartRemoteDevice(pi, id, sessionId, ffi.dialogManager)),
actionId: kShortcutActionRestartRemote),
); );
} }
// insertLock // insertLock
@@ -460,14 +333,6 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
onPressed: ffi.ffiModel.timerScreenshot != null onPressed: ffi.ffiModel.timerScreenshot != null
? null ? null
: () { : () {
// Live cooldown check: the menu rebuilds onPressed=null
// whenever toolbarControls runs and finds timerScreenshot
// != null, but the keyboard-shortcut callback holds onto
// the originally-enabled closure across cooldown periods
// (toolbarControls only re-runs on menu open). Without
// this guard the second shortcut press during the 30s
// cooldown still fires sessionTakeScreenshot.
if (ffi.ffiModel.timerScreenshot != null) return;
if (pi.currentDisplay == kAllDisplayValue) { if (pi.currentDisplay == kAllDisplayValue) {
msgBox( msgBox(
sessionId, sessionId,
@@ -496,15 +361,11 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
onPressed: () => onCopyFingerprint(FingerprintState.find(id).value), onPressed: () => onCopyFingerprint(FingerprintState.find(id).value),
)); ));
} }
// Register tagged TTextMenu callbacks. The else-unregister is defense in // Register tagged callbacks with the shortcut model so global keyboard
// depth for actionIds tagged but missing from `_kToolbarOwnedActionIds`. // shortcuts can dispatch the same actions as the toolbar menu items.
for (final menu in v) { for (final menu in v) {
final actionId = menu.actionId; if (menu.actionId != null && menu.onPressed != null) {
if (actionId == null) continue; ffi.shortcutModel.register(menu.actionId!, menu.onPressed!);
if (menu.onPressed != null) {
ffi.shortcutModel.register(actionId, menu.onPressed!);
} else {
ffi.shortcutModel.unregister(actionId);
} }
} }
return v; return v;
@@ -521,26 +382,23 @@ Future<List<TRadioMenu<String>>> toolbarViewStyle(
.then((_) => ffi.canvasModel.updateViewStyle()); .then((_) => ffi.canvasModel.updateViewStyle());
} }
return _registerRadioMenuShortcuts(ffi, [ return [
TRadioMenu<String>( TRadioMenu<String>(
child: Text(translate('Scale original')), child: Text(translate('Scale original')),
value: kRemoteViewStyleOriginal, value: kRemoteViewStyleOriginal,
groupValue: groupValue, groupValue: groupValue,
onChanged: onChanged, onChanged: onChanged),
actionId: kShortcutActionViewModeOriginal),
TRadioMenu<String>( TRadioMenu<String>(
child: Text(translate('Scale adaptive')), child: Text(translate('Scale adaptive')),
value: kRemoteViewStyleAdaptive, value: kRemoteViewStyleAdaptive,
groupValue: groupValue, groupValue: groupValue,
onChanged: onChanged, onChanged: onChanged),
actionId: kShortcutActionViewModeAdaptive),
TRadioMenu<String>( TRadioMenu<String>(
child: Text(translate('Scale custom')), child: Text(translate('Scale custom')),
value: kRemoteViewStyleCustom, value: kRemoteViewStyleCustom,
groupValue: groupValue, groupValue: groupValue,
onChanged: onChanged, onChanged: onChanged)
actionId: kShortcutActionViewModeCustom) ];
], ownedActionIds: _kToolbarViewStyleActionIds);
} }
Future<List<TRadioMenu<String>>> toolbarImageQuality( Future<List<TRadioMenu<String>>> toolbarImageQuality(
@@ -552,25 +410,22 @@ Future<List<TRadioMenu<String>>> toolbarImageQuality(
await bind.sessionSetImageQuality(sessionId: ffi.sessionId, value: value); await bind.sessionSetImageQuality(sessionId: ffi.sessionId, value: value);
} }
return _registerRadioMenuShortcuts(ffi, [ return [
TRadioMenu<String>( TRadioMenu<String>(
child: Text(translate('Good image quality')), child: Text(translate('Good image quality')),
value: kRemoteImageQualityBest, value: kRemoteImageQualityBest,
groupValue: groupValue, groupValue: groupValue,
onChanged: onChanged, onChanged: onChanged),
actionId: kShortcutActionImageQualityBest),
TRadioMenu<String>( TRadioMenu<String>(
child: Text(translate('Balanced')), child: Text(translate('Balanced')),
value: kRemoteImageQualityBalanced, value: kRemoteImageQualityBalanced,
groupValue: groupValue, groupValue: groupValue,
onChanged: onChanged, onChanged: onChanged),
actionId: kShortcutActionImageQualityBalanced),
TRadioMenu<String>( TRadioMenu<String>(
child: Text(translate('Optimize reaction time')), child: Text(translate('Optimize reaction time')),
value: kRemoteImageQualityLow, value: kRemoteImageQualityLow,
groupValue: groupValue, groupValue: groupValue,
onChanged: onChanged, onChanged: onChanged),
actionId: kShortcutActionImageQualityLow),
TRadioMenu<String>( TRadioMenu<String>(
child: Text(translate('Custom')), child: Text(translate('Custom')),
value: kRemoteImageQualityCustom, value: kRemoteImageQualityCustom,
@@ -580,7 +435,7 @@ Future<List<TRadioMenu<String>>> toolbarImageQuality(
customImageQualityDialog(ffi.sessionId, id, ffi); customImageQualityDialog(ffi.sessionId, id, ffi);
}, },
), ),
], ownedActionIds: _kToolbarImageQualityActionIds); ];
} }
Future<List<TRadioMenu<String>>> toolbarCodec( Future<List<TRadioMenu<String>>> toolbarCodec(
@@ -607,10 +462,7 @@ Future<List<TRadioMenu<String>>> toolbarCodec(
} }
final visible = final visible =
codecs.length == 4 && (codecs[0] || codecs[1] || codecs[2] || codecs[3]); codecs.length == 4 && (codecs[0] || codecs[1] || codecs[2] || codecs[3]);
if (!visible) { if (!visible) return [];
return _registerRadioMenuShortcuts<String>(ffi, [],
ownedActionIds: _kToolbarCodecActionIds);
}
onChanged(String? value) async { onChanged(String? value) async {
if (value == null) return; if (value == null) return;
await bind.sessionPeerOption( await bind.sessionPeerOption(
@@ -618,14 +470,12 @@ Future<List<TRadioMenu<String>>> toolbarCodec(
bind.sessionChangePreferCodec(sessionId: sessionId); bind.sessionChangePreferCodec(sessionId: sessionId);
} }
TRadioMenu<String> radio( TRadioMenu<String> radio(String label, String value, bool enabled) {
String label, String value, bool enabled, String actionId) {
return TRadioMenu<String>( return TRadioMenu<String>(
child: Text(label), child: Text(label),
value: value, value: value,
groupValue: groupValue, groupValue: groupValue,
onChanged: enabled ? onChanged : null, onChanged: enabled ? onChanged : null);
actionId: actionId);
} }
var autoLabel = translate('Auto'); var autoLabel = translate('Auto');
@@ -633,14 +483,14 @@ Future<List<TRadioMenu<String>>> toolbarCodec(
ffi.qualityMonitorModel.data.codecFormat != null) { ffi.qualityMonitorModel.data.codecFormat != null) {
autoLabel = '$autoLabel (${ffi.qualityMonitorModel.data.codecFormat})'; autoLabel = '$autoLabel (${ffi.qualityMonitorModel.data.codecFormat})';
} }
return _registerRadioMenuShortcuts(ffi, [ return [
radio(autoLabel, 'auto', true, kShortcutActionCodecAuto), radio(autoLabel, 'auto', true),
if (codecs[0]) radio('VP8', 'vp8', codecs[0], kShortcutActionCodecVp8), if (codecs[0]) radio('VP8', 'vp8', codecs[0]),
radio('VP9', 'vp9', true, kShortcutActionCodecVp9), radio('VP9', 'vp9', true),
if (codecs[1]) radio('AV1', 'av1', codecs[1], kShortcutActionCodecAv1), if (codecs[1]) radio('AV1', 'av1', codecs[1]),
if (codecs[2]) radio('H264', 'h264', codecs[2], kShortcutActionCodecH264), if (codecs[2]) radio('H264', 'h264', codecs[2]),
if (codecs[3]) radio('H265', 'h265', codecs[3], kShortcutActionCodecH265), if (codecs[3]) radio('H265', 'h265', codecs[3]),
], ownedActionIds: _kToolbarCodecActionIds); ];
} }
Future<List<TToggleMenu>> toolbarCursor( Future<List<TToggleMenu>> toolbarCursor(
@@ -665,7 +515,6 @@ Future<List<TToggleMenu>> toolbarCursor(
v.add(TToggleMenu( v.add(TToggleMenu(
child: Text(translate('Show remote cursor')), child: Text(translate('Show remote cursor')),
value: state.value, value: state.value,
actionId: kShortcutActionToggleShowRemoteCursor,
onChanged: enabled && !lockState.value onChanged: enabled && !lockState.value
? (value) async { ? (value) async {
if (value == null) return; if (value == null) return;
@@ -702,7 +551,6 @@ Future<List<TToggleMenu>> toolbarCursor(
v.add(TToggleMenu( v.add(TToggleMenu(
child: Text(translate('Follow remote cursor')), child: Text(translate('Follow remote cursor')),
value: value, value: value,
actionId: kShortcutActionToggleFollowRemoteCursor,
onChanged: (value) async { onChanged: (value) async {
if (value == null) return; if (value == null) return;
await bind.sessionToggleOption(sessionId: sessionId, value: option); await bind.sessionToggleOption(sessionId: sessionId, value: option);
@@ -731,7 +579,6 @@ Future<List<TToggleMenu>> toolbarCursor(
v.add(TToggleMenu( v.add(TToggleMenu(
child: Text(translate('Follow remote window focus')), child: Text(translate('Follow remote window focus')),
value: value, value: value,
actionId: kShortcutActionToggleFollowRemoteWindow,
onChanged: (value) async { onChanged: (value) async {
if (value == null) return; if (value == null) return;
await bind.sessionToggleOption(sessionId: sessionId, value: option); await bind.sessionToggleOption(sessionId: sessionId, value: option);
@@ -749,7 +596,6 @@ Future<List<TToggleMenu>> toolbarCursor(
v.add(TToggleMenu( v.add(TToggleMenu(
child: Text(translate('Zoom cursor')), child: Text(translate('Zoom cursor')),
value: peerState.value, value: peerState.value,
actionId: kShortcutActionToggleZoomCursor,
onChanged: (value) async { onChanged: (value) async {
if (value == null) return; if (value == null) return;
await bind.sessionToggleOption(sessionId: sessionId, value: option); await bind.sessionToggleOption(sessionId: sessionId, value: option);
@@ -758,8 +604,7 @@ Future<List<TToggleMenu>> toolbarCursor(
}, },
)); ));
} }
return _registerToggleMenuShortcuts(ffi, v, return v;
ownedActionIds: _kToolbarCursorActionIds);
} }
Future<List<TToggleMenu>> toolbarDisplayToggle( Future<List<TToggleMenu>> toolbarDisplayToggle(
@@ -775,7 +620,6 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
final option = 'show-quality-monitor'; final option = 'show-quality-monitor';
v.add(TToggleMenu( v.add(TToggleMenu(
value: bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option), value: bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option),
actionId: kShortcutActionToggleQualityMonitor,
onChanged: (value) async { onChanged: (value) async {
if (value == null) return; if (value == null) return;
await bind.sessionToggleOption(sessionId: sessionId, value: option); await bind.sessionToggleOption(sessionId: sessionId, value: option);
@@ -789,7 +633,6 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option); bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option);
v.add(TToggleMenu( v.add(TToggleMenu(
value: value, value: value,
actionId: kShortcutActionToggleMute,
onChanged: (value) { onChanged: (value) {
if (value == null) return; if (value == null) return;
bind.sessionToggleOption(sessionId: sessionId, value: option); bind.sessionToggleOption(sessionId: sessionId, value: option);
@@ -814,7 +657,6 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
sessionId: sessionId, arg: kOptionEnableFileCopyPaste); sessionId: sessionId, arg: kOptionEnableFileCopyPaste);
v.add(TToggleMenu( v.add(TToggleMenu(
value: value, value: value,
actionId: kShortcutActionToggleEnableFileCopyPaste,
onChanged: enabled onChanged: enabled
? (value) { ? (value) {
if (value == null) return; if (value == null) return;
@@ -833,7 +675,6 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
if (ffiModel.viewOnly) value = true; if (ffiModel.viewOnly) value = true;
v.add(TToggleMenu( v.add(TToggleMenu(
value: value, value: value,
actionId: kShortcutActionToggleDisableClipboard,
onChanged: enabled onChanged: enabled
? (value) { ? (value) {
if (value == null) return; if (value == null) return;
@@ -850,7 +691,6 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option); bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option);
v.add(TToggleMenu( v.add(TToggleMenu(
value: value, value: value,
actionId: kShortcutActionToggleLockAfterSessionEnd,
onChanged: enabled onChanged: enabled
? (value) { ? (value) {
if (value == null) return; if (value == null) return;
@@ -901,7 +741,6 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option); bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option);
v.add(TToggleMenu( v.add(TToggleMenu(
value: value, value: value,
actionId: kShortcutActionToggleTrueColor,
onChanged: (value) async { onChanged: (value) async {
if (value == null) return; if (value == null) return;
await bind.sessionToggleOption(sessionId: sessionId, value: option); await bind.sessionToggleOption(sessionId: sessionId, value: option);
@@ -926,8 +765,7 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
}, },
child: Text(translate('View Mode')))); child: Text(translate('View Mode'))));
} }
return _registerToggleMenuShortcuts(ffi, v, return v;
ownedActionIds: _kToolbarDisplayToggleActionIds);
} }
var togglePrivacyModeTime = DateTime.now().subtract(const Duration(hours: 1)); var togglePrivacyModeTime = DateTime.now().subtract(const Duration(hours: 1));
@@ -1026,7 +864,6 @@ List<TToggleMenu> toolbarKeyboardToggles(FFI ffi) {
final enabled = !ffi.ffiModel.viewOnly; final enabled = !ffi.ffiModel.viewOnly;
v.add(TToggleMenu( v.add(TToggleMenu(
value: value, value: value,
actionId: kShortcutActionToggleSwapCtrlCmd,
onChanged: enabled ? onChanged : null, onChanged: enabled ? onChanged : null,
child: Text(translate('Swap control-command key')))); child: Text(translate('Swap control-command key'))));
} }
@@ -1092,27 +929,10 @@ List<TToggleMenu> toolbarKeyboardToggles(FFI ffi) {
final enabled = !ffi.ffiModel.viewOnly; final enabled = !ffi.ffiModel.viewOnly;
v.add(TToggleMenu( v.add(TToggleMenu(
value: value, value: value,
actionId: kShortcutActionToggleSwapLeftRightMouse,
onChanged: enabled ? onChanged : null, onChanged: enabled ? onChanged : null,
child: Text(translate('swap-left-right-mouse')))); child: Text(translate('swap-left-right-mouse'))));
} }
return _registerToggleMenuShortcuts(ffi, v, return v;
ownedActionIds: _kToolbarKeyboardToggleActionIds);
}
/// Drive each toolbar helper for its registration side effect, so a shortcut
/// fires from the first keystroke without needing the user to open the
/// matching submenu. Mobile gets `toolbarKeyboardToggles` via
/// `toolbarDisplayToggle`'s `isMobile` branch — calling it explicitly there
/// would double-register.
void registerToolbarShortcuts(BuildContext context, String id, FFI ffi) {
if (isDesktop) toolbarKeyboardToggles(ffi);
unawaited(toolbarCursor(context, id, ffi));
unawaited(toolbarDisplayToggle(context, id, ffi));
unawaited(toolbarViewStyle(context, id, ffi));
unawaited(toolbarImageQuality(context, id, ffi));
unawaited(toolbarCodec(context, id, ffi));
toolbarPrivacyMode(PrivacyModeState.find(id), context, id, ffi);
} }
bool showVirtualDisplayMenu(FFI ffi) { bool showVirtualDisplayMenu(FFI ffi) {

View File

@@ -4,8 +4,6 @@ import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/models/state_model.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
export 'common/widgets/keyboard_shortcuts/shortcut_constants.dart';
const int kMaxVirtualDisplayCount = 4; const int kMaxVirtualDisplayCount = 4;
const int kAllVirtualDisplay = -1; const int kAllVirtualDisplay = -1;
@@ -688,3 +686,24 @@ extension WindowsTargetExt on int {
} }
const kCheckSoftwareUpdateFinish = 'check_software_update_finish'; const kCheckSoftwareUpdateFinish = 'check_software_update_finish';
// Keyboard shortcut Action IDs - must match src/keyboard/shortcuts.rs::action_id.
const kShortcutActionSendCtrlAltDel = 'send_ctrl_alt_del';
const kShortcutActionToggleFullscreen = 'toggle_fullscreen';
const kShortcutActionSwitchDisplayNext = 'switch_display_next';
const kShortcutActionSwitchDisplayPrev = 'switch_display_prev';
const kShortcutActionScreenshot = 'screenshot';
const kShortcutActionInsertLock = 'insert_lock';
const kShortcutActionRefresh = 'refresh';
const kShortcutActionToggleAudio = 'toggle_audio';
const kShortcutActionToggleBlockInput = 'toggle_block_input';
const kShortcutActionToggleRecording = 'toggle_recording';
const kShortcutActionTogglePrivacyMode = 'toggle_privacy_mode';
const kShortcutActionViewMode1to1 = 'view_mode_1_to_1';
const kShortcutActionViewModeShrink = 'view_mode_shrink';
const kShortcutActionViewModeStretch = 'view_mode_stretch';
const kShortcutActionSwitchSides = 'switch_sides';
String kShortcutActionSwitchTab(int n) => 'switch_tab_$n';
const kShortcutLocalConfigKey = 'keyboard-shortcuts';
const kShortcutEventName = 'shortcut_triggered';

View File

@@ -37,14 +37,11 @@ class _DesktopKeyboardShortcutsPageState
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final foregroundColor =
AppBarTheme.of(context).titleTextStyle?.color ?? Colors.white;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(translate('Keyboard Shortcuts')), title: Text(translate('Keyboard Shortcuts')),
actions: [ actions: [
TextButton.icon( TextButton.icon(
style: TextButton.styleFrom(foregroundColor: foregroundColor),
onPressed: () => onPressed: () =>
_bodyKey.currentState?.resetToDefaultsWithConfirm(), _bodyKey.currentState?.resetToDefaultsWithConfirm(),
icon: const Icon(Icons.restore), icon: const Icon(Icons.restore),
@@ -55,11 +52,6 @@ class _DesktopKeyboardShortcutsPageState
body: KeyboardShortcutsPageBody( body: KeyboardShortcutsPageBody(
key: _bodyKey, key: _bodyKey,
compact: true, compact: true,
// Desktop's General settings tab already exposes the Enable +
// Pass-through checkboxes (it's the only entry point to this page),
// so we hide the duplicates here. Mobile shells keep the default
// (true) because their entry tile doesn't carry the toggles.
showMasterToggles: false,
), ),
); );
} }

View File

@@ -10,7 +10,6 @@ import 'package:flutter_hbb/common/widgets/audio_input.dart';
import 'package:flutter_hbb/common/widgets/setting_widgets.dart'; import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart'; import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart';
import 'package:flutter_hbb/common/widgets/keyboard_shortcuts/page_body.dart';
import 'package:flutter_hbb/desktop/pages/desktop_keyboard_shortcuts_page.dart'; import 'package:flutter_hbb/desktop/pages/desktop_keyboard_shortcuts_page.dart';
import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart'; import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart';
import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart'; import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart';
@@ -431,41 +430,48 @@ class _GeneralState extends State<_General> {
} }
Widget keyboardShortcuts() { Widget keyboardShortcuts() {
// The bindings JSON (LocalConfig key `keyboard-shortcuts`) holds three // The bindings JSON (LocalConfig key `keyboard-shortcuts`) is the single
// flags + the bindings list: {enabled, pass_through, bindings}. When the // source of truth — it embeds an `enabled` boolean alongside the bindings
// master is off, the pass-through toggle and the Configure entry are // list. We mutate the JSON in place via _OptionCheckBox's optGetter /
// hidden — both are meaningless without an active matcher. // optSetter hooks rather than introducing a parallel boolean key, so the
return StatefulBuilder(builder: (context, setLocalState) { // Rust matcher and the Web matcher both read the same flag without drift.
final enabled = ShortcutModel.isEnabled(); return _Card(title: 'Keyboard Shortcuts', children: [
return _Card(title: 'Keyboard Shortcuts', children: [ _OptionCheckBox(
_OptionCheckBox( context,
context, 'Enable keyboard shortcuts in remote session',
'Enable keyboard shortcuts in remote session', kShortcutLocalConfigKey,
kShortcutLocalConfigKey, isServer: false,
isServer: false, optGetter: ShortcutModel.isEnabled,
optGetter: ShortcutModel.isEnabled, optSetter: (k, v) async {
optSetter: (_, v) async { final raw = bind.mainGetLocalOption(key: k);
await ShortcutModel.setEnabled(v); Map<String, dynamic> parsed = {};
setLocalState(() {}); if (raw.isNotEmpty) {
}, try {
), parsed = jsonDecode(raw) as Map<String, dynamic>;
if (enabled) ...[ } catch (_) {
_OptionCheckBox( parsed = {};
context, }
'Pass-through to remote', }
kShortcutLocalConfigKey, parsed['enabled'] = v;
isServer: false, parsed['bindings'] ??= <dynamic>[];
optGetter: ShortcutModel.isPassThrough, // Seed defaults the first time the user enables shortcuts so the
optSetter: (_, v) async { // common combos (Ctrl+Alt+Shift+Enter for fullscreen, etc.) work
await ShortcutModel.setPassThrough(v); // out of the box. Mirrors the same logic on the dedicated config
setLocalState(() {}); // page.
}, final list = (parsed['bindings'] as List?) ?? const [];
trailing: const InfoTooltipIcon(tipKey: 'shortcut-passthrough-tip'), if (v && list.isEmpty) {
), parsed['bindings'] =
_ShortcutsConfigureRow(), jsonDecode(bind.mainGetDefaultKeyboardShortcuts());
], }
]); await bind.mainSetLocalOption(key: k, value: jsonEncode(parsed));
}); // Refresh the matcher cache so the new flag / bindings take effect
// immediately. On native this hits the Rust matcher; on Web the
// bridge forwards to the JS-side matcher in flutter/web/js/.
bind.mainReloadKeyboardShortcuts();
},
),
_ShortcutsConfigureRow(),
]);
} }
Widget theme() { Widget theme() {
@@ -2534,8 +2540,6 @@ Widget _OptionCheckBox(
bool isServer = true, bool isServer = true,
bool Function()? optGetter, bool Function()? optGetter,
Future<void> Function(String, bool)? optSetter, Future<void> Function(String, bool)? optSetter,
// Optional widget rendered between the label and the trailing space.
Widget? trailing,
}) { }) {
getOpt() => optGetter != null getOpt() => optGetter != null
? optGetter() ? optGetter()
@@ -2579,23 +2583,11 @@ Widget _OptionCheckBox(
offstage: !ref.value || checkedIcon == null, offstage: !ref.value || checkedIcon == null,
child: checkedIcon?.marginOnly(right: 5), child: checkedIcon?.marginOnly(right: 5),
), ),
// Without `trailing`, keep the original Expanded(Text) layout. Expanded(
if (trailing == null)
Expanded(
child: Text(
translate(label),
style: TextStyle(color: disabledTextColor(context, enabled)),
))
else ...[
Flexible(
child: Text( child: Text(
translate(label), translate(label),
style: TextStyle(color: disabledTextColor(context, enabled)), style: TextStyle(color: disabledTextColor(context, enabled)),
), ))
),
trailing,
const Spacer(),
],
], ],
), ),
).marginOnly(left: _kCheckBoxLeftMargin), ).marginOnly(left: _kCheckBoxLeftMargin),

View File

@@ -134,10 +134,11 @@ class _RemotePageState extends State<RemotePage>
// what we want here. // what we want here.
if (mounted) { if (mounted) {
toolbarControls(context, widget.id, _ffi); toolbarControls(context, widget.id, _ffi);
// Register the default-bound actions that `toolbarControls` doesn't
// own (fullscreen, switch display, switch tab). Done in addition,
// not instead of, the toolbar registration above.
registerSessionShortcutActions(_ffi, registerSessionShortcutActions(_ffi,
tabController: widget.tabController, tabController: widget.tabController);
toolbarState: widget.toolbarState);
registerToolbarShortcuts(context, widget.id, _ffi);
} }
}); });
_ffi.canvasModel.initializeEdgeScrollFallback(this); _ffi.canvasModel.initializeEdgeScrollFallback(this);

View File

@@ -611,9 +611,8 @@ class _MonitorMenu extends StatelessWidget {
tooltip: isMulti tooltip: isMulti
? '' ? ''
: isAllMonitors : isAllMonitors
? translate('All monitors') ? 'all monitors'
: translate('Monitor #{}') : '#${i + 1} monitor',
.replaceAll('{}', '${i + 1}'),
hMargin: isMulti ? null : 6, hMargin: isMulti ? null : 6,
vMargin: isMulti ? null : 12, vMargin: isMulti ? null : 12,
topLevel: false, topLevel: false,
@@ -774,20 +773,16 @@ class _ControlMenu extends StatelessWidget {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Flexible(child: e.child), Flexible(child: e.child),
Flexible( Padding(
child: Padding( padding: const EdgeInsets.only(left: 16),
padding: const EdgeInsets.only(left: 16), child: Text(
child: Text( hint,
hint, style: Theme.of(context)
maxLines: 1, .textTheme
overflow: TextOverflow.ellipsis, .bodySmall
style: Theme.of(context) ?.copyWith(
.textTheme color: Theme.of(context).hintColor,
.bodySmall ),
?.copyWith(
color: Theme.of(context).hintColor,
),
),
), ),
), ),
], ],

View File

@@ -127,10 +127,10 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
// what we want here. // what we want here.
if (mounted) { if (mounted) {
toolbarControls(context, widget.id, gFFI); toolbarControls(context, widget.id, gFFI);
// Mobile has no DesktopTabController, so tab-switch shortcuts will // Mobile has no DesktopTabController, so tab-switch shortcuts
// log a no-handler debug line if a user binds one. // remain unregistered (they will simply log a no-handler debug
// line if a mobile user binds one — they have no tabs to switch).
registerSessionShortcutActions(gFFI); registerSessionShortcutActions(gFFI);
registerToolbarShortcuts(context, widget.id, gFFI);
} }
}); });
WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance.addObserver(this);

View File

@@ -821,24 +821,22 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
showThemeSettings(gFFI.dialogManager); showThemeSettings(gFFI.dialogManager);
}, },
), ),
if (!disabledSettings) SettingsTile.navigation(
SettingsTile.navigation( leading: Icon(Icons.keyboard_outlined),
leading: Icon(Icons.keyboard_outlined), title: Text(translate('Keyboard Shortcuts')),
title: Text(translate('Keyboard Shortcuts')), description: Text(ShortcutModel.isEnabled()
description: Text(ShortcutModel.isEnabled() ? translate('On')
? translate('On') : translate('Off')),
: translate('Off')), onPressed: (context) {
trailing: Icon(Icons.arrow_forward_ios), Navigator.push(
onPressed: (context) { context,
Navigator.push( MaterialPageRoute(
context, builder: (_) => const MobileKeyboardShortcutsPage(),
MaterialPageRoute( )).then((_) {
builder: (_) => const MobileKeyboardShortcutsPage(), if (mounted) setState(() {});
)).then((_) { });
if (mounted) setState(() {}); },
}); ),
},
),
if (!bind.isDisableAccount()) if (!bind.isDisableAccount())
SettingsTile.switchTile( SettingsTile.switchTile(
title: Text(translate('note-at-conn-end-tip')), title: Text(translate('note-at-conn-end-tip')),
@@ -1372,3 +1370,4 @@ SettingsTile _getPopupDialogRadioEntry({
), ),
); );
} }

View File

@@ -15,9 +15,7 @@ import 'package:get/get.dart';
import '../../models/model.dart'; import '../../models/model.dart';
import '../../models/platform_model.dart'; import '../../models/platform_model.dart';
import '../../models/shortcut_model.dart';
import '../../models/state_model.dart'; import '../../models/state_model.dart';
import '../common/widgets/keyboard_shortcuts/shortcut_utils.dart';
import 'input_modifier_utils.dart'; import 'input_modifier_utils.dart';
import 'relative_mouse_model.dart'; import 'relative_mouse_model.dart';
import '../common.dart'; import '../common.dart';
@@ -348,7 +346,7 @@ class InputModel {
/// which runs per-engine, so each isolate registers its own handler tied /// which runs per-engine, so each isolate registers its own handler tied
/// to its own set of InputModels. /// to its own set of InputModels.
static void initSideButtonChannel() { static void initSideButtonChannel() {
if (!isLinux) return; if (!Platform.isLinux) return;
if (_sideButtonChannelInitialized) return; if (_sideButtonChannelInitialized) return;
_sideButtonChannelInitialized = true; _sideButtonChannelInitialized = true;
@@ -701,6 +699,7 @@ class InputModel {
} }
} }
<<<<<<< HEAD
// Safe: this only re-dispatches synthesized Shift key-up events. // Safe: this only re-dispatches synthesized Shift key-up events.
// The key-up path clears the tracked Shift state so this does not loop. // The key-up path clears the tracked Shift state so this does not loop.
void _releaseTrackedShiftKeyEventIfNeeded() { void _releaseTrackedShiftKeyEventIfNeeded() {
@@ -828,9 +827,7 @@ class InputModel {
return KeyEventResult.ignored; return KeyEventResult.ignored;
} }
} }
if (_tryDispatchWebFlutterShortcut(e)) {
return KeyEventResult.handled;
}
if (isWindows || isLinux) { if (isWindows || isLinux) {
// Ignore meta keys. Because flutter window will loose focus if meta key is pressed. // Ignore meta keys. Because flutter window will loose focus if meta key is pressed.
if (e.physicalKey == PhysicalKeyboardKey.metaLeft || if (e.physicalKey == PhysicalKeyboardKey.metaLeft ||
@@ -925,53 +922,6 @@ class InputModel {
return KeyEventResult.handled; return KeyEventResult.handled;
} }
bool _tryDispatchWebFlutterShortcut(KeyEvent e) {
if (!isWeb || !isInputSourceFlutter) return false;
if (e is! KeyDownEvent && e is! KeyRepeatEvent) return false;
if (!ShortcutModel.isEnabled() || ShortcutModel.isPassThrough()) {
return false;
}
final keyName = logicalKeyName(e.logicalKey);
if (keyName == null) return false;
final mods = canonicalShortcutModsForSave(_webFlutterShortcutMods());
final action = _matchWebFlutterShortcut(keyName, mods);
if (action == null) return false;
if (e is KeyDownEvent) {
parent.target?.shortcutModel.onTriggered(action);
}
return true;
}
Set<String> _webFlutterShortcutMods() {
final keyboard = HardwareKeyboard.instance;
final mods = <String>{};
if (isMacOS || isIOS || isWebOnMacOs) {
if (keyboard.isMetaPressed) mods.add('primary');
if (keyboard.isControlPressed) mods.add('ctrl');
} else if (keyboard.isControlPressed) {
mods.add('primary');
}
if (keyboard.isAltPressed) mods.add('alt');
if (keyboard.isShiftPressed) mods.add('shift');
return mods;
}
String? _matchWebFlutterShortcut(String keyName, List<String> mods) {
for (final binding in ShortcutModel.readBindings()) {
final action = binding['action'];
final key = binding['key'];
final bindingMods =
canonicalShortcutModsForSave(shortcutModSetFrom(binding['mods']));
if (action is String &&
key == keyName &&
bindingMods.isNotEmpty &&
listEquals(bindingMods, mods)) {
return action;
}
}
return null;
}
/// Send Key Event /// Send Key Event
void newKeyboardMode( void newKeyboardMode(
String character, int usbHid, bool down, bool iosCapsLock) { String character, int usbHid, bool down, bool iosCapsLock) {

View File

@@ -3933,7 +3933,6 @@ class FFI {
ffiModel.pi.currentDisplay); ffiModel.pi.currentDisplay);
} }
imageModel.callbacksOnFirstImage.clear(); imageModel.callbacksOnFirstImage.clear();
shortcutModel.clear();
await imageModel.update(null); await imageModel.update(null);
cursorModel.clear(); cursorModel.clear();
ffiModel.clear(); ffiModel.clear();

View File

@@ -1,23 +1,14 @@
import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import '../common.dart'; import '../common.dart';
import '../common/shared_state.dart' show PrivacyModeState;
import '../common/widgets/dialog.dart'
show desktopTryShowTabAuditDialogCloseCancelled;
import '../common/widgets/keyboard_shortcuts/shortcut_utils.dart';
import '../consts.dart'; import '../consts.dart';
import '../desktop/widgets/remote_toolbar.dart' show ToolbarState;
import 'chat_model.dart' show VoiceCallStatus;
import '../desktop/widgets/tabbar_widget.dart' show DesktopTabController; import '../desktop/widgets/tabbar_widget.dart' show DesktopTabController;
import '../models/model.dart'; import '../models/model.dart';
import '../models/platform_model.dart'; import '../models/platform_model.dart';
import '../models/state_model.dart'; import '../models/state_model.dart';
typedef ShortcutCallback = FutureOr<void> Function();
/// Per-session shortcut dispatcher. Attached to FFI when a session is created. /// Per-session shortcut dispatcher. Attached to FFI when a session is created.
/// ///
/// The Rust matcher (src/keyboard/shortcuts.rs) emits `shortcut_triggered` /// The Rust matcher (src/keyboard/shortcuts.rs) emits `shortcut_triggered`
@@ -26,49 +17,27 @@ typedef ShortcutCallback = FutureOr<void> Function();
/// via [onTriggered], which runs whatever callback the toolbar / menu /// via [onTriggered], which runs whatever callback the toolbar / menu
/// builders previously registered for that action id. /// builders previously registered for that action id.
class ShortcutModel { class ShortcutModel {
static WeakReference<ShortcutModel>? _activeWebModel;
final WeakReference<FFI> parent; final WeakReference<FFI> parent;
final Map<String, ShortcutCallback> _callbacks = {}; final Map<String, VoidCallback> _callbacks = {};
ShortcutModel(this.parent); ShortcutModel(this.parent);
/// Called by toolbar / menu builders to register what to do when the /// Called by toolbar / menu builders to register what to do when the
/// matched shortcut fires. /// matched shortcut fires.
void register(String actionId, ShortcutCallback callback) { void register(String actionId, VoidCallback callback) {
_callbacks[actionId] = callback; _callbacks[actionId] = callback;
_activeWebModel = WeakReference(this);
} }
void unregister(String actionId) { void unregister(String actionId) {
_callbacks.remove(actionId); _callbacks.remove(actionId);
} }
void clear() {
_callbacks.clear();
if (identical(_activeWebModel?.target, this)) {
_activeWebModel = null;
}
}
static void onWebTriggered(String actionId) {
final model = _activeWebModel?.target;
if (model != null) {
model.onTriggered(actionId);
} else {
debugPrint('shortcut_triggered: no active web shortcut model');
}
}
/// Called by the session event listener when a `shortcut_triggered` event /// Called by the session event listener when a `shortcut_triggered` event
/// arrives for this session. /// arrives for this session.
void onTriggered(String actionId) { void onTriggered(String actionId) {
final cb = _callbacks[actionId]; final cb = _callbacks[actionId];
if (cb != null) { if (cb != null) {
unawaited(Future.sync(cb).catchError((e, st) { cb();
debugPrint(
'shortcut_triggered: handler failed for $actionId: $e\n$st');
}));
} else { } else {
debugPrint('shortcut_triggered: no handler for $actionId'); debugPrint('shortcut_triggered: no handler for $actionId');
} }
@@ -80,7 +49,8 @@ class ShortcutModel {
if (raw.isEmpty) return []; if (raw.isEmpty) return [];
try { try {
final parsed = jsonDecode(raw) as Map<String, dynamic>; final parsed = jsonDecode(raw) as Map<String, dynamic>;
return shortcutBindingMapsFrom(parsed['bindings']); final list = (parsed['bindings'] as List?) ?? [];
return list.cast<Map<String, dynamic>>();
} catch (_) { } catch (_) {
return []; return [];
} }
@@ -96,111 +66,6 @@ class ShortcutModel {
return false; return false;
} }
} }
static bool isPassThrough() {
final raw = bind.mainGetLocalOption(key: kShortcutLocalConfigKey);
if (raw.isEmpty) return false;
try {
final parsed = jsonDecode(raw) as Map<String, dynamic>;
return parsed['pass_through'] == true;
} catch (_) {
return false;
}
}
/// Persistent companion to [isEnabled]: when on, the matchers return early
/// and every keystroke flows through to the remote (i.e. all bindings are
/// suspended). Stored in the same JSON blob so a single reload refreshes
/// both flags on every active matcher.
static Future<void> setPassThrough(bool v) async {
final raw = bind.mainGetLocalOption(key: kShortcutLocalConfigKey);
Map<String, dynamic> json = {};
if (raw.isNotEmpty) {
try {
json = jsonDecode(raw) as Map<String, dynamic>;
} catch (_) {
json = {};
}
}
json['pass_through'] = v;
await bind.mainSetLocalOption(
key: kShortcutLocalConfigKey, value: jsonEncode(json));
bind.mainReloadKeyboardShortcuts();
}
/// Flip the master `enabled` flag and persist. On the first enable we seed
/// the default bindings so common combos work out of the box; otherwise we
/// preserve whatever the user already has. Refreshes the matcher cache so
/// the change takes effect immediately (Rust on native, JS via the bridge
/// on Web).
static Future<void> setEnabled(bool v) async {
final raw = bind.mainGetLocalOption(key: kShortcutLocalConfigKey);
Map<String, dynamic> json = {};
if (raw.isNotEmpty) {
try {
json = jsonDecode(raw) as Map<String, dynamic>;
} catch (_) {
json = {};
}
}
json['enabled'] = v;
final list = shortcutBindingMapsFrom(json['bindings']);
if (v && list.isEmpty) {
json['bindings'] = filterDefaultBindingsForPlatform(
jsonDecode(bind.mainGetDefaultKeyboardShortcuts()) as List,
currentPlatformCapabilities(),
);
} else {
json['bindings'] = list;
}
await bind.mainSetLocalOption(
key: kShortcutLocalConfigKey, value: jsonEncode(json));
bind.mainReloadKeyboardShortcuts();
}
/// Single source of truth for the per-platform "is this shortcut applicable"
/// decisions. Both [setEnabled]'s default-seeding pass and the configuration
/// page's reset / list-rendering paths read from here, so the seed list and
/// the visible action list can never disagree on which platform a given
/// action belongs to.
///
/// Capability rationale:
/// * Fullscreen / Toolbar / Pin / View Mode: rendered wherever the
/// desktop layout applies (native desktop + Web). Native mobile is
/// permanently full-screen and doesn't have a desktop-style toolbar.
/// * Screenshot / Switch Sides: native desktop only. The Web bridge
/// throws UnimplementedError for `sessionTakeScreenshot`; mobile
/// toolbars don't surface either action.
/// * Tab navigation / Close Tab: only native desktop ships
/// `DesktopTabController`; Web's `RemotePage` is invoked without one.
/// * Recording: native desktop has the `_RecordMenu` widget +
/// `registerSessionShortcutActions` registration; native Android has
/// the `toolbarControls` entry; iOS short-circuits inside
/// `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() {
final desktopLayout = isDesktop || isWeb;
return ShortcutPlatformCapabilities(
includeFullscreenShortcut: desktopLayout,
includeScreenshotShortcut: isDesktop,
includeTabShortcuts: isDesktop,
includeToolbarShortcut: desktopLayout,
includeCloseTabShortcut: isDesktop,
includeSwitchSidesShortcut: isDesktop,
includeRecordingShortcut: !isWeb && !isIOS,
includeResetCanvasShortcut: isMobile,
includePinToolbarShortcut: desktopLayout,
includeViewModeShortcut: desktopLayout,
includeInputSourceShortcut: !isWeb,
includeVoiceCallShortcut: !isWeb,
);
}
} }
/// Register the default-bound shortcut actions that aren't already wired by /// Register the default-bound shortcut actions that aren't already wired by
@@ -208,13 +73,6 @@ class ShortcutModel {
/// screenshot action). Called once per session from the desktop / mobile /// screenshot action). Called once per session from the desktop / mobile
/// remote page, after the toolbar registrations have run. /// remote page, after the toolbar registrations have run.
/// ///
/// We register unconditionally — even when shortcuts are master-disabled —
/// because the matcher (Rust + JS) gates dispatch via the `enabled` flag,
/// so registered closures are functionally invisible until the user flips
/// shortcuts on. This keeps the wiring simple (no rebind callbacks across
/// sessions) and lets the user toggle shortcuts mid-session without
/// reconnecting.
///
/// [tabController] is the desktop window's tab controller; `null` on mobile / /// [tabController] is the desktop window's tab controller; `null` on mobile /
/// web (where tab-switch shortcuts don't apply). /// web (where tab-switch shortcuts don't apply).
/// ///
@@ -223,60 +81,29 @@ class ShortcutModel {
void registerSessionShortcutActions( void registerSessionShortcutActions(
FFI ffi, { FFI ffi, {
DesktopTabController? tabController, DesktopTabController? tabController,
ToolbarState? toolbarState,
}) { }) {
final sessionId = ffi.sessionId; final sessionId = ffi.sessionId;
// Note on disposal: every closure registered below captures `ffi` via // Toggle Fullscreen — desktop & web-desktop only. `stateGlobal.setFullscreen`
// closure environment, so the FFI object stays alive for the duration of // handles native window vs. browser fullscreen; on mobile fullscreen is the
// the closure's execution — even across awaits, even if the session is // permanent default, so we leave the action unregistered (becomes a logged
// closed mid-execution. We therefore don't add per-closure liveness // no-op if a mobile user binds it).
// guards: a `WeakReference<FFI>` check would never go null while the if (isDesktop || isWebDesktop) {
// closure is on the call stack, and the underlying `bind.session*` /
// model setters tolerate stale-session calls (they no-op on torn-down
// sessions). ShortcutModel.onTriggered's existing entry guard
// (`_callbacks` lookup returning null after disposal) is the actual
// liveness gate.
// Toggle Fullscreen — available wherever the desktop layout renders
// (native desktop + every Web browser, since Web uses the desktop
// RemotePage). `stateGlobal.setFullscreen` handles native window vs.
// browser fullscreen. Native mobile is permanently full-screen, so the
// action is intentionally not registered there.
if (isDesktop || isWeb) {
ffi.shortcutModel.register(kShortcutActionToggleFullscreen, () { ffi.shortcutModel.register(kShortcutActionToggleFullscreen, () {
stateGlobal.setFullscreen(!stateGlobal.fullscreen.value); stateGlobal.setFullscreen(!stateGlobal.fullscreen.value);
}); });
} }
// Toggle Recording — desktop only here. Mobile already wires this through
// `toolbarControls` (which adds a recording entry on `!(isDesktop||isWeb)`),
// but the desktop toolbar uses a separate `_RecordMenu` widget that has no
// `actionId`. Without this explicit registration a desktop user could bind
// Toggle Recording in settings and the press would have no handler.
// `recordingModel.toggle()` itself short-circuits on iOS and on sessions
// without recording permission.
if (isDesktop) {
ffi.shortcutModel.register(kShortcutActionToggleRecording, () {
ffi.recordingModel.toggle();
});
}
// Switch Display Next / Prev — requires the peer to have at least 2 // Switch Display Next / Prev — requires the peer to have at least 2
// displays. From the "All displays" merged view, Next jumps to display 0 // displays. No-op when only one display is available or when the user has
// and Prev to the last display, so the user can always escape the merged // selected the "All displays" pseudo-display.
// view via these shortcuts.
void switchDisplayBy(int delta) { void switchDisplayBy(int delta) {
final pi = ffi.ffiModel.pi; final pi = ffi.ffiModel.pi;
final count = pi.displays.length; final count = pi.displays.length;
if (count <= 1) return; if (count <= 1) return;
final current = pi.currentDisplay; final current = pi.currentDisplay;
final int next; if (current == kAllDisplayValue) return;
if (current == kAllDisplayValue) { final next = ((current + delta) % count + count) % count;
next = delta > 0 ? 0 : count - 1;
} else {
next = ((current + delta) % count + count) % count;
}
bind.sessionSwitchDisplay( bind.sessionSwitchDisplay(
isDesktop: isDesktop, isDesktop: isDesktop,
sessionId: sessionId, sessionId: sessionId,
@@ -296,265 +123,19 @@ void registerSessionShortcutActions(
switchDisplayBy(-1); switchDisplayBy(-1);
}); });
// Switch to all-monitors view — mirrors the toolbar Monitor menu's // Switch Tab 1..9 — desktop only. The remote-screen tabs live in the
// "all monitors" button (only built when peer has >1 display). Not a // window-scoped DesktopTabController, not on the FFI itself, so we need
// toggle: the toolbar button just sets the merged view; another action // the controller from the page that owns this session. No-op on mobile /
// (Switch to next/previous display, or another monitor button) takes // web (no controller passed) and when the requested tab index is out of
// you back to a single display. // range.
//
// Use `openMonitorInTheSameTab(kAllDisplayValue, ...)` rather than calling
// `sessionSwitchDisplay` with `[kAllDisplayValue]` directly — the toolbar
// path treats `kAllDisplayValue` as a UI sentinel and expands it to the
// real display index list (`[0, 1, ...]`) before sending, then updates
// local FfiModel state. Sending `[-1]` raw produces a wire value the
// remote can't act on and skips the local state update, so the merged
// view never engages.
ffi.shortcutModel.register(kShortcutActionSwitchDisplayAll, () {
final pi = ffi.ffiModel.pi;
if (pi.displays.length <= 1) return;
if (pi.currentDisplay == kAllDisplayValue) return;
openMonitorInTheSameTab(kAllDisplayValue, ffi, pi);
});
// Switch tab next / prev — desktop only. The remote-screen tabs live in
// the window-scoped DesktopTabController, not on the FFI itself, so we
// need the controller from the page that owns this session. We
// intentionally don't expose positional ("Switch to tab N") shortcuts:
// counting tabs in a long list is impractical, and AnyDesk / Chrome
// standard practice is to favour next/prev navigation.
if (tabController != null) { if (tabController != null) {
void switchTabBy(int delta) { for (var n = 1; n <= 9; n++) {
final tabs = tabController.state.value.tabs; final idx = n - 1;
if (tabs.length <= 1) return; ffi.shortcutModel.register(kShortcutActionSwitchTab(n), () {
final cur = tabs.indexWhere((t) => t.key == ffi.id); if (tabController.state.value.tabs.length > idx) {
if (cur < 0) return; tabController.jumpTo(idx);
final next = (cur + delta + tabs.length) % tabs.length; }
tabController.jumpTo(next); });
} }
ffi.shortcutModel
.register(kShortcutActionSwitchTabNext, () => switchTabBy(1));
ffi.shortcutModel
.register(kShortcutActionSwitchTabPrev, () => switchTabBy(-1));
// Close Tab — desktop only. Mirrors the tab right-click "Close" entry,
// including the audit-log confirmation dialog so a shortcut close goes
// through the same path as a menu close.
ffi.shortcutModel.register(kShortcutActionCloseTab, () async {
if (await desktopTryShowTabAuditDialogCloseCancelled(
id: ffi.id,
tabController: tabController,
)) {
return;
}
tabController.closeBy(ffi.id);
});
}
// Toggle Toolbar — desktop only. ToolbarState is window/session-scoped,
// owned by the RemotePage that hosts this session.
if (toolbarState != null) {
ffi.shortcutModel.register(kShortcutActionToggleToolbar, () {
toolbarState.switchHide(sessionId);
});
ffi.shortcutModel.register(kShortcutActionPinToolbar, () {
toolbarState.switchPin();
});
}
// Toggle Chat overlay (open/close the chat panel for this session).
// _ChatMenu is a standalone toolbar icon — not part of any toolbar
// helper that returns a TToggleMenu list — so its handler is wired
// here rather than picked up by helper auto-register.
ffi.shortcutModel.register(kShortcutActionToggleChat, () {
ffi.chatModel.toggleChatOverlay();
});
// Toggle Voice Call — start when idle, hang up when active. Mirrors the
// toolbar's `_VoiceCallMenu` state-driven button. Web bridge throws
// UnimplementedError on both sessionRequestVoiceCall and
// sessionCloseVoiceCall, so we don't register on web.
if (!isWeb) {
ffi.shortcutModel.register(kShortcutActionToggleVoiceCall, () {
final status = ffi.chatModel.voiceCallStatus.value;
if (status == VoiceCallStatus.connected ||
status == VoiceCallStatus.waitingForResponse) {
bind.sessionCloseVoiceCall(sessionId: sessionId);
} else {
bind.sessionRequestVoiceCall(sessionId: sessionId);
}
});
}
// ── Inline _KeyboardMenu items + actions with no toolbar TToggleMenu/TRadioMenu ─
// The toolbar's TToggleMenu / TRadioMenu helpers (toolbarDisplayToggle,
// toolbarCursor, toolbarKeyboardToggles, toolbarCodec, toolbarPrivacyMode,
// toolbarViewStyle, toolbarImageQuality) auto-register their tagged entries
// from the bottom of each helper. The handlers below cover what those
// helpers DON'T own:
// * Show my cursor / Keyboard mode (Map/Translate/Legacy) / View Only
// (desktop) — built as widgets directly in `_KeyboardMenu`, not as
// TToggleMenu lists. (Mobile View Only IS in toolbarDisplayToggle and
// auto-registers; the desktop session-start handler below registers
// first and the helper's auto-register on mobile takes over after its
// unawaited future resolves.)
// * Plug out all virtual displays — built in `getVirtualDisplayMenuChildren`
// as a MenuButton, not a TToggleMenu.
// * Toggle Input Source — cycle action; the toolbar exposes per-source
// radios but no single "cycle to next source" entry.
// Show my cursor — toolbar (`_KeyboardMenu.showMyCursor`) pushes the new
// value into FfiModel.setShowMyCursor and auto-enables view-only when the
// toggle goes on, so the user can never control the remote with their own
// cursor visible.
ffi.shortcutModel.register(kShortcutActionToggleShowMyCursor, () async {
await bind.sessionToggleOption(
sessionId: sessionId, value: kOptionToggleShowMyCursor);
final showMyCursor = await bind.sessionGetToggleOption(
sessionId: sessionId, arg: kOptionToggleShowMyCursor) ??
false;
ffi.ffiModel.setShowMyCursor(showMyCursor);
if (showMyCursor && !ffi.ffiModel.viewOnly) {
await bind.sessionToggleOption(
sessionId: sessionId, value: kOptionToggleViewOnly);
final viewOnly = await bind.sessionGetToggleOption(
sessionId: sessionId, arg: kOptionToggleViewOnly) ??
false;
ffi.ffiModel.setViewOnly(ffi.id, viewOnly);
}
});
// Keyboard mode (Map / Translate / Legacy). Mirrors the radio buttons in
// `_KeyboardMenu.keyboardMode()` (built as RdoMenuButton, not TRadioMenu).
void registerKeyboardMode(String actionId, String mode) {
ffi.shortcutModel.register(actionId, () async {
await bind.sessionSetKeyboardMode(sessionId: sessionId, value: mode);
await ffi.inputModel.updateKeyboardMode();
});
}
registerKeyboardMode(kShortcutActionKeyboardModeMap, kKeyMapMode);
registerKeyboardMode(kShortcutActionKeyboardModeTranslate, kKeyTranslateMode);
registerKeyboardMode(kShortcutActionKeyboardModeLegacy, kKeyLegacyMode);
// Plug out all virtual displays (Windows + IDD only). Mirrors the toolbar's
// "Plug out all" button — present in both IDD modes (RustDesk + Amyuni),
// built as a MenuButton inside `getVirtualDisplayMenuChildren`.
ffi.shortcutModel.register(kShortcutActionPlugOutAllVirtualDisplays, () {
bind.sessionToggleVirtualDisplay(
sessionId: sessionId,
index: kAllVirtualDisplay,
on: false,
);
});
// Privacy mode 1 / 2 — fallback handlers for the single-impl and null-impls
// branches of `toolbarPrivacyMode`. The multi-impl branch tags each entry
// with the matching actionId and `_registerToggleMenuShortcuts` overrides
// these closures with the toolbar's own onChanged. But when the peer only
// advertises a single impl (older Linux peers, certain platform configs)
// toolbarPrivacyMode returns a `getDefaultMenu` entry without an actionId,
// so the auto-register pass skips it — these fallbacks are what actually
// wire the shortcut in that case.
String? findPrivacyImpl(String nameKey) {
final impls = ffi.ffiModel.pi
.platformAdditions[kPlatformAdditionsSupportedPrivacyModeImpl]
as List<dynamic>?;
if (impls == null) return null;
for (final e in impls) {
if (e is List && e.length >= 2 && e[1] == nameKey) return e[0] as String;
}
return null;
}
// Match the multi-impl branch of `toolbarPrivacyMode`: turn this impl on iff
// the active impl isn't already this one. Comparing `.value == implKey`
// (rather than `.value.isEmpty`) means pressing the mode-1 shortcut while
// mode 2 is on correctly turns mode 1 ON, instead of misreading the
// "any-mode-active" state as "this-mode-active" and toggling OFF.
ffi.shortcutModel.register(kShortcutActionPrivacyMode1, () {
final implKey = findPrivacyImpl('privacy_mode_impl_mag_tip');
if (implKey == null) return;
bind.sessionTogglePrivacyMode(
sessionId: sessionId,
implKey: implKey,
on: PrivacyModeState.find(ffi.id).value != implKey,
);
});
ffi.shortcutModel.register(kShortcutActionPrivacyMode2, () {
final implKey = findPrivacyImpl('privacy_mode_impl_virtual_display_tip');
if (implKey == null) return;
bind.sessionTogglePrivacyMode(
sessionId: sessionId,
implKey: implKey,
on: PrivacyModeState.find(ffi.id).value != implKey,
);
});
// View Only — desktop toolbar exposes this inline in `_KeyboardMenu.viewMode`
// (mobile is in toolbarDisplayToggle and goes through helper auto-register).
// Mirrors the desktop callback: toggle + sync FfiModel.viewOnly +
// FfiModel.showMyCursor (the toolbar keeps these in step).
ffi.shortcutModel.register(kShortcutActionToggleViewOnly, () async {
await bind.sessionToggleOption(
sessionId: sessionId, value: kOptionToggleViewOnly);
final viewOnly = await bind.sessionGetToggleOption(
sessionId: sessionId, arg: kOptionToggleViewOnly) ??
false;
ffi.ffiModel.setViewOnly(ffi.id, viewOnly);
final showMyCursor = await bind.sessionGetToggleOption(
sessionId: sessionId, arg: kOptionToggleShowMyCursor) ??
false;
ffi.ffiModel.setShowMyCursor(showMyCursor);
});
// Toggle Reverse mouse wheel — read current 'Y'/'N' (falling back to user
// default), flip, write back.
ffi.shortcutModel.register(kShortcutActionToggleReverseMouseWheel, () async {
var cur = bind.sessionGetReverseMouseWheelSync(sessionId: sessionId) ?? '';
if (cur == '') {
cur = bind.mainGetUserDefaultOption(key: kKeyReverseMouseWheel);
}
final next = cur == 'Y' ? 'N' : 'Y';
await bind.sessionSetReverseMouseWheel(sessionId: sessionId, value: next);
});
// Toggle Relative mouse mode (gaming mode). Desktop only.
if (isDesktop && !isWeb) {
ffi.shortcutModel.register(kShortcutActionToggleRelativeMouseMode, () {
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();
});
} }
} }

View File

@@ -126,7 +126,6 @@ class PlatformFFI {
gFFI.dialogManager.dismissAll(); gFFI.dialogManager.dismissAll();
closeConnection(); closeConnection();
}; };
await _ffiBind.mainInit(appDir: '');
context.callMethod('init'); context.callMethod('init');
version = getByName('version'); version = getByName('version');
window.onContextMenu.listen((event) { window.onContextMenu.listen((event) {

View File

@@ -7,7 +7,7 @@ import 'package:uuid/uuid.dart';
import 'dart:html' as html; import 'dart:html' as html;
import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/models/shortcut_model.dart'; import 'package:flutter_hbb/common.dart' as common;
final _privateConstructorUsedError = UnsupportedError( final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
@@ -938,12 +938,21 @@ class RustdeskImpl {
js.context.callMethod('reloadShortcuts', []); js.context.callMethod('reloadShortcuts', []);
} }
// Web has no Rust at runtime, so the defaults seed comes from the // Mirror of `default_bindings()` in `src/keyboard/shortcuts.rs`. Keep these
// [kDefaultShortcutBindings] canonical in shortcut_constants.dart. Parity // two lists in sync — if you add or change a default binding on the Rust
// with Rust's `default_bindings()` is enforced by tests on both sides // side, update the literal below to match.
// against `flutter/test/fixtures/default_keyboard_shortcuts.json`.
String mainGetDefaultKeyboardShortcuts({dynamic hint}) { String mainGetDefaultKeyboardShortcuts({dynamic hint}) {
return jsonEncode(kDefaultShortcutBindings); const prefix = ['primary', 'alt', 'shift'];
final list = <Map<String, dynamic>>[
{'action': 'send_ctrl_alt_del', 'mods': prefix, 'key': 'delete'},
{'action': 'toggle_fullscreen', 'mods': prefix, 'key': 'enter'},
{'action': 'switch_display_next', 'mods': prefix, 'key': 'arrow_right'},
{'action': 'switch_display_prev', 'mods': prefix, 'key': 'arrow_left'},
{'action': 'screenshot', 'mods': prefix, 'key': 'p'},
for (var n = 1; n <= 9; n++)
{'action': 'switch_tab_$n', 'mods': prefix, 'key': 'digit$n'},
];
return jsonEncode(list);
} }
String mainGetInputSource({dynamic hint}) { String mainGetInputSource({dynamic hint}) {
@@ -1195,11 +1204,10 @@ class RustdeskImpl {
// JS -> Dart shortcut bridge. The matcher in flutter/web/js/src/ // JS -> Dart shortcut bridge. The matcher in flutter/web/js/src/
// shortcut_matcher.ts calls `window.onShortcutTriggered(actionId)` when a // shortcut_matcher.ts calls `window.onShortcutTriggered(actionId)` when a
// binding fires; route it to the active session's ShortcutModel. // binding fires; route it to the active session's ShortcutModel.
// Web uses a JS-side connection, so the event does not arrive through the // Web is single-window so `gFFI` is always the active session.
// native session event stream.
js.context['onShortcutTriggered'] = (dynamic action) { js.context['onShortcutTriggered'] = (dynamic action) {
if (action is String) { if (action is String) {
ShortcutModel.onWebTriggered(action); common.gFFI.shortcutModel.onTriggered(action);
} }
}; };
return Future.value(); return Future.value();

View File

@@ -1,11 +0,0 @@
[
{"action": "send_ctrl_alt_del", "mods": ["primary", "alt", "shift"], "key": "delete"},
{"action": "toggle_fullscreen", "mods": ["primary", "alt", "shift"], "key": "enter"},
{"action": "switch_display_next", "mods": ["primary", "alt", "shift"], "key": "arrow_right"},
{"action": "switch_display_prev", "mods": ["primary", "alt", "shift"], "key": "arrow_left"},
{"action": "screenshot", "mods": ["primary", "alt", "shift"], "key": "p"},
{"action": "toggle_show_remote_cursor", "mods": ["primary", "alt", "shift"], "key": "m"},
{"action": "toggle_mute", "mods": ["primary", "alt", "shift"], "key": "s"},
{"action": "toggle_block_input", "mods": ["primary", "alt", "shift"], "key": "i"},
{"action": "toggle_chat", "mods": ["primary", "alt", "shift"], "key": "c"}
]

View File

@@ -1,11 +0,0 @@
[
"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m",
"n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z",
"digit0", "digit1", "digit2", "digit3", "digit4",
"digit5", "digit6", "digit7", "digit8", "digit9",
"f1", "f2", "f3", "f4", "f5", "f6",
"f7", "f8", "f9", "f10", "f11", "f12",
"delete", "backspace", "tab", "space", "enter",
"arrow_left", "arrow_right", "arrow_up", "arrow_down",
"home", "end", "page_up", "page_down", "insert"
]

View File

@@ -1,465 +0,0 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_hbb/common/widgets/keyboard_shortcuts/shortcut_actions.dart';
import 'package:flutter_hbb/common/widgets/keyboard_shortcuts/shortcut_constants.dart';
import 'package:flutter_hbb/common/widgets/keyboard_shortcuts/shortcut_utils.dart';
ShortcutPlatformCapabilities capabilities({
bool includeFullscreenShortcut = true,
bool includeScreenshotShortcut = true,
bool includeTabShortcuts = true,
bool includeToolbarShortcut = true,
bool includeCloseTabShortcut = true,
bool includeSwitchSidesShortcut = true,
bool includeRecordingShortcut = true,
bool includeResetCanvasShortcut = true,
bool includePinToolbarShortcut = true,
bool includeViewModeShortcut = true,
bool includeInputSourceShortcut = true,
bool includeVoiceCallShortcut = true,
}) {
return ShortcutPlatformCapabilities(
includeFullscreenShortcut: includeFullscreenShortcut,
includeScreenshotShortcut: includeScreenshotShortcut,
includeTabShortcuts: includeTabShortcuts,
includeToolbarShortcut: includeToolbarShortcut,
includeCloseTabShortcut: includeCloseTabShortcut,
includeSwitchSidesShortcut: includeSwitchSidesShortcut,
includeRecordingShortcut: includeRecordingShortcut,
includeResetCanvasShortcut: includeResetCanvasShortcut,
includePinToolbarShortcut: includePinToolbarShortcut,
includeViewModeShortcut: includeViewModeShortcut,
includeInputSourceShortcut: includeInputSourceShortcut,
includeVoiceCallShortcut: includeVoiceCallShortcut,
);
}
void main() {
test('kDefaultShortcutBindings matches fixture', () {
// The fixture is the cross-language source of truth for default
// bindings. Rust has its own parity test against the same file
// (`default_bindings_match_fixture_json` in src/keyboard/shortcuts.rs),
// so a drift on either side breaks CI.
final fixturePath = 'test/fixtures/default_keyboard_shortcuts.json';
final fixture =
jsonDecode(File(fixturePath).readAsStringSync()) as List<dynamic>;
expect(kDefaultShortcutBindings, equals(fixture),
reason: 'kDefaultShortcutBindings drifted from $fixturePath — update '
'shortcut_constants.dart, the fixture, and Rust default_bindings() '
'together');
});
test('save order preserves macOS control modifier', () {
expect(canonicalShortcutModsForSave({'ctrl'}), ['ctrl']);
expect(canonicalShortcutModsForSave({'shift', 'ctrl', 'primary', 'alt'}),
['primary', 'ctrl', 'alt', 'shift']);
});
test('shortcutBindingMapsFrom ignores malformed bindings', () {
expect(shortcutBindingMapsFrom('not a list'), isEmpty);
final bindings = shortcutBindingMapsFrom([
{
'action': kShortcutActionScreenshot,
'mods': ['primary'],
'key': 'p',
},
'bad',
1,
{
'action': kShortcutActionToggleMute,
'mods': ['alt'],
'key': 's',
},
]);
expect(bindings, hasLength(2));
expect(bindings.map((binding) => binding['action']), [
kShortcutActionScreenshot,
kShortcutActionToggleMute,
]);
});
test('shortcutModSetFrom ignores malformed modifiers', () {
expect(shortcutModSetFrom('not a list'), isEmpty);
expect(shortcutModSetFrom(['primary', 1, 'alt', null, 'primary']), {
'primary',
'alt',
});
});
test('non-desktop defaults exclude desktop-only and tab shortcuts', () {
final defaults = [
{
'action': kShortcutActionSendCtrlAltDel,
'mods': ['primary', 'alt', 'shift'],
'key': 'delete',
},
{
'action': kShortcutActionToggleFullscreen,
'mods': ['primary', 'alt', 'shift'],
'key': 'enter',
},
{
'action': kShortcutActionSwitchDisplayNext,
'mods': ['primary', 'alt', 'shift'],
'key': 'arrow_right',
},
{
'action': kShortcutActionScreenshot,
'mods': ['primary', 'alt', 'shift'],
'key': 'p',
},
{
'action': kShortcutActionSwitchTabNext,
'mods': ['primary', 'alt', 'shift'],
'key': 'right_bracket',
},
{
'action': kShortcutActionToggleRelativeMouseMode,
'mods': ['primary', 'alt', 'shift'],
'key': 'g',
},
];
final filtered = filterDefaultBindingsForPlatform(
defaults,
capabilities(
includeFullscreenShortcut: false,
includeScreenshotShortcut: false,
includeTabShortcuts: false,
includeToolbarShortcut: false,
includeCloseTabShortcut: false,
includeSwitchSidesShortcut: false,
includeRecordingShortcut: false,
includeResetCanvasShortcut: false,
includePinToolbarShortcut: false,
includeViewModeShortcut: false,
includeInputSourceShortcut: false,
includeVoiceCallShortcut: false,
),
);
expect(filtered.map((binding) => binding['action']), [
kShortcutActionSendCtrlAltDel,
kShortcutActionSwitchDisplayNext,
]);
});
Set<String> idSet(Iterable<KeyboardShortcutActionGroup> groups) =>
{for (final e in allActionEntries(groups)) e.id};
/// Convenience: extract the children of the named group as a flat list of
/// human-readable tokens. Subgroups appear as `'group:<title>'` followed
/// by their entries, so call sites can assert on full ordering (subgroups
/// interleaved with direct items) in one expectation.
List<String> childTokens(
List<KeyboardShortcutActionGroup> groups, String titleKey) {
final group = groups.firstWhere((g) => g.titleKey == titleKey);
final out = <String>[];
for (final child in group.children) {
switch (child) {
case KeyboardShortcutActionEntry():
out.add(child.id);
case KeyboardShortcutActionSubgroup():
out.add('group:${child.titleKey}');
for (final entry in child.entries) {
out.add(' ${entry.id}');
}
}
}
return out;
}
test('filterKeyboardShortcutActionGroupsForPlatform strips desktop-only', () {
final groups = filterKeyboardShortcutActionGroupsForPlatform(
capabilities(
includeFullscreenShortcut: false,
includeScreenshotShortcut: false,
includeTabShortcuts: false,
includeToolbarShortcut: false,
includeCloseTabShortcut: false,
includeSwitchSidesShortcut: false,
// Recording / Reset Canvas are intentionally still included here —
// they have non-desktop platforms (mobile Android / mobile both).
includeRecordingShortcut: true,
includeResetCanvasShortcut: true,
includePinToolbarShortcut: false,
includeViewModeShortcut: false,
includeInputSourceShortcut: false,
includeVoiceCallShortcut: false,
),
);
final ids = idSet(groups);
// Desktop-only actions are stripped.
expect(ids, isNot(contains(kShortcutActionToggleFullscreen)));
expect(ids, isNot(contains(kShortcutActionToggleRelativeMouseMode)));
expect(ids, isNot(contains(kShortcutActionScreenshot)));
expect(ids, isNot(contains(kShortcutActionToggleToolbar)));
expect(ids, isNot(contains(kShortcutActionCloseTab)));
expect(ids, isNot(contains(kShortcutActionSwitchSides)));
expect(ids, isNot(contains(kShortcutActionPinToolbar)));
expect(ids, isNot(contains(kShortcutActionViewModeOriginal)));
expect(ids, isNot(contains(kShortcutActionViewModeAdaptive)));
expect(ids, isNot(contains(kShortcutActionSwitchTabNext)));
expect(ids, isNot(contains(kShortcutActionSwitchTabPrev)));
// Cross-platform actions survive.
expect(ids, contains(kShortcutActionSendCtrlAltDel));
expect(ids, contains(kShortcutActionInsertLock));
expect(ids, contains(kShortcutActionRestartRemote));
expect(ids, contains(kShortcutActionSwitchDisplayNext));
expect(ids, contains(kShortcutActionToggleRecording));
expect(ids, contains(kShortcutActionResetCanvas));
expect(ids, contains(kShortcutActionToggleMute));
});
test(
'filterKeyboardShortcutActionGroupsForPlatform hides Toggle Recording on Web/iOS',
() {
final groups = filterKeyboardShortcutActionGroupsForPlatform(
capabilities(includeRecordingShortcut: false),
);
final ids = idSet(groups);
expect(ids, isNot(contains(kShortcutActionToggleRecording)));
// Other Session Control entries unaffected.
expect(ids, contains(kShortcutActionSendCtrlAltDel));
expect(ids, contains(kShortcutActionInsertLock));
});
test(
'filterKeyboardShortcutActionGroupsForPlatform keeps full set on desktop',
() {
final groups =
filterKeyboardShortcutActionGroupsForPlatform(capabilities());
expect(idSet(groups), equals(idSet(kKeyboardShortcutActionGroups)));
});
test('shortcut action groups follow toolbar menu order', () {
final groups = kKeyboardShortcutActionGroups;
// Top-level groups in toolbar order.
expect(
groups.map((g) => g.titleKey).toList(),
['Monitor', 'Control Actions', 'Display', 'Keyboard', 'Chat', 'Other'],
);
// Display: subgroups (View Mode → Image Quality → Codec → Virtual
// display) first, then direct items (cursor toggles + display toggles),
// then Privacy mode subgroup last — exactly matching `_DisplayMenu`.
expect(childTokens(groups, 'Display'), [
'group:View Mode',
' $kShortcutActionViewModeOriginal',
' $kShortcutActionViewModeAdaptive',
' $kShortcutActionViewModeCustom',
'group:Image Quality',
' $kShortcutActionImageQualityBest',
' $kShortcutActionImageQualityBalanced',
' $kShortcutActionImageQualityLow',
'group:Codec',
' $kShortcutActionCodecAuto',
' $kShortcutActionCodecVp8',
' $kShortcutActionCodecVp9',
' $kShortcutActionCodecAv1',
' $kShortcutActionCodecH264',
' $kShortcutActionCodecH265',
'group:Virtual display',
' $kShortcutActionPlugOutAllVirtualDisplays',
kShortcutActionToggleShowRemoteCursor,
kShortcutActionToggleFollowRemoteCursor,
kShortcutActionToggleFollowRemoteWindow,
kShortcutActionToggleZoomCursor,
kShortcutActionToggleQualityMonitor,
kShortcutActionToggleMute,
kShortcutActionToggleEnableFileCopyPaste,
kShortcutActionToggleDisableClipboard,
kShortcutActionToggleLockAfterSessionEnd,
kShortcutActionToggleTrueColor,
'group:Privacy mode',
' $kShortcutActionPrivacyMode1',
' $kShortcutActionPrivacyMode2',
]);
// Privacy mode is the last child under Display (matching the toolbar's
// submenu order — `_DisplayMenu` adds Privacy mode after the toggles).
final displayChildren =
groups.firstWhere((g) => g.titleKey == 'Display').children;
expect(displayChildren.last, isA<KeyboardShortcutActionSubgroup>());
expect(
(displayChildren.last as KeyboardShortcutActionSubgroup).titleKey,
'Privacy mode',
);
// Keyboard: Keyboard mode subgroup first, then direct items —
// matching `_KeyboardMenu`.
expect(childTokens(groups, 'Keyboard'), [
'group:Keyboard mode',
' $kShortcutActionKeyboardModeLegacy',
' $kShortcutActionKeyboardModeMap',
' $kShortcutActionKeyboardModeTranslate',
kShortcutActionToggleInputSource,
kShortcutActionToggleViewOnly,
kShortcutActionToggleShowMyCursor,
kShortcutActionToggleSwapCtrlCmd,
kShortcutActionToggleRelativeMouseMode,
kShortcutActionToggleReverseMouseWheel,
kShortcutActionToggleSwapLeftRightMouse,
]);
});
test('filterKeyboardShortcutActionGroupsForPlatform drops empty groups', () {
// Sanity: KeyboardShortcutActionGroup ctor still accepts a single direct
// entry as a child.
final original = [
KeyboardShortcutActionGroup('TestGroup', [
KeyboardShortcutActionEntry(kShortcutActionCloseTab, 'Close Tab'),
]),
];
expect(original.first.children, hasLength(1));
// With every capability flag off, groups whose items are all behind
// those flags get dropped. Display / Keyboard parent groups still carry
// cross-platform direct items so they survive even when the gated
// subgroups thin out.
final groups = filterKeyboardShortcutActionGroupsForPlatform(
capabilities(
includeFullscreenShortcut: false,
includeScreenshotShortcut: false,
includeTabShortcuts: false,
includeToolbarShortcut: false,
includeCloseTabShortcut: false,
includeSwitchSidesShortcut: false,
includeRecordingShortcut: false,
includeResetCanvasShortcut: false,
includePinToolbarShortcut: false,
includeViewModeShortcut: false,
includeInputSourceShortcut: false,
includeVoiceCallShortcut: false,
),
);
final titles = groups.map((g) => g.titleKey).toList();
// "Other" has nothing but platform-gated entries → dropped entirely.
expect(titles, isNot(contains('Other')));
// Parent groups with cross-platform direct items survive.
expect(titles, contains('Display'));
expect(titles, contains('Keyboard'));
// The "View Mode" subgroup under Display is gated by includeViewModeShortcut,
// so it must be absent from Display's surviving children.
final displayChildren =
groups.firstWhere((g) => g.titleKey == 'Display').children;
final subgroupTitles = displayChildren
.whereType<KeyboardShortcutActionSubgroup>()
.map((s) => s.titleKey)
.toList();
expect(subgroupTitles, isNot(contains('View Mode')));
// No surviving group is empty either way.
expect(groups.every((g) => g.children.isNotEmpty), isTrue);
// No surviving subgroup is empty.
for (final group in groups) {
for (final child in group.children) {
if (child is KeyboardShortcutActionSubgroup) {
expect(child.entries, isNotEmpty,
reason: 'subgroup "${child.titleKey}" should not be empty');
}
}
}
});
test('logicalKeyName covers the supported-keys fixture', () {
// The fixture is the cross-language source of truth for the full set of
// shortcut-bindable key names. Rust has a mirror test against the same
// file (`supported_keys_match_fixture` in src/keyboard/shortcuts.rs).
// Drift on either side breaks one of the two tests.
final fixturePath = 'test/fixtures/supported_shortcut_keys.json';
final fixture =
(jsonDecode(File(fixturePath).readAsStringSync()) as List<dynamic>)
.cast<String>()
.toSet();
// Hand-rolled (LogicalKeyboardKey, name) round-trip table. Adding a key
// requires updates in three places: the fixture, this table, and Rust's
// matching table — that's the price of the parity guarantee.
final mappings = <(LogicalKeyboardKey, String)>[
for (var c = 0; c < 26; c++)
(
LogicalKeyboardKey(0x00000000061 + c),
String.fromCharCode(0x61 + c),
),
for (var d = 0; d < 10; d++)
(LogicalKeyboardKey(0x00000000030 + d), 'digit$d'),
(LogicalKeyboardKey.f1, 'f1'),
(LogicalKeyboardKey.f2, 'f2'),
(LogicalKeyboardKey.f3, 'f3'),
(LogicalKeyboardKey.f4, 'f4'),
(LogicalKeyboardKey.f5, 'f5'),
(LogicalKeyboardKey.f6, 'f6'),
(LogicalKeyboardKey.f7, 'f7'),
(LogicalKeyboardKey.f8, 'f8'),
(LogicalKeyboardKey.f9, 'f9'),
(LogicalKeyboardKey.f10, 'f10'),
(LogicalKeyboardKey.f11, 'f11'),
(LogicalKeyboardKey.f12, 'f12'),
(LogicalKeyboardKey.delete, 'delete'),
(LogicalKeyboardKey.backspace, 'backspace'),
(LogicalKeyboardKey.tab, 'tab'),
(LogicalKeyboardKey.space, 'space'),
(LogicalKeyboardKey.enter, 'enter'),
(LogicalKeyboardKey.numpadEnter, 'enter'),
(LogicalKeyboardKey.arrowLeft, 'arrow_left'),
(LogicalKeyboardKey.arrowRight, 'arrow_right'),
(LogicalKeyboardKey.arrowUp, 'arrow_up'),
(LogicalKeyboardKey.arrowDown, 'arrow_down'),
(LogicalKeyboardKey.home, 'home'),
(LogicalKeyboardKey.end, 'end'),
(LogicalKeyboardKey.pageUp, 'page_up'),
(LogicalKeyboardKey.pageDown, 'page_down'),
(LogicalKeyboardKey.insert, 'insert'),
];
// Round-trip: every (key, name) pair must agree with logicalKeyName.
for (final (key, name) in mappings) {
expect(logicalKeyName(key), equals(name),
reason: 'logicalKeyName($key) should be "$name"');
}
// The set of names produced by the table must equal the fixture.
final namesFromTable = mappings.map((e) => e.$2).toSet();
expect(namesFromTable, equals(fixture),
reason: 'logicalKeyName vocabulary drifted from $fixturePath — update '
'shortcut_utils.dart::logicalKeyName, the fixture, and Rust '
'event_to_key_name together');
// Modifier-only / unsupported keys must return null.
expect(logicalKeyName(LogicalKeyboardKey.shift), isNull);
expect(logicalKeyName(LogicalKeyboardKey.escape), isNull);
expect(logicalKeyName(LogicalKeyboardKey.f13), isNull);
});
test('configurable shortcut list does not include known-removed action IDs',
() {
// These IDs were briefly defined without handlers (a "ghost action"
// footgun). If you intend to re-add one of these as a real action,
// wire up its handler and add a constant + group entry — do not just
// resurrect the literal string below.
//
// Note: `toggle_privacy_mode` was once on this list but is now a real
// implemented action (registered in shortcut_model.dart). The other
// legacy IDs (toggle_audio, view_mode_shrink/stretch, view_mode_1_to_1)
// were renamed: their replacements are kShortcutActionToggleMute and
// kShortcutActionViewModeOriginal/Adaptive/Custom.
const knownRemoved = [
'toggle_audio',
'view_mode_1_to_1',
'view_mode_shrink',
'view_mode_stretch',
];
final actions = idSet(kKeyboardShortcutActionGroups);
for (final id in knownRemoved) {
expect(actions, isNot(contains(id)),
reason:
'"$id" was a known ghost action — wire a real handler before re-adding it');
}
});
}

View File

@@ -624,7 +624,6 @@ void CliprdrStream_Delete(CliprdrStream *instance)
if (instance) if (instance)
{ {
free(instance->iStream.lpVtbl); free(instance->iStream.lpVtbl);
instance->iStream.lpVtbl = NULL;
free(instance); free(instance);
} }
} }
@@ -2161,7 +2160,7 @@ static BOOL wf_cliprdr_add_to_file_arrays(wfClipboard *clipboard, WCHAR *full_fi
return FALSE; return FALSE;
/* add to name array */ /* add to name array */
clipboard->file_names[clipboard->nFiles] = (LPWSTR)malloc((size_t)MAX_PATH * sizeof(WCHAR)); clipboard->file_names[clipboard->nFiles] = (LPWSTR)malloc(MAX_PATH * 2);
if (!clipboard->file_names[clipboard->nFiles]) if (!clipboard->file_names[clipboard->nFiles])
return FALSE; return FALSE;

View File

@@ -326,25 +326,26 @@ pub mod client {
// Only fires on KeyPress (event_to_key_name in shortcuts.rs returns None // Only fires on KeyPress (event_to_key_name in shortcuts.rs returns None
// for KeyRelease and other non-press events), so flushed releases from // for KeyRelease and other non-press events), so flushed releases from
// release_remote_keys pass straight through to the encode/forward path. // release_remote_keys pass straight through to the encode/forward path.
// if let Some(action_id) = crate::keyboard::shortcuts::match_event(event) {
// NOTE: Shortcut matching intentionally happens BEFORE any key swapping #[cfg(feature = "flutter")]
// (swap_modifier_key) so that shortcuts bind to the physical keys pressed, {
// not the swapped keys. This makes shortcut setup intuitive: users bind // The rdev grab loop is genuinely process-wide: it does not know which
// shortcuts to the actual keys they press, regardless of swap settings. // Flutter SessionID the keystroke was meant for, so we route to the
// Key swapping only affects what gets sent to the remote. // globally-current session via flutter::get_cur_session_id() (maintained
// // by session_enter_or_leave). This is the only behavior available on the
// Gated on `feature = "flutter"` because the dispatch target // rdev path; the Flutter path threads the explicit per-call SessionID
// (`flutter::push_session_event`) is Flutter-only. Sciter builds never // through process_event_with_session instead.
// call `reload_from_config`, so the cache stays disabled and the let session_id = crate::flutter::get_cur_session_id();
// matcher would no-op anyway — but we still skip the call entirely so crate::flutter::push_session_event(
// a hand-edited config can't silently swallow keys on a UI that has &session_id,
// no way to surface the action. "shortcut_triggered",
// vec![("action", &action_id)],
// `None` for session_id makes the helper resolve through );
// `flutter::get_cur_session_id()` — the rdev grab loop is process-wide }
// and has no per-event session context to thread. #[cfg(not(feature = "flutter"))]
#[cfg(feature = "flutter")] {
if crate::keyboard::shortcuts::try_dispatch(None, event, keyboard_mode) { let _ = action_id;
}
return; return;
} }
@@ -365,17 +366,30 @@ pub mod client {
session: &Session<T>, session: &Session<T>,
session_id: SessionID, session_id: SessionID,
) { ) {
// Shortcut intercept — see the long comment in `process_event` above // Shortcut intercept — must come before any wire encoding.
// for the KeyPress-only / feature-gate rationale. The only difference // Only fires on KeyPress (event_to_key_name in shortcuts.rs returns None
// here is that the Flutter FFI path threads an explicit SessionID // for KeyRelease and other non-press events), so flushed releases from
// through, so dispatch targets the exact tab the keystroke originated // release_remote_keys pass straight through to the encode/forward path.
// from — no dependency on the global focus tracker. if let Some(action_id) = crate::keyboard::shortcuts::match_event(event) {
#[cfg(feature = "flutter")] #[cfg(feature = "flutter")]
if crate::keyboard::shortcuts::try_dispatch(Some(&session_id), event, keyboard_mode) { {
// The Flutter path threads the explicit SessionID from the FFI entry
// (session_handle_flutter_*key_event) through this call, so the dispatch
// targets the exact tab the keystroke originated from — no dependency on
// the global focus tracker and no multi-window race.
crate::flutter::push_session_event(
&session_id,
"shortcut_triggered",
vec![("action", &action_id)],
);
}
#[cfg(not(feature = "flutter"))]
{
let _ = action_id;
let _ = session_id;
}
return; return;
} }
#[cfg(not(feature = "flutter"))]
let _ = session_id;
let keyboard_mode = get_keyboard_mode_enum(keyboard_mode); let keyboard_mode = get_keyboard_mode_enum(keyboard_mode);
if is_long_press(&event) { if is_long_press(&event) {

View File

@@ -2,7 +2,6 @@
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
use hbb_common::log;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
const LOCAL_CONFIG_KEY: &str = "keyboard-shortcuts"; const LOCAL_CONFIG_KEY: &str = "keyboard-shortcuts";
@@ -21,65 +20,39 @@ pub mod action_id {
pub const TOGGLE_FULLSCREEN: &str = "toggle_fullscreen"; pub const TOGGLE_FULLSCREEN: &str = "toggle_fullscreen";
pub const SWITCH_DISPLAY_NEXT: &str = "switch_display_next"; pub const SWITCH_DISPLAY_NEXT: &str = "switch_display_next";
pub const SWITCH_DISPLAY_PREV: &str = "switch_display_prev"; pub const SWITCH_DISPLAY_PREV: &str = "switch_display_prev";
pub const SWITCH_DISPLAY_ALL: &str = "switch_display_all";
pub const SCREENSHOT: &str = "screenshot"; pub const SCREENSHOT: &str = "screenshot";
pub const INSERT_LOCK: &str = "insert_lock"; pub const INSERT_LOCK: &str = "insert_lock";
pub const REFRESH: &str = "refresh"; pub const REFRESH: &str = "refresh";
pub const TOGGLE_AUDIO: &str = "toggle_audio";
pub const TOGGLE_BLOCK_INPUT: &str = "toggle_block_input"; pub const TOGGLE_BLOCK_INPUT: &str = "toggle_block_input";
pub const TOGGLE_RECORDING: &str = "toggle_recording"; pub const TOGGLE_RECORDING: &str = "toggle_recording";
pub const TOGGLE_PRIVACY_MODE: &str = "toggle_privacy_mode";
pub const VIEW_MODE_1_TO_1: &str = "view_mode_1_to_1";
pub const VIEW_MODE_SHRINK: &str = "view_mode_shrink";
pub const VIEW_MODE_STRETCH: &str = "view_mode_stretch";
pub const SWITCH_SIDES: &str = "switch_sides"; pub const SWITCH_SIDES: &str = "switch_sides";
pub const CLOSE_TAB: &str = "close_tab"; // switch_tab_1 .. switch_tab_9 are generated below.
pub const TOGGLE_TOOLBAR: &str = "toggle_toolbar"; }
pub const RESTART_REMOTE: &str = "restart_remote";
pub const RESET_CANVAS: &str = "reset_canvas"; pub fn switch_tab_action_id(n: u8) -> Option<&'static str> {
pub const TOGGLE_MUTE: &str = "toggle_mute"; match n {
pub const PIN_TOOLBAR: &str = "pin_toolbar"; 1 => Some("switch_tab_1"),
pub const VIEW_MODE_ORIGINAL: &str = "view_mode_original"; 2 => Some("switch_tab_2"),
pub const VIEW_MODE_ADAPTIVE: &str = "view_mode_adaptive"; 3 => Some("switch_tab_3"),
pub const TOGGLE_CHAT: &str = "toggle_chat"; 4 => Some("switch_tab_4"),
pub const TOGGLE_QUALITY_MONITOR: &str = "toggle_quality_monitor"; 5 => Some("switch_tab_5"),
pub const TOGGLE_SHOW_REMOTE_CURSOR: &str = "toggle_show_remote_cursor"; 6 => Some("switch_tab_6"),
pub const TOGGLE_SHOW_MY_CURSOR: &str = "toggle_show_my_cursor"; 7 => Some("switch_tab_7"),
pub const TOGGLE_DISABLE_CLIPBOARD: &str = "toggle_disable_clipboard"; 8 => Some("switch_tab_8"),
pub const PRIVACY_MODE_1: &str = "privacy_mode_1"; 9 => Some("switch_tab_9"),
pub const PRIVACY_MODE_2: &str = "privacy_mode_2"; _ => None,
pub const KEYBOARD_MODE_MAP: &str = "keyboard_mode_map"; }
pub const KEYBOARD_MODE_TRANSLATE: &str = "keyboard_mode_translate";
pub const KEYBOARD_MODE_LEGACY: &str = "keyboard_mode_legacy";
pub const CODEC_AUTO: &str = "codec_auto";
pub const CODEC_VP8: &str = "codec_vp8";
pub const CODEC_VP9: &str = "codec_vp9";
pub const CODEC_AV1: &str = "codec_av1";
pub const CODEC_H264: &str = "codec_h264";
pub const CODEC_H265: &str = "codec_h265";
pub const PLUG_OUT_ALL_VIRTUAL_DISPLAYS: &str = "plug_out_all_virtual_displays";
pub const TOGGLE_RELATIVE_MOUSE_MODE: &str = "toggle_relative_mouse_mode";
pub const TOGGLE_FOLLOW_REMOTE_CURSOR: &str = "toggle_follow_remote_cursor";
pub const TOGGLE_FOLLOW_REMOTE_WINDOW: &str = "toggle_follow_remote_window";
pub const TOGGLE_ZOOM_CURSOR: &str = "toggle_zoom_cursor";
pub const TOGGLE_REVERSE_MOUSE_WHEEL: &str = "toggle_reverse_mouse_wheel";
pub const TOGGLE_SWAP_LEFT_RIGHT_MOUSE: &str = "toggle_swap_left_right_mouse";
pub const TOGGLE_LOCK_AFTER_SESSION_END: &str = "toggle_lock_after_session_end";
pub const TOGGLE_TRUE_COLOR: &str = "toggle_true_color";
pub const TOGGLE_SWAP_CTRL_CMD: &str = "toggle_swap_ctrl_cmd";
pub const TOGGLE_ENABLE_FILE_COPY_PASTE: &str = "toggle_enable_file_copy_paste";
pub const VIEW_MODE_CUSTOM: &str = "view_mode_custom";
pub const IMAGE_QUALITY_BEST: &str = "image_quality_best";
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";
pub const TOGGLE_VIEW_ONLY: &str = "toggle_view_only";
} }
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum Modifier { pub enum Modifier {
Primary, Primary,
Ctrl,
Alt, Alt,
Shift, Shift,
} }
@@ -95,52 +68,38 @@ pub struct Binding {
pub struct Bindings { pub struct Bindings {
#[serde(default)] #[serde(default)]
pub enabled: bool, pub enabled: bool,
/// Persistent companion to `enabled`: when true, the matcher returns early
/// and every keystroke flows through to the remote (i.e. all bindings are
/// suspended). Stored alongside `enabled` and `bindings` so a single
/// reload refreshes both flags.
#[serde(default)]
pub pass_through: bool,
#[serde(default)] #[serde(default)]
pub bindings: Vec<Binding>, pub bindings: Vec<Binding>,
} }
pub fn default_bindings() -> Vec<Binding> { pub fn default_bindings() -> Vec<Binding> {
let prefix = || vec![Modifier::Primary, Modifier::Alt, Modifier::Shift]; let prefix = || vec![Modifier::Primary, Modifier::Alt, Modifier::Shift];
// Defaults align with AnyDesk's M/S/I/C/Delete/Arrow/Digit conventions let mut v = vec![
// where applicable; "P" for screenshot also matches AnyDesk. Binding { action: action_id::SEND_CTRL_ALT_DEL.into(), mods: prefix(), key: "delete".into() },
vec![ Binding { action: action_id::TOGGLE_FULLSCREEN.into(), mods: prefix(), key: "enter".into() },
Binding { action: action_id::SEND_CTRL_ALT_DEL.into(), mods: prefix(), key: "delete".into() }, Binding { action: action_id::SWITCH_DISPLAY_NEXT.into(), mods: prefix(), key: "arrow_right".into() },
Binding { action: action_id::TOGGLE_FULLSCREEN.into(), mods: prefix(), key: "enter".into() }, Binding { action: action_id::SWITCH_DISPLAY_PREV.into(), mods: prefix(), key: "arrow_left".into() },
Binding { action: action_id::SWITCH_DISPLAY_NEXT.into(), mods: prefix(), key: "arrow_right".into() }, Binding { action: action_id::SCREENSHOT.into(), mods: prefix(), key: "p".into() },
Binding { action: action_id::SWITCH_DISPLAY_PREV.into(), mods: prefix(), key: "arrow_left".into() }, ];
Binding { action: action_id::SCREENSHOT.into(), mods: prefix(), key: "p".into() }, for n in 1..=9u8 {
Binding { action: action_id::TOGGLE_SHOW_REMOTE_CURSOR.into(), mods: prefix(), key: "m".into() }, if let Some(action) = switch_tab_action_id(n) {
Binding { action: action_id::TOGGLE_MUTE.into(), mods: prefix(), key: "s".into() }, v.push(Binding {
Binding { action: action_id::TOGGLE_BLOCK_INPUT.into(), mods: prefix(), key: "i".into() }, action: action.into(),
Binding { action: action_id::TOGGLE_CHAT.into(), mods: prefix(), key: "c".into() }, mods: prefix(),
] key: format!("digit{n}"),
});
}
}
v
} }
/// Match a normalized (key, modifiers) pair against the given bindings. /// Match a normalized (key, modifiers) pair against the given bindings.
/// Returns the matched action ID, or None when the matcher is off /// Returns the matched action ID, or None.
/// (`enabled == false`), suspended (`pass_through == true`), or no binding
/// fires for this combo.
///
/// Defense-in-depth: bindings with an empty modifier list are skipped here
/// even though the recording dialog refuses to save them. A hand-edited
/// config (or a future writer-side bug) that lets an empty-mods binding
/// through would otherwise turn that key's every press into a swallowed
/// shortcut, breaking normal typing in the remote session — a much worse
/// failure than the binding simply not firing.
pub fn match_normalized<'a>(key: &str, mods: &[Modifier], b: &'a Bindings) -> Option<&'a str> { pub fn match_normalized<'a>(key: &str, mods: &[Modifier], b: &'a Bindings) -> Option<&'a str> {
if !b.enabled || b.pass_through { if !b.enabled {
return None; return None;
} }
for binding in &b.bindings { for binding in &b.bindings {
if binding.mods.is_empty() {
continue;
}
if binding.key == key && mods_equal(&binding.mods, mods) { if binding.key == key && mods_equal(&binding.mods, mods) {
return Some(binding.action.as_str()); return Some(binding.action.as_str());
} }
@@ -149,19 +108,9 @@ pub fn match_normalized<'a>(key: &str, mods: &[Modifier], b: &'a Bindings) -> Op
} }
pub fn normalize_modifiers(alt: bool, ctrl: bool, shift: bool, command: bool) -> Vec<Modifier> { pub fn normalize_modifiers(alt: bool, ctrl: bool, shift: bool, command: bool) -> Vec<Modifier> {
// iOS shares Apple's keyboard semantics with macOS — recording dialog
// already treats iOS as `_isMac`, so the matcher must too.
//
// AltGr conflation: `get_modifiers_state` ORs Alt and AltGr, so an
// AltGr+key press satisfies `Modifier::Alt`. Theoretical collision only;
// fix at `get_modifiers_state` if a real bug surfaces.
let mut v = Vec::new(); let mut v = Vec::new();
if cfg!(any(target_os = "macos", target_os = "ios")) { let primary = if cfg!(target_os = "macos") { command } else { ctrl };
if command { v.push(Modifier::Primary); } if primary { v.push(Modifier::Primary); }
if ctrl { v.push(Modifier::Ctrl); }
} else {
if ctrl { v.push(Modifier::Primary); }
}
if alt { v.push(Modifier::Alt); } if alt { v.push(Modifier::Alt); }
if shift { v.push(Modifier::Shift); } if shift { v.push(Modifier::Shift); }
v v
@@ -177,19 +126,7 @@ pub fn event_to_key_name(event: &rdev::Event) -> Option<String> {
}; };
Some(match key { Some(match key {
Key::Delete => "delete".into(), Key::Delete => "delete".into(),
Key::Backspace => "backspace".into(), Key::Return => "enter".into(),
Key::Tab => "tab".into(),
Key::Space => "space".into(),
Key::Home => "home".into(),
Key::End => "end".into(),
Key::PageUp => "page_up".into(),
Key::PageDown => "page_down".into(),
Key::Insert => "insert".into(),
// Numpad Enter (`KpReturn`) shares the "enter" name with the main
// Return key — matches the Web matcher (`NumpadEnter` -> "enter") and
// matches user expectation that the two physical Enters are
// interchangeable for shortcuts.
Key::Return | Key::KpReturn => "enter".into(),
Key::LeftArrow => "arrow_left".into(), Key::LeftArrow => "arrow_left".into(),
Key::RightArrow => "arrow_right".into(), Key::RightArrow => "arrow_right".into(),
Key::UpArrow => "arrow_up".into(), Key::UpArrow => "arrow_up".into(),
@@ -220,7 +157,6 @@ pub fn event_to_key_name(event: &rdev::Event) -> Option<String> {
Key::KeyX => "x".into(), Key::KeyX => "x".into(),
Key::KeyY => "y".into(), Key::KeyY => "y".into(),
Key::KeyZ => "z".into(), Key::KeyZ => "z".into(),
Key::Num0 => "digit0".into(),
Key::Num1 => "digit1".into(), Key::Num1 => "digit1".into(),
Key::Num2 => "digit2".into(), Key::Num2 => "digit2".into(),
Key::Num3 => "digit3".into(), Key::Num3 => "digit3".into(),
@@ -230,18 +166,6 @@ pub fn event_to_key_name(event: &rdev::Event) -> Option<String> {
Key::Num7 => "digit7".into(), Key::Num7 => "digit7".into(),
Key::Num8 => "digit8".into(), Key::Num8 => "digit8".into(),
Key::Num9 => "digit9".into(), Key::Num9 => "digit9".into(),
Key::F1 => "f1".into(),
Key::F2 => "f2".into(),
Key::F3 => "f3".into(),
Key::F4 => "f4".into(),
Key::F5 => "f5".into(),
Key::F6 => "f6".into(),
Key::F7 => "f7".into(),
Key::F8 => "f8".into(),
Key::F9 => "f9".into(),
Key::F10 => "f10".into(),
Key::F11 => "f11".into(),
Key::F12 => "f12".into(),
_ => return None, _ => return None,
}) })
} }
@@ -256,91 +180,30 @@ pub fn reload_from_config() {
let parsed = if raw.is_empty() { let parsed = if raw.is_empty() {
Bindings::default() Bindings::default()
} else { } else {
match serde_json::from_str(&raw) { serde_json::from_str(&raw).unwrap_or_default()
Ok(parsed) => parsed,
Err(e) => {
log::warn!("Failed to parse keyboard shortcut config: {}", e);
Bindings::default()
}
}
}; };
match CACHE.write() { if let Ok(mut w) = CACHE.write() {
Ok(mut w) => { *w = Arc::new(parsed);
*w = Arc::new(parsed);
}
Err(poison) => {
log::error!("Keyboard shortcut cache write lock is poisoned");
*poison.into_inner() = Arc::new(parsed);
}
} }
} }
/// Snapshot of the currently cached bindings. Cheap (one atomic increment) — /// Snapshot of the currently cached bindings. Cheap (one atomic increment) —
/// safe to call on every keystroke. /// safe to call on every keystroke.
pub fn current() -> Arc<Bindings> { pub fn current() -> Arc<Bindings> {
match CACHE.read() { CACHE
Ok(b) => Arc::clone(&b), .read()
Err(poison) => { .map(|b| Arc::clone(&b))
log::error!("Keyboard shortcut cache read lock is poisoned"); .unwrap_or_else(|_| Arc::new(Bindings::default()))
Arc::clone(&poison.into_inner())
}
}
} }
/// Match an `rdev::Event` against the cached bindings. Returns the matched /// Match an `rdev::Event` against the cached bindings. Returns the matched
/// action id, or `None` if no binding fires. The Flutter side ignores unknown /// action id, or `None` if no binding fires. The Flutter side ignores unknown
/// action ids (logged as "no handler"), so no whitelist check is needed here. /// action ids (logged as "no handler"), so no whitelist check is needed here.
///
/// ── Two known minor warts. DO NOT add global state to "fix" either: ──
///
/// 1. Orphan KeyRelease forwarded to peer.
/// When a shortcut matches we eat the KeyPress, but the matching
/// KeyRelease (whose `event_type` returns None from `event_to_key_name`)
/// still flows through to the peer. The remote sees a release for a
/// press it never received. Every input server we forward to ignores
/// releases for unpressed keys, so user-visible impact is nil — the
/// pre-existing hard-coded screenshot-shortcut path had the same shape
/// for years without a single bug report.
///
/// 2. OS auto-repeat re-dispatches a held shortcut.
/// rdev does not expose an `is_repeat` flag, so a held combo
/// (Cmd+Alt+Shift+P) would dispatch every ~30-50ms while the keys are
/// down — toggle actions oscillate, screenshot fires many times. In
/// practice the OS initial auto-repeat delay is ~250ms and a normal
/// shortcut press is 50-100ms, so the user has to *deliberately* hold
/// the combo to hit this. The Web side gets a free fix via the
/// browser's `KeyboardEvent.repeat`; on native we accept the wart.
///
/// The "fix" for either would be a process-global `HashSet<rdev::Key>` (or
/// equivalent) with paired insert-on-press / remove-on-release logic in
/// both `process_event*` paths plus a clear-on-leave hook. The cost:
///
/// * Lock contention on the hot keystroke path.
/// * Three input sources (rdev grab, Flutter raw key, Flutter USB HID)
/// all converge to `rdev::Key`, so correctness depends on
/// `rdev::key_from_code` / `rdev::usb_hid_key_from_code` /
/// `rdev::get_win_key` agreeing on the same physical key — the project
/// already has scattered swap_modifier_key / ControlLeft↔MetaLeft
/// fixups for places where they historically *didn't* agree. Any new
/// mismatch silently leaks the set; "shortcut stopped responding"
/// after a stuck entry is a worse failure mode than "shortcut fired
/// twice."
/// * Leak risk on focus loss / disconnect, requiring a clear hook the
/// callers must remember to invoke.
/// * Two new code paths to keep in lockstep with two existing keyboard
/// pipelines.
///
/// For two warts whose user-visible impact is nil-to-marginal, that
/// trade-off goes the wrong way. Leave it. If a real user bug shows up
/// here, revisit then with concrete repro — not pre-emptively.
pub fn match_event(event: &rdev::Event) -> Option<String> { pub fn match_event(event: &rdev::Event) -> Option<String> {
let bindings = current(); let bindings = current();
if !bindings.enabled || bindings.pass_through { if !bindings.enabled {
return None; return None;
} }
// Note: `match_normalized` re-checks both flags below — this short-circuit
// is just to avoid the `event_to_key_name` + `get_modifiers_state` work
// in the common bypass case.
let key_name = event_to_key_name(event)?; let key_name = event_to_key_name(event)?;
let (alt, ctrl, shift, command) = let (alt, ctrl, shift, command) =
crate::keyboard::client::get_modifiers_state(false, false, false, false); crate::keyboard::client::get_modifiers_state(false, false, false, false);
@@ -348,39 +211,6 @@ pub fn match_event(event: &rdev::Event) -> Option<String> {
match_normalized(&key_name, &mods, &bindings).map(str::to_owned) match_normalized(&key_name, &mods, &bindings).map(str::to_owned)
} }
/// Match `event` against the cached bindings; if it matched, push a
/// `shortcut_triggered` Flutter session event and return `true` so the caller
/// can `return` early. Returns `false` when no shortcut fired (caller should
/// continue with normal key handling).
///
/// `session_id`:
/// * `Some(&id)` — Flutter FFI path: dispatch to the exact session whose key
/// event we're processing. No dependence on the global focus tracker.
/// * `None` — rdev grab loop: the loop is process-wide and has no way to know
/// which Flutter session id the keystroke was meant for, so route to the
/// globally-current session via `flutter::get_cur_session_id()`.
#[cfg(feature = "flutter")]
pub fn try_dispatch(
session_id: Option<&hbb_common::SessionID>,
event: &rdev::Event,
keyboard_mode: &str,
) -> bool {
let Some(action_id) = match_event(event) else {
return false;
};
let resolved;
let sid = match session_id {
Some(id) => id,
None => {
resolved = crate::flutter::get_cur_session_id();
&resolved
}
};
crate::keyboard::release_remote_keys(keyboard_mode);
crate::flutter::push_session_event(sid, "shortcut_triggered", vec![("action", &action_id)]);
true
}
fn mods_bits(m: &[Modifier]) -> u8 { fn mods_bits(m: &[Modifier]) -> u8 {
let mut bits = 0u8; let mut bits = 0u8;
for x in m { for x in m {
@@ -388,12 +218,6 @@ fn mods_bits(m: &[Modifier]) -> u8 {
Modifier::Primary => 1, Modifier::Primary => 1,
Modifier::Alt => 2, Modifier::Alt => 2,
Modifier::Shift => 4, Modifier::Shift => 4,
// macOS users can bind shortcuts that use Control independently
// of Command. On Win/Linux this variant should never appear in a
// saved binding (`normalize_modifiers` collapses Ctrl into
// Primary), but we still give it a distinct bit so a hand-edited
// config can't accidentally collide with another modifier.
Modifier::Ctrl => 8,
}; };
} }
bits bits
@@ -407,65 +231,6 @@ fn mods_equal(a: &[Modifier], b: &[Modifier]) -> bool {
mod tests { mod tests {
use super::*; use super::*;
fn make_press(k: rdev::Key) -> rdev::Event {
rdev::Event {
time: std::time::SystemTime::now(),
unicode: None,
platform_code: 0,
position_code: 0,
event_type: rdev::EventType::KeyPress(k),
usb_hid: 0,
#[cfg(any(target_os = "windows", target_os = "macos"))]
extra_data: 0,
}
}
#[test]
fn event_to_key_name_handles_f_keys() {
use rdev::Key;
assert_eq!(event_to_key_name(&make_press(Key::F1)), Some("f1".into()));
assert_eq!(event_to_key_name(&make_press(Key::F5)), Some("f5".into()));
assert_eq!(event_to_key_name(&make_press(Key::F12)), Some("f12".into()));
}
/// Cross-language parity for default bindings. The fixture file is the
/// shared source of truth — Dart has a mirror test against the same file
/// (`kDefaultShortcutBindings matches fixture` in
/// `flutter/test/keyboard_shortcuts_test.dart`). Any drift on either
/// side breaks one of the two tests.
#[test]
fn default_bindings_match_fixture_json() {
let fixture: serde_json::Value = serde_json::from_str(include_str!(
"../../flutter/test/fixtures/default_keyboard_shortcuts.json"
))
.expect("fixture is valid JSON");
let actual: serde_json::Value =
serde_json::to_value(default_bindings()).expect("serialize defaults");
assert_eq!(
fixture, actual,
"default_bindings() drifted from \
flutter/test/fixtures/default_keyboard_shortcuts.json — update \
shortcuts.rs, the fixture, and Dart kDefaultShortcutBindings together"
);
}
#[test]
fn event_to_key_name_treats_numpad_enter_as_enter() {
use rdev::{Event, EventType, Key};
let make = |k: Key| Event {
time: std::time::SystemTime::now(),
unicode: None,
platform_code: 0,
position_code: 0,
event_type: EventType::KeyPress(k),
usb_hid: 0,
#[cfg(any(target_os = "windows", target_os = "macos"))]
extra_data: 0,
};
assert_eq!(event_to_key_name(&make(Key::Return)), Some("enter".into()));
assert_eq!(event_to_key_name(&make(Key::KpReturn)), Some("enter".into()));
}
#[test] #[test]
fn bindings_round_trip_json() { fn bindings_round_trip_json() {
let json = r#"{ let json = r#"{
@@ -495,10 +260,8 @@ mod tests {
assert!(actions.contains(&action_id::SWITCH_DISPLAY_NEXT)); assert!(actions.contains(&action_id::SWITCH_DISPLAY_NEXT));
assert!(actions.contains(&action_id::SWITCH_DISPLAY_PREV)); assert!(actions.contains(&action_id::SWITCH_DISPLAY_PREV));
assert!(actions.contains(&action_id::SCREENSHOT)); assert!(actions.contains(&action_id::SCREENSHOT));
assert!(actions.contains(&action_id::TOGGLE_SHOW_REMOTE_CURSOR)); assert!(actions.contains(&"switch_tab_1"));
assert!(actions.contains(&action_id::TOGGLE_MUTE)); assert!(actions.contains(&"switch_tab_9"));
assert!(actions.contains(&action_id::TOGGLE_BLOCK_INPUT));
assert!(actions.contains(&action_id::TOGGLE_CHAT));
// every default binding includes the three-modifier prefix // every default binding includes the three-modifier prefix
for b in &defaults { for b in &defaults {
assert!(b.mods.contains(&Modifier::Primary)); assert!(b.mods.contains(&Modifier::Primary));
@@ -511,38 +274,23 @@ mod tests {
match_normalized(key, mods, b) match_normalized(key, mods, b)
} }
#[test]
fn match_returns_none_when_pass_through() {
let bindings = Bindings {
enabled: true,
pass_through: true,
bindings: default_bindings(),
};
let result = match_normalized(
"p",
&[Modifier::Primary, Modifier::Alt, Modifier::Shift],
&bindings,
);
assert_eq!(result, None);
}
#[test] #[test]
fn match_returns_none_when_disabled() { fn match_returns_none_when_disabled() {
let bindings = Bindings { enabled: false, pass_through: false, bindings: default_bindings() }; let bindings = Bindings { enabled: false, bindings: default_bindings() };
let result = match_for_test("p", &[Modifier::Primary, Modifier::Alt, Modifier::Shift], &bindings); let result = match_for_test("p", &[Modifier::Primary, Modifier::Alt, Modifier::Shift], &bindings);
assert_eq!(result, None); assert_eq!(result, None);
} }
#[test] #[test]
fn match_screenshot_when_enabled() { fn match_screenshot_when_enabled() {
let bindings = Bindings { enabled: true, pass_through: false, bindings: default_bindings() }; let bindings = Bindings { enabled: true, bindings: default_bindings() };
let result = match_for_test("p", &[Modifier::Primary, Modifier::Alt, Modifier::Shift], &bindings); let result = match_for_test("p", &[Modifier::Primary, Modifier::Alt, Modifier::Shift], &bindings);
assert_eq!(result, Some(action_id::SCREENSHOT)); assert_eq!(result, Some(action_id::SCREENSHOT));
} }
#[test] #[test]
fn match_returns_none_when_modifiers_partial() { fn match_returns_none_when_modifiers_partial() {
let bindings = Bindings { enabled: true, pass_through: false, bindings: default_bindings() }; let bindings = Bindings { enabled: true, bindings: default_bindings() };
// missing Shift // missing Shift
let result = match_for_test("p", &[Modifier::Primary, Modifier::Alt], &bindings); let result = match_for_test("p", &[Modifier::Primary, Modifier::Alt], &bindings);
assert_eq!(result, None); assert_eq!(result, None);
@@ -550,7 +298,7 @@ mod tests {
#[test] #[test]
fn match_does_not_fire_on_extra_unbound_keys() { fn match_does_not_fire_on_extra_unbound_keys() {
let bindings = Bindings { enabled: true, pass_through: false, bindings: default_bindings() }; let bindings = Bindings { enabled: true, bindings: default_bindings() };
let result = match_for_test("z", &[Modifier::Primary, Modifier::Alt, Modifier::Shift], &bindings); let result = match_for_test("z", &[Modifier::Primary, Modifier::Alt, Modifier::Shift], &bindings);
assert_eq!(result, None); assert_eq!(result, None);
} }
@@ -561,7 +309,6 @@ mod tests {
// treat the modifier list as a set, not a multiset. // treat the modifier list as a set, not a multiset.
let bindings = Bindings { let bindings = Bindings {
enabled: true, enabled: true,
pass_through: false,
bindings: vec![Binding { bindings: vec![Binding {
action: "x".into(), action: "x".into(),
mods: vec![Modifier::Primary, Modifier::Alt], mods: vec![Modifier::Primary, Modifier::Alt],
@@ -584,10 +331,9 @@ mod tests {
fn modifier_normalization_primary_resolves_per_os() { fn modifier_normalization_primary_resolves_per_os() {
// On Win/Linux: pressing Ctrl satisfies Primary // On Win/Linux: pressing Ctrl satisfies Primary
let mods = normalize_modifiers(/*alt=*/true, /*ctrl=*/true, /*shift=*/true, /*command=*/false); let mods = normalize_modifiers(/*alt=*/true, /*ctrl=*/true, /*shift=*/true, /*command=*/false);
if cfg!(any(target_os = "macos", target_os = "ios")) { if cfg!(target_os = "macos") {
// On Apple platforms Ctrl is NOT primary // On macOS Ctrl is NOT primary
assert!(!mods.contains(&Modifier::Primary)); assert!(!mods.contains(&Modifier::Primary));
assert!(mods.contains(&Modifier::Ctrl));
} else { } else {
assert!(mods.contains(&Modifier::Primary)); assert!(mods.contains(&Modifier::Primary));
} }
@@ -596,9 +342,9 @@ mod tests {
} }
#[test] #[test]
fn modifier_normalization_command_is_primary_on_apple() { fn modifier_normalization_command_is_primary_on_mac() {
let mods = normalize_modifiers(true, false, true, /*command=*/true); let mods = normalize_modifiers(true, false, true, /*command=*/true);
if cfg!(any(target_os = "macos", target_os = "ios")) { if cfg!(target_os = "macos") {
assert!(mods.contains(&Modifier::Primary)); assert!(mods.contains(&Modifier::Primary));
} else { } else {
// On Win/Linux Command/Meta is NOT primary // On Win/Linux Command/Meta is NOT primary
@@ -606,110 +352,6 @@ mod tests {
} }
} }
#[test]
fn match_refuses_zero_modifier_bindings() {
// Defense-in-depth: a hand-edited config with empty `mods` must NOT
// turn every plain "P" press into a screenshot shortcut, which would
// swallow all typing in the remote session. The recording dialog
// already refuses to save such bindings, but the matcher must hold
// the line independently.
let bindings = Bindings {
enabled: true,
pass_through: false,
bindings: vec![Binding {
action: "screenshot".into(),
mods: vec![],
key: "p".into(),
}],
};
assert_eq!(match_normalized("p", &[], &bindings), None);
// Even with extra modifiers held by the user, a zero-mod binding
// still doesn't match (no shape of held modifiers can equal the
// empty saved set after the empty-check skips the entry).
assert_eq!(
match_normalized("p", &[Modifier::Primary], &bindings),
None,
);
}
/// Cross-language parity for the full set of shortcut-bindable key
/// names (not just the defaults). The fixture lists every name the
/// matcher accepts; this test verifies the (rdev::Key → name) round-trip
/// covers exactly that set. Dart has a mirror test against the same
/// fixture (`logicalKeyName covers the supported-keys fixture` in
/// `flutter/test/keyboard_shortcuts_test.dart`).
///
/// Adding a key requires updates in three places: the fixture, this
/// table, and the Dart `logicalKeyName` — that's the price of the
/// parity guarantee. Drift on any side breaks one of the two tests.
#[test]
fn supported_keys_match_fixture() {
use rdev::Key;
use std::collections::BTreeSet;
let table: &[(&str, Key)] = &[
("a", Key::KeyA), ("b", Key::KeyB), ("c", Key::KeyC),
("d", Key::KeyD), ("e", Key::KeyE), ("f", Key::KeyF),
("g", Key::KeyG), ("h", Key::KeyH), ("i", Key::KeyI),
("j", Key::KeyJ), ("k", Key::KeyK), ("l", Key::KeyL),
("m", Key::KeyM), ("n", Key::KeyN), ("o", Key::KeyO),
("p", Key::KeyP), ("q", Key::KeyQ), ("r", Key::KeyR),
("s", Key::KeyS), ("t", Key::KeyT), ("u", Key::KeyU),
("v", Key::KeyV), ("w", Key::KeyW), ("x", Key::KeyX),
("y", Key::KeyY), ("z", Key::KeyZ),
("digit0", Key::Num0), ("digit1", Key::Num1),
("digit2", Key::Num2), ("digit3", Key::Num3),
("digit4", Key::Num4), ("digit5", Key::Num5),
("digit6", Key::Num6), ("digit7", Key::Num7),
("digit8", Key::Num8), ("digit9", Key::Num9),
("f1", Key::F1), ("f2", Key::F2), ("f3", Key::F3),
("f4", Key::F4), ("f5", Key::F5), ("f6", Key::F6),
("f7", Key::F7), ("f8", Key::F8), ("f9", Key::F9),
("f10", Key::F10), ("f11", Key::F11), ("f12", Key::F12),
("delete", Key::Delete),
("backspace", Key::Backspace),
("tab", Key::Tab),
("space", Key::Space),
("enter", Key::Return),
("enter", Key::KpReturn),
("arrow_left", Key::LeftArrow),
("arrow_right", Key::RightArrow),
("arrow_up", Key::UpArrow),
("arrow_down", Key::DownArrow),
("home", Key::Home),
("end", Key::End),
("page_up", Key::PageUp),
("page_down", Key::PageDown),
("insert", Key::Insert),
];
// Round-trip: every entry in the table must map through
// event_to_key_name to its declared name.
for (name, key) in table {
assert_eq!(
event_to_key_name(&make_press(*key)).as_deref(),
Some(*name),
"rdev::Key::{:?} should map to {:?}",
key, name,
);
}
// The set of names produced by the table must equal the fixture.
let actual: BTreeSet<&str> = table.iter().map(|(n, _)| *n).collect();
let fixture_raw: Vec<String> = serde_json::from_str(include_str!(
"../../flutter/test/fixtures/supported_shortcut_keys.json"
))
.expect("fixture is valid JSON");
let expected: BTreeSet<&str> =
fixture_raw.iter().map(String::as_str).collect();
assert_eq!(
actual, expected,
"event_to_key_name vocabulary drifted from \
flutter/test/fixtures/supported_shortcut_keys.json — update \
shortcuts.rs, the fixture, and Dart logicalKeyName together"
);
}
#[test] #[test]
fn reload_handles_missing_and_invalid_json() { fn reload_handles_missing_and_invalid_json() {
// empty (no value set) → defaults // empty (no value set) → defaults

View File

@@ -743,39 +743,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Display Name", "اسم العرض"), ("Display Name", "اسم العرض"),
("password-hidden-tip", "كلمة المرور مخفية"), ("password-hidden-tip", "كلمة المرور مخفية"),
("preset-password-in-use-tip", "كلمة المرور المحددة مسبقًا قيد الاستخدام"), ("preset-password-in-use-tip", "كلمة المرور المحددة مسبقًا قيد الاستخدام"),
("Keyboard Shortcuts", ""),
("Configure shortcuts...", ""),
("Enable keyboard shortcuts in remote session", ""),
("shortcut-page-description", ""),
("shortcut-passthrough-tip", ""),
("Pass-through to remote", ""),
("Reset to defaults", ""),
("shortcut-reset-confirm-tip", ""),
("Monitor", ""),
("Keyboard", ""),
("Toggle fullscreen", ""),
("Switch to next display", ""),
("Switch to previous display", ""),
("All monitors", ""),
("Monitor #{}", ""),
("Switch to next tab", ""),
("Switch to previous tab", ""),
("Toggle session recording", ""),
("Close tab", ""),
("Toggle toolbar", ""),
("Toggle input source", ""),
("Edit", ""),
("Save", ""),
("Set Shortcut", ""),
("shortcut-recording-instruction", ""),
("shortcut-recording-press-keys-tip", ""),
("shortcut-must-include-modifiers", ""),
("shortcut-already-bound-to", ""),
("Replace", ""),
("Valid", ""),
("shortcut-mobile-physical-keyboard-tip", ""),
("shortcut-key-not-supported", ""),
("On", ""),
("Off", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -743,39 +743,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Display Name", "Імя для адлюстравання"), ("Display Name", "Імя для адлюстравання"),
("password-hidden-tip", "Зададзены пастаянны пароль (скрыты)."), ("password-hidden-tip", "Зададзены пастаянны пароль (скрыты)."),
("preset-password-in-use-tip", "Пададзены пароль цяпер выкарыстоўваецца"), ("preset-password-in-use-tip", "Пададзены пароль цяпер выкарыстоўваецца"),
("Keyboard Shortcuts", ""),
("Configure shortcuts...", ""),
("Enable keyboard shortcuts in remote session", ""),
("shortcut-page-description", ""),
("shortcut-passthrough-tip", ""),
("Pass-through to remote", ""),
("Reset to defaults", ""),
("shortcut-reset-confirm-tip", ""),
("Monitor", ""),
("Keyboard", ""),
("Toggle fullscreen", ""),
("Switch to next display", ""),
("Switch to previous display", ""),
("All monitors", ""),
("Monitor #{}", ""),
("Switch to next tab", ""),
("Switch to previous tab", ""),
("Toggle session recording", ""),
("Close tab", ""),
("Toggle toolbar", ""),
("Toggle input source", ""),
("Edit", ""),
("Save", ""),
("Set Shortcut", ""),
("shortcut-recording-instruction", ""),
("shortcut-recording-press-keys-tip", ""),
("shortcut-must-include-modifiers", ""),
("shortcut-already-bound-to", ""),
("Replace", ""),
("Valid", ""),
("shortcut-mobile-physical-keyboard-tip", ""),
("shortcut-key-not-supported", ""),
("On", ""),
("Off", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -743,39 +743,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Display Name", ""), ("Display Name", ""),
("password-hidden-tip", ""), ("password-hidden-tip", ""),
("preset-password-in-use-tip", ""), ("preset-password-in-use-tip", ""),
("Keyboard Shortcuts", ""),
("Configure shortcuts...", ""),
("Enable keyboard shortcuts in remote session", ""),
("shortcut-page-description", ""),
("shortcut-passthrough-tip", ""),
("Pass-through to remote", ""),
("Reset to defaults", ""),
("shortcut-reset-confirm-tip", ""),
("Monitor", ""),
("Keyboard", ""),
("Toggle fullscreen", ""),
("Switch to next display", ""),
("Switch to previous display", ""),
("All monitors", ""),
("Monitor #{}", ""),
("Switch to next tab", ""),
("Switch to previous tab", ""),
("Toggle session recording", ""),
("Close tab", ""),
("Toggle toolbar", ""),
("Toggle input source", ""),
("Edit", ""),
("Save", ""),
("Set Shortcut", ""),
("shortcut-recording-instruction", ""),
("shortcut-recording-press-keys-tip", ""),
("shortcut-must-include-modifiers", ""),
("shortcut-already-bound-to", ""),
("Replace", ""),
("Valid", ""),
("shortcut-mobile-physical-keyboard-tip", ""),
("shortcut-key-not-supported", ""),
("On", ""),
("Off", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -743,39 +743,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Display Name", ""), ("Display Name", ""),
("password-hidden-tip", ""), ("password-hidden-tip", ""),
("preset-password-in-use-tip", ""), ("preset-password-in-use-tip", ""),
("Keyboard Shortcuts", ""),
("Configure shortcuts...", ""),
("Enable keyboard shortcuts in remote session", ""),
("shortcut-page-description", ""),
("shortcut-passthrough-tip", ""),
("Pass-through to remote", ""),
("Reset to defaults", ""),
("shortcut-reset-confirm-tip", ""),
("Monitor", ""),
("Keyboard", ""),
("Toggle fullscreen", ""),
("Switch to next display", ""),
("Switch to previous display", ""),
("All monitors", ""),
("Monitor #{}", ""),
("Switch to next tab", ""),
("Switch to previous tab", ""),
("Toggle session recording", ""),
("Close tab", ""),
("Toggle toolbar", ""),
("Toggle input source", ""),
("Edit", ""),
("Save", ""),
("Set Shortcut", ""),
("shortcut-recording-instruction", ""),
("shortcut-recording-press-keys-tip", ""),
("shortcut-must-include-modifiers", ""),
("shortcut-already-bound-to", ""),
("Replace", ""),
("Valid", ""),
("shortcut-mobile-physical-keyboard-tip", ""),
("shortcut-key-not-supported", ""),
("On", ""),
("Off", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -746,35 +746,40 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Keyboard Shortcuts", "键盘快捷键"), ("Keyboard Shortcuts", "键盘快捷键"),
("Configure shortcuts...", "配置快捷键..."), ("Configure shortcuts...", "配置快捷键..."),
("Enable keyboard shortcuts in remote session", "在远程会话中启用键盘快捷键"), ("Enable keyboard shortcuts in remote session", "在远程会话中启用键盘快捷键"),
("shortcut-page-description", "为下列每项会话操作绑定一个组合键。每个绑定至少需要包含一个修饰符"), ("shortcut-page-description", "启用后,列出的组合键将在本地触发会话操作,而不会发送到远程端。所有快捷键必须包含 Ctrl+Alt+ShiftmacOS 上为 Cmd+Option+Shift以避免与正常输入冲突"),
("shortcut-passthrough-tip", "开启后,所有已绑定的组合键都会原样转发到远端。适合在某个组合键与远端需要使用的快捷键冲突时打开。"),
("Pass-through to remote", "穿透到远端"),
("Reset to defaults", "恢复默认设置"), ("Reset to defaults", "恢复默认设置"),
("shortcut-reset-confirm-tip", "这将以默认快捷键替换所有当前绑定。是否继续?"), ("shortcut-reset-confirm-tip", "这将以默认快捷键替换所有当前绑定。是否继续?"),
("Monitor", "显示器"), ("Session Control", "会话控制"),
("Keyboard", "键盘"), ("Toggle Fullscreen", "切换全屏"),
("Toggle fullscreen", "切换全屏"),
("Switch to next display", "切换到下一个显示器"), ("Switch to next display", "切换到下一个显示器"),
("Switch to previous display", "切换到上一个显示器"), ("Switch to previous display", "切换到上一个显示器"),
("All monitors", "所有显示器"), ("View Mode 1:1", "原始大小"),
("Monitor #{}", "{} 号显示器"), ("View Mode Shrink", "缩小"),
("Switch to next tab", "切换到下一个标签"), ("View Mode Stretch", "拉伸"),
("Switch to previous tab", "切换到上一个标签"), ("Take Screenshot", "截图"),
("Toggle session recording", "切换会话录制"), ("Toggle Audio", "切换音频"),
("Close tab", "关闭标签页"), ("Toggle Privacy Mode", "切换隐私模式"),
("Toggle toolbar", "切换工具栏可见性"), ("Toggle Recording", "切换录制"),
("Toggle input source", "切换输入"), ("Toggle Block User Input", "切换屏蔽用户输入"),
("Switch Tab 1", "切换到第 1 个标签"),
("Switch Tab 2", "切换到第 2 个标签"),
("Switch Tab 3", "切换到第 3 个标签"),
("Switch Tab 4", "切换到第 4 个标签"),
("Switch Tab 5", "切换到第 5 个标签"),
("Switch Tab 6", "切换到第 6 个标签"),
("Switch Tab 7", "切换到第 7 个标签"),
("Switch Tab 8", "切换到第 8 个标签"),
("Switch Tab 9", "切换到第 9 个标签"),
("Edit", "编辑"), ("Edit", "编辑"),
("Save", "保存"), ("Save", "保存"),
("Set Shortcut", "设置快捷键"), ("Set Shortcut", "设置快捷键"),
("shortcut-recording-instruction", "请按下您想使用的组合键。"), ("shortcut-recording-instruction", "请按下您想使用的组合键。"),
("shortcut-recording-press-keys-tip", "请按下组合键..."), ("shortcut-recording-press-keys-tip", "请按下组合键..."),
("shortcut-must-include-modifiers", "必须至少包含一个修饰符:{}"), ("shortcut-must-include-prefix", "必须包含"),
("shortcut-already-bound-to", "已绑定到"), ("shortcut-already-bound-to", "已绑定到"),
("Replace", "替换"), ("Replace", "替换"),
("Valid", "有效"), ("Valid", "有效"),
("shortcut-mobile-physical-keyboard-tip", "录制需要使用物理键盘,不支持软键盘。"), ("shortcut-mobile-physical-keyboard-tip", "录制需要使用物理键盘,不支持软键盘。"),
("shortcut-key-not-supported", "“{}” 不能用作快捷键。"),
("On", ""), ("On", ""),
("Off", ""), ("Off", ""),
].iter().cloned().collect(); ].iter().cloned().collect();

View File

@@ -743,39 +743,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Display Name", ""), ("Display Name", ""),
("password-hidden-tip", ""), ("password-hidden-tip", ""),
("preset-password-in-use-tip", ""), ("preset-password-in-use-tip", ""),
("Keyboard Shortcuts", ""),
("Configure shortcuts...", ""),
("Enable keyboard shortcuts in remote session", ""),
("shortcut-page-description", ""),
("shortcut-passthrough-tip", ""),
("Pass-through to remote", ""),
("Reset to defaults", ""),
("shortcut-reset-confirm-tip", ""),
("Monitor", ""),
("Keyboard", ""),
("Toggle fullscreen", ""),
("Switch to next display", ""),
("Switch to previous display", ""),
("All monitors", ""),
("Monitor #{}", ""),
("Switch to next tab", ""),
("Switch to previous tab", ""),
("Toggle session recording", ""),
("Close tab", ""),
("Toggle toolbar", ""),
("Toggle input source", ""),
("Edit", ""),
("Save", ""),
("Set Shortcut", ""),
("shortcut-recording-instruction", ""),
("shortcut-recording-press-keys-tip", ""),
("shortcut-must-include-modifiers", ""),
("shortcut-already-bound-to", ""),
("Replace", ""),
("Valid", ""),
("shortcut-mobile-physical-keyboard-tip", ""),
("shortcut-key-not-supported", ""),
("On", ""),
("Off", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -743,39 +743,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Display Name", ""), ("Display Name", ""),
("password-hidden-tip", ""), ("password-hidden-tip", ""),
("preset-password-in-use-tip", ""), ("preset-password-in-use-tip", ""),
("Keyboard Shortcuts", ""),
("Configure shortcuts...", ""),
("Enable keyboard shortcuts in remote session", ""),
("shortcut-page-description", ""),
("shortcut-passthrough-tip", ""),
("Pass-through to remote", ""),
("Reset to defaults", ""),
("shortcut-reset-confirm-tip", ""),
("Monitor", ""),
("Keyboard", ""),
("Toggle fullscreen", ""),
("Switch to next display", ""),
("Switch to previous display", ""),
("All monitors", ""),
("Monitor #{}", ""),
("Switch to next tab", ""),
("Switch to previous tab", ""),
("Toggle session recording", ""),
("Close tab", ""),
("Toggle toolbar", ""),
("Toggle input source", ""),
("Edit", ""),
("Save", ""),
("Set Shortcut", ""),
("shortcut-recording-instruction", ""),
("shortcut-recording-press-keys-tip", ""),
("shortcut-must-include-modifiers", ""),
("shortcut-already-bound-to", ""),
("Replace", ""),
("Valid", ""),
("shortcut-mobile-physical-keyboard-tip", ""),
("shortcut-key-not-supported", ""),
("On", ""),
("Off", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -743,39 +743,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Display Name", "Anzeigename"), ("Display Name", "Anzeigename"),
("password-hidden-tip", "Ein permanentes Passwort wurde festgelegt (ausgeblendet)."), ("password-hidden-tip", "Ein permanentes Passwort wurde festgelegt (ausgeblendet)."),
("preset-password-in-use-tip", "Das voreingestellte Passwort wird derzeit verwendet."), ("preset-password-in-use-tip", "Das voreingestellte Passwort wird derzeit verwendet."),
("Keyboard Shortcuts", ""),
("Configure shortcuts...", ""),
("Enable keyboard shortcuts in remote session", ""),
("shortcut-page-description", ""),
("shortcut-passthrough-tip", ""),
("Pass-through to remote", ""),
("Reset to defaults", ""),
("shortcut-reset-confirm-tip", ""),
("Monitor", ""),
("Keyboard", ""),
("Toggle fullscreen", ""),
("Switch to next display", ""),
("Switch to previous display", ""),
("All monitors", ""),
("Monitor #{}", ""),
("Switch to next tab", ""),
("Switch to previous tab", ""),
("Toggle session recording", ""),
("Close tab", ""),
("Toggle toolbar", ""),
("Toggle input source", ""),
("Edit", ""),
("Save", ""),
("Set Shortcut", ""),
("shortcut-recording-instruction", ""),
("shortcut-recording-press-keys-tip", ""),
("shortcut-must-include-modifiers", ""),
("shortcut-already-bound-to", ""),
("Replace", ""),
("Valid", ""),
("shortcut-mobile-physical-keyboard-tip", ""),
("shortcut-key-not-supported", ""),
("On", ""),
("Off", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -743,39 +743,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Display Name", "Εμφανιζόμενο όνομα"), ("Display Name", "Εμφανιζόμενο όνομα"),
("password-hidden-tip", ""), ("password-hidden-tip", ""),
("preset-password-in-use-tip", ""), ("preset-password-in-use-tip", ""),
("Keyboard Shortcuts", ""),
("Configure shortcuts...", ""),
("Enable keyboard shortcuts in remote session", ""),
("shortcut-page-description", ""),
("shortcut-passthrough-tip", ""),
("Pass-through to remote", ""),
("Reset to defaults", ""),
("shortcut-reset-confirm-tip", ""),
("Monitor", ""),
("Keyboard", ""),
("Toggle fullscreen", ""),
("Switch to next display", ""),
("Switch to previous display", ""),
("All monitors", ""),
("Monitor #{}", ""),
("Switch to next tab", ""),
("Switch to previous tab", ""),
("Toggle session recording", ""),
("Close tab", ""),
("Toggle toolbar", ""),
("Toggle input source", ""),
("Edit", ""),
("Save", ""),
("Set Shortcut", ""),
("shortcut-recording-instruction", ""),
("shortcut-recording-press-keys-tip", ""),
("shortcut-must-include-modifiers", ""),
("shortcut-already-bound-to", ""),
("Replace", ""),
("Valid", ""),
("shortcut-mobile-physical-keyboard-tip", ""),
("shortcut-key-not-supported", ""),
("On", ""),
("Off", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -274,14 +274,47 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("keep-awake-during-incoming-sessions-label", "Keep screen awake during incoming sessions"), ("keep-awake-during-incoming-sessions-label", "Keep screen awake during incoming sessions"),
("password-hidden-tip", "Permanent password is set (hidden)."), ("password-hidden-tip", "Permanent password is set (hidden)."),
("preset-password-in-use-tip", "Preset password is currently in use."), ("preset-password-in-use-tip", "Preset password is currently in use."),
("shortcut-page-description", "Bind a key combination to each session action below. Each binding must include at least one modifier."), ("Keyboard Shortcuts", ""),
("shortcut-passthrough-tip", "When on, every bound combination is forwarded to the remote. Useful when a binding collides with something you need on the remote."), ("Configure shortcuts...", ""),
("Enable keyboard shortcuts in remote session", ""),
("shortcut-page-description", "When enabled, listed key combinations trigger session actions locally instead of being sent to the remote. All bindings must include Ctrl+Alt+Shift (Cmd+Option+Shift on macOS) to avoid conflicts with normal typing."),
("Reset to defaults", ""),
("shortcut-reset-confirm-tip", "This will replace all current bindings with the default set. Continue?"), ("shortcut-reset-confirm-tip", "This will replace all current bindings with the default set. Continue?"),
("Session Control", ""),
("Display", ""),
("Other", ""),
("Toggle Fullscreen", ""),
("Switch to next display", ""),
("Switch to previous display", ""),
("View Mode 1:1", ""),
("View Mode Shrink", ""),
("View Mode Stretch", ""),
("Take Screenshot", ""),
("Toggle Audio", ""),
("Toggle Privacy Mode", ""),
("Toggle Recording", ""),
("Toggle Block User Input", ""),
("Switch Tab 1", ""),
("Switch Tab 2", ""),
("Switch Tab 3", ""),
("Switch Tab 4", ""),
("Switch Tab 5", ""),
("Switch Tab 6", ""),
("Switch Tab 7", ""),
("Switch Tab 8", ""),
("Switch Tab 9", ""),
("Edit", ""),
("Save", ""),
("Set Shortcut", ""),
("shortcut-recording-instruction", "Press the key combination you want to use."), ("shortcut-recording-instruction", "Press the key combination you want to use."),
("shortcut-recording-press-keys-tip", "Press a key combination..."), ("shortcut-recording-press-keys-tip", "Press a key combination..."),
("shortcut-must-include-modifiers", "Must include at least one modifier: {}"), ("shortcut-must-include-prefix", "Must include"),
("shortcut-already-bound-to", "Already bound to"), ("shortcut-already-bound-to", "Already bound to"),
("Replace", ""),
("Valid", ""),
("shortcut-mobile-physical-keyboard-tip", "Recording requires a physical keyboard. Soft keyboards are not supported."), ("shortcut-mobile-physical-keyboard-tip", "Recording requires a physical keyboard. Soft keyboards are not supported."),
("shortcut-key-not-supported", "\"{}\" can't be used as a shortcut."), ("Clear", ""),
("On", ""),
("Off", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -743,39 +743,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Display Name", ""), ("Display Name", ""),
("password-hidden-tip", ""), ("password-hidden-tip", ""),
("preset-password-in-use-tip", ""), ("preset-password-in-use-tip", ""),
("Keyboard Shortcuts", ""),
("Configure shortcuts...", ""),
("Enable keyboard shortcuts in remote session", ""),
("shortcut-page-description", ""),
("shortcut-passthrough-tip", ""),
("Pass-through to remote", ""),
("Reset to defaults", ""),
("shortcut-reset-confirm-tip", ""),
("Monitor", ""),
("Keyboard", ""),
("Toggle fullscreen", ""),
("Switch to next display", ""),
("Switch to previous display", ""),
("All monitors", ""),
("Monitor #{}", ""),
("Switch to next tab", ""),
("Switch to previous tab", ""),
("Toggle session recording", ""),
("Close tab", ""),
("Toggle toolbar", ""),
("Toggle input source", ""),
("Edit", ""),
("Save", ""),
("Set Shortcut", ""),
("shortcut-recording-instruction", ""),
("shortcut-recording-press-keys-tip", ""),
("shortcut-must-include-modifiers", ""),
("shortcut-already-bound-to", ""),
("Replace", ""),
("Valid", ""),
("shortcut-mobile-physical-keyboard-tip", ""),
("shortcut-key-not-supported", ""),
("On", ""),
("Off", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -743,39 +743,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Display Name", ""), ("Display Name", ""),
("password-hidden-tip", ""), ("password-hidden-tip", ""),
("preset-password-in-use-tip", ""), ("preset-password-in-use-tip", ""),
("Keyboard Shortcuts", ""),
("Configure shortcuts...", ""),
("Enable keyboard shortcuts in remote session", ""),
("shortcut-page-description", ""),
("shortcut-passthrough-tip", ""),
("Pass-through to remote", ""),
("Reset to defaults", ""),
("shortcut-reset-confirm-tip", ""),
("Monitor", ""),
("Keyboard", ""),
("Toggle fullscreen", ""),
("Switch to next display", ""),
("Switch to previous display", ""),
("All monitors", ""),
("Monitor #{}", ""),
("Switch to next tab", ""),
("Switch to previous tab", ""),
("Toggle session recording", ""),
("Close tab", ""),
("Toggle toolbar", ""),
("Toggle input source", ""),
("Edit", ""),
("Save", ""),
("Set Shortcut", ""),
("shortcut-recording-instruction", ""),
("shortcut-recording-press-keys-tip", ""),
("shortcut-must-include-modifiers", ""),
("shortcut-already-bound-to", ""),
("Replace", ""),
("Valid", ""),
("shortcut-mobile-physical-keyboard-tip", ""),
("shortcut-key-not-supported", ""),
("On", ""),
("Off", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -743,39 +743,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Display Name", ""), ("Display Name", ""),
("password-hidden-tip", ""), ("password-hidden-tip", ""),
("preset-password-in-use-tip", ""), ("preset-password-in-use-tip", ""),
("Keyboard Shortcuts", ""),
("Configure shortcuts...", ""),
("Enable keyboard shortcuts in remote session", ""),
("shortcut-page-description", ""),
("shortcut-passthrough-tip", ""),
("Pass-through to remote", ""),
("Reset to defaults", ""),
("shortcut-reset-confirm-tip", ""),
("Monitor", ""),
("Keyboard", ""),
("Toggle fullscreen", ""),
("Switch to next display", ""),
("Switch to previous display", ""),
("All monitors", ""),
("Monitor #{}", ""),
("Switch to next tab", ""),
("Switch to previous tab", ""),
("Toggle session recording", ""),
("Close tab", ""),
("Toggle toolbar", ""),
("Toggle input source", ""),
("Edit", ""),
("Save", ""),
("Set Shortcut", ""),
("shortcut-recording-instruction", ""),
("shortcut-recording-press-keys-tip", ""),
("shortcut-must-include-modifiers", ""),
("shortcut-already-bound-to", ""),
("Replace", ""),
("Valid", ""),
("shortcut-mobile-physical-keyboard-tip", ""),
("shortcut-key-not-supported", ""),
("On", ""),
("Off", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -743,39 +743,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Display Name", ""), ("Display Name", ""),
("password-hidden-tip", ""), ("password-hidden-tip", ""),
("preset-password-in-use-tip", ""), ("preset-password-in-use-tip", ""),
("Keyboard Shortcuts", ""),
("Configure shortcuts...", ""),
("Enable keyboard shortcuts in remote session", ""),
("shortcut-page-description", ""),
("shortcut-passthrough-tip", ""),
("Pass-through to remote", ""),
("Reset to defaults", ""),
("shortcut-reset-confirm-tip", ""),
("Monitor", ""),
("Keyboard", ""),
("Toggle fullscreen", ""),
("Switch to next display", ""),
("Switch to previous display", ""),
("All monitors", ""),
("Monitor #{}", ""),
("Switch to next tab", ""),
("Switch to previous tab", ""),
("Toggle session recording", ""),
("Close tab", ""),
("Toggle toolbar", ""),
("Toggle input source", ""),
("Edit", ""),
("Save", ""),
("Set Shortcut", ""),
("shortcut-recording-instruction", ""),
("shortcut-recording-press-keys-tip", ""),
("shortcut-must-include-modifiers", ""),
("shortcut-already-bound-to", ""),
("Replace", ""),
("Valid", ""),
("shortcut-mobile-physical-keyboard-tip", ""),
("shortcut-key-not-supported", ""),
("On", ""),
("Off", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -743,39 +743,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Display Name", ""), ("Display Name", ""),
("password-hidden-tip", ""), ("password-hidden-tip", ""),
("preset-password-in-use-tip", ""), ("preset-password-in-use-tip", ""),
("Keyboard Shortcuts", ""),
("Configure shortcuts...", ""),
("Enable keyboard shortcuts in remote session", ""),
("shortcut-page-description", ""),
("shortcut-passthrough-tip", ""),
("Pass-through to remote", ""),
("Reset to defaults", ""),
("shortcut-reset-confirm-tip", ""),
("Monitor", ""),
("Keyboard", ""),
("Toggle fullscreen", ""),
("Switch to next display", ""),
("Switch to previous display", ""),
("All monitors", ""),
("Monitor #{}", ""),
("Switch to next tab", ""),
("Switch to previous tab", ""),
("Toggle session recording", ""),
("Close tab", ""),
("Toggle toolbar", ""),
("Toggle input source", ""),
("Edit", ""),
("Save", ""),
("Set Shortcut", ""),
("shortcut-recording-instruction", ""),
("shortcut-recording-press-keys-tip", ""),
("shortcut-must-include-modifiers", ""),
("shortcut-already-bound-to", ""),
("Replace", ""),
("Valid", ""),
("shortcut-mobile-physical-keyboard-tip", ""),
("shortcut-key-not-supported", ""),
("On", ""),
("Off", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -743,39 +743,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Display Name", ""), ("Display Name", ""),
("password-hidden-tip", ""), ("password-hidden-tip", ""),
("preset-password-in-use-tip", ""), ("preset-password-in-use-tip", ""),
("Keyboard Shortcuts", ""),
("Configure shortcuts...", ""),
("Enable keyboard shortcuts in remote session", ""),
("shortcut-page-description", ""),
("shortcut-passthrough-tip", ""),
("Pass-through to remote", ""),
("Reset to defaults", ""),
("shortcut-reset-confirm-tip", ""),
("Monitor", ""),
("Keyboard", ""),
("Toggle fullscreen", ""),
("Switch to next display", ""),
("Switch to previous display", ""),
("All monitors", ""),
("Monitor #{}", ""),
("Switch to next tab", ""),
("Switch to previous tab", ""),
("Toggle session recording", ""),
("Close tab", ""),
("Toggle toolbar", ""),
("Toggle input source", ""),
("Edit", ""),
("Save", ""),
("Set Shortcut", ""),
("shortcut-recording-instruction", ""),
("shortcut-recording-press-keys-tip", ""),
("shortcut-must-include-modifiers", ""),
("shortcut-already-bound-to", ""),
("Replace", ""),
("Valid", ""),
("shortcut-mobile-physical-keyboard-tip", ""),
("shortcut-key-not-supported", ""),
("On", ""),
("Off", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -743,39 +743,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Display Name", "Nom daffichage"), ("Display Name", "Nom daffichage"),
("password-hidden-tip", "Le mot de passe permanent est défini (masqué)."), ("password-hidden-tip", "Le mot de passe permanent est défini (masqué)."),
("preset-password-in-use-tip", "Le mot de passe prédéfini est actuellement utilisé."), ("preset-password-in-use-tip", "Le mot de passe prédéfini est actuellement utilisé."),
("Keyboard Shortcuts", ""),
("Configure shortcuts...", ""),
("Enable keyboard shortcuts in remote session", ""),
("shortcut-page-description", ""),
("shortcut-passthrough-tip", ""),
("Pass-through to remote", ""),
("Reset to defaults", ""),
("shortcut-reset-confirm-tip", ""),
("Monitor", ""),
("Keyboard", ""),
("Toggle fullscreen", ""),
("Switch to next display", ""),
("Switch to previous display", ""),
("All monitors", ""),
("Monitor #{}", ""),
("Switch to next tab", ""),
("Switch to previous tab", ""),
("Toggle session recording", ""),
("Close tab", ""),
("Toggle toolbar", ""),
("Toggle input source", ""),
("Edit", ""),
("Save", ""),
("Set Shortcut", ""),
("shortcut-recording-instruction", ""),
("shortcut-recording-press-keys-tip", ""),
("shortcut-must-include-modifiers", ""),
("shortcut-already-bound-to", ""),
("Replace", ""),
("Valid", ""),
("shortcut-mobile-physical-keyboard-tip", ""),
("shortcut-key-not-supported", ""),
("On", ""),
("Off", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -743,39 +743,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Display Name", ""), ("Display Name", ""),
("password-hidden-tip", ""), ("password-hidden-tip", ""),
("preset-password-in-use-tip", ""), ("preset-password-in-use-tip", ""),
("Keyboard Shortcuts", ""),
("Configure shortcuts...", ""),
("Enable keyboard shortcuts in remote session", ""),
("shortcut-page-description", ""),
("shortcut-passthrough-tip", ""),
("Pass-through to remote", ""),
("Reset to defaults", ""),
("shortcut-reset-confirm-tip", ""),
("Monitor", ""),
("Keyboard", ""),
("Toggle fullscreen", ""),
("Switch to next display", ""),
("Switch to previous display", ""),
("All monitors", ""),
("Monitor #{}", ""),
("Switch to next tab", ""),
("Switch to previous tab", ""),
("Toggle session recording", ""),
("Close tab", ""),
("Toggle toolbar", ""),
("Toggle input source", ""),
("Edit", ""),
("Save", ""),
("Set Shortcut", ""),
("shortcut-recording-instruction", ""),
("shortcut-recording-press-keys-tip", ""),
("shortcut-must-include-modifiers", ""),
("shortcut-already-bound-to", ""),
("Replace", ""),
("Valid", ""),
("shortcut-mobile-physical-keyboard-tip", ""),
("shortcut-key-not-supported", ""),
("On", ""),
("Off", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -654,7 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Accessible devices", "એક્સેસિબલ ઉપકરણો"), ("Accessible devices", "એક્સેસિબલ ઉપકરણો"),
("upgrade_remote_rustdesk_client_to_{}_tip", "રિમોટ ક્લાયન્ટને {} માં અપગ્રેડ કરો"), ("upgrade_remote_rustdesk_client_to_{}_tip", "રિમોટ ક્લાયન્ટને {} માં અપગ્રેડ કરો"),
("d3d_render_tip", "D3D રેન્ડરિંગ વાપરો"), ("d3d_render_tip", "D3D રેન્ડરિંગ વાપરો"),
("Use D3D rendering", ""),
("Printer", "પ્રિન્ટર"), ("Printer", "પ્રિન્ટર"),
("printer-os-requirement-tip", "પ્રિન્ટિંગ માટે Windows જરૂરી છે."), ("printer-os-requirement-tip", "પ્રિન્ટિંગ માટે Windows જરૂરી છે."),
("printer-requires-installed-{}-client-tip", "આ માટે {} ક્લાયન્ટ ઇન્સ્ટોલ હોવું જોઈએ."), ("printer-requires-installed-{}-client-tip", "આ માટે {} ક્લાયન્ટ ઇન્સ્ટોલ હોવું જોઈએ."),
@@ -743,39 +742,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Display Name", "ડિસ્પ્લે નામ"), ("Display Name", "ડિસ્પ્લે નામ"),
("password-hidden-tip", "સુરક્ષા માટે પાસવર્ડ છુપાવેલ છે."), ("password-hidden-tip", "સુરક્ષા માટે પાસવર્ડ છુપાવેલ છે."),
("preset-password-in-use-tip", "પ્રીસેટ પાસવર્ડ વપરાશમાં છે."), ("preset-password-in-use-tip", "પ્રીસેટ પાસવર્ડ વપરાશમાં છે."),
("Keyboard Shortcuts", ""),
("Configure shortcuts...", ""),
("Enable keyboard shortcuts in remote session", ""),
("shortcut-page-description", ""),
("shortcut-passthrough-tip", ""),
("Pass-through to remote", ""),
("Reset to defaults", ""),
("shortcut-reset-confirm-tip", ""),
("Monitor", ""),
("Keyboard", ""),
("Toggle fullscreen", ""),
("Switch to next display", ""),
("Switch to previous display", ""),
("All monitors", ""),
("Monitor #{}", ""),
("Switch to next tab", ""),
("Switch to previous tab", ""),
("Toggle session recording", ""),
("Close tab", ""),
("Toggle toolbar", ""),
("Toggle input source", ""),
("Edit", ""),
("Save", ""),
("Set Shortcut", ""),
("shortcut-recording-instruction", ""),
("shortcut-recording-press-keys-tip", ""),
("shortcut-must-include-modifiers", ""),
("shortcut-already-bound-to", ""),
("Replace", ""),
("Valid", ""),
("shortcut-mobile-physical-keyboard-tip", ""),
("shortcut-key-not-supported", ""),
("On", ""),
("Off", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -743,39 +743,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Display Name", ""), ("Display Name", ""),
("password-hidden-tip", ""), ("password-hidden-tip", ""),
("preset-password-in-use-tip", ""), ("preset-password-in-use-tip", ""),
("Keyboard Shortcuts", ""),
("Configure shortcuts...", ""),
("Enable keyboard shortcuts in remote session", ""),
("shortcut-page-description", ""),
("shortcut-passthrough-tip", ""),
("Pass-through to remote", ""),
("Reset to defaults", ""),
("shortcut-reset-confirm-tip", ""),
("Monitor", ""),
("Keyboard", ""),
("Toggle fullscreen", ""),
("Switch to next display", ""),
("Switch to previous display", ""),
("All monitors", ""),
("Monitor #{}", ""),
("Switch to next tab", ""),
("Switch to previous tab", ""),
("Toggle session recording", ""),
("Close tab", ""),
("Toggle toolbar", ""),
("Toggle input source", ""),
("Edit", ""),
("Save", ""),
("Set Shortcut", ""),
("shortcut-recording-instruction", ""),
("shortcut-recording-press-keys-tip", ""),
("shortcut-must-include-modifiers", ""),
("shortcut-already-bound-to", ""),
("Replace", ""),
("Valid", ""),
("shortcut-mobile-physical-keyboard-tip", ""),
("shortcut-key-not-supported", ""),
("On", ""),
("Off", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -654,7 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Accessible devices", "सुलभ डिवाइस"), ("Accessible devices", "सुलभ डिवाइस"),
("upgrade_remote_rustdesk_client_to_{}_tip", "रिमोट RustDesk क्लाइंट को संस्करण {} में अपग्रेड करें"), ("upgrade_remote_rustdesk_client_to_{}_tip", "रिमोट RustDesk क्लाइंट को संस्करण {} में अपग्रेड करें"),
("d3d_render_tip", "D3D रेंडरिंग का उपयोग करें"), ("d3d_render_tip", "D3D रेंडरिंग का उपयोग करें"),
("Use D3D rendering", ""),
("Printer", "प्रिंटर"), ("Printer", "प्रिंटर"),
("printer-os-requirement-tip", "प्रिंटिंग के लिए Windows आवश्यक है।"), ("printer-os-requirement-tip", "प्रिंटिंग के लिए Windows आवश्यक है।"),
("printer-requires-installed-{}-client-tip", "इसके लिए क्लाइंट साइड पर {} इंस्टॉल होना चाहिए।"), ("printer-requires-installed-{}-client-tip", "इसके लिए क्लाइंट साइड पर {} इंस्टॉल होना चाहिए।"),
@@ -743,39 +742,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Display Name", "प्रदर्शित नाम"), ("Display Name", "प्रदर्शित नाम"),
("password-hidden-tip", "पासवर्ड सुरक्षा के लिए छिपा हुआ है।"), ("password-hidden-tip", "पासवर्ड सुरक्षा के लिए छिपा हुआ है।"),
("preset-password-in-use-tip", "पूर्व-निर्धारित पासवर्ड उपयोग में है।"), ("preset-password-in-use-tip", "पूर्व-निर्धारित पासवर्ड उपयोग में है।"),
("Keyboard Shortcuts", ""),
("Configure shortcuts...", ""),
("Enable keyboard shortcuts in remote session", ""),
("shortcut-page-description", ""),
("shortcut-passthrough-tip", ""),
("Pass-through to remote", ""),
("Reset to defaults", ""),
("shortcut-reset-confirm-tip", ""),
("Monitor", ""),
("Keyboard", ""),
("Toggle fullscreen", ""),
("Switch to next display", ""),
("Switch to previous display", ""),
("All monitors", ""),
("Monitor #{}", ""),
("Switch to next tab", ""),
("Switch to previous tab", ""),
("Toggle session recording", ""),
("Close tab", ""),
("Toggle toolbar", ""),
("Toggle input source", ""),
("Edit", ""),
("Save", ""),
("Set Shortcut", ""),
("shortcut-recording-instruction", ""),
("shortcut-recording-press-keys-tip", ""),
("shortcut-must-include-modifiers", ""),
("shortcut-already-bound-to", ""),
("Replace", ""),
("Valid", ""),
("shortcut-mobile-physical-keyboard-tip", ""),
("shortcut-key-not-supported", ""),
("On", ""),
("Off", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -743,39 +743,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Display Name", ""), ("Display Name", ""),
("password-hidden-tip", ""), ("password-hidden-tip", ""),
("preset-password-in-use-tip", ""), ("preset-password-in-use-tip", ""),
("Keyboard Shortcuts", ""),
("Configure shortcuts...", ""),
("Enable keyboard shortcuts in remote session", ""),
("shortcut-page-description", ""),
("shortcut-passthrough-tip", ""),
("Pass-through to remote", ""),
("Reset to defaults", ""),
("shortcut-reset-confirm-tip", ""),
("Monitor", ""),
("Keyboard", ""),
("Toggle fullscreen", ""),
("Switch to next display", ""),
("Switch to previous display", ""),
("All monitors", ""),
("Monitor #{}", ""),
("Switch to next tab", ""),
("Switch to previous tab", ""),
("Toggle session recording", ""),
("Close tab", ""),
("Toggle toolbar", ""),
("Toggle input source", ""),
("Edit", ""),
("Save", ""),
("Set Shortcut", ""),
("shortcut-recording-instruction", ""),
("shortcut-recording-press-keys-tip", ""),
("shortcut-must-include-modifiers", ""),
("shortcut-already-bound-to", ""),
("Replace", ""),
("Valid", ""),
("shortcut-mobile-physical-keyboard-tip", ""),
("shortcut-key-not-supported", ""),
("On", ""),
("Off", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -743,39 +743,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Display Name", "Kijelző név"), ("Display Name", "Kijelző név"),
("password-hidden-tip", "Állandó jelszó lett beállítva (rejtett)."), ("password-hidden-tip", "Állandó jelszó lett beállítva (rejtett)."),
("preset-password-in-use-tip", "Jelenleg az alapértelmezett jelszót használja."), ("preset-password-in-use-tip", "Jelenleg az alapértelmezett jelszót használja."),
("Keyboard Shortcuts", ""),
("Configure shortcuts...", ""),
("Enable keyboard shortcuts in remote session", ""),
("shortcut-page-description", ""),
("shortcut-passthrough-tip", ""),
("Pass-through to remote", ""),
("Reset to defaults", ""),
("shortcut-reset-confirm-tip", ""),
("Monitor", ""),
("Keyboard", ""),
("Toggle fullscreen", ""),
("Switch to next display", ""),
("Switch to previous display", ""),
("All monitors", ""),
("Monitor #{}", ""),
("Switch to next tab", ""),
("Switch to previous tab", ""),
("Toggle session recording", ""),
("Close tab", ""),
("Toggle toolbar", ""),
("Toggle input source", ""),
("Edit", ""),
("Save", ""),
("Set Shortcut", ""),
("shortcut-recording-instruction", ""),
("shortcut-recording-press-keys-tip", ""),
("shortcut-must-include-modifiers", ""),
("shortcut-already-bound-to", ""),
("Replace", ""),
("Valid", ""),
("shortcut-mobile-physical-keyboard-tip", ""),
("shortcut-key-not-supported", ""),
("On", ""),
("Off", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -743,39 +743,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Display Name", ""), ("Display Name", ""),
("password-hidden-tip", ""), ("password-hidden-tip", ""),
("preset-password-in-use-tip", ""), ("preset-password-in-use-tip", ""),
("Keyboard Shortcuts", ""),
("Configure shortcuts...", ""),
("Enable keyboard shortcuts in remote session", ""),
("shortcut-page-description", ""),
("shortcut-passthrough-tip", ""),
("Pass-through to remote", ""),
("Reset to defaults", ""),
("shortcut-reset-confirm-tip", ""),
("Monitor", ""),
("Keyboard", ""),
("Toggle fullscreen", ""),
("Switch to next display", ""),
("Switch to previous display", ""),
("All monitors", ""),
("Monitor #{}", ""),
("Switch to next tab", ""),
("Switch to previous tab", ""),
("Toggle session recording", ""),
("Close tab", ""),
("Toggle toolbar", ""),
("Toggle input source", ""),
("Edit", ""),
("Save", ""),
("Set Shortcut", ""),
("shortcut-recording-instruction", ""),
("shortcut-recording-press-keys-tip", ""),
("shortcut-must-include-modifiers", ""),
("shortcut-already-bound-to", ""),
("Replace", ""),
("Valid", ""),
("shortcut-mobile-physical-keyboard-tip", ""),
("shortcut-key-not-supported", ""),
("On", ""),
("Off", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -743,39 +743,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Display Name", "Visualizza nome"), ("Display Name", "Visualizza nome"),
("password-hidden-tip", "È impostata una password permanente (nascosta)."), ("password-hidden-tip", "È impostata una password permanente (nascosta)."),
("preset-password-in-use-tip", "È attualmente in uso la password preimpostata."), ("preset-password-in-use-tip", "È attualmente in uso la password preimpostata."),
("Keyboard Shortcuts", ""),
("Configure shortcuts...", ""),
("Enable keyboard shortcuts in remote session", ""),
("shortcut-page-description", ""),
("shortcut-passthrough-tip", ""),
("Pass-through to remote", ""),
("Reset to defaults", ""),
("shortcut-reset-confirm-tip", ""),
("Monitor", ""),
("Keyboard", ""),
("Toggle fullscreen", ""),
("Switch to next display", ""),
("Switch to previous display", ""),
("All monitors", ""),
("Monitor #{}", ""),
("Switch to next tab", ""),
("Switch to previous tab", ""),
("Toggle session recording", ""),
("Close tab", ""),
("Toggle toolbar", ""),
("Toggle input source", ""),
("Edit", ""),
("Save", ""),
("Set Shortcut", ""),
("shortcut-recording-instruction", ""),
("shortcut-recording-press-keys-tip", ""),
("shortcut-must-include-modifiers", ""),
("shortcut-already-bound-to", ""),
("Replace", ""),
("Valid", ""),
("shortcut-mobile-physical-keyboard-tip", ""),
("shortcut-key-not-supported", ""),
("On", ""),
("Off", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -743,39 +743,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Display Name", "表示名"), ("Display Name", "表示名"),
("password-hidden-tip", "永続的なパスワードが設定されています (非表示)"), ("password-hidden-tip", "永続的なパスワードが設定されています (非表示)"),
("preset-password-in-use-tip", "プリセットパスワードが現在使用されています"), ("preset-password-in-use-tip", "プリセットパスワードが現在使用されています"),
("Keyboard Shortcuts", ""),
("Configure shortcuts...", ""),
("Enable keyboard shortcuts in remote session", ""),
("shortcut-page-description", ""),
("shortcut-passthrough-tip", ""),
("Pass-through to remote", ""),
("Reset to defaults", ""),
("shortcut-reset-confirm-tip", ""),
("Monitor", ""),
("Keyboard", ""),
("Toggle fullscreen", ""),
("Switch to next display", ""),
("Switch to previous display", ""),
("All monitors", ""),
("Monitor #{}", ""),
("Switch to next tab", ""),
("Switch to previous tab", ""),
("Toggle session recording", ""),
("Close tab", ""),
("Toggle toolbar", ""),
("Toggle input source", ""),
("Edit", ""),
("Save", ""),
("Set Shortcut", ""),
("shortcut-recording-instruction", ""),
("shortcut-recording-press-keys-tip", ""),
("shortcut-must-include-modifiers", ""),
("shortcut-already-bound-to", ""),
("Replace", ""),
("Valid", ""),
("shortcut-mobile-physical-keyboard-tip", ""),
("shortcut-key-not-supported", ""),
("On", ""),
("Off", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -743,39 +743,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Display Name", "표시 이름"), ("Display Name", "표시 이름"),
("password-hidden-tip", "영구 비밀번호가 설정되었습니다 (숨김)."), ("password-hidden-tip", "영구 비밀번호가 설정되었습니다 (숨김)."),
("preset-password-in-use-tip", "현재 사전 설정된 비밀번호가 사용 중입니다."), ("preset-password-in-use-tip", "현재 사전 설정된 비밀번호가 사용 중입니다."),
("Keyboard Shortcuts", ""),
("Configure shortcuts...", ""),
("Enable keyboard shortcuts in remote session", ""),
("shortcut-page-description", ""),
("shortcut-passthrough-tip", ""),
("Pass-through to remote", ""),
("Reset to defaults", ""),
("shortcut-reset-confirm-tip", ""),
("Monitor", ""),
("Keyboard", ""),
("Toggle fullscreen", ""),
("Switch to next display", ""),
("Switch to previous display", ""),
("All monitors", ""),
("Monitor #{}", ""),
("Switch to next tab", ""),
("Switch to previous tab", ""),
("Toggle session recording", ""),
("Close tab", ""),
("Toggle toolbar", ""),
("Toggle input source", ""),
("Edit", ""),
("Save", ""),
("Set Shortcut", ""),
("shortcut-recording-instruction", ""),
("shortcut-recording-press-keys-tip", ""),
("shortcut-must-include-modifiers", ""),
("shortcut-already-bound-to", ""),
("Replace", ""),
("Valid", ""),
("shortcut-mobile-physical-keyboard-tip", ""),
("shortcut-key-not-supported", ""),
("On", ""),
("Off", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -743,39 +743,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Display Name", ""), ("Display Name", ""),
("password-hidden-tip", ""), ("password-hidden-tip", ""),
("preset-password-in-use-tip", ""), ("preset-password-in-use-tip", ""),
("Keyboard Shortcuts", ""),
("Configure shortcuts...", ""),
("Enable keyboard shortcuts in remote session", ""),
("shortcut-page-description", ""),
("shortcut-passthrough-tip", ""),
("Pass-through to remote", ""),
("Reset to defaults", ""),
("shortcut-reset-confirm-tip", ""),
("Monitor", ""),
("Keyboard", ""),
("Toggle fullscreen", ""),
("Switch to next display", ""),
("Switch to previous display", ""),
("All monitors", ""),
("Monitor #{}", ""),
("Switch to next tab", ""),
("Switch to previous tab", ""),
("Toggle session recording", ""),
("Close tab", ""),
("Toggle toolbar", ""),
("Toggle input source", ""),
("Edit", ""),
("Save", ""),
("Set Shortcut", ""),
("shortcut-recording-instruction", ""),
("shortcut-recording-press-keys-tip", ""),
("shortcut-must-include-modifiers", ""),
("shortcut-already-bound-to", ""),
("Replace", ""),
("Valid", ""),
("shortcut-mobile-physical-keyboard-tip", ""),
("shortcut-key-not-supported", ""),
("On", ""),
("Off", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -743,39 +743,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Display Name", ""), ("Display Name", ""),
("password-hidden-tip", ""), ("password-hidden-tip", ""),
("preset-password-in-use-tip", ""), ("preset-password-in-use-tip", ""),
("Keyboard Shortcuts", ""),
("Configure shortcuts...", ""),
("Enable keyboard shortcuts in remote session", ""),
("shortcut-page-description", ""),
("shortcut-passthrough-tip", ""),
("Pass-through to remote", ""),
("Reset to defaults", ""),
("shortcut-reset-confirm-tip", ""),
("Monitor", ""),
("Keyboard", ""),
("Toggle fullscreen", ""),
("Switch to next display", ""),
("Switch to previous display", ""),
("All monitors", ""),
("Monitor #{}", ""),
("Switch to next tab", ""),
("Switch to previous tab", ""),
("Toggle session recording", ""),
("Close tab", ""),
("Toggle toolbar", ""),
("Toggle input source", ""),
("Edit", ""),
("Save", ""),
("Set Shortcut", ""),
("shortcut-recording-instruction", ""),
("shortcut-recording-press-keys-tip", ""),
("shortcut-must-include-modifiers", ""),
("shortcut-already-bound-to", ""),
("Replace", ""),
("Valid", ""),
("shortcut-mobile-physical-keyboard-tip", ""),
("shortcut-key-not-supported", ""),
("On", ""),
("Off", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -743,39 +743,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Display Name", ""), ("Display Name", ""),
("password-hidden-tip", ""), ("password-hidden-tip", ""),
("preset-password-in-use-tip", ""), ("preset-password-in-use-tip", ""),
("Keyboard Shortcuts", ""),
("Configure shortcuts...", ""),
("Enable keyboard shortcuts in remote session", ""),
("shortcut-page-description", ""),
("shortcut-passthrough-tip", ""),
("Pass-through to remote", ""),
("Reset to defaults", ""),
("shortcut-reset-confirm-tip", ""),
("Monitor", ""),
("Keyboard", ""),
("Toggle fullscreen", ""),
("Switch to next display", ""),
("Switch to previous display", ""),
("All monitors", ""),
("Monitor #{}", ""),
("Switch to next tab", ""),
("Switch to previous tab", ""),
("Toggle session recording", ""),
("Close tab", ""),
("Toggle toolbar", ""),
("Toggle input source", ""),
("Edit", ""),
("Save", ""),
("Set Shortcut", ""),
("shortcut-recording-instruction", ""),
("shortcut-recording-press-keys-tip", ""),
("shortcut-must-include-modifiers", ""),
("shortcut-already-bound-to", ""),
("Replace", ""),
("Valid", ""),
("shortcut-mobile-physical-keyboard-tip", ""),
("shortcut-key-not-supported", ""),
("On", ""),
("Off", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -654,7 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Accessible devices", "ലഭ്യമായ ഉപകരണങ്ങൾ"), ("Accessible devices", "ലഭ്യമായ ഉപകരണങ്ങൾ"),
("upgrade_remote_rustdesk_client_to_{}_tip", "റിമോട്ട് പതിപ്പ് {} ലേക്ക് മാറ്റുക"), ("upgrade_remote_rustdesk_client_to_{}_tip", "റിമോട്ട് പതിപ്പ് {} ലേക്ക് മാറ്റുക"),
("d3d_render_tip", "D3D റെൻഡറിംഗ് ഉപയോഗിക്കുക"), ("d3d_render_tip", "D3D റെൻഡറിംഗ് ഉപയോഗിക്കുക"),
("Use D3D rendering", ""),
("Printer", "പ്രിന്റർ"), ("Printer", "പ്രിന്റർ"),
("printer-os-requirement-tip", "പ്രിന്റിംഗിന് വിൻഡോസ് വേണം."), ("printer-os-requirement-tip", "പ്രിന്റിംഗിന് വിൻഡോസ് വേണം."),
("printer-requires-installed-{}-client-tip", "ഇതിന് {} ക്ലയന്റ് ഇൻസ്റ്റാൾ ചെയ്യണം."), ("printer-requires-installed-{}-client-tip", "ഇതിന് {} ക്ലയന്റ് ഇൻസ്റ്റാൾ ചെയ്യണം."),
@@ -743,39 +742,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Display Name", "ഡിസ്‌പ്ലേ പേര്"), ("Display Name", "ഡിസ്‌പ്ലേ പേര്"),
("password-hidden-tip", "സുരക്ഷയ്ക്കായി പാസ്‌വേഡ് മറച്ചിരിക്കുന്നു."), ("password-hidden-tip", "സുരക്ഷയ്ക്കായി പാസ്‌വേഡ് മറച്ചിരിക്കുന്നു."),
("preset-password-in-use-tip", "പ്രീസെറ്റ് പാസ്‌വേഡ് ഉപയോഗത്തിലാണ്."), ("preset-password-in-use-tip", "പ്രീസെറ്റ് പാസ്‌വേഡ് ഉപയോഗത്തിലാണ്."),
("Keyboard Shortcuts", ""),
("Configure shortcuts...", ""),
("Enable keyboard shortcuts in remote session", ""),
("shortcut-page-description", ""),
("shortcut-passthrough-tip", ""),
("Pass-through to remote", ""),
("Reset to defaults", ""),
("shortcut-reset-confirm-tip", ""),
("Monitor", ""),
("Keyboard", ""),
("Toggle fullscreen", ""),
("Switch to next display", ""),
("Switch to previous display", ""),
("All monitors", ""),
("Monitor #{}", ""),
("Switch to next tab", ""),
("Switch to previous tab", ""),
("Toggle session recording", ""),
("Close tab", ""),
("Toggle toolbar", ""),
("Toggle input source", ""),
("Edit", ""),
("Save", ""),
("Set Shortcut", ""),
("shortcut-recording-instruction", ""),
("shortcut-recording-press-keys-tip", ""),
("shortcut-must-include-modifiers", ""),
("shortcut-already-bound-to", ""),
("Replace", ""),
("Valid", ""),
("shortcut-mobile-physical-keyboard-tip", ""),
("shortcut-key-not-supported", ""),
("On", ""),
("Off", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -743,39 +743,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Display Name", ""), ("Display Name", ""),
("password-hidden-tip", ""), ("password-hidden-tip", ""),
("preset-password-in-use-tip", ""), ("preset-password-in-use-tip", ""),
("Keyboard Shortcuts", ""),
("Configure shortcuts...", ""),
("Enable keyboard shortcuts in remote session", ""),
("shortcut-page-description", ""),
("shortcut-passthrough-tip", ""),
("Pass-through to remote", ""),
("Reset to defaults", ""),
("shortcut-reset-confirm-tip", ""),
("Monitor", ""),
("Keyboard", ""),
("Toggle fullscreen", ""),
("Switch to next display", ""),
("Switch to previous display", ""),
("All monitors", ""),
("Monitor #{}", ""),
("Switch to next tab", ""),
("Switch to previous tab", ""),
("Toggle session recording", ""),
("Close tab", ""),
("Toggle toolbar", ""),
("Toggle input source", ""),
("Edit", ""),
("Save", ""),
("Set Shortcut", ""),
("shortcut-recording-instruction", ""),
("shortcut-recording-press-keys-tip", ""),
("shortcut-must-include-modifiers", ""),
("shortcut-already-bound-to", ""),
("Replace", ""),
("Valid", ""),
("shortcut-mobile-physical-keyboard-tip", ""),
("shortcut-key-not-supported", ""),
("On", ""),
("Off", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -743,39 +743,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Display Name", "Naam Weergeven"), ("Display Name", "Naam Weergeven"),
("password-hidden-tip", "Er is een permanent wachtwoord ingesteld (verborgen)."), ("password-hidden-tip", "Er is een permanent wachtwoord ingesteld (verborgen)."),
("preset-password-in-use-tip", "Het basis wachtwoord is momenteel in gebruik."), ("preset-password-in-use-tip", "Het basis wachtwoord is momenteel in gebruik."),
("Keyboard Shortcuts", ""),
("Configure shortcuts...", ""),
("Enable keyboard shortcuts in remote session", ""),
("shortcut-page-description", ""),
("shortcut-passthrough-tip", ""),
("Pass-through to remote", ""),
("Reset to defaults", ""),
("shortcut-reset-confirm-tip", ""),
("Monitor", ""),
("Keyboard", ""),
("Toggle fullscreen", ""),
("Switch to next display", ""),
("Switch to previous display", ""),
("All monitors", ""),
("Monitor #{}", ""),
("Switch to next tab", ""),
("Switch to previous tab", ""),
("Toggle session recording", ""),
("Close tab", ""),
("Toggle toolbar", ""),
("Toggle input source", ""),
("Edit", ""),
("Save", ""),
("Set Shortcut", ""),
("shortcut-recording-instruction", ""),
("shortcut-recording-press-keys-tip", ""),
("shortcut-must-include-modifiers", ""),
("shortcut-already-bound-to", ""),
("Replace", ""),
("Valid", ""),
("shortcut-mobile-physical-keyboard-tip", ""),
("shortcut-key-not-supported", ""),
("On", ""),
("Off", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -743,39 +743,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Display Name", "Nazwa wyświetlana"), ("Display Name", "Nazwa wyświetlana"),
("password-hidden-tip", "Ustawiono (ukryto) stare hasło."), ("password-hidden-tip", "Ustawiono (ukryto) stare hasło."),
("preset-password-in-use-tip", "Obecnie używane jest hasło domyślne."), ("preset-password-in-use-tip", "Obecnie używane jest hasło domyślne."),
("Keyboard Shortcuts", ""),
("Configure shortcuts...", ""),
("Enable keyboard shortcuts in remote session", ""),
("shortcut-page-description", ""),
("shortcut-passthrough-tip", ""),
("Pass-through to remote", ""),
("Reset to defaults", ""),
("shortcut-reset-confirm-tip", ""),
("Monitor", ""),
("Keyboard", ""),
("Toggle fullscreen", ""),
("Switch to next display", ""),
("Switch to previous display", ""),
("All monitors", ""),
("Monitor #{}", ""),
("Switch to next tab", ""),
("Switch to previous tab", ""),
("Toggle session recording", ""),
("Close tab", ""),
("Toggle toolbar", ""),
("Toggle input source", ""),
("Edit", ""),
("Save", ""),
("Set Shortcut", ""),
("shortcut-recording-instruction", ""),
("shortcut-recording-press-keys-tip", ""),
("shortcut-must-include-modifiers", ""),
("shortcut-already-bound-to", ""),
("Replace", ""),
("Valid", ""),
("shortcut-mobile-physical-keyboard-tip", ""),
("shortcut-key-not-supported", ""),
("On", ""),
("Off", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -743,39 +743,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Display Name", ""), ("Display Name", ""),
("password-hidden-tip", ""), ("password-hidden-tip", ""),
("preset-password-in-use-tip", ""), ("preset-password-in-use-tip", ""),
("Keyboard Shortcuts", ""),
("Configure shortcuts...", ""),
("Enable keyboard shortcuts in remote session", ""),
("shortcut-page-description", ""),
("shortcut-passthrough-tip", ""),
("Pass-through to remote", ""),
("Reset to defaults", ""),
("shortcut-reset-confirm-tip", ""),
("Monitor", ""),
("Keyboard", ""),
("Toggle fullscreen", ""),
("Switch to next display", ""),
("Switch to previous display", ""),
("All monitors", ""),
("Monitor #{}", ""),
("Switch to next tab", ""),
("Switch to previous tab", ""),
("Toggle session recording", ""),
("Close tab", ""),
("Toggle toolbar", ""),
("Toggle input source", ""),
("Edit", ""),
("Save", ""),
("Set Shortcut", ""),
("shortcut-recording-instruction", ""),
("shortcut-recording-press-keys-tip", ""),
("shortcut-must-include-modifiers", ""),
("shortcut-already-bound-to", ""),
("Replace", ""),
("Valid", ""),
("shortcut-mobile-physical-keyboard-tip", ""),
("shortcut-key-not-supported", ""),
("On", ""),
("Off", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -743,39 +743,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Display Name", ""), ("Display Name", ""),
("password-hidden-tip", ""), ("password-hidden-tip", ""),
("preset-password-in-use-tip", ""), ("preset-password-in-use-tip", ""),
("Keyboard Shortcuts", ""),
("Configure shortcuts...", ""),
("Enable keyboard shortcuts in remote session", ""),
("shortcut-page-description", ""),
("shortcut-passthrough-tip", ""),
("Pass-through to remote", ""),
("Reset to defaults", ""),
("shortcut-reset-confirm-tip", ""),
("Monitor", ""),
("Keyboard", ""),
("Toggle fullscreen", ""),
("Switch to next display", ""),
("Switch to previous display", ""),
("All monitors", ""),
("Monitor #{}", ""),
("Switch to next tab", ""),
("Switch to previous tab", ""),
("Toggle session recording", ""),
("Close tab", ""),
("Toggle toolbar", ""),
("Toggle input source", ""),
("Edit", ""),
("Save", ""),
("Set Shortcut", ""),
("shortcut-recording-instruction", ""),
("shortcut-recording-press-keys-tip", ""),
("shortcut-must-include-modifiers", ""),
("shortcut-already-bound-to", ""),
("Replace", ""),
("Valid", ""),
("shortcut-mobile-physical-keyboard-tip", ""),
("shortcut-key-not-supported", ""),
("On", ""),
("Off", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -540,7 +540,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("auto_disconnect_option_tip", "Deconectează automat sesiunile de la distanță după o perioadă de inactivitate."), ("auto_disconnect_option_tip", "Deconectează automat sesiunile de la distanță după o perioadă de inactivitate."),
("Connection failed due to inactivity", "Conexiunea a eșuat din cauza inactivității"), ("Connection failed due to inactivity", "Conexiunea a eșuat din cauza inactivității"),
("Check for software update on startup", "Verifică actualizări la pornire"), ("Check for software update on startup", "Verifică actualizări la pornire"),
("upgrade_rustdesk_server_pro_to_{}_tip", ""), ("upgrade_rustdesk_server_pro_{}_tip", "Versiunea serverului RustDesk Pro este mai mică decât {}. Te rugăm să o actualizezi."),
("pull_group_failed_tip", "Sincronizarea grupului a eșuat. Verifică conexiunea la rețea sau autentifică-te din nou."), ("pull_group_failed_tip", "Sincronizarea grupului a eșuat. Verifică conexiunea la rețea sau autentifică-te din nou."),
("Filter by intersection", "Filtrează prin intersecție"), ("Filter by intersection", "Filtrează prin intersecție"),
("Remove wallpaper during incoming sessions", "Elimină imaginea de fundal în timpul sesiunilor primite"), ("Remove wallpaper during incoming sessions", "Elimină imaginea de fundal în timpul sesiunilor primite"),
@@ -743,39 +743,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Display Name", "Nume afișat"), ("Display Name", "Nume afișat"),
("password-hidden-tip", "Parola este ascunsă din motive de securitate. Fă clic pe pictograma ochiului pentru a o afișa."), ("password-hidden-tip", "Parola este ascunsă din motive de securitate. Fă clic pe pictograma ochiului pentru a o afișa."),
("preset-password-in-use-tip", "Se folosește o parolă prestabilită. Se recomandă setarea unei parole personalizate pentru securitate sporită."), ("preset-password-in-use-tip", "Se folosește o parolă prestabilită. Se recomandă setarea unei parole personalizate pentru securitate sporită."),
("Keyboard Shortcuts", ""),
("Configure shortcuts...", ""),
("Enable keyboard shortcuts in remote session", ""),
("shortcut-page-description", ""),
("shortcut-passthrough-tip", ""),
("Pass-through to remote", ""),
("Reset to defaults", ""),
("shortcut-reset-confirm-tip", ""),
("Monitor", ""),
("Keyboard", ""),
("Toggle fullscreen", ""),
("Switch to next display", ""),
("Switch to previous display", ""),
("All monitors", ""),
("Monitor #{}", ""),
("Switch to next tab", ""),
("Switch to previous tab", ""),
("Toggle session recording", ""),
("Close tab", ""),
("Toggle toolbar", ""),
("Toggle input source", ""),
("Edit", ""),
("Save", ""),
("Set Shortcut", ""),
("shortcut-recording-instruction", ""),
("shortcut-recording-press-keys-tip", ""),
("shortcut-must-include-modifiers", ""),
("shortcut-already-bound-to", ""),
("Replace", ""),
("Valid", ""),
("shortcut-mobile-physical-keyboard-tip", ""),
("shortcut-key-not-supported", ""),
("On", ""),
("Off", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -743,39 +743,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Display Name", "Отображаемое имя"), ("Display Name", "Отображаемое имя"),
("password-hidden-tip", "Установлен постоянный пароль (скрытый)."), ("password-hidden-tip", "Установлен постоянный пароль (скрытый)."),
("preset-password-in-use-tip", "Установленный пароль сейчас используется."), ("preset-password-in-use-tip", "Установленный пароль сейчас используется."),
("Keyboard Shortcuts", ""),
("Configure shortcuts...", ""),
("Enable keyboard shortcuts in remote session", ""),
("shortcut-page-description", ""),
("shortcut-passthrough-tip", ""),
("Pass-through to remote", ""),
("Reset to defaults", ""),
("shortcut-reset-confirm-tip", ""),
("Monitor", ""),
("Keyboard", ""),
("Toggle fullscreen", ""),
("Switch to next display", ""),
("Switch to previous display", ""),
("All monitors", ""),
("Monitor #{}", ""),
("Switch to next tab", ""),
("Switch to previous tab", ""),
("Toggle session recording", ""),
("Close tab", ""),
("Toggle toolbar", ""),
("Toggle input source", ""),
("Edit", ""),
("Save", ""),
("Set Shortcut", ""),
("shortcut-recording-instruction", ""),
("shortcut-recording-press-keys-tip", ""),
("shortcut-must-include-modifiers", ""),
("shortcut-already-bound-to", ""),
("Replace", ""),
("Valid", ""),
("shortcut-mobile-physical-keyboard-tip", ""),
("shortcut-key-not-supported", ""),
("On", ""),
("Off", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -743,39 +743,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Display Name", ""), ("Display Name", ""),
("password-hidden-tip", ""), ("password-hidden-tip", ""),
("preset-password-in-use-tip", ""), ("preset-password-in-use-tip", ""),
("Keyboard Shortcuts", ""),
("Configure shortcuts...", ""),
("Enable keyboard shortcuts in remote session", ""),
("shortcut-page-description", ""),
("shortcut-passthrough-tip", ""),
("Pass-through to remote", ""),
("Reset to defaults", ""),
("shortcut-reset-confirm-tip", ""),
("Monitor", ""),
("Keyboard", ""),
("Toggle fullscreen", ""),
("Switch to next display", ""),
("Switch to previous display", ""),
("All monitors", ""),
("Monitor #{}", ""),
("Switch to next tab", ""),
("Switch to previous tab", ""),
("Toggle session recording", ""),
("Close tab", ""),
("Toggle toolbar", ""),
("Toggle input source", ""),
("Edit", ""),
("Save", ""),
("Set Shortcut", ""),
("shortcut-recording-instruction", ""),
("shortcut-recording-press-keys-tip", ""),
("shortcut-must-include-modifiers", ""),
("shortcut-already-bound-to", ""),
("Replace", ""),
("Valid", ""),
("shortcut-mobile-physical-keyboard-tip", ""),
("shortcut-key-not-supported", ""),
("On", ""),
("Off", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -743,39 +743,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Display Name", ""), ("Display Name", ""),
("password-hidden-tip", ""), ("password-hidden-tip", ""),
("preset-password-in-use-tip", ""), ("preset-password-in-use-tip", ""),
("Keyboard Shortcuts", ""),
("Configure shortcuts...", ""),
("Enable keyboard shortcuts in remote session", ""),
("shortcut-page-description", ""),
("shortcut-passthrough-tip", ""),
("Pass-through to remote", ""),
("Reset to defaults", ""),
("shortcut-reset-confirm-tip", ""),
("Monitor", ""),
("Keyboard", ""),
("Toggle fullscreen", ""),
("Switch to next display", ""),
("Switch to previous display", ""),
("All monitors", ""),
("Monitor #{}", ""),
("Switch to next tab", ""),
("Switch to previous tab", ""),
("Toggle session recording", ""),
("Close tab", ""),
("Toggle toolbar", ""),
("Toggle input source", ""),
("Edit", ""),
("Save", ""),
("Set Shortcut", ""),
("shortcut-recording-instruction", ""),
("shortcut-recording-press-keys-tip", ""),
("shortcut-must-include-modifiers", ""),
("shortcut-already-bound-to", ""),
("Replace", ""),
("Valid", ""),
("shortcut-mobile-physical-keyboard-tip", ""),
("shortcut-key-not-supported", ""),
("On", ""),
("Off", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -743,39 +743,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Display Name", ""), ("Display Name", ""),
("password-hidden-tip", ""), ("password-hidden-tip", ""),
("preset-password-in-use-tip", ""), ("preset-password-in-use-tip", ""),
("Keyboard Shortcuts", ""),
("Configure shortcuts...", ""),
("Enable keyboard shortcuts in remote session", ""),
("shortcut-page-description", ""),
("shortcut-passthrough-tip", ""),
("Pass-through to remote", ""),
("Reset to defaults", ""),
("shortcut-reset-confirm-tip", ""),
("Monitor", ""),
("Keyboard", ""),
("Toggle fullscreen", ""),
("Switch to next display", ""),
("Switch to previous display", ""),
("All monitors", ""),
("Monitor #{}", ""),
("Switch to next tab", ""),
("Switch to previous tab", ""),
("Toggle session recording", ""),
("Close tab", ""),
("Toggle toolbar", ""),
("Toggle input source", ""),
("Edit", ""),
("Save", ""),
("Set Shortcut", ""),
("shortcut-recording-instruction", ""),
("shortcut-recording-press-keys-tip", ""),
("shortcut-must-include-modifiers", ""),
("shortcut-already-bound-to", ""),
("Replace", ""),
("Valid", ""),
("shortcut-mobile-physical-keyboard-tip", ""),
("shortcut-key-not-supported", ""),
("On", ""),
("Off", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -743,39 +743,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Display Name", ""), ("Display Name", ""),
("password-hidden-tip", ""), ("password-hidden-tip", ""),
("preset-password-in-use-tip", ""), ("preset-password-in-use-tip", ""),
("Keyboard Shortcuts", ""),
("Configure shortcuts...", ""),
("Enable keyboard shortcuts in remote session", ""),
("shortcut-page-description", ""),
("shortcut-passthrough-tip", ""),
("Pass-through to remote", ""),
("Reset to defaults", ""),
("shortcut-reset-confirm-tip", ""),
("Monitor", ""),
("Keyboard", ""),
("Toggle fullscreen", ""),
("Switch to next display", ""),
("Switch to previous display", ""),
("All monitors", ""),
("Monitor #{}", ""),
("Switch to next tab", ""),
("Switch to previous tab", ""),
("Toggle session recording", ""),
("Close tab", ""),
("Toggle toolbar", ""),
("Toggle input source", ""),
("Edit", ""),
("Save", ""),
("Set Shortcut", ""),
("shortcut-recording-instruction", ""),
("shortcut-recording-press-keys-tip", ""),
("shortcut-must-include-modifiers", ""),
("shortcut-already-bound-to", ""),
("Replace", ""),
("Valid", ""),
("shortcut-mobile-physical-keyboard-tip", ""),
("shortcut-key-not-supported", ""),
("On", ""),
("Off", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -743,39 +743,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Display Name", ""), ("Display Name", ""),
("password-hidden-tip", ""), ("password-hidden-tip", ""),
("preset-password-in-use-tip", ""), ("preset-password-in-use-tip", ""),
("Keyboard Shortcuts", ""),
("Configure shortcuts...", ""),
("Enable keyboard shortcuts in remote session", ""),
("shortcut-page-description", ""),
("shortcut-passthrough-tip", ""),
("Pass-through to remote", ""),
("Reset to defaults", ""),
("shortcut-reset-confirm-tip", ""),
("Monitor", ""),
("Keyboard", ""),
("Toggle fullscreen", ""),
("Switch to next display", ""),
("Switch to previous display", ""),
("All monitors", ""),
("Monitor #{}", ""),
("Switch to next tab", ""),
("Switch to previous tab", ""),
("Toggle session recording", ""),
("Close tab", ""),
("Toggle toolbar", ""),
("Toggle input source", ""),
("Edit", ""),
("Save", ""),
("Set Shortcut", ""),
("shortcut-recording-instruction", ""),
("shortcut-recording-press-keys-tip", ""),
("shortcut-must-include-modifiers", ""),
("shortcut-already-bound-to", ""),
("Replace", ""),
("Valid", ""),
("shortcut-mobile-physical-keyboard-tip", ""),
("shortcut-key-not-supported", ""),
("On", ""),
("Off", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -743,39 +743,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Display Name", ""), ("Display Name", ""),
("password-hidden-tip", ""), ("password-hidden-tip", ""),
("preset-password-in-use-tip", ""), ("preset-password-in-use-tip", ""),
("Keyboard Shortcuts", ""),
("Configure shortcuts...", ""),
("Enable keyboard shortcuts in remote session", ""),
("shortcut-page-description", ""),
("shortcut-passthrough-tip", ""),
("Pass-through to remote", ""),
("Reset to defaults", ""),
("shortcut-reset-confirm-tip", ""),
("Monitor", ""),
("Keyboard", ""),
("Toggle fullscreen", ""),
("Switch to next display", ""),
("Switch to previous display", ""),
("All monitors", ""),
("Monitor #{}", ""),
("Switch to next tab", ""),
("Switch to previous tab", ""),
("Toggle session recording", ""),
("Close tab", ""),
("Toggle toolbar", ""),
("Toggle input source", ""),
("Edit", ""),
("Save", ""),
("Set Shortcut", ""),
("shortcut-recording-instruction", ""),
("shortcut-recording-press-keys-tip", ""),
("shortcut-must-include-modifiers", ""),
("shortcut-already-bound-to", ""),
("Replace", ""),
("Valid", ""),
("shortcut-mobile-physical-keyboard-tip", ""),
("shortcut-key-not-supported", ""),
("On", ""),
("Off", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -743,39 +743,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Display Name", ""), ("Display Name", ""),
("password-hidden-tip", ""), ("password-hidden-tip", ""),
("preset-password-in-use-tip", ""), ("preset-password-in-use-tip", ""),
("Keyboard Shortcuts", ""),
("Configure shortcuts...", ""),
("Enable keyboard shortcuts in remote session", ""),
("shortcut-page-description", ""),
("shortcut-passthrough-tip", ""),
("Pass-through to remote", ""),
("Reset to defaults", ""),
("shortcut-reset-confirm-tip", ""),
("Monitor", ""),
("Keyboard", ""),
("Toggle fullscreen", ""),
("Switch to next display", ""),
("Switch to previous display", ""),
("All monitors", ""),
("Monitor #{}", ""),
("Switch to next tab", ""),
("Switch to previous tab", ""),
("Toggle session recording", ""),
("Close tab", ""),
("Toggle toolbar", ""),
("Toggle input source", ""),
("Edit", ""),
("Save", ""),
("Set Shortcut", ""),
("shortcut-recording-instruction", ""),
("shortcut-recording-press-keys-tip", ""),
("shortcut-must-include-modifiers", ""),
("shortcut-already-bound-to", ""),
("Replace", ""),
("Valid", ""),
("shortcut-mobile-physical-keyboard-tip", ""),
("shortcut-key-not-supported", ""),
("On", ""),
("Off", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -743,39 +743,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Display Name", ""), ("Display Name", ""),
("password-hidden-tip", ""), ("password-hidden-tip", ""),
("preset-password-in-use-tip", ""), ("preset-password-in-use-tip", ""),
("Keyboard Shortcuts", ""),
("Configure shortcuts...", ""),
("Enable keyboard shortcuts in remote session", ""),
("shortcut-page-description", ""),
("shortcut-passthrough-tip", ""),
("Pass-through to remote", ""),
("Reset to defaults", ""),
("shortcut-reset-confirm-tip", ""),
("Monitor", ""),
("Keyboard", ""),
("Toggle fullscreen", ""),
("Switch to next display", ""),
("Switch to previous display", ""),
("All monitors", ""),
("Monitor #{}", ""),
("Switch to next tab", ""),
("Switch to previous tab", ""),
("Toggle session recording", ""),
("Close tab", ""),
("Toggle toolbar", ""),
("Toggle input source", ""),
("Edit", ""),
("Save", ""),
("Set Shortcut", ""),
("shortcut-recording-instruction", ""),
("shortcut-recording-press-keys-tip", ""),
("shortcut-must-include-modifiers", ""),
("shortcut-already-bound-to", ""),
("Replace", ""),
("Valid", ""),
("shortcut-mobile-physical-keyboard-tip", ""),
("shortcut-key-not-supported", ""),
("On", ""),
("Off", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -743,39 +743,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Display Name", ""), ("Display Name", ""),
("password-hidden-tip", ""), ("password-hidden-tip", ""),
("preset-password-in-use-tip", ""), ("preset-password-in-use-tip", ""),
("Keyboard Shortcuts", ""),
("Configure shortcuts...", ""),
("Enable keyboard shortcuts in remote session", ""),
("shortcut-page-description", ""),
("shortcut-passthrough-tip", ""),
("Pass-through to remote", ""),
("Reset to defaults", ""),
("shortcut-reset-confirm-tip", ""),
("Monitor", ""),
("Keyboard", ""),
("Toggle fullscreen", ""),
("Switch to next display", ""),
("Switch to previous display", ""),
("All monitors", ""),
("Monitor #{}", ""),
("Switch to next tab", ""),
("Switch to previous tab", ""),
("Toggle session recording", ""),
("Close tab", ""),
("Toggle toolbar", ""),
("Toggle input source", ""),
("Edit", ""),
("Save", ""),
("Set Shortcut", ""),
("shortcut-recording-instruction", ""),
("shortcut-recording-press-keys-tip", ""),
("shortcut-must-include-modifiers", ""),
("shortcut-already-bound-to", ""),
("Replace", ""),
("Valid", ""),
("shortcut-mobile-physical-keyboard-tip", ""),
("shortcut-key-not-supported", ""),
("On", ""),
("Off", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -743,39 +743,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Display Name", "Görünen Ad"), ("Display Name", "Görünen Ad"),
("password-hidden-tip", "Şifre gizli"), ("password-hidden-tip", "Şifre gizli"),
("preset-password-in-use-tip", "Önceden ayarlanmış şifre kullanılıyor"), ("preset-password-in-use-tip", "Önceden ayarlanmış şifre kullanılıyor"),
("Keyboard Shortcuts", ""),
("Configure shortcuts...", ""),
("Enable keyboard shortcuts in remote session", ""),
("shortcut-page-description", ""),
("shortcut-passthrough-tip", ""),
("Pass-through to remote", ""),
("Reset to defaults", ""),
("shortcut-reset-confirm-tip", ""),
("Monitor", ""),
("Keyboard", ""),
("Toggle fullscreen", ""),
("Switch to next display", ""),
("Switch to previous display", ""),
("All monitors", ""),
("Monitor #{}", ""),
("Switch to next tab", ""),
("Switch to previous tab", ""),
("Toggle session recording", ""),
("Close tab", ""),
("Toggle toolbar", ""),
("Toggle input source", ""),
("Edit", ""),
("Save", ""),
("Set Shortcut", ""),
("shortcut-recording-instruction", ""),
("shortcut-recording-press-keys-tip", ""),
("shortcut-must-include-modifiers", ""),
("shortcut-already-bound-to", ""),
("Replace", ""),
("Valid", ""),
("shortcut-mobile-physical-keyboard-tip", ""),
("shortcut-key-not-supported", ""),
("On", ""),
("Off", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -743,39 +743,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Display Name", "顯示名稱"), ("Display Name", "顯示名稱"),
("password-hidden-tip", "固定密碼已設定(已隱藏)"), ("password-hidden-tip", "固定密碼已設定(已隱藏)"),
("preset-password-in-use-tip", "目前正在使用預設密碼"), ("preset-password-in-use-tip", "目前正在使用預設密碼"),
("Keyboard Shortcuts", ""),
("Configure shortcuts...", ""),
("Enable keyboard shortcuts in remote session", ""),
("shortcut-page-description", ""),
("shortcut-passthrough-tip", ""),
("Pass-through to remote", ""),
("Reset to defaults", ""),
("shortcut-reset-confirm-tip", ""),
("Monitor", ""),
("Keyboard", ""),
("Toggle fullscreen", ""),
("Switch to next display", ""),
("Switch to previous display", ""),
("All monitors", ""),
("Monitor #{}", ""),
("Switch to next tab", ""),
("Switch to previous tab", ""),
("Toggle session recording", ""),
("Close tab", ""),
("Toggle toolbar", ""),
("Toggle input source", ""),
("Edit", ""),
("Save", ""),
("Set Shortcut", ""),
("shortcut-recording-instruction", ""),
("shortcut-recording-press-keys-tip", ""),
("shortcut-must-include-modifiers", ""),
("shortcut-already-bound-to", ""),
("Replace", ""),
("Valid", ""),
("shortcut-mobile-physical-keyboard-tip", ""),
("shortcut-key-not-supported", ""),
("On", ""),
("Off", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -743,39 +743,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Display Name", ""), ("Display Name", ""),
("password-hidden-tip", ""), ("password-hidden-tip", ""),
("preset-password-in-use-tip", ""), ("preset-password-in-use-tip", ""),
("Keyboard Shortcuts", ""),
("Configure shortcuts...", ""),
("Enable keyboard shortcuts in remote session", ""),
("shortcut-page-description", ""),
("shortcut-passthrough-tip", ""),
("Pass-through to remote", ""),
("Reset to defaults", ""),
("shortcut-reset-confirm-tip", ""),
("Monitor", ""),
("Keyboard", ""),
("Toggle fullscreen", ""),
("Switch to next display", ""),
("Switch to previous display", ""),
("All monitors", ""),
("Monitor #{}", ""),
("Switch to next tab", ""),
("Switch to previous tab", ""),
("Toggle session recording", ""),
("Close tab", ""),
("Toggle toolbar", ""),
("Toggle input source", ""),
("Edit", ""),
("Save", ""),
("Set Shortcut", ""),
("shortcut-recording-instruction", ""),
("shortcut-recording-press-keys-tip", ""),
("shortcut-must-include-modifiers", ""),
("shortcut-already-bound-to", ""),
("Replace", ""),
("Valid", ""),
("shortcut-mobile-physical-keyboard-tip", ""),
("shortcut-key-not-supported", ""),
("On", ""),
("Off", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -743,39 +743,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Display Name", ""), ("Display Name", ""),
("password-hidden-tip", ""), ("password-hidden-tip", ""),
("preset-password-in-use-tip", ""), ("preset-password-in-use-tip", ""),
("Keyboard Shortcuts", ""),
("Configure shortcuts...", ""),
("Enable keyboard shortcuts in remote session", ""),
("shortcut-page-description", ""),
("shortcut-passthrough-tip", ""),
("Pass-through to remote", ""),
("Reset to defaults", ""),
("shortcut-reset-confirm-tip", ""),
("Monitor", ""),
("Keyboard", ""),
("Toggle fullscreen", ""),
("Switch to next display", ""),
("Switch to previous display", ""),
("All monitors", ""),
("Monitor #{}", ""),
("Switch to next tab", ""),
("Switch to previous tab", ""),
("Toggle session recording", ""),
("Close tab", ""),
("Toggle toolbar", ""),
("Toggle input source", ""),
("Edit", ""),
("Save", ""),
("Set Shortcut", ""),
("shortcut-recording-instruction", ""),
("shortcut-recording-press-keys-tip", ""),
("shortcut-must-include-modifiers", ""),
("shortcut-already-bound-to", ""),
("Replace", ""),
("Valid", ""),
("shortcut-mobile-physical-keyboard-tip", ""),
("shortcut-key-not-supported", ""),
("On", ""),
("Off", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }