mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-05-08 07:08:09 +03:00
fix web break introduced in 38f130071 fix(linux): enable mouse side buttons in remote sessions (#14848)
This commit is contained in:
@@ -16,16 +16,43 @@ import 'package:get/get.dart';
|
|||||||
|
|
||||||
bool isEditOsPassword = false;
|
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 {
|
class TTextMenu {
|
||||||
final Widget child;
|
final Widget child;
|
||||||
final VoidCallback? onPressed;
|
final VoidCallback? onPressed;
|
||||||
Widget? trailingIcon;
|
Widget? trailingIcon;
|
||||||
bool divider;
|
bool divider;
|
||||||
|
final String? actionId;
|
||||||
TTextMenu(
|
TTextMenu(
|
||||||
{required this.child,
|
{required this.child,
|
||||||
required this.onPressed,
|
required this.onPressed,
|
||||||
this.trailingIcon,
|
this.trailingIcon,
|
||||||
this.divider = false});
|
this.divider = false,
|
||||||
|
this.actionId});
|
||||||
|
|
||||||
Widget getChild() {
|
Widget getChild() {
|
||||||
if (trailingIcon != null) {
|
if (trailingIcon != null) {
|
||||||
@@ -94,6 +121,20 @@ 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 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 = [];
|
List<TTextMenu> v = [];
|
||||||
// elevation
|
// elevation
|
||||||
if (isDefaultConn &&
|
if (isDefaultConn &&
|
||||||
@@ -229,7 +270,8 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
|||||||
v.add(
|
v.add(
|
||||||
TTextMenu(
|
TTextMenu(
|
||||||
child: Text('${translate("Insert Ctrl + Alt + Del")}'),
|
child: Text('${translate("Insert Ctrl + Alt + Del")}'),
|
||||||
onPressed: () => bind.sessionCtrlAltDel(sessionId: sessionId)),
|
onPressed: () => bind.sessionCtrlAltDel(sessionId: sessionId),
|
||||||
|
actionId: kShortcutActionSendCtrlAltDel),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// restart
|
// restart
|
||||||
@@ -242,7 +284,8 @@ 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
|
||||||
@@ -250,7 +293,8 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
|||||||
v.add(
|
v.add(
|
||||||
TTextMenu(
|
TTextMenu(
|
||||||
child: Text(translate('Insert Lock')),
|
child: Text(translate('Insert Lock')),
|
||||||
onPressed: () => bind.sessionLockScreen(sessionId: sessionId)),
|
onPressed: () => bind.sessionLockScreen(sessionId: sessionId),
|
||||||
|
actionId: kShortcutActionInsertLock),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// blockUserInput
|
// blockUserInput
|
||||||
@@ -268,7 +312,8 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
|||||||
sessionId: sessionId,
|
sessionId: sessionId,
|
||||||
value: '${blockInput.value ? 'un' : ''}block-input');
|
value: '${blockInput.value ? 'un' : ''}block-input');
|
||||||
blockInput.value = !blockInput.value;
|
blockInput.value = !blockInput.value;
|
||||||
}));
|
},
|
||||||
|
actionId: kShortcutActionToggleBlockInput));
|
||||||
}
|
}
|
||||||
// switchSides
|
// switchSides
|
||||||
if (isDefaultConn &&
|
if (isDefaultConn &&
|
||||||
@@ -280,13 +325,15 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
|||||||
v.add(TTextMenu(
|
v.add(TTextMenu(
|
||||||
child: Text(translate('Switch Sides')),
|
child: Text(translate('Switch Sides')),
|
||||||
onPressed: () =>
|
onPressed: () =>
|
||||||
showConfirmSwitchSidesDialog(sessionId, id, ffi.dialogManager)));
|
showConfirmSwitchSidesDialog(sessionId, id, ffi.dialogManager),
|
||||||
|
actionId: kShortcutActionSwitchSides));
|
||||||
}
|
}
|
||||||
// refresh
|
// refresh
|
||||||
if (pi.version.isNotEmpty) {
|
if (pi.version.isNotEmpty) {
|
||||||
v.add(TTextMenu(
|
v.add(TTextMenu(
|
||||||
child: Text(translate('Refresh')),
|
child: Text(translate('Refresh')),
|
||||||
onPressed: () => sessionRefreshVideo(sessionId, pi),
|
onPressed: () => sessionRefreshVideo(sessionId, pi),
|
||||||
|
actionId: kShortcutActionRefresh,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
// record
|
// record
|
||||||
@@ -308,7 +355,8 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
|||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
onPressed: () => ffi.recordingModel.toggle()));
|
onPressed: () => ffi.recordingModel.toggle(),
|
||||||
|
actionId: kShortcutActionToggleRecording));
|
||||||
}
|
}
|
||||||
|
|
||||||
// to-do:
|
// to-do:
|
||||||
@@ -325,6 +373,14 @@ 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,
|
||||||
@@ -342,6 +398,7 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
actionId: kShortcutActionScreenshot,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -352,6 +409,28 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
|||||||
onPressed: () => onCopyFingerprint(FingerprintState.find(id).value),
|
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;
|
return v;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ 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;
|
||||||
|
|
||||||
|
|||||||
@@ -10,12 +10,14 @@ 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/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';
|
||||||
import 'package:flutter_hbb/mobile/widgets/dialog.dart';
|
import 'package:flutter_hbb/mobile/widgets/dialog.dart';
|
||||||
import 'package:flutter_hbb/models/platform_model.dart';
|
import 'package:flutter_hbb/models/platform_model.dart';
|
||||||
import 'package:flutter_hbb/models/printer_model.dart';
|
import 'package:flutter_hbb/models/printer_model.dart';
|
||||||
import 'package:flutter_hbb/models/server_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/models/state_model.dart';
|
||||||
import 'package:flutter_hbb/plugin/manager.dart';
|
import 'package:flutter_hbb/plugin/manager.dart';
|
||||||
import 'package:flutter_hbb/plugin/widgets/desktop_settings.dart';
|
import 'package:flutter_hbb/plugin/widgets/desktop_settings.dart';
|
||||||
@@ -421,11 +423,49 @@ class _GeneralState extends State<_General> {
|
|||||||
if (!isWeb) audio(context),
|
if (!isWeb) audio(context),
|
||||||
if (!isWeb) record(context),
|
if (!isWeb) record(context),
|
||||||
if (!isWeb) WaylandCard(),
|
if (!isWeb) WaylandCard(),
|
||||||
other()
|
other(),
|
||||||
|
if (!bind.isIncomingOnly()) keyboardShortcuts(),
|
||||||
],
|
],
|
||||||
).marginOnly(bottom: _kListViewBottomMargin);
|
).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() {
|
Widget theme() {
|
||||||
final current = MyTheme.getThemeModePreference().toShortString();
|
final current = MyTheme.getThemeModePreference().toShortString();
|
||||||
onChanged(String value) async {
|
onChanged(String value) async {
|
||||||
@@ -2946,6 +2986,37 @@ 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
|
//#endregion
|
||||||
|
|
||||||
//#region dialogs
|
//#region dialogs
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import '../../common/widgets/toolbar.dart';
|
|||||||
import '../../models/model.dart';
|
import '../../models/model.dart';
|
||||||
import '../../models/input_model.dart';
|
import '../../models/input_model.dart';
|
||||||
import '../../models/platform_model.dart';
|
import '../../models/platform_model.dart';
|
||||||
|
import '../../models/shortcut_model.dart';
|
||||||
import '../../common/shared_state.dart';
|
import '../../common/shared_state.dart';
|
||||||
import '../../utils/image.dart';
|
import '../../utils/image.dart';
|
||||||
import '../widgets/remote_toolbar.dart';
|
import '../widgets/remote_toolbar.dart';
|
||||||
@@ -126,6 +127,20 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
_ffi.ffiModel.pi.platform, _ffi.dialogManager);
|
_ffi.ffiModel.pi.platform, _ffi.dialogManager);
|
||||||
_ffi.recordingModel
|
_ffi.recordingModel
|
||||||
.updateStatus(bind.sessionGetIsRecording(sessionId: _ffi.sessionId));
|
.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.canvasModel.initializeEdgeScrollFallback(this);
|
||||||
_ffi.start(
|
_ffi.start(
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_hbb/common/widgets/audio_input.dart';
|
import 'package:flutter_hbb/common/widgets/audio_input.dart';
|
||||||
import 'package:flutter_hbb/common/widgets/dialog.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/common/widgets/toolbar.dart';
|
||||||
import 'package:flutter_hbb/models/chat_model.dart';
|
import 'package:flutter_hbb/models/chat_model.dart';
|
||||||
import 'package:flutter_hbb/models/state_model.dart';
|
import 'package:flutter_hbb/models/state_model.dart';
|
||||||
@@ -763,8 +764,31 @@ class _ControlMenu extends StatelessWidget {
|
|||||||
if (e.divider) {
|
if (e.divider) {
|
||||||
return Divider();
|
return Divider();
|
||||||
} else {
|
} 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(
|
return MenuButton(
|
||||||
child: e.child,
|
child: child,
|
||||||
onPressed: e.onPressed,
|
onPressed: e.onPressed,
|
||||||
ffi: ffi,
|
ffi: ffi,
|
||||||
trailingIcon: e.trailingIcon);
|
trailingIcon: e.trailingIcon);
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import '../../common/widgets/remote_input.dart';
|
|||||||
import '../../models/input_model.dart';
|
import '../../models/input_model.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 '../../utils/image.dart';
|
import '../../utils/image.dart';
|
||||||
import '../widgets/dialog.dart';
|
import '../widgets/dialog.dart';
|
||||||
import '../widgets/custom_scale_widget.dart';
|
import '../widgets/custom_scale_widget.dart';
|
||||||
@@ -119,6 +120,18 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
|||||||
}
|
}
|
||||||
_disableAndroidSoftKeyboard(
|
_disableAndroidSoftKeyboard(
|
||||||
isKeyboardVisible: keyboardVisibilityController.isVisible);
|
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);
|
WidgetsBinding.instance.addObserver(this);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,8 +17,10 @@ import '../../common/widgets/login.dart';
|
|||||||
import '../../consts.dart';
|
import '../../consts.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 '../widgets/dialog.dart';
|
import '../widgets/dialog.dart';
|
||||||
import 'home_page.dart';
|
import 'home_page.dart';
|
||||||
|
import 'mobile_keyboard_shortcuts_page.dart';
|
||||||
import 'scan_page.dart';
|
import 'scan_page.dart';
|
||||||
|
|
||||||
class SettingsPage extends StatefulWidget implements PageShape {
|
class SettingsPage extends StatefulWidget implements PageShape {
|
||||||
@@ -819,6 +821,22 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
showThemeSettings(gFFI.dialogManager);
|
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())
|
if (!bind.isDisableAccount())
|
||||||
SettingsTile.switchTile(
|
SettingsTile.switchTile(
|
||||||
title: Text(translate('note-at-conn-end-tip')),
|
title: Text(translate('note-at-conn-end-tip')),
|
||||||
@@ -1352,3 +1370,4 @@ SettingsTile _getPopupDialogRadioEntry({
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -346,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 (!Platform.isLinux) return;
|
if (!isLinux) return;
|
||||||
if (_sideButtonChannelInitialized) return;
|
if (_sideButtonChannelInitialized) return;
|
||||||
_sideButtonChannelInitialized = true;
|
_sideButtonChannelInitialized = true;
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import 'package:flutter_hbb/models/peer_model.dart';
|
|||||||
import 'package:flutter_hbb/models/peer_tab_model.dart';
|
import 'package:flutter_hbb/models/peer_tab_model.dart';
|
||||||
import 'package:flutter_hbb/models/printer_model.dart';
|
import 'package:flutter_hbb/models/printer_model.dart';
|
||||||
import 'package:flutter_hbb/models/server_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/user_model.dart';
|
||||||
import 'package:flutter_hbb/models/state_model.dart';
|
import 'package:flutter_hbb/models/state_model.dart';
|
||||||
import 'package:flutter_hbb/models/desktop_render_texture.dart';
|
import 'package:flutter_hbb/models/desktop_render_texture.dart';
|
||||||
@@ -476,6 +477,11 @@ class FfiModel with ChangeNotifier {
|
|||||||
} else if (name == 'exit_relative_mouse_mode') {
|
} else if (name == 'exit_relative_mouse_mode') {
|
||||||
// Handle exit shortcut from rdev grab loop (Ctrl+Alt on Win/Linux, Cmd+G on macOS)
|
// Handle exit shortcut from rdev grab loop (Ctrl+Alt on Win/Linux, Cmd+G on macOS)
|
||||||
parent.target?.inputModel.exitRelativeMouseModeWithKeyRelease();
|
parent.target?.inputModel.exitRelativeMouseModeWithKeyRelease();
|
||||||
|
} else if (name == kShortcutEventName) {
|
||||||
|
final action = evt['action'];
|
||||||
|
if (action is String) {
|
||||||
|
parent.target?.shortcutModel.onTriggered(action);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
debugPrint('Event is not handled in the fixed branch: $name');
|
debugPrint('Event is not handled in the fixed branch: $name');
|
||||||
}
|
}
|
||||||
@@ -3623,6 +3629,7 @@ class FFI {
|
|||||||
late final ElevationModel elevationModel; // session
|
late final ElevationModel elevationModel; // session
|
||||||
late final CmFileModel cmFileModel; // cm
|
late final CmFileModel cmFileModel; // cm
|
||||||
late final TextureModel textureModel; //session
|
late final TextureModel textureModel; //session
|
||||||
|
late final ShortcutModel shortcutModel; // session
|
||||||
late final Peers recentPeersModel; // global
|
late final Peers recentPeersModel; // global
|
||||||
late final Peers favoritePeersModel; // global
|
late final Peers favoritePeersModel; // global
|
||||||
late final Peers lanPeersModel; // global
|
late final Peers lanPeersModel; // global
|
||||||
@@ -3652,6 +3659,7 @@ class FFI {
|
|||||||
elevationModel = ElevationModel(WeakReference(this));
|
elevationModel = ElevationModel(WeakReference(this));
|
||||||
cmFileModel = CmFileModel(WeakReference(this));
|
cmFileModel = CmFileModel(WeakReference(this));
|
||||||
textureModel = TextureModel(WeakReference(this));
|
textureModel = TextureModel(WeakReference(this));
|
||||||
|
shortcutModel = ShortcutModel(WeakReference(this));
|
||||||
recentPeersModel = Peers(
|
recentPeersModel = Peers(
|
||||||
name: PeersModelName.recent,
|
name: PeersModelName.recent,
|
||||||
loadEvent: LoadEvent.recent,
|
loadEvent: LoadEvent.recent,
|
||||||
|
|||||||
@@ -7,6 +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/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');
|
||||||
@@ -930,6 +931,21 @@ 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}) {
|
String mainGetInputSource({dynamic hint}) {
|
||||||
final inputSource =
|
final inputSource =
|
||||||
js.context.callMethod('getByName', ['option:local', 'input-source']);
|
js.context.callMethod('getByName', ['option:local', 'input-source']);
|
||||||
@@ -1176,6 +1192,15 @@ class RustdeskImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> mainInit({required String appDir, dynamic hint}) {
|
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();
|
return Future.value();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user