mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-05-07 22:58:10 +03:00
fix(keyboard): shortcuts, harden config and callback lifecycle
Signed-off-by: fufesou <linlong1266@gmail.com>
This commit is contained in:
@@ -3,6 +3,7 @@ import 'dart:convert';
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import '../../../consts.dart';
|
import '../../../consts.dart';
|
||||||
import '../../../models/platform_model.dart';
|
import '../../../models/platform_model.dart';
|
||||||
|
import 'shortcut_utils.dart';
|
||||||
|
|
||||||
/// Read the bindings JSON and produce a human-readable shortcut string for
|
/// Read the bindings JSON and produce a human-readable shortcut string for
|
||||||
/// `actionId`, formatted for the current OS. Returns null if unbound, or —
|
/// `actionId`, formatted for the current OS. Returns null if unbound, or —
|
||||||
@@ -44,7 +45,7 @@ class ShortcutDisplay {
|
|||||||
// would press it expecting the local action and instead the keys would go
|
// would press it expecting the local action and instead the keys would go
|
||||||
// to the remote. Treat as unbound for display purposes.
|
// to the remote. Treat as unbound for display purposes.
|
||||||
if (requireEnabled && parsed['pass_through'] == true) return null;
|
if (requireEnabled && parsed['pass_through'] == true) return null;
|
||||||
final list = (parsed['bindings'] as List? ?? []).cast<Map<String, dynamic>>();
|
final list = shortcutBindingMapsFrom(parsed['bindings']);
|
||||||
final found = list.firstWhere(
|
final found = list.firstWhere(
|
||||||
(b) => b['action'] == actionId,
|
(b) => b['action'] == actionId,
|
||||||
orElse: () => {},
|
orElse: () => {},
|
||||||
|
|||||||
@@ -109,9 +109,7 @@ class KeyboardShortcutsPageBodyState extends State<KeyboardShortcutsPageBody> {
|
|||||||
String? clearActionId,
|
String? clearActionId,
|
||||||
}) async {
|
}) async {
|
||||||
final json = _readJson();
|
final json = _readJson();
|
||||||
final list = ((json['bindings'] as List?) ?? <dynamic>[])
|
final list = shortcutBindingMapsFrom(json['bindings']);
|
||||||
.cast<Map<String, dynamic>>()
|
|
||||||
.toList();
|
|
||||||
list.removeWhere((b) {
|
list.removeWhere((b) {
|
||||||
final a = b['action'];
|
final a = b['action'];
|
||||||
return a == actionId || (clearActionId != null && a == clearActionId);
|
return a == actionId || (clearActionId != null && a == clearActionId);
|
||||||
@@ -172,8 +170,7 @@ class KeyboardShortcutsPageBodyState extends State<KeyboardShortcutsPageBody> {
|
|||||||
|
|
||||||
Future<void> _onEdit(KeyboardShortcutActionEntry entry) async {
|
Future<void> _onEdit(KeyboardShortcutActionEntry entry) async {
|
||||||
final json = _readJson();
|
final json = _readJson();
|
||||||
final bindings = ((json['bindings'] as List?) ?? <dynamic>[])
|
final bindings = shortcutBindingMapsFrom(json['bindings']);
|
||||||
.cast<Map<String, dynamic>>();
|
|
||||||
final result = await showRecordingDialog(
|
final result = await showRecordingDialog(
|
||||||
context: context,
|
context: context,
|
||||||
actionId: entry.id,
|
actionId: entry.id,
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
//
|
//
|
||||||
// Modal dialog used by the Keyboard Shortcuts settings page to capture a new
|
// Modal dialog used by the Keyboard Shortcuts settings page to capture a new
|
||||||
// key combination for a given action. The dialog listens for KeyDown events,
|
// key combination for a given action. The dialog listens for KeyDown events,
|
||||||
// extracts the modifier set + non-modifier key, validates against the
|
// extracts the modifier set + non-modifier key, validates that at least one
|
||||||
// "must include Ctrl+Alt+Shift (Cmd+Option+Shift on macOS)" rule, and reports
|
// modifier is present, and reports any conflict with another already-bound
|
||||||
// any conflict with another already-bound action.
|
// action.
|
||||||
//
|
//
|
||||||
// On Save, returns the new binding map ({action, mods, key}) plus the
|
// On Save, returns the new binding map ({action, mods, key}) plus the
|
||||||
// optional id of the action whose binding should be cleared (the conflict
|
// optional id of the action whose binding should be cleared (the conflict
|
||||||
@@ -147,8 +147,7 @@ class _RecordingDialogState extends State<_RecordingDialog> {
|
|||||||
final otherAction = b['action'] as String?;
|
final otherAction = b['action'] as String?;
|
||||||
if (otherAction == null || otherAction == widget.actionId) continue;
|
if (otherAction == null || otherAction == widget.actionId) continue;
|
||||||
final otherKey = b['key'] as String?;
|
final otherKey = b['key'] as String?;
|
||||||
final otherMods =
|
final otherMods = shortcutModSetFrom(b['mods']);
|
||||||
((b['mods'] as List?) ?? const []).cast<String>().toSet();
|
|
||||||
if (otherKey == _key &&
|
if (otherKey == _key &&
|
||||||
otherMods.length == _mods.length &&
|
otherMods.length == _mods.length &&
|
||||||
otherMods.containsAll(_mods)) {
|
otherMods.containsAll(_mods)) {
|
||||||
|
|||||||
@@ -229,6 +229,10 @@ List<KeyboardShortcutActionGroup> filterKeyboardShortcutActionGroupsForPlatform(
|
|||||||
if (!cap.includeScreenshotShortcut && id == kShortcutActionScreenshot) {
|
if (!cap.includeScreenshotShortcut && id == kShortcutActionScreenshot) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (!cap.includeScreenshotShortcut &&
|
||||||
|
id == kShortcutActionToggleRelativeMouseMode) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (!cap.includeTabShortcuts && isSwitchTabShortcutAction(id)) return false;
|
if (!cap.includeTabShortcuts && isSwitchTabShortcutAction(id)) return false;
|
||||||
if (!cap.includeToolbarShortcut && id == kShortcutActionToggleToolbar) {
|
if (!cap.includeToolbarShortcut && id == kShortcutActionToggleToolbar) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -11,6 +11,30 @@ List<String> canonicalShortcutModsForSave(Set<String> mods) {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<Map<String, dynamic>> shortcutBindingMapsFrom(dynamic rawBindings) {
|
||||||
|
if (rawBindings is! Iterable) return <Map<String, dynamic>>[];
|
||||||
|
final bindings = <Map<String, dynamic>>[];
|
||||||
|
for (final raw in rawBindings) {
|
||||||
|
if (raw is! Map) continue;
|
||||||
|
final binding = <String, dynamic>{};
|
||||||
|
for (final entry in raw.entries) {
|
||||||
|
final key = entry.key;
|
||||||
|
if (key is String) {
|
||||||
|
binding[key] = entry.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (binding.isNotEmpty) {
|
||||||
|
bindings.add(binding);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bindings;
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<String> shortcutModSetFrom(dynamic rawMods) {
|
||||||
|
if (rawMods is! Iterable) return <String>{};
|
||||||
|
return rawMods.whereType<String>().toSet();
|
||||||
|
}
|
||||||
|
|
||||||
bool isSwitchTabShortcutAction(String? actionId) {
|
bool isSwitchTabShortcutAction(String? actionId) {
|
||||||
return actionId == kShortcutActionSwitchTabNext ||
|
return actionId == kShortcutActionSwitchTabNext ||
|
||||||
actionId == kShortcutActionSwitchTabPrev;
|
actionId == kShortcutActionSwitchTabPrev;
|
||||||
@@ -144,9 +168,7 @@ List<Map<String, dynamic>> filterDefaultBindingsForPlatform(
|
|||||||
ShortcutPlatformCapabilities cap,
|
ShortcutPlatformCapabilities cap,
|
||||||
) {
|
) {
|
||||||
final filtered = <Map<String, dynamic>>[];
|
final filtered = <Map<String, dynamic>>[];
|
||||||
for (final raw in bindings) {
|
for (final binding in shortcutBindingMapsFrom(bindings)) {
|
||||||
if (raw is! Map) continue;
|
|
||||||
final binding = Map<String, dynamic>.from(raw);
|
|
||||||
final action = binding['action'] as String?;
|
final action = binding['action'] as String?;
|
||||||
if (!cap.includeFullscreenShortcut &&
|
if (!cap.includeFullscreenShortcut &&
|
||||||
action == kShortcutActionToggleFullscreen) {
|
action == kShortcutActionToggleFullscreen) {
|
||||||
@@ -155,6 +177,10 @@ List<Map<String, dynamic>> filterDefaultBindingsForPlatform(
|
|||||||
if (!cap.includeScreenshotShortcut && action == kShortcutActionScreenshot) {
|
if (!cap.includeScreenshotShortcut && action == kShortcutActionScreenshot) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (!cap.includeScreenshotShortcut &&
|
||||||
|
action == kShortcutActionToggleRelativeMouseMode) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (!cap.includeTabShortcuts && isSwitchTabShortcutAction(action)) {
|
if (!cap.includeTabShortcuts && isSwitchTabShortcutAction(action)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,48 @@ const _kToolbarOwnedActionIds = <String>[
|
|||||||
kShortcutActionSendClipboardKeystrokes,
|
kShortcutActionSendClipboardKeystrokes,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const _kToolbarViewStyleActionIds = <String>[
|
||||||
|
kShortcutActionViewModeOriginal,
|
||||||
|
kShortcutActionViewModeAdaptive,
|
||||||
|
kShortcutActionViewModeCustom,
|
||||||
|
];
|
||||||
|
|
||||||
|
const _kToolbarImageQualityActionIds = <String>[
|
||||||
|
kShortcutActionImageQualityBest,
|
||||||
|
kShortcutActionImageQualityBalanced,
|
||||||
|
kShortcutActionImageQualityLow,
|
||||||
|
];
|
||||||
|
|
||||||
|
const _kToolbarCodecActionIds = <String>[
|
||||||
|
kShortcutActionCodecAuto,
|
||||||
|
kShortcutActionCodecVp8,
|
||||||
|
kShortcutActionCodecVp9,
|
||||||
|
kShortcutActionCodecAv1,
|
||||||
|
kShortcutActionCodecH264,
|
||||||
|
kShortcutActionCodecH265,
|
||||||
|
];
|
||||||
|
|
||||||
|
const _kToolbarCursorActionIds = <String>[
|
||||||
|
kShortcutActionToggleShowRemoteCursor,
|
||||||
|
kShortcutActionToggleFollowRemoteCursor,
|
||||||
|
kShortcutActionToggleFollowRemoteWindow,
|
||||||
|
kShortcutActionToggleZoomCursor,
|
||||||
|
];
|
||||||
|
|
||||||
|
const _kToolbarDisplayToggleActionIds = <String>[
|
||||||
|
kShortcutActionToggleQualityMonitor,
|
||||||
|
kShortcutActionToggleMute,
|
||||||
|
kShortcutActionToggleEnableFileCopyPaste,
|
||||||
|
kShortcutActionToggleDisableClipboard,
|
||||||
|
kShortcutActionToggleLockAfterSessionEnd,
|
||||||
|
kShortcutActionToggleTrueColor,
|
||||||
|
];
|
||||||
|
|
||||||
|
const _kToolbarKeyboardToggleActionIds = <String>[
|
||||||
|
kShortcutActionToggleSwapCtrlCmd,
|
||||||
|
kShortcutActionToggleSwapLeftRightMouse,
|
||||||
|
];
|
||||||
|
|
||||||
class TTextMenu {
|
class TTextMenu {
|
||||||
final Widget child;
|
final Widget child;
|
||||||
final VoidCallback? onPressed;
|
final VoidCallback? onPressed;
|
||||||
@@ -92,7 +134,13 @@ class TToggleMenu {
|
|||||||
/// Register each tagged entry's `onChanged` with the session [ShortcutModel].
|
/// Register each tagged entry's `onChanged` with the session [ShortcutModel].
|
||||||
/// Passthrough — returns [menus] so a caller can wrap `return [...]` directly.
|
/// Passthrough — returns [menus] so a caller can wrap `return [...]` directly.
|
||||||
List<TToggleMenu> _registerToggleMenuShortcuts(
|
List<TToggleMenu> _registerToggleMenuShortcuts(
|
||||||
FFI ffi, List<TToggleMenu> menus) {
|
FFI ffi,
|
||||||
|
List<TToggleMenu> menus, {
|
||||||
|
List<String> ownedActionIds = const [],
|
||||||
|
}) {
|
||||||
|
for (final actionId in ownedActionIds) {
|
||||||
|
ffi.shortcutModel.unregister(actionId);
|
||||||
|
}
|
||||||
for (final menu in menus) {
|
for (final menu in menus) {
|
||||||
final actionId = menu.actionId;
|
final actionId = menu.actionId;
|
||||||
if (actionId == null) continue;
|
if (actionId == null) continue;
|
||||||
@@ -109,7 +157,13 @@ List<TToggleMenu> _registerToggleMenuShortcuts(
|
|||||||
|
|
||||||
/// Radio variant of [_registerToggleMenuShortcuts].
|
/// Radio variant of [_registerToggleMenuShortcuts].
|
||||||
List<TRadioMenu<T>> _registerRadioMenuShortcuts<T>(
|
List<TRadioMenu<T>> _registerRadioMenuShortcuts<T>(
|
||||||
FFI ffi, List<TRadioMenu<T>> menus) {
|
FFI ffi,
|
||||||
|
List<TRadioMenu<T>> menus, {
|
||||||
|
List<String> ownedActionIds = const [],
|
||||||
|
}) {
|
||||||
|
for (final actionId in ownedActionIds) {
|
||||||
|
ffi.shortcutModel.unregister(actionId);
|
||||||
|
}
|
||||||
for (final menu in menus) {
|
for (final menu in menus) {
|
||||||
final actionId = menu.actionId;
|
final actionId = menu.actionId;
|
||||||
if (actionId == null) continue;
|
if (actionId == null) continue;
|
||||||
@@ -486,7 +540,7 @@ Future<List<TRadioMenu<String>>> toolbarViewStyle(
|
|||||||
groupValue: groupValue,
|
groupValue: groupValue,
|
||||||
onChanged: onChanged,
|
onChanged: onChanged,
|
||||||
actionId: kShortcutActionViewModeCustom)
|
actionId: kShortcutActionViewModeCustom)
|
||||||
]);
|
], ownedActionIds: _kToolbarViewStyleActionIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<TRadioMenu<String>>> toolbarImageQuality(
|
Future<List<TRadioMenu<String>>> toolbarImageQuality(
|
||||||
@@ -526,7 +580,7 @@ Future<List<TRadioMenu<String>>> toolbarImageQuality(
|
|||||||
customImageQualityDialog(ffi.sessionId, id, ffi);
|
customImageQualityDialog(ffi.sessionId, id, ffi);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
]);
|
], ownedActionIds: _kToolbarImageQualityActionIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<TRadioMenu<String>>> toolbarCodec(
|
Future<List<TRadioMenu<String>>> toolbarCodec(
|
||||||
@@ -553,7 +607,10 @@ Future<List<TRadioMenu<String>>> toolbarCodec(
|
|||||||
}
|
}
|
||||||
final visible =
|
final visible =
|
||||||
codecs.length == 4 && (codecs[0] || codecs[1] || codecs[2] || codecs[3]);
|
codecs.length == 4 && (codecs[0] || codecs[1] || codecs[2] || codecs[3]);
|
||||||
if (!visible) return [];
|
if (!visible) {
|
||||||
|
return _registerRadioMenuShortcuts<String>(ffi, [],
|
||||||
|
ownedActionIds: _kToolbarCodecActionIds);
|
||||||
|
}
|
||||||
onChanged(String? value) async {
|
onChanged(String? value) async {
|
||||||
if (value == null) return;
|
if (value == null) return;
|
||||||
await bind.sessionPeerOption(
|
await bind.sessionPeerOption(
|
||||||
@@ -583,7 +640,7 @@ Future<List<TRadioMenu<String>>> toolbarCodec(
|
|||||||
if (codecs[1]) radio('AV1', 'av1', codecs[1], kShortcutActionCodecAv1),
|
if (codecs[1]) radio('AV1', 'av1', codecs[1], kShortcutActionCodecAv1),
|
||||||
if (codecs[2]) radio('H264', 'h264', codecs[2], kShortcutActionCodecH264),
|
if (codecs[2]) radio('H264', 'h264', codecs[2], kShortcutActionCodecH264),
|
||||||
if (codecs[3]) radio('H265', 'h265', codecs[3], kShortcutActionCodecH265),
|
if (codecs[3]) radio('H265', 'h265', codecs[3], kShortcutActionCodecH265),
|
||||||
]);
|
], ownedActionIds: _kToolbarCodecActionIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<TToggleMenu>> toolbarCursor(
|
Future<List<TToggleMenu>> toolbarCursor(
|
||||||
@@ -701,7 +758,8 @@ Future<List<TToggleMenu>> toolbarCursor(
|
|||||||
},
|
},
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
return _registerToggleMenuShortcuts(ffi, v);
|
return _registerToggleMenuShortcuts(ffi, v,
|
||||||
|
ownedActionIds: _kToolbarCursorActionIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<TToggleMenu>> toolbarDisplayToggle(
|
Future<List<TToggleMenu>> toolbarDisplayToggle(
|
||||||
@@ -868,7 +926,8 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
|||||||
},
|
},
|
||||||
child: Text(translate('View Mode'))));
|
child: Text(translate('View Mode'))));
|
||||||
}
|
}
|
||||||
return _registerToggleMenuShortcuts(ffi, v);
|
return _registerToggleMenuShortcuts(ffi, v,
|
||||||
|
ownedActionIds: _kToolbarDisplayToggleActionIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
var togglePrivacyModeTime = DateTime.now().subtract(const Duration(hours: 1));
|
var togglePrivacyModeTime = DateTime.now().subtract(const Duration(hours: 1));
|
||||||
@@ -1037,7 +1096,8 @@ List<TToggleMenu> toolbarKeyboardToggles(FFI ffi) {
|
|||||||
onChanged: enabled ? onChanged : null,
|
onChanged: enabled ? onChanged : null,
|
||||||
child: Text(translate('swap-left-right-mouse'))));
|
child: Text(translate('swap-left-right-mouse'))));
|
||||||
}
|
}
|
||||||
return _registerToggleMenuShortcuts(ffi, v);
|
return _registerToggleMenuShortcuts(ffi, v,
|
||||||
|
ownedActionIds: _kToolbarKeyboardToggleActionIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Drive each toolbar helper for its registration side effect, so a shortcut
|
/// Drive each toolbar helper for its registration side effect, so a shortcut
|
||||||
|
|||||||
@@ -774,16 +774,20 @@ class _ControlMenu extends StatelessWidget {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Flexible(child: e.child),
|
Flexible(child: e.child),
|
||||||
Padding(
|
Flexible(
|
||||||
padding: const EdgeInsets.only(left: 16),
|
child: Padding(
|
||||||
child: Text(
|
padding: const EdgeInsets.only(left: 16),
|
||||||
hint,
|
child: Text(
|
||||||
style: Theme.of(context)
|
hint,
|
||||||
.textTheme
|
maxLines: 1,
|
||||||
.bodySmall
|
overflow: TextOverflow.ellipsis,
|
||||||
?.copyWith(
|
style: Theme.of(context)
|
||||||
color: Theme.of(context).hintColor,
|
.textTheme
|
||||||
),
|
.bodySmall
|
||||||
|
?.copyWith(
|
||||||
|
color: Theme.of(context).hintColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -821,22 +821,23 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
showThemeSettings(gFFI.dialogManager);
|
showThemeSettings(gFFI.dialogManager);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
SettingsTile.navigation(
|
if (!disabledSettings)
|
||||||
leading: Icon(Icons.keyboard_outlined),
|
SettingsTile.navigation(
|
||||||
title: Text(translate('Keyboard Shortcuts')),
|
leading: Icon(Icons.keyboard_outlined),
|
||||||
description: Text(ShortcutModel.isEnabled()
|
title: Text(translate('Keyboard Shortcuts')),
|
||||||
? translate('On')
|
description: Text(ShortcutModel.isEnabled()
|
||||||
: translate('Off')),
|
? translate('On')
|
||||||
onPressed: (context) {
|
: translate('Off')),
|
||||||
Navigator.push(
|
onPressed: (context) {
|
||||||
context,
|
Navigator.push(
|
||||||
MaterialPageRoute(
|
context,
|
||||||
builder: (_) => const MobileKeyboardShortcutsPage(),
|
MaterialPageRoute(
|
||||||
)).then((_) {
|
builder: (_) => const MobileKeyboardShortcutsPage(),
|
||||||
if (mounted) setState(() {});
|
)).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')),
|
||||||
|
|||||||
@@ -3933,6 +3933,7 @@ class FFI {
|
|||||||
ffiModel.pi.currentDisplay);
|
ffiModel.pi.currentDisplay);
|
||||||
}
|
}
|
||||||
imageModel.callbacksOnFirstImage.clear();
|
imageModel.callbacksOnFirstImage.clear();
|
||||||
|
shortcutModel.clear();
|
||||||
await imageModel.update(null);
|
await imageModel.update(null);
|
||||||
cursorModel.clear();
|
cursorModel.clear();
|
||||||
ffiModel.clear();
|
ffiModel.clear();
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
@@ -15,6 +16,8 @@ import '../models/model.dart';
|
|||||||
import '../models/platform_model.dart';
|
import '../models/platform_model.dart';
|
||||||
import '../models/state_model.dart';
|
import '../models/state_model.dart';
|
||||||
|
|
||||||
|
typedef ShortcutCallback = FutureOr<void> Function();
|
||||||
|
|
||||||
/// Per-session shortcut dispatcher. Attached to FFI when a session is created.
|
/// Per-session shortcut dispatcher. Attached to FFI when a session is created.
|
||||||
///
|
///
|
||||||
/// The Rust matcher (src/keyboard/shortcuts.rs) emits `shortcut_triggered`
|
/// The Rust matcher (src/keyboard/shortcuts.rs) emits `shortcut_triggered`
|
||||||
@@ -24,13 +27,13 @@ import '../models/state_model.dart';
|
|||||||
/// builders previously registered for that action id.
|
/// builders previously registered for that action id.
|
||||||
class ShortcutModel {
|
class ShortcutModel {
|
||||||
final WeakReference<FFI> parent;
|
final WeakReference<FFI> parent;
|
||||||
final Map<String, VoidCallback> _callbacks = {};
|
final Map<String, ShortcutCallback> _callbacks = {};
|
||||||
|
|
||||||
ShortcutModel(this.parent);
|
ShortcutModel(this.parent);
|
||||||
|
|
||||||
/// Called by toolbar / menu builders to register what to do when the
|
/// Called by toolbar / menu builders to register what to do when the
|
||||||
/// matched shortcut fires.
|
/// matched shortcut fires.
|
||||||
void register(String actionId, VoidCallback callback) {
|
void register(String actionId, ShortcutCallback callback) {
|
||||||
_callbacks[actionId] = callback;
|
_callbacks[actionId] = callback;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,12 +41,19 @@ class ShortcutModel {
|
|||||||
_callbacks.remove(actionId);
|
_callbacks.remove(actionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void clear() {
|
||||||
|
_callbacks.clear();
|
||||||
|
}
|
||||||
|
|
||||||
/// Called by the session event listener when a `shortcut_triggered` event
|
/// Called by the session event listener when a `shortcut_triggered` event
|
||||||
/// arrives for this session.
|
/// arrives for this session.
|
||||||
void onTriggered(String actionId) {
|
void onTriggered(String actionId) {
|
||||||
final cb = _callbacks[actionId];
|
final cb = _callbacks[actionId];
|
||||||
if (cb != null) {
|
if (cb != null) {
|
||||||
cb();
|
unawaited(Future.sync(cb).catchError((e, st) {
|
||||||
|
debugPrint(
|
||||||
|
'shortcut_triggered: handler failed for $actionId: $e\n$st');
|
||||||
|
}));
|
||||||
} else {
|
} else {
|
||||||
debugPrint('shortcut_triggered: no handler for $actionId');
|
debugPrint('shortcut_triggered: no handler for $actionId');
|
||||||
}
|
}
|
||||||
@@ -55,8 +65,7 @@ class ShortcutModel {
|
|||||||
if (raw.isEmpty) return [];
|
if (raw.isEmpty) return [];
|
||||||
try {
|
try {
|
||||||
final parsed = jsonDecode(raw) as Map<String, dynamic>;
|
final parsed = jsonDecode(raw) as Map<String, dynamic>;
|
||||||
final list = (parsed['bindings'] as List?) ?? [];
|
return shortcutBindingMapsFrom(parsed['bindings']);
|
||||||
return list.cast<Map<String, dynamic>>();
|
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -120,14 +129,14 @@ class ShortcutModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
json['enabled'] = v;
|
json['enabled'] = v;
|
||||||
final list = (json['bindings'] as List?) ?? const [];
|
final list = shortcutBindingMapsFrom(json['bindings']);
|
||||||
if (v && list.isEmpty) {
|
if (v && list.isEmpty) {
|
||||||
json['bindings'] = filterDefaultBindingsForPlatform(
|
json['bindings'] = filterDefaultBindingsForPlatform(
|
||||||
jsonDecode(bind.mainGetDefaultKeyboardShortcuts()) as List,
|
jsonDecode(bind.mainGetDefaultKeyboardShortcuts()) as List,
|
||||||
currentPlatformCapabilities(),
|
currentPlatformCapabilities(),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
json['bindings'] ??= <dynamic>[];
|
json['bindings'] = list;
|
||||||
}
|
}
|
||||||
await bind.mainSetLocalOption(
|
await bind.mainSetLocalOption(
|
||||||
key: kShortcutLocalConfigKey, value: jsonEncode(json));
|
key: kShortcutLocalConfigKey, value: jsonEncode(json));
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
|
|
||||||
|
use hbb_common::log;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
const LOCAL_CONFIG_KEY: &str = "keyboard-shortcuts";
|
const LOCAL_CONFIG_KEY: &str = "keyboard-shortcuts";
|
||||||
@@ -255,20 +256,35 @@ pub fn reload_from_config() {
|
|||||||
let parsed = if raw.is_empty() {
|
let parsed = if raw.is_empty() {
|
||||||
Bindings::default()
|
Bindings::default()
|
||||||
} else {
|
} else {
|
||||||
serde_json::from_str(&raw).unwrap_or_default()
|
match serde_json::from_str(&raw) {
|
||||||
|
Ok(parsed) => parsed,
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("Failed to parse keyboard shortcut config: {}", e);
|
||||||
|
Bindings::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
if let Ok(mut w) = CACHE.write() {
|
match CACHE.write() {
|
||||||
*w = Arc::new(parsed);
|
Ok(mut w) => {
|
||||||
|
*w = Arc::new(parsed);
|
||||||
|
}
|
||||||
|
Err(poison) => {
|
||||||
|
log::error!("Keyboard shortcut cache write lock is poisoned");
|
||||||
|
*poison.into_inner() = Arc::new(parsed);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Snapshot of the currently cached bindings. Cheap (one atomic increment) —
|
/// Snapshot of the currently cached bindings. Cheap (one atomic increment) —
|
||||||
/// safe to call on every keystroke.
|
/// safe to call on every keystroke.
|
||||||
pub fn current() -> Arc<Bindings> {
|
pub fn current() -> Arc<Bindings> {
|
||||||
CACHE
|
match CACHE.read() {
|
||||||
.read()
|
Ok(b) => Arc::clone(&b),
|
||||||
.map(|b| Arc::clone(&b))
|
Err(poison) => {
|
||||||
.unwrap_or_else(|_| Arc::new(Bindings::default()))
|
log::error!("Keyboard shortcut cache read lock is poisoned");
|
||||||
|
Arc::clone(&poison.into_inner())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Match an `rdev::Event` against the cached bindings. Returns the matched
|
/// Match an `rdev::Event` against the cached bindings. Returns the matched
|
||||||
|
|||||||
Reference in New Issue
Block a user