mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-05-08 07:08:09 +03:00
Adds a keyboard shortcut feature (Rust matcher + Dart UI + cross-language
parity tests) that lets users bind combinations like Ctrl+Alt+Shift+P to
session actions. Bindings are stored in LocalConfig under
`keyboard-shortcuts`; the matcher gates dispatch on `enabled` and
`pass_through` flags so flipping the master switch off is a hard stop.
Wire-up summary:
- src/keyboard/shortcuts.rs: matcher, default bindings, parity test against
flutter/test/fixtures/default_keyboard_shortcuts.json
- src/keyboard.rs: shortcut intercept in process_event{,_with_session},
feature-gated to `flutter`; runs before key swapping so users bind to
physical keys
- src/flutter_ffi.rs: main_reload_keyboard_shortcuts +
main_get_default_keyboard_shortcuts; reload_from_config seeded in main_init
- flutter/lib/common/widgets/keyboard_shortcuts/: shared config page body,
recording dialog, shortcut display formatter, action group registry
- flutter/lib/desktop/pages/desktop_keyboard_shortcuts_page.dart and
flutter/lib/mobile/pages/mobile_keyboard_shortcuts_page.dart: platform
shells around the shared body
- flutter/lib/models/shortcut_model.dart: per-session ShortcutModel +
registerSessionShortcutActions for actions with no toolbar TToggleMenu /
TRadioMenu (fullscreen, switch display/tab, close tab, voice call, etc.)
- flutter/lib/common/widgets/toolbar.dart: optional `actionId` field on
TToggleMenu / TRadioMenu, plus per-helper auto-register pass that wires
tagged entries' existing onChanged into the ShortcutModel
- flutter/test/keyboard_shortcuts_test.dart + fixtures: cross-language
parity (default bindings, supported key vocabulary)
Design principles applied during review:
1. Additions are fine; modifications to original logic must be deliberate.
Tagging an existing TToggleMenu entry with `actionId:` is an addition.
Rewriting its onChanged to satisfy a new contract is a modification —
and was reverted for every case where the original click behavior was
working. Four closures were touched and then reverted (mobile View
Mode, Privacy mode multi-impl, Relative mouse mode, Reverse mouse
wheel); their shortcuts are wired via standalone closures in
shortcut_model.dart instead.
2. Toolbar auto-register is reserved for entries whose onChanged is
inherently self-flipping — typically `sessionToggleOption(name)` where
the named option is flipped in place and the input bool is unused. The
register pass passes `!menu.value` from registration time, which is
harmless under self-flipping but wrong for closures that consume the
input bool directly. Tagging a non-self-flipping entry forces a closure
rewrite; choose non-toolbar registration in that case.
3. When shortcuts are disabled, toolbar behavior must be bit-for-bit
unchanged. The matcher's `enabled`-gate already guarantees no
dispatch; the auto-register pass is left unconditional (its only effect
is HashMap operations on a separate ShortcutModel) so mid-session
enable works without a reconnect. The trade-off is intentional and
documented at the top of toolbarControls.
4. Comments stay terse. Rationale lives in one place — the doc comment of
the helper or registration site, not duplicated at every call site.
5. Where an existing helper needs a new optional behavior (e.g.
`_OptionCheckBox` gaining a tooltip slot), the new branch must reduce
to byte-identical output for existing callers (`trailing == null`
case → original `Expanded(Text)` layout). Verified.
6. Action IDs and labels stay consistent. Renamed `reset_cursor` →
`reset_canvas` so the action ID matches its user-facing label
("Reset canvas") and capability flag.
Out-of-scope but included:
- AGENTS.md: documents flutter_rust_bridge no-codegen workflow and the
Web target's hand-written TS client, since both are load-bearing for
any new FFI work.
- remote_toolbar.dart: i18n fix for the per-monitor tooltip ("All
monitors" / "Monitor #N"), unrelated to shortcuts but kept here.
96 lines
3.4 KiB
Dart
96 lines
3.4 KiB
Dart
// flutter/lib/mobile/pages/mobile_keyboard_shortcuts_page.dart
|
|
//
|
|
// Mobile shell for the Keyboard Shortcuts configuration page. Mirrors
|
|
// `desktop/pages/desktop_keyboard_shortcuts_page.dart` but with a touch-
|
|
// friendly layout (ListTile rows instead of dense rows) and a hint banner
|
|
// that explains the recording flow only works with a physical keyboard.
|
|
//
|
|
// All actual logic — group definitions, JSON I/O, conflict-replace flow,
|
|
// recording-dialog round-trip, "Reset to defaults" — lives in the shared
|
|
// `common/widgets/keyboard_shortcuts/page_body.dart`. This file only
|
|
// supplies the AppBar, the AppBar action, and the platform hint banner.
|
|
//
|
|
// Mobile keyboard detection limitation: Flutter has no reliable
|
|
// "is a physical keyboard attached?" API on iOS or Android. Soft keyboards
|
|
// don't generate the `KeyDownEvent`s the recording dialog listens for, so
|
|
// in practice the dialog only does anything useful when the user actually
|
|
// has a hardware keyboard plugged in (USB / Bluetooth / Smart Connector).
|
|
// For V1 we don't try to detect attachment — we just surface the
|
|
// requirement as an in-page hint instead of disabling the Edit button.
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import '../../common.dart';
|
|
import '../../common/widgets/keyboard_shortcuts/page_body.dart';
|
|
|
|
class MobileKeyboardShortcutsPage extends StatefulWidget {
|
|
const MobileKeyboardShortcutsPage({Key? key}) : super(key: key);
|
|
|
|
@override
|
|
State<MobileKeyboardShortcutsPage> createState() =>
|
|
_MobileKeyboardShortcutsPageState();
|
|
}
|
|
|
|
class _MobileKeyboardShortcutsPageState
|
|
extends State<MobileKeyboardShortcutsPage> {
|
|
final GlobalKey<KeyboardShortcutsPageBodyState> _bodyKey = GlobalKey();
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: Text(translate('Keyboard Shortcuts')),
|
|
actions: [
|
|
IconButton(
|
|
tooltip: translate('Reset to defaults'),
|
|
onPressed: () =>
|
|
_bodyKey.currentState?.resetToDefaultsWithConfirm(),
|
|
icon: const Icon(Icons.restore),
|
|
),
|
|
],
|
|
),
|
|
body: KeyboardShortcutsPageBody(
|
|
key: _bodyKey,
|
|
compact: false,
|
|
editButtonHint: translate('shortcut-mobile-physical-keyboard-tip'),
|
|
headerBanner: _PhysicalKeyboardHintBanner(theme: theme),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// A muted info banner shown above the master toggle on mobile. We can't
|
|
/// reliably detect whether a physical keyboard is attached, so instead of
|
|
/// disabling the Edit button we surface the requirement up front.
|
|
class _PhysicalKeyboardHintBanner extends StatelessWidget {
|
|
final ThemeData theme;
|
|
const _PhysicalKeyboardHintBanner({required this.theme});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final color = theme.colorScheme.primary.withOpacity(0.08);
|
|
return Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: color,
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Icon(Icons.info_outline,
|
|
size: 18, color: theme.colorScheme.primary),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: Text(
|
|
translate('shortcut-mobile-physical-keyboard-tip'),
|
|
style: TextStyle(color: theme.colorScheme.onSurface),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|