diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index 984d6a25c..721f5ac85 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -15,7 +15,9 @@ import 'package:get/get.dart'; import '../../models/model.dart'; import '../../models/platform_model.dart'; +import '../../models/shortcut_model.dart'; import '../../models/state_model.dart'; +import '../common/widgets/keyboard_shortcuts/shortcut_utils.dart'; import 'input_modifier_utils.dart'; import 'relative_mouse_model.dart'; import '../common.dart'; @@ -826,6 +828,9 @@ class InputModel { return KeyEventResult.ignored; } } + if (_tryDispatchWebFlutterShortcut(e)) { + return KeyEventResult.handled; + } if (isWindows || isLinux) { // Ignore meta keys. Because flutter window will loose focus if meta key is pressed. if (e.physicalKey == PhysicalKeyboardKey.metaLeft || @@ -920,6 +925,53 @@ class InputModel { return KeyEventResult.handled; } + bool _tryDispatchWebFlutterShortcut(KeyEvent e) { + if (!isWeb || !isInputSourceFlutter) return false; + if (e is! KeyDownEvent && e is! KeyRepeatEvent) return false; + if (!ShortcutModel.isEnabled() || ShortcutModel.isPassThrough()) { + return false; + } + final keyName = logicalKeyName(e.logicalKey); + if (keyName == null) return false; + final mods = canonicalShortcutModsForSave(_webFlutterShortcutMods()); + final action = _matchWebFlutterShortcut(keyName, mods); + if (action == null) return false; + if (e is KeyDownEvent) { + parent.target?.shortcutModel.onTriggered(action); + } + return true; + } + + Set _webFlutterShortcutMods() { + final keyboard = HardwareKeyboard.instance; + final mods = {}; + if (isMacOS || isIOS || isWebOnMacOs) { + if (keyboard.isMetaPressed) mods.add('primary'); + if (keyboard.isControlPressed) mods.add('ctrl'); + } else if (keyboard.isControlPressed) { + mods.add('primary'); + } + if (keyboard.isAltPressed) mods.add('alt'); + if (keyboard.isShiftPressed) mods.add('shift'); + return mods; + } + + String? _matchWebFlutterShortcut(String keyName, List mods) { + for (final binding in ShortcutModel.readBindings()) { + final action = binding['action']; + final key = binding['key']; + final bindingMods = + canonicalShortcutModsForSave(shortcutModSetFrom(binding['mods'])); + if (action is String && + key == keyName && + bindingMods.isNotEmpty && + listEquals(bindingMods, mods)) { + return action; + } + } + return null; + } + /// Send Key Event void newKeyboardMode( String character, int usbHid, bool down, bool iosCapsLock) { diff --git a/flutter/lib/models/shortcut_model.dart b/flutter/lib/models/shortcut_model.dart index a0dc26267..33804f189 100644 --- a/flutter/lib/models/shortcut_model.dart +++ b/flutter/lib/models/shortcut_model.dart @@ -26,6 +26,8 @@ typedef ShortcutCallback = FutureOr Function(); /// via [onTriggered], which runs whatever callback the toolbar / menu /// builders previously registered for that action id. class ShortcutModel { + static WeakReference? _activeWebModel; + final WeakReference parent; final Map _callbacks = {}; @@ -35,6 +37,7 @@ class ShortcutModel { /// matched shortcut fires. void register(String actionId, ShortcutCallback callback) { _callbacks[actionId] = callback; + _activeWebModel = WeakReference(this); } void unregister(String actionId) { @@ -43,6 +46,18 @@ class ShortcutModel { void clear() { _callbacks.clear(); + if (identical(_activeWebModel?.target, this)) { + _activeWebModel = null; + } + } + + static void onWebTriggered(String actionId) { + final model = _activeWebModel?.target; + if (model != null) { + model.onTriggered(actionId); + } else { + debugPrint('shortcut_triggered: no active web shortcut model'); + } } /// Called by the session event listener when a `shortcut_triggered` event diff --git a/flutter/lib/models/web_model.dart b/flutter/lib/models/web_model.dart index 5241c3974..a67e26bc0 100644 --- a/flutter/lib/models/web_model.dart +++ b/flutter/lib/models/web_model.dart @@ -126,6 +126,7 @@ class PlatformFFI { gFFI.dialogManager.dismissAll(); closeConnection(); }; + await _ffiBind.mainInit(appDir: ''); context.callMethod('init'); version = getByName('version'); window.onContextMenu.listen((event) { diff --git a/flutter/lib/web/bridge.dart b/flutter/lib/web/bridge.dart index 73f86f518..c34028242 100644 --- a/flutter/lib/web/bridge.dart +++ b/flutter/lib/web/bridge.dart @@ -7,7 +7,7 @@ import 'package:uuid/uuid.dart'; import 'dart:html' as html; import 'package:flutter_hbb/consts.dart'; -import 'package:flutter_hbb/common.dart' as common; +import 'package:flutter_hbb/models/shortcut_model.dart'; 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'); @@ -1195,10 +1195,11 @@ class RustdeskImpl { // 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. + // Web uses a JS-side connection, so the event does not arrive through the + // native session event stream. js.context['onShortcutTriggered'] = (dynamic action) { if (action is String) { - common.gFFI.shortcutModel.onTriggered(action); + ShortcutModel.onWebTriggered(action); } }; return Future.value();