From 3168f6327f22318b6b3c1bc14681f78b97ea061f Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Wed, 15 Jul 2026 06:40:47 +0200 Subject: [PATCH] fix(tv): make select activation one-shot --- lib/screens/profile/pin_entry_dialog.dart | 8 ++-- lib/widgets/tv_virtual_keyboard.dart | 9 ++-- test/widgets/pin_entry_dialog_test.dart | 51 ++++++++++++++++++++++ test/widgets/tv_virtual_keyboard_test.dart | 15 +++++++ 4 files changed, 73 insertions(+), 10 deletions(-) diff --git a/lib/screens/profile/pin_entry_dialog.dart b/lib/screens/profile/pin_entry_dialog.dart index e95b8ea2..df25dd4d 100644 --- a/lib/screens/profile/pin_entry_dialog.dart +++ b/lib/screens/profile/pin_entry_dialog.dart @@ -380,6 +380,9 @@ class _TvPinInputState extends State<_TvPinInput> with ControllerDisposerMixin { final backResult = handleBackKeyAction(event, widget.onCancel); if (backResult != KeyEventResult.ignored) return backResult; + if (event.isTvSelectEvent || (PlatformDetector.isTV() && event.isPhysicalKeyboardEnter)) { + return handleOneShotSelect(event, () => _activate(_rows[_row][_column])); + } if (event is KeyDownEvent || event is KeyRepeatEvent) { if (key == LogicalKeyboardKey.backspace || key == LogicalKeyboardKey.delete) { @@ -387,11 +390,6 @@ class _TvPinInputState extends State<_TvPinInput> with ControllerDisposerMixin { return KeyEventResult.handled; } - if (event.isTvSelectEvent || (PlatformDetector.isTV() && event.isPhysicalKeyboardEnter)) { - _activate(_rows[_row][_column]); - return KeyEventResult.handled; - } - if (event.isPhysicalKeyboardEnter) { _trySubmit(); return KeyEventResult.handled; diff --git a/lib/widgets/tv_virtual_keyboard.dart b/lib/widgets/tv_virtual_keyboard.dart index 8ed5da83..1b3ae138 100644 --- a/lib/widgets/tv_virtual_keyboard.dart +++ b/lib/widgets/tv_virtual_keyboard.dart @@ -5,6 +5,7 @@ import 'package:flutter/services.dart'; import 'package:material_symbols_icons/symbols.dart'; import '../focus/dpad_navigator.dart'; +import '../focus/key_event_utils.dart'; import '../i18n/strings.g.dart'; import '../utils/platform_detector.dart'; import 'app_icon.dart'; @@ -348,6 +349,9 @@ class _TvVirtualKeyboardDialogState extends State<_TvVirtualKeyboardDialog> { if (event is KeyUpEvent) Navigator.of(context).pop(); return KeyEventResult.handled; } + if (event.isTvSelectEvent) { + return handleOneShotSelect(event, () => _activate(_rows[_row][_column])); + } if (event is KeyDownEvent || event is KeyRepeatEvent) { if (_handlePhysicalKeyboardTextInput(event)) return KeyEventResult.handled; @@ -370,11 +374,6 @@ class _TvVirtualKeyboardDialogState extends State<_TvVirtualKeyboardDialog> { return KeyEventResult.handled; } - if (event.isTvSelectEvent) { - _activate(_rows[_row][_column]); - return KeyEventResult.handled; - } - if (key.isUpKey) { _moveVertical(-1); return KeyEventResult.handled; diff --git a/test/widgets/pin_entry_dialog_test.dart b/test/widgets/pin_entry_dialog_test.dart index 84d68751..55945455 100644 --- a/test/widgets/pin_entry_dialog_test.dart +++ b/test/widgets/pin_entry_dialog_test.dart @@ -219,6 +219,29 @@ void main() { expect(find.byType(PinEntryDialog), findsNothing); }); + testWidgets('held D-pad select activates each PIN key once', (tester) async { + TvDetectionService.debugSetAppleTVOverride(true); + String? result; + + await _pumpPinDialogLauncher(tester, onResult: (pin) => result = pin); + await tester.tap(find.text('Open')); + await tester.pumpAndSettle(); + + await _holdDpadSelect(tester); // 1 + await _pressDpadKey(tester, LogicalKeyboardKey.arrowRight, PhysicalKeyboardKey.arrowRight); + await _pressDpadKey(tester, LogicalKeyboardKey.select, PhysicalKeyboardKey.select); // 2 + await _pressDpadKey(tester, LogicalKeyboardKey.arrowRight, PhysicalKeyboardKey.arrowRight); + await _pressDpadKey(tester, LogicalKeyboardKey.select, PhysicalKeyboardKey.select); // 3 + await _pressDpadKey(tester, LogicalKeyboardKey.arrowLeft, PhysicalKeyboardKey.arrowLeft); + await _pressDpadKey(tester, LogicalKeyboardKey.arrowLeft, PhysicalKeyboardKey.arrowLeft); + await _pressDpadKey(tester, LogicalKeyboardKey.arrowDown, PhysicalKeyboardKey.arrowDown); + await _pressDpadKey(tester, LogicalKeyboardKey.select, PhysicalKeyboardKey.select); // 4 + await tester.pumpAndSettle(); + + expect(result, '1234'); + expect(find.byType(PinEntryDialog), findsNothing); + }); + testWidgets('Android TV keyboard-mapped enter/select activates highlighted PIN key', (tester) async { TvDetectionService.debugSetAppleTVOverride(true); String? result; @@ -359,6 +382,34 @@ Future _pressKey(WidgetTester tester, LogicalKeyboardKey key) async { await tester.pump(); } +Future _holdDpadSelect(WidgetTester tester) async { + _dispatchKey( + const KeyDownEvent( + physicalKey: PhysicalKeyboardKey.select, + logicalKey: LogicalKeyboardKey.select, + timeStamp: Duration.zero, + deviceType: ui.KeyEventDeviceType.directionalPad, + ), + ); + _dispatchKey( + const KeyRepeatEvent( + physicalKey: PhysicalKeyboardKey.select, + logicalKey: LogicalKeyboardKey.select, + timeStamp: Duration(milliseconds: 100), + deviceType: ui.KeyEventDeviceType.directionalPad, + ), + ); + _dispatchKey( + const KeyUpEvent( + physicalKey: PhysicalKeyboardKey.select, + logicalKey: LogicalKeyboardKey.select, + timeStamp: Duration(milliseconds: 200), + deviceType: ui.KeyEventDeviceType.directionalPad, + ), + ); + await tester.pump(); +} + Future _pressDpadKey(WidgetTester tester, LogicalKeyboardKey logicalKey, PhysicalKeyboardKey physicalKey) async { _dispatchKey( KeyDownEvent( diff --git a/test/widgets/tv_virtual_keyboard_test.dart b/test/widgets/tv_virtual_keyboard_test.dart index 7c442f1f..51807bff 100644 --- a/test/widgets/tv_virtual_keyboard_test.dart +++ b/test/widgets/tv_virtual_keyboard_test.dart @@ -43,6 +43,21 @@ void main() { expect(find.byType(Dialog), findsOneWidget); }); + testWidgets('held select activates the highlighted key once', (tester) async { + final controller = TextEditingController(); + addTearDown(controller.dispose); + + await _pumpKeyboard(tester, controller: controller); + + await tester.sendKeyDownEvent(LogicalKeyboardKey.select); + await tester.sendKeyRepeatEvent(LogicalKeyboardKey.select); + await tester.sendKeyUpEvent(LogicalKeyboardKey.select); + await tester.pump(); + + expect(controller.text, '1'); + expect(find.byType(Dialog), findsOneWidget); + }); + testWidgets('directional pad enter activates highlighted key', (tester) async { final controller = TextEditingController(); addTearDown(controller.dispose);