From 5439ec38b663c2ff9de1063ac125f6ac61d78ae2 Mon Sep 17 00:00:00 2001 From: 21pages Date: Wed, 6 May 2026 20:20:17 +0800 Subject: [PATCH] Revert "fix web break introduced in 38f130071 fix(linux): enable mouse side buttons in remote sessions (#14848)" (#14973) This reverts commit d5d0b01266edc8af6baabc2004a1096dd7088a02. --- flutter/lib/common/widgets/toolbar.dart | 93 ++----------------- flutter/lib/consts.dart | 2 - .../desktop/pages/desktop_setting_page.dart | 73 +-------------- flutter/lib/desktop/pages/remote_page.dart | 15 --- .../lib/desktop/widgets/remote_toolbar.dart | 26 +----- flutter/lib/mobile/pages/remote_page.dart | 13 --- flutter/lib/mobile/pages/settings_page.dart | 19 ---- flutter/lib/models/input_model.dart | 2 +- flutter/lib/models/model.dart | 8 -- flutter/lib/web/bridge.dart | 25 ----- 10 files changed, 10 insertions(+), 266 deletions(-) diff --git a/flutter/lib/common/widgets/toolbar.dart b/flutter/lib/common/widgets/toolbar.dart index da79c106e..2e7247d95 100644 --- a/flutter/lib/common/widgets/toolbar.dart +++ b/flutter/lib/common/widgets/toolbar.dart @@ -16,43 +16,16 @@ import 'package:get/get.dart'; bool isEditOsPassword = false; -/// Action IDs that `toolbarControls` is the sole registrar for. Each call to -/// `toolbarControls` (e.g. opening the toolbar menu after a permission was -/// revoked or a state changed) wipes these so a previously-registered closure -/// can't outlive the menu entry that owns it. The for-loop at the bottom of -/// `toolbarControls` then re-registers whichever entries are still present in -/// the rebuilt menu list. -/// -/// Actions registered elsewhere — `registerSessionShortcutActions` on desktop -/// owns toggle_recording, fullscreen, switch_display, switch_tab, close_tab, -/// toggle_toolbar — MUST NOT appear here, otherwise this list would clobber -/// their registration on every menu rebuild. -/// -/// `kShortcutActionToggleRecording` is platform-conditional (mobile-only — -/// see the `!(isDesktop || isWeb)` guard in `toolbarControls`). It is handled -/// separately in the unregister pass rather than appearing in this const list. -const _kToolbarOwnedActionIds = [ - kShortcutActionSendCtrlAltDel, - kShortcutActionRestartRemote, - kShortcutActionInsertLock, - kShortcutActionToggleBlockInput, - kShortcutActionSwitchSides, - kShortcutActionRefresh, - kShortcutActionScreenshot, -]; - class TTextMenu { final Widget child; final VoidCallback? onPressed; Widget? trailingIcon; bool divider; - final String? actionId; TTextMenu( {required this.child, required this.onPressed, this.trailingIcon, - this.divider = false, - this.actionId}); + this.divider = false}); Widget getChild() { if (trailingIcon != null) { @@ -121,20 +94,6 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { final sessionId = ffi.sessionId; final isDefaultConn = ffi.connType == ConnType.defaultConn; - // Wipe everything `toolbarControls` could have registered last call so - // stale closures (e.g. for a menu entry whose permission has since been - // revoked) don't outlive the menu rebuild. See _kToolbarOwnedActionIds. - for (final actionId in _kToolbarOwnedActionIds) { - ffi.shortcutModel.unregister(actionId); - } - // toggle_recording is platform-conditional — toolbarControls only builds - // the menu entry on `!(isDesktop || isWeb)`. On desktop the registration - // is owned by `registerSessionShortcutActions` and must NOT be touched - // here. See the recording menu entry below. - if (!(isDesktop || isWeb)) { - ffi.shortcutModel.unregister(kShortcutActionToggleRecording); - } - List v = []; // elevation if (isDefaultConn && @@ -270,8 +229,7 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { v.add( TTextMenu( child: Text('${translate("Insert Ctrl + Alt + Del")}'), - onPressed: () => bind.sessionCtrlAltDel(sessionId: sessionId), - actionId: kShortcutActionSendCtrlAltDel), + onPressed: () => bind.sessionCtrlAltDel(sessionId: sessionId)), ); } // restart @@ -284,8 +242,7 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { TTextMenu( child: Text(translate('Restart remote device')), onPressed: () => - showRestartRemoteDevice(pi, id, sessionId, ffi.dialogManager), - actionId: kShortcutActionRestartRemote), + showRestartRemoteDevice(pi, id, sessionId, ffi.dialogManager)), ); } // insertLock @@ -293,8 +250,7 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { v.add( TTextMenu( child: Text(translate('Insert Lock')), - onPressed: () => bind.sessionLockScreen(sessionId: sessionId), - actionId: kShortcutActionInsertLock), + onPressed: () => bind.sessionLockScreen(sessionId: sessionId)), ); } // blockUserInput @@ -312,8 +268,7 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { sessionId: sessionId, value: '${blockInput.value ? 'un' : ''}block-input'); blockInput.value = !blockInput.value; - }, - actionId: kShortcutActionToggleBlockInput)); + })); } // switchSides if (isDefaultConn && @@ -325,15 +280,13 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { v.add(TTextMenu( child: Text(translate('Switch Sides')), onPressed: () => - showConfirmSwitchSidesDialog(sessionId, id, ffi.dialogManager), - actionId: kShortcutActionSwitchSides)); + showConfirmSwitchSidesDialog(sessionId, id, ffi.dialogManager))); } // refresh if (pi.version.isNotEmpty) { v.add(TTextMenu( child: Text(translate('Refresh')), onPressed: () => sessionRefreshVideo(sessionId, pi), - actionId: kShortcutActionRefresh, )); } // record @@ -355,8 +308,7 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { ) ], ), - onPressed: () => ffi.recordingModel.toggle(), - actionId: kShortcutActionToggleRecording)); + onPressed: () => ffi.recordingModel.toggle())); } // to-do: @@ -373,14 +325,6 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { onPressed: ffi.ffiModel.timerScreenshot != 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) { msgBox( sessionId, @@ -398,7 +342,6 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { }); } }, - actionId: kShortcutActionScreenshot, )); } } @@ -409,28 +352,6 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { onPressed: () => onCopyFingerprint(FingerprintState.find(id).value), )); } - // Register tagged callbacks with the shortcut model so global keyboard - // shortcuts can dispatch the same actions as the toolbar menu items. - // - // For action IDs already cleared at the top of this function (i.e. those - // in [_kToolbarOwnedActionIds] plus the conditional toggle_recording), - // the `else` branch below is a redundant idempotent no-op — `unregister` - // just calls `Map.remove` on something already absent. - // - // The branch is kept as **defense in depth** for the case where a future - // contributor tags a menu item with an actionId that they forget to add - // to [_kToolbarOwnedActionIds]: without this `else`, the original - // "stale-closure-outlives-disabled-state" bug (e.g. Screenshot cooldown - // bypass) would silently come back for that new action only. - for (final menu in v) { - final actionId = menu.actionId; - if (actionId == null) continue; - if (menu.onPressed != null) { - ffi.shortcutModel.register(actionId, menu.onPressed!); - } else { - ffi.shortcutModel.unregister(actionId); - } - } return v; } diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 8362ed36e..832b96d24 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -4,8 +4,6 @@ import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/models/state_model.dart'; import 'package:get/get.dart'; -export 'common/widgets/keyboard_shortcuts/shortcut_constants.dart'; - const int kMaxVirtualDisplayCount = 4; const int kAllVirtualDisplay = -1; diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index b13b2c9cd..2841c1d27 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -10,14 +10,12 @@ import 'package:flutter_hbb/common/widgets/audio_input.dart'; import 'package:flutter_hbb/common/widgets/setting_widgets.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/desktop/pages/desktop_home_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/widgets/remote_toolbar.dart'; import 'package:flutter_hbb/mobile/widgets/dialog.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/printer_model.dart'; import 'package:flutter_hbb/models/server_model.dart'; -import 'package:flutter_hbb/models/shortcut_model.dart'; import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/plugin/manager.dart'; import 'package:flutter_hbb/plugin/widgets/desktop_settings.dart'; @@ -423,49 +421,11 @@ class _GeneralState extends State<_General> { if (!isWeb) audio(context), if (!isWeb) record(context), if (!isWeb) WaylandCard(), - other(), - if (!bind.isIncomingOnly()) keyboardShortcuts(), + other() ], ).marginOnly(bottom: _kListViewBottomMargin); } - Widget keyboardShortcuts() { - // The bindings JSON (LocalConfig key `keyboard-shortcuts`) holds three - // flags + the bindings list: {enabled, pass_through, bindings}. When the - // master is off, the pass-through toggle and the Configure entry are - // hidden — both are meaningless without an active matcher. - return StatefulBuilder(builder: (context, setLocalState) { - final enabled = ShortcutModel.isEnabled(); - return _Card(title: 'Keyboard Shortcuts', children: [ - _OptionCheckBox( - context, - 'Enable keyboard shortcuts in remote session', - kShortcutLocalConfigKey, - isServer: false, - optGetter: ShortcutModel.isEnabled, - optSetter: (_, v) async { - await ShortcutModel.setEnabled(v); - setLocalState(() {}); - }, - ), - if (enabled) ...[ - _OptionCheckBox( - context, - 'Pass-through to remote', - kShortcutLocalConfigKey, - isServer: false, - optGetter: ShortcutModel.isPassThrough, - optSetter: (_, v) async { - await ShortcutModel.setPassThrough(v); - setLocalState(() {}); - }, - ), - _ShortcutsConfigureRow(), - ], - ]); - }); - } - Widget theme() { final current = MyTheme.getThemeModePreference().toShortString(); onChanged(String value) async { @@ -2990,37 +2950,6 @@ class _CountDownButtonState extends State<_CountDownButton> { } } -// Tappable row that pushes the shortcut configuration page. -class _ShortcutsConfigureRow extends StatelessWidget { - // ignore: unused_element - const _ShortcutsConfigureRow({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return InkWell( - onTap: () { - Navigator.of(context).push(MaterialPageRoute( - builder: (_) => const DesktopKeyboardShortcutsPage(), - )); - }, - child: Row( - children: [ - Expanded( - child: Text(translate('Configure shortcuts...')), - ), - Icon(Icons.arrow_forward_ios, - size: 16, color: disabledTextColor(context, true)) - .marginOnly(right: 4), - ], - ).marginOnly( - left: _kCheckBoxLeftMargin, - top: 6, - bottom: 6, - ), - ); - } -} - //#endregion //#region dialogs diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 944962573..29e710bbc 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -17,7 +17,6 @@ import '../../common/widgets/toolbar.dart'; import '../../models/model.dart'; import '../../models/input_model.dart'; import '../../models/platform_model.dart'; -import '../../models/shortcut_model.dart'; import '../../common/shared_state.dart'; import '../../utils/image.dart'; import '../widgets/remote_toolbar.dart'; @@ -127,20 +126,6 @@ class _RemotePageState extends State _ffi.ffiModel.pi.platform, _ffi.dialogManager); _ffi.recordingModel .updateStatus(bind.sessionGetIsRecording(sessionId: _ffi.sessionId)); - // Seed shortcut action callbacks once the session is ready, so that - // global keyboard shortcuts work even if the user never opens the - // toolbar menu. The returned list is intentionally discarded — the - // side effect of registering callbacks (inside toolbarControls) is - // what we want here. - if (mounted) { - 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, - tabController: widget.tabController, - toolbarState: widget.toolbarState); - } }); _ffi.canvasModel.initializeEdgeScrollFallback(this); _ffi.start( diff --git a/flutter/lib/desktop/widgets/remote_toolbar.dart b/flutter/lib/desktop/widgets/remote_toolbar.dart index 038c264aa..5da253e80 100644 --- a/flutter/lib/desktop/widgets/remote_toolbar.dart +++ b/flutter/lib/desktop/widgets/remote_toolbar.dart @@ -5,7 +5,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hbb/common/widgets/audio_input.dart'; import 'package:flutter_hbb/common/widgets/dialog.dart'; -import 'package:flutter_hbb/common/widgets/keyboard_shortcuts/display.dart'; import 'package:flutter_hbb/common/widgets/toolbar.dart'; import 'package:flutter_hbb/models/chat_model.dart'; import 'package:flutter_hbb/models/state_model.dart'; @@ -764,31 +763,8 @@ class _ControlMenu extends StatelessWidget { if (e.divider) { return Divider(); } else { - final hint = e.actionId == null - ? null - : ShortcutDisplay.formatFor(e.actionId!); - final child = hint == null - ? e.child - : Row( - mainAxisSize: MainAxisSize.min, - children: [ - Flexible(child: e.child), - Padding( - padding: const EdgeInsets.only(left: 16), - child: Text( - hint, - style: Theme.of(context) - .textTheme - .bodySmall - ?.copyWith( - color: Theme.of(context).hintColor, - ), - ), - ), - ], - ); return MenuButton( - child: child, + child: e.child, onPressed: e.onPressed, ffi: ffi, trailingIcon: e.trailingIcon); diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 3a5256841..74a5af45c 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -21,7 +21,6 @@ import '../../common/widgets/remote_input.dart'; import '../../models/input_model.dart'; import '../../models/model.dart'; import '../../models/platform_model.dart'; -import '../../models/shortcut_model.dart'; import '../../utils/image.dart'; import '../widgets/dialog.dart'; import '../widgets/custom_scale_widget.dart'; @@ -120,18 +119,6 @@ class _RemotePageState extends State with WidgetsBindingObserver { } _disableAndroidSoftKeyboard( isKeyboardVisible: keyboardVisibilityController.isVisible); - // Seed shortcut action callbacks once the session is ready, so that - // global keyboard shortcuts work even if the user never opens the - // toolbar menu. The returned list is intentionally discarded — the - // side effect of registering callbacks (inside toolbarControls) is - // what we want here. - if (mounted) { - toolbarControls(context, widget.id, gFFI); - // Mobile has no DesktopTabController, so tab-switch shortcuts - // 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); - } }); WidgetsBinding.instance.addObserver(this); } diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index ed766cf76..509260636 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -17,10 +17,8 @@ import '../../common/widgets/login.dart'; import '../../consts.dart'; import '../../models/model.dart'; import '../../models/platform_model.dart'; -import '../../models/shortcut_model.dart'; import '../widgets/dialog.dart'; import 'home_page.dart'; -import 'mobile_keyboard_shortcuts_page.dart'; import 'scan_page.dart'; class SettingsPage extends StatefulWidget implements PageShape { @@ -821,22 +819,6 @@ class _SettingsState extends State with WidgetsBindingObserver { showThemeSettings(gFFI.dialogManager); }, ), - SettingsTile.navigation( - leading: Icon(Icons.keyboard_outlined), - title: Text(translate('Keyboard Shortcuts')), - description: Text(ShortcutModel.isEnabled() - ? translate('On') - : translate('Off')), - onPressed: (context) { - Navigator.push( - context, - MaterialPageRoute( - builder: (_) => const MobileKeyboardShortcutsPage(), - )).then((_) { - if (mounted) setState(() {}); - }); - }, - ), if (!bind.isDisableAccount()) SettingsTile.switchTile( title: Text(translate('note-at-conn-end-tip')), @@ -1370,4 +1352,3 @@ SettingsTile _getPopupDialogRadioEntry({ ), ); } - diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index 984d6a25c..6fdffd796 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -346,7 +346,7 @@ class InputModel { /// which runs per-engine, so each isolate registers its own handler tied /// to its own set of InputModels. static void initSideButtonChannel() { - if (!isLinux) return; + if (!Platform.isLinux) return; if (_sideButtonChannelInitialized) return; _sideButtonChannelInitialized = true; diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 72ecdc99d..e94834a2b 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -21,7 +21,6 @@ import 'package:flutter_hbb/models/peer_model.dart'; import 'package:flutter_hbb/models/peer_tab_model.dart'; import 'package:flutter_hbb/models/printer_model.dart'; import 'package:flutter_hbb/models/server_model.dart'; -import 'package:flutter_hbb/models/shortcut_model.dart'; import 'package:flutter_hbb/models/user_model.dart'; import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/models/desktop_render_texture.dart'; @@ -477,11 +476,6 @@ class FfiModel with ChangeNotifier { } else if (name == 'exit_relative_mouse_mode') { // Handle exit shortcut from rdev grab loop (Ctrl+Alt on Win/Linux, Cmd+G on macOS) parent.target?.inputModel.exitRelativeMouseModeWithKeyRelease(); - } else if (name == kShortcutEventName) { - final action = evt['action']; - if (action is String) { - parent.target?.shortcutModel.onTriggered(action); - } } else { debugPrint('Event is not handled in the fixed branch: $name'); } @@ -3629,7 +3623,6 @@ class FFI { late final ElevationModel elevationModel; // session late final CmFileModel cmFileModel; // cm late final TextureModel textureModel; //session - late final ShortcutModel shortcutModel; // session late final Peers recentPeersModel; // global late final Peers favoritePeersModel; // global late final Peers lanPeersModel; // global @@ -3659,7 +3652,6 @@ class FFI { elevationModel = ElevationModel(WeakReference(this)); cmFileModel = CmFileModel(WeakReference(this)); textureModel = TextureModel(WeakReference(this)); - shortcutModel = ShortcutModel(WeakReference(this)); recentPeersModel = Peers( name: PeersModelName.recent, loadEvent: LoadEvent.recent, diff --git a/flutter/lib/web/bridge.dart b/flutter/lib/web/bridge.dart index f151a6e46..54e6a9a9b 100644 --- a/flutter/lib/web/bridge.dart +++ b/flutter/lib/web/bridge.dart @@ -7,7 +7,6 @@ import 'package:uuid/uuid.dart'; import 'dart:html' as html; import 'package:flutter_hbb/consts.dart'; -import 'package:flutter_hbb/common.dart' as common; 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'); @@ -931,21 +930,6 @@ class RustdeskImpl { ])); } - // Tell the JS-side matcher (flutter/web/js/src/shortcut_matcher.ts) to - // re-read its bindings from LocalStorage. Mirrors the native call which - // refreshes the Rust matcher's in-memory cache. - void mainReloadKeyboardShortcuts({dynamic hint}) { - js.context.callMethod('reloadShortcuts', []); - } - - // Web has no Rust at runtime, so the defaults seed comes from the - // [kDefaultShortcutBindings] canonical in shortcut_constants.dart. Parity - // with Rust's `default_bindings()` is enforced by tests on both sides - // against `flutter/test/fixtures/default_keyboard_shortcuts.json`. - String mainGetDefaultKeyboardShortcuts({dynamic hint}) { - return jsonEncode(kDefaultShortcutBindings); - } - String mainGetInputSource({dynamic hint}) { final inputSource = js.context.callMethod('getByName', ['option:local', 'input-source']); @@ -1192,15 +1176,6 @@ class RustdeskImpl { } Future mainInit({required String appDir, dynamic hint}) { - // JS -> Dart shortcut bridge. The matcher in flutter/web/js/src/ - // shortcut_matcher.ts calls `window.onShortcutTriggered(actionId)` when a - // binding fires; route it to the active session's ShortcutModel. - // Web is single-window so `gFFI` is always the active session. - js.context['onShortcutTriggered'] = (dynamic action) { - if (action is String) { - common.gFFI.shortcutModel.onTriggered(action); - } - }; return Future.value(); }