import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import '../focus/dpad_navigator.dart'; import '../models/hotkey_model.dart'; /// Captures a key combination from the user and calls [onHotKeyRecorded]. class HotKeyRecorder extends StatefulWidget { const HotKeyRecorder({ super.key, this.initalHotKey, required this.onHotKeyRecorded, this.enabled = true, this.placeholder, }); final HotKey? initalHotKey; final ValueChanged onHotKeyRecorded; final bool enabled; final Widget? placeholder; @override State createState() => _HotKeyRecorderState(); } class _HotKeyRecorderState extends State { HotKey? _hotKey; @override void initState() { super.initState(); _hotKey = widget.initalHotKey; HardwareKeyboard.instance.addHandler(_handleKeyEvent); } @override void didUpdateWidget(HotKeyRecorder oldWidget) { super.didUpdateWidget(oldWidget); if (widget.initalHotKey != oldWidget.initalHotKey) { _hotKey = widget.initalHotKey; } } @override void dispose() { HardwareKeyboard.instance.removeHandler(_handleKeyEvent); super.dispose(); } bool _handleKeyEvent(KeyEvent keyEvent) { if (!widget.enabled) return false; if (keyEvent.logicalKey.isBackKey) return false; if (keyEvent is KeyUpEvent) return false; final physicalKeysPressed = HardwareKeyboard.instance.physicalKeysPressed; final PhysicalKeyboardKey key = keyEvent.physicalKey; // Detect which modifiers are currently held down, excluding the primary key final List modifiers = HotKeyModifier.values .where((m) => m.physicalKeys.any(physicalKeysPressed.contains)) .where((m) => !m.physicalKeys.contains(key)) .toList(); final hotKey = HotKey(key: key, modifiers: modifiers.isNotEmpty ? modifiers : null); setState(() => _hotKey = hotKey); final isModifierOnly = HotKeyModifier.values.any((modifier) => modifier.physicalKeys.contains(key)); if (isModifierOnly) return true; if (keyEvent.logicalKey.isSelectKey) { SelectKeyUpSuppressor.suppressSelectUntilKeyUp(); } widget.onHotKeyRecorded(hotKey); return true; } @override Widget build(BuildContext context) { if (_hotKey == null) return widget.placeholder ?? const SizedBox.shrink(); return HotKeyVirtualView(hotKey: _hotKey!); } } List _hotKeyDisplayLabels(HotKey hotKey) => [ for (final modifier in hotKey.modifiers ?? []) physicalKeyLabel(modifier.physicalKeys.first), physicalKeyLabel(hotKey.key), ]; /// Formats a shortcut from the same ordered key labels rendered by /// [HotKeyVirtualView]. String formatHotKeyDisplay(HotKey hotKey) => _hotKeyDisplayLabels(hotKey).join(' + '); /// Renders a [HotKey] as a row of styled key label chips. class HotKeyVirtualView extends StatelessWidget { const HotKeyVirtualView({super.key, required this.hotKey}); final HotKey hotKey; @override Widget build(BuildContext context) { final keyLabels = _hotKeyDisplayLabels(hotKey); return Wrap(spacing: 8, children: [for (final keyLabel in keyLabels) _VirtualKeyView(keyLabel: keyLabel)]); } } class _VirtualKeyView extends StatelessWidget { const _VirtualKeyView({required this.keyLabel}); final String keyLabel; @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 3), decoration: BoxDecoration( color: Theme.of(context).canvasColor, border: Border.fromBorderSide(BorderSide(color: Theme.of(context).dividerColor)), borderRadius: const BorderRadius.all(Radius.circular(3)), boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.3), offset: const Offset(0.0, 1.0))], ), child: Text(keyLabel, style: TextStyle(color: Theme.of(context).textTheme.bodyMedium?.color, fontSize: 12)), ); } }