diff --git a/lib/focus/focusable_text_field.dart b/lib/focus/focusable_text_field.dart index a54fbca7..ea574608 100644 --- a/lib/focus/focusable_text_field.dart +++ b/lib/focus/focusable_text_field.dart @@ -12,9 +12,10 @@ import 'key_event_utils.dart'; import 'owned_focus_node_binding.dart'; enum TvTextInputPresentation { - /// Use the native platform keyboard for single-line input on every TV and - /// the Flutter overlay for multiline input, whose newline/caret handling - /// the TV IMEs do not cover well. + /// Use the native platform keyboard wherever it can host the field: always + /// on Android TV (its docked IME handles multiline input), and for + /// single-line input on Apple TV, whose modal fullscreen keyboard cannot + /// edit multiline text — that falls back to the Flutter overlay. automatic, /// Always use the platform text input implementation. @@ -27,7 +28,8 @@ enum TvTextInputPresentation { bool _usesTvKeyboard({required TvTextInputPresentation presentation, TextInputType? keyboardType, int? maxLines}) { if (!PlatformDetector.isTV()) return false; return switch (presentation) { - TvTextInputPresentation.automatic => _isMultilineTextInput(keyboardType: keyboardType, maxLines: maxLines), + TvTextInputPresentation.automatic => + PlatformDetector.isAppleTV() && _isMultilineTextInput(keyboardType: keyboardType, maxLines: maxLines), TvTextInputPresentation.platform => false, TvTextInputPresentation.flutterOverlay => true, }; diff --git a/lib/screens/settings/add_jellyfin_screen.dart b/lib/screens/settings/add_jellyfin_screen.dart index aa8325a0..fcd3154f 100644 --- a/lib/screens/settings/add_jellyfin_screen.dart +++ b/lib/screens/settings/add_jellyfin_screen.dart @@ -499,9 +499,9 @@ class _AddJellyfinScreenState extends State with AsyncFormSta FocusableTextFormField( controller: _urlController, focusNode: _urlFocus, - // Native on every TV: `automatic` would route this wrap-to-4-lines - // field to the Flutter overlay, but it is logically single-line URL - // input and the platform IME handles it (#1051, #1079). + // Native on every TV: on Apple TV `automatic` would route this + // wrap-to-4-lines field to the Flutter overlay, but it is logically + // single-line URL input the system keyboard handles (#1051, #1079). tvTextInputPresentation: TvTextInputPresentation.platform, autofocus: true, tvTextInputAutoOpenBehavior: deferredUrlFieldAutoOpen, diff --git a/test/widgets/focusable_text_field_test.dart b/test/widgets/focusable_text_field_test.dart index 87c3e6c6..fc2ac0d3 100644 --- a/test/widgets/focusable_text_field_test.dart +++ b/test/widgets/focusable_text_field_test.dart @@ -849,6 +849,37 @@ void main() { expect(find.byKey(const Key('tv_virtual_keyboard_panel')), findsNothing); }); + testWidgets('Android TV automatic multiline input uses the platform field', (tester) async { + TvDetectionService.debugSetAppleTVOverride(null); + await TvDetectionService.getInstance(forceTv: true); + TvDetectionService.setForceTVSync(true); + final controller = TextEditingController(); + final fieldFocusNode = FocusNode(debugLabel: 'notes_field'); + addTearDown(controller.dispose); + addTearDown(fieldFocusNode.dispose); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: FocusableTextField( + controller: controller, + focusNode: fieldFocusNode, + keyboardType: TextInputType.multiline, + maxLines: 4, + ), + ), + ), + ); + + fieldFocusNode.requestFocus(); + await tester.pumpAndSettle(); + + // Unlike Apple TV's modal fullscreen keyboard, the docked Android IME + // hosts multiline input natively — no Flutter overlay. + expect(tester.widget(find.byType(TextField)).readOnly, isFalse); + expect(find.byKey(const Key('tv_virtual_keyboard_panel')), findsNothing); + }); + testWidgets('Android TV after-first-focus skips initial auto-open and opens on refocus', (tester) async { TvDetectionService.debugSetAppleTVOverride(null); await TvDetectionService.getInstance(forceTv: true);