Revert "fix web break introduced in 38f130071 fix(linux): enable mouse side buttons in remote sessions (#14848)" (#14973)

This reverts commit d5d0b01266.
This commit is contained in:
21pages
2026-05-06 20:20:17 +08:00
committed by GitHub
parent 8b8a64f870
commit 5439ec38b6
10 changed files with 10 additions and 266 deletions

View File

@@ -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 = <String>[
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<TTextMenu> 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<TTextMenu> v = [];
// elevation
if (isDefaultConn &&
@@ -270,8 +229,7 @@ List<TTextMenu> 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<TTextMenu> 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<TTextMenu> 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<TTextMenu> 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<TTextMenu> 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<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
)
],
),
onPressed: () => ffi.recordingModel.toggle(),
actionId: kShortcutActionToggleRecording));
onPressed: () => ffi.recordingModel.toggle()));
}
// to-do:
@@ -373,14 +325,6 @@ List<TTextMenu> 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<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
});
}
},
actionId: kShortcutActionScreenshot,
));
}
}
@@ -409,28 +352,6 @@ List<TTextMenu> 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;
}

View File

@@ -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;

View File

@@ -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

View File

@@ -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<RemotePage>
_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(

View File

@@ -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);

View File

@@ -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<RemotePage> 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);
}

View File

@@ -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<SettingsPage> 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({
),
);
}

View File

@@ -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;

View File

@@ -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,

View File

@@ -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<void> 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();
}