Files
rustdesk/flutter/lib/mobile/pages/mobile_keyboard_shortcuts_page.dart
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

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),
),
),
],
),
);
}
}