/// Layout-independent representation of a single keystroke. /// /// The consultant's note (T-110) flagged the old `KeybindingResolver` /// for keying off `LogicalKeyboardKey.keyLabel`, which is locale-aware /// (US-QWERTY `Ctrl+/` differs from AZERTY `Ctrl+:`). We key off /// `LogicalKeyboardKey.keyId` instead — a stable u32 that survives /// layout changes. library; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; /// One of the four POSIX-style modifier keys. Order is the canonical /// presentation order in YAML and toString output. enum KeyModifier { ctrl, alt, shift, meta; /// Lowercase short form used in YAML (`ctrl`, `alt`, `shift`, `meta`). String get yaml => name; /// Display string used in palette + tooltip hints. String get display => switch (this) { KeyModifier.ctrl => 'Ctrl', KeyModifier.alt => 'Alt', KeyModifier.shift => 'Shift', KeyModifier.meta => 'Cmd', }; } /// A modifier-set + a single key, identified by layout-independent /// `LogicalKeyboardKey.keyId`. Canonicalised on construction /// (modifiers sorted by enum order) so equality + hashing work for /// lookup-keying. @immutable class KeyChord { factory KeyChord({Set modifiers = const {}, required LogicalKeyboardKey key}) { final sorted = modifiers.toList()..sort((a, b) => a.index.compareTo(b.index)); return KeyChord._(List.unmodifiable(sorted), key); } const KeyChord._(this.modifiers, this.key); final List modifiers; final LogicalKeyboardKey key; /// Build from a Flutter [KeyEvent]. Returns null for non-down events /// or events whose logical key has no meaningful id (e.g. a bare /// modifier press in isolation). static KeyChord? fromKeyEvent(KeyEvent event, HardwareKeyboard kb) { if (event is! KeyDownEvent && event is! KeyRepeatEvent) return null; final logical = event.logicalKey; // Bare modifier presses don't form a chord on their own. if (_isBareModifier(logical)) return null; final mods = { if (kb.isControlPressed) KeyModifier.ctrl, if (kb.isAltPressed) KeyModifier.alt, if (kb.isShiftPressed) KeyModifier.shift, if (kb.isMetaPressed) KeyModifier.meta, }; return KeyChord(modifiers: mods, key: logical); } /// Parse a YAML chord spec like `ctrl+shift+p`, `cmd+enter`, `escape`. /// Whitespace tolerated. Throws [FormatException] on unknown tokens /// or empty input. static KeyChord parse(String spec) { final trimmed = spec.trim(); if (trimmed.isEmpty) throw const FormatException('empty key chord'); final parts = trimmed.split('+').map((s) => s.trim()).toList(); final keyName = parts.removeLast(); if (keyName.isEmpty) throw FormatException('missing key in chord: "$spec"'); final mods = {}; for (final m in parts) { final mod = _modByName(m); if (mod == null) throw FormatException('unknown modifier "$m" in chord: "$spec"'); mods.add(mod); } final key = _keyByName(keyName); if (key == null) throw FormatException('unknown key "$keyName" in chord: "$spec"'); return KeyChord(modifiers: mods, key: key); } /// Canonical YAML form: `ctrl+shift+p`. String get canonical { final modPart = modifiers.map((m) => m.yaml).join('+'); final keyPart = _keyName(key); return modPart.isEmpty ? keyPart : '$modPart+$keyPart'; } /// Display form for UI hints: `Ctrl+Shift+P`. String get display { final modPart = modifiers.map((m) => m.display).join('+'); final keyPart = _keyName(key).toUpperCase(); return modPart.isEmpty ? keyPart : '$modPart+$keyPart'; } @override bool operator ==(Object other) => other is KeyChord && other.key == key && listEquals(other.modifiers, modifiers); @override int get hashCode => Object.hash(key, Object.hashAll(modifiers)); @override String toString() => 'KeyChord($canonical)'; } bool _isBareModifier(LogicalKeyboardKey k) => k == LogicalKeyboardKey.control || k == LogicalKeyboardKey.controlLeft || k == LogicalKeyboardKey.controlRight || k == LogicalKeyboardKey.alt || k == LogicalKeyboardKey.altLeft || k == LogicalKeyboardKey.altRight || k == LogicalKeyboardKey.shift || k == LogicalKeyboardKey.shiftLeft || k == LogicalKeyboardKey.shiftRight || k == LogicalKeyboardKey.meta || k == LogicalKeyboardKey.metaLeft || k == LogicalKeyboardKey.metaRight || k == LogicalKeyboardKey.fn; KeyModifier? _modByName(String name) { switch (name.toLowerCase()) { case 'ctrl': case 'control': return KeyModifier.ctrl; case 'alt': case 'option': return KeyModifier.alt; case 'shift': return KeyModifier.shift; case 'meta': case 'cmd': case 'command': case 'super': case 'win': return KeyModifier.meta; } return null; } // -- Key name <-> LogicalKeyboardKey ---------------------------------------- // // We map YAML names to LogicalKeyboardKey instances. The map covers // every key a binding can plausibly want; unknown names throw on parse. const Map _byName = { // Letters 'a': LogicalKeyboardKey.keyA, 'b': LogicalKeyboardKey.keyB, 'c': LogicalKeyboardKey.keyC, 'd': LogicalKeyboardKey.keyD, 'e': LogicalKeyboardKey.keyE, 'f': LogicalKeyboardKey.keyF, 'g': LogicalKeyboardKey.keyG, 'h': LogicalKeyboardKey.keyH, 'i': LogicalKeyboardKey.keyI, 'j': LogicalKeyboardKey.keyJ, 'k': LogicalKeyboardKey.keyK, 'l': LogicalKeyboardKey.keyL, 'm': LogicalKeyboardKey.keyM, 'n': LogicalKeyboardKey.keyN, 'o': LogicalKeyboardKey.keyO, 'p': LogicalKeyboardKey.keyP, 'q': LogicalKeyboardKey.keyQ, 'r': LogicalKeyboardKey.keyR, 's': LogicalKeyboardKey.keyS, 't': LogicalKeyboardKey.keyT, 'u': LogicalKeyboardKey.keyU, 'v': LogicalKeyboardKey.keyV, 'w': LogicalKeyboardKey.keyW, 'x': LogicalKeyboardKey.keyX, 'y': LogicalKeyboardKey.keyY, 'z': LogicalKeyboardKey.keyZ, // Digits '0': LogicalKeyboardKey.digit0, '1': LogicalKeyboardKey.digit1, '2': LogicalKeyboardKey.digit2, '3': LogicalKeyboardKey.digit3, '4': LogicalKeyboardKey.digit4, '5': LogicalKeyboardKey.digit5, '6': LogicalKeyboardKey.digit6, '7': LogicalKeyboardKey.digit7, '8': LogicalKeyboardKey.digit8, '9': LogicalKeyboardKey.digit9, // Function keys 'f1': LogicalKeyboardKey.f1, 'f2': LogicalKeyboardKey.f2, 'f3': LogicalKeyboardKey.f3, 'f4': LogicalKeyboardKey.f4, 'f5': LogicalKeyboardKey.f5, 'f6': LogicalKeyboardKey.f6, 'f7': LogicalKeyboardKey.f7, 'f8': LogicalKeyboardKey.f8, 'f9': LogicalKeyboardKey.f9, 'f10': LogicalKeyboardKey.f10, 'f11': LogicalKeyboardKey.f11, 'f12': LogicalKeyboardKey.f12, // Arrows 'left': LogicalKeyboardKey.arrowLeft, 'right': LogicalKeyboardKey.arrowRight, 'up': LogicalKeyboardKey.arrowUp, 'down': LogicalKeyboardKey.arrowDown, // Common control keys 'enter': LogicalKeyboardKey.enter, 'return': LogicalKeyboardKey.enter, 'escape': LogicalKeyboardKey.escape, 'esc': LogicalKeyboardKey.escape, 'tab': LogicalKeyboardKey.tab, 'space': LogicalKeyboardKey.space, 'backspace': LogicalKeyboardKey.backspace, 'delete': LogicalKeyboardKey.delete, 'home': LogicalKeyboardKey.home, 'end': LogicalKeyboardKey.end, 'pageup': LogicalKeyboardKey.pageUp, 'pagedown': LogicalKeyboardKey.pageDown, 'insert': LogicalKeyboardKey.insert, // Punctuation (US-QWERTY positions; preset authors can rely on these names). 'minus': LogicalKeyboardKey.minus, '-': LogicalKeyboardKey.minus, 'equal': LogicalKeyboardKey.equal, '=': LogicalKeyboardKey.equal, 'comma': LogicalKeyboardKey.comma, ',': LogicalKeyboardKey.comma, 'period': LogicalKeyboardKey.period, '.': LogicalKeyboardKey.period, 'slash': LogicalKeyboardKey.slash, '/': LogicalKeyboardKey.slash, 'backslash': LogicalKeyboardKey.backslash, r'\\': LogicalKeyboardKey.backslash, 'semicolon': LogicalKeyboardKey.semicolon, ';': LogicalKeyboardKey.semicolon, 'quote': LogicalKeyboardKey.quote, "'": LogicalKeyboardKey.quote, 'bracketLeft': LogicalKeyboardKey.bracketLeft, '[': LogicalKeyboardKey.bracketLeft, 'bracketRight': LogicalKeyboardKey.bracketRight, ']': LogicalKeyboardKey.bracketRight, 'backquote': LogicalKeyboardKey.backquote, '`': LogicalKeyboardKey.backquote, }; LogicalKeyboardKey? _keyByName(String name) => _byName[name.toLowerCase()]; String _keyName(LogicalKeyboardKey key) { // Reverse lookup; prefer the canonical (first) name for each key. for (final entry in _byName.entries) { if (entry.value == key) return entry.key; } // Fallback: use the debugName-like representation. return key.keyLabel.isNotEmpty ? key.keyLabel.toLowerCase() : 'key(0x${key.keyId.toRadixString(16)})'; }