import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import '../l10n/generated/app_localizations.dart'; import '../src/rust/api.dart' as rust; const int pttMouseBackButtonBitmask = 0x08; const int pttMouseForwardButtonBitmask = 0x10; /// Translate a [LogicalKeyboardKey] into the platform-neutral label /// stored by the PTT binding flow. String? pttDisplayLabelForKey(LogicalKeyboardKey k) { if (k == LogicalKeyboardKey.space) return 'Space'; if (k == LogicalKeyboardKey.enter || k == LogicalKeyboardKey.numpadEnter) { return 'Enter'; } if (k == LogicalKeyboardKey.tab) return 'Tab'; if (k == LogicalKeyboardKey.escape) return 'Escape'; if (k == LogicalKeyboardKey.backspace) return 'Backspace'; if (k == LogicalKeyboardKey.delete) return 'Delete'; if (k == LogicalKeyboardKey.insert) return 'Insert'; if (k == LogicalKeyboardKey.home) return 'Home'; if (k == LogicalKeyboardKey.end) return 'End'; if (k == LogicalKeyboardKey.pageUp) return 'Page Up'; if (k == LogicalKeyboardKey.pageDown) return 'Page Down'; if (k == LogicalKeyboardKey.arrowUp) return 'Arrow Up'; if (k == LogicalKeyboardKey.arrowDown) return 'Arrow Down'; if (k == LogicalKeyboardKey.arrowLeft) return 'Arrow Left'; if (k == LogicalKeyboardKey.arrowRight) return 'Arrow Right'; if (k == LogicalKeyboardKey.shift || k == LogicalKeyboardKey.shiftLeft || k == LogicalKeyboardKey.shiftRight || k == LogicalKeyboardKey.control || k == LogicalKeyboardKey.controlLeft || k == LogicalKeyboardKey.controlRight || k == LogicalKeyboardKey.alt || k == LogicalKeyboardKey.altLeft || k == LogicalKeyboardKey.altRight || k == LogicalKeyboardKey.meta || k == LogicalKeyboardKey.metaLeft || k == LogicalKeyboardKey.metaRight || k == LogicalKeyboardKey.capsLock || k == LogicalKeyboardKey.numLock || k == LogicalKeyboardKey.scrollLock) { return null; } final fallback = k.keyLabel.trim(); if (fallback.isEmpty) return null; return fallback; } String? pttMouseSideButtonPlatformKeyForLogicalKey(LogicalKeyboardKey key) { return switch (key) { LogicalKeyboardKey.browserBack || LogicalKeyboardKey.goBack => 'mouse-side-button:$pttMouseBackButtonBitmask', LogicalKeyboardKey.browserForward => 'mouse-side-button:$pttMouseForwardButtonBitmask', _ => null, }; } String? pttMouseSideButtonPlatformKeyForButtons(int buttons) { if ((buttons & pttMouseBackButtonBitmask) == pttMouseBackButtonBitmask) { return 'mouse-side-button:$pttMouseBackButtonBitmask'; } if ((buttons & pttMouseForwardButtonBitmask) == pttMouseForwardButtonBitmask) { return 'mouse-side-button:$pttMouseForwardButtonBitmask'; } return null; } /// Result of a successful PTT binding capture. class CapturedBinding { const CapturedBinding({required this.inputClass, required this.platformKey}); /// Coarse input class, safe to persist and display. final rust.BridgePttInputClass inputClass; /// Opaque platform-neutral key label. final String platformKey; } /// 'Save bookmark' name-entry dialog. class BookmarkNameDialog extends StatefulWidget { /// Construct a bookmark-name dialog. const BookmarkNameDialog({super.key, required this.initialName}); /// Initial display name. final String initialName; @override State createState() => _BookmarkNameDialogState(); } class _BookmarkNameDialogState extends State { late final TextEditingController _ctl = TextEditingController( text: widget.initialName, ); @override void dispose() { _ctl.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final l10n = AppL10n.of(context); return AlertDialog( title: Text(l10n.bookmarkAddTitle), content: TextField( controller: _ctl, autofocus: true, decoration: InputDecoration(labelText: l10n.fieldDisplayName), onSubmitted: (_) => Navigator.of(context).pop(_ctl.text), ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: Text(l10n.closeAction), ), FilledButton( onPressed: () => Navigator.of(context).pop(_ctl.text), child: Text(l10n.bookmarkAddAction), ), ], ); } } /// Channel-password dialog. class ChannelPasswordDialog extends StatefulWidget { /// Construct a channel-password dialog. const ChannelPasswordDialog({super.key}); @override State createState() => _ChannelPasswordDialogState(); } class _ChannelPasswordDialogState extends State { final TextEditingController _ctl = TextEditingController(); @override void dispose() { _ctl.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final l10n = AppL10n.of(context); return AlertDialog( title: Text(l10n.channelPasswordTitle), content: TextField( controller: _ctl, obscureText: true, autofocus: true, decoration: InputDecoration(labelText: l10n.fieldPassword), onSubmitted: (_) => Navigator.of(context).pop(_ctl.text), ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: Text(l10n.closeAction), ), FilledButton( onPressed: () => Navigator.of(context).pop(_ctl.text), child: Text(l10n.connectAction), ), ], ); } } /// Focus-scoped dialog that captures the next key press or mouse /// side-button click. class PttBindingCaptureDialog extends StatefulWidget { /// Construct a PTT binding capture dialog. const PttBindingCaptureDialog({super.key}); @override State createState() => _PttBindingCaptureDialogState(); } class _PttBindingCaptureDialogState extends State { final FocusNode _focusNode = FocusNode(); String? _captured; rust.BridgePttInputClass _capturedClass = rust.BridgePttInputClass.none; @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { _focusNode.requestFocus(); }); } @override void dispose() { _focusNode.dispose(); super.dispose(); } KeyEventResult _onKeyEvent(FocusNode node, KeyEvent event) { if (event is! KeyDownEvent) return KeyEventResult.ignored; final mouseSideButton = pttMouseSideButtonPlatformKeyForLogicalKey( event.logicalKey, ); if (mouseSideButton != null) { setState(() { _captured = mouseSideButton; _capturedClass = rust.BridgePttInputClass.mouseSideButton; }); return KeyEventResult.handled; } final label = pttDisplayLabelForKey(event.logicalKey); if (label == null) return KeyEventResult.ignored; setState(() { _captured = label; _capturedClass = rust.BridgePttInputClass.keyboard; }); return KeyEventResult.handled; } void _captureMouseSideButton(String platformKey) { setState(() { _captured = platformKey; _capturedClass = rust.BridgePttInputClass.mouseSideButton; }); } @override Widget build(BuildContext context) { final l10n = AppL10n.of(context); final theme = Theme.of(context); return AlertDialog( title: Text(l10n.pttConfigureTitle), content: SizedBox( width: 360, child: Focus( focusNode: _focusNode, onKeyEvent: _onKeyEvent, autofocus: true, child: Listener( behavior: HitTestBehavior.opaque, onPointerDown: (e) { final platformKey = pttMouseSideButtonPlatformKeyForButtons( e.buttons, ); if (platformKey != null) { _captureMouseSideButton(platformKey); } }, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( l10n.pttConfigurePrompt, style: theme.textTheme.bodyMedium, ), const SizedBox(height: 12), Container( padding: const EdgeInsets.symmetric( vertical: 12, horizontal: 16, ), decoration: BoxDecoration( color: theme.colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(6), ), child: Text( _captured == null ? l10n.pttConfigureWaiting : '${l10n.pttConfigureCaptured}: $_captured', style: theme.textTheme.bodyMedium?.copyWith( fontFamily: 'monospace', ), ), ), const SizedBox(height: 12), Text( l10n.pttConfigurePrivacyNote, style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), ), ], ), ), ), ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: Text(l10n.closeAction), ), FilledButton( onPressed: _captured == null ? null : () => Navigator.of(context).pop( CapturedBinding( inputClass: _capturedClass, platformKey: _captured!, ), ), child: Text(l10n.pttConfigureSaveAction), ), ], ); } }