fix(tv): host automatic multiline input in the Android IME

Android TV's docked keyboard handles multiline editors natively, so
`automatic` no longer diverts them to the Flutter overlay there. Only
Apple TV keeps the overlay for multiline input — its modal fullscreen
system keyboard cannot edit multiline text. Surfaces that want the
overlay for editing ergonomics (mpv config, connection editor, dialog
text areas) already pin flutterOverlay explicitly.
This commit is contained in:
edde746
2026-08-09 10:06:29 +02:00
parent ce9556db22
commit 0b4fd9e8f3
3 changed files with 40 additions and 7 deletions
+6 -4
View File
@@ -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,
};
@@ -499,9 +499,9 @@ class _AddJellyfinScreenState extends State<AddJellyfinScreen> 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,
@@ -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<TextField>(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);