fix(keyboard): shortcuts, harden config and callback lifecycle

Signed-off-by: fufesou <linlong1266@gmail.com>
This commit is contained in:
fufesou
2026-05-05 21:34:22 +08:00
parent 42a88ac1f0
commit d403d640f8
11 changed files with 181 additions and 63 deletions

View File

@@ -3,6 +3,7 @@ import 'dart:convert';
import 'package:flutter/foundation.dart';
import '../../../consts.dart';
import '../../../models/platform_model.dart';
import 'shortcut_utils.dart';
/// Read the bindings JSON and produce a human-readable shortcut string for
/// `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
// to the remote. Treat as unbound for display purposes.
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(
(b) => b['action'] == actionId,
orElse: () => {},

View File

@@ -109,9 +109,7 @@ class KeyboardShortcutsPageBodyState extends State<KeyboardShortcutsPageBody> {
String? clearActionId,
}) async {
final json = _readJson();
final list = ((json['bindings'] as List?) ?? <dynamic>[])
.cast<Map<String, dynamic>>()
.toList();
final list = shortcutBindingMapsFrom(json['bindings']);
list.removeWhere((b) {
final a = b['action'];
return a == actionId || (clearActionId != null && a == clearActionId);
@@ -172,8 +170,7 @@ class KeyboardShortcutsPageBodyState extends State<KeyboardShortcutsPageBody> {
Future<void> _onEdit(KeyboardShortcutActionEntry entry) async {
final json = _readJson();
final bindings = ((json['bindings'] as List?) ?? <dynamic>[])
.cast<Map<String, dynamic>>();
final bindings = shortcutBindingMapsFrom(json['bindings']);
final result = await showRecordingDialog(
context: context,
actionId: entry.id,

View File

@@ -2,9 +2,9 @@
//
// 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,
// extracts the modifier set + non-modifier key, validates against the
// "must include Ctrl+Alt+Shift (Cmd+Option+Shift on macOS)" rule, and reports
// any conflict with another already-bound action.
// extracts the modifier set + non-modifier key, validates that at least one
// modifier is present, and reports any conflict with another already-bound
// action.
//
// On Save, returns the new binding map ({action, mods, key}) plus the
// 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?;
if (otherAction == null || otherAction == widget.actionId) continue;
final otherKey = b['key'] as String?;
final otherMods =
((b['mods'] as List?) ?? const []).cast<String>().toSet();
final otherMods = shortcutModSetFrom(b['mods']);
if (otherKey == _key &&
otherMods.length == _mods.length &&
otherMods.containsAll(_mods)) {

View File

@@ -229,6 +229,10 @@ List<KeyboardShortcutActionGroup> filterKeyboardShortcutActionGroupsForPlatform(
if (!cap.includeScreenshotShortcut && id == kShortcutActionScreenshot) {
return false;
}
if (!cap.includeScreenshotShortcut &&
id == kShortcutActionToggleRelativeMouseMode) {
return false;
}
if (!cap.includeTabShortcuts && isSwitchTabShortcutAction(id)) return false;
if (!cap.includeToolbarShortcut && id == kShortcutActionToggleToolbar) {
return false;

View File

@@ -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) {
return actionId == kShortcutActionSwitchTabNext ||
actionId == kShortcutActionSwitchTabPrev;
@@ -144,9 +168,7 @@ List<Map<String, dynamic>> filterDefaultBindingsForPlatform(
ShortcutPlatformCapabilities cap,
) {
final filtered = <Map<String, dynamic>>[];
for (final raw in bindings) {
if (raw is! Map) continue;
final binding = Map<String, dynamic>.from(raw);
for (final binding in shortcutBindingMapsFrom(bindings)) {
final action = binding['action'] as String?;
if (!cap.includeFullscreenShortcut &&
action == kShortcutActionToggleFullscreen) {
@@ -155,6 +177,10 @@ List<Map<String, dynamic>> filterDefaultBindingsForPlatform(
if (!cap.includeScreenshotShortcut && action == kShortcutActionScreenshot) {
continue;
}
if (!cap.includeScreenshotShortcut &&
action == kShortcutActionToggleRelativeMouseMode) {
continue;
}
if (!cap.includeTabShortcuts && isSwitchTabShortcutAction(action)) {
continue;
}