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:
@@ -12,9 +12,10 @@ import 'key_event_utils.dart';
|
|||||||
import 'owned_focus_node_binding.dart';
|
import 'owned_focus_node_binding.dart';
|
||||||
|
|
||||||
enum TvTextInputPresentation {
|
enum TvTextInputPresentation {
|
||||||
/// Use the native platform keyboard for single-line input on every TV and
|
/// Use the native platform keyboard wherever it can host the field: always
|
||||||
/// the Flutter overlay for multiline input, whose newline/caret handling
|
/// on Android TV (its docked IME handles multiline input), and for
|
||||||
/// the TV IMEs do not cover well.
|
/// single-line input on Apple TV, whose modal fullscreen keyboard cannot
|
||||||
|
/// edit multiline text — that falls back to the Flutter overlay.
|
||||||
automatic,
|
automatic,
|
||||||
|
|
||||||
/// Always use the platform text input implementation.
|
/// Always use the platform text input implementation.
|
||||||
@@ -27,7 +28,8 @@ enum TvTextInputPresentation {
|
|||||||
bool _usesTvKeyboard({required TvTextInputPresentation presentation, TextInputType? keyboardType, int? maxLines}) {
|
bool _usesTvKeyboard({required TvTextInputPresentation presentation, TextInputType? keyboardType, int? maxLines}) {
|
||||||
if (!PlatformDetector.isTV()) return false;
|
if (!PlatformDetector.isTV()) return false;
|
||||||
return switch (presentation) {
|
return switch (presentation) {
|
||||||
TvTextInputPresentation.automatic => _isMultilineTextInput(keyboardType: keyboardType, maxLines: maxLines),
|
TvTextInputPresentation.automatic =>
|
||||||
|
PlatformDetector.isAppleTV() && _isMultilineTextInput(keyboardType: keyboardType, maxLines: maxLines),
|
||||||
TvTextInputPresentation.platform => false,
|
TvTextInputPresentation.platform => false,
|
||||||
TvTextInputPresentation.flutterOverlay => true,
|
TvTextInputPresentation.flutterOverlay => true,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -499,9 +499,9 @@ class _AddJellyfinScreenState extends State<AddJellyfinScreen> with AsyncFormSta
|
|||||||
FocusableTextFormField(
|
FocusableTextFormField(
|
||||||
controller: _urlController,
|
controller: _urlController,
|
||||||
focusNode: _urlFocus,
|
focusNode: _urlFocus,
|
||||||
// Native on every TV: `automatic` would route this wrap-to-4-lines
|
// Native on every TV: on Apple TV `automatic` would route this
|
||||||
// field to the Flutter overlay, but it is logically single-line URL
|
// wrap-to-4-lines field to the Flutter overlay, but it is logically
|
||||||
// input and the platform IME handles it (#1051, #1079).
|
// single-line URL input the system keyboard handles (#1051, #1079).
|
||||||
tvTextInputPresentation: TvTextInputPresentation.platform,
|
tvTextInputPresentation: TvTextInputPresentation.platform,
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
tvTextInputAutoOpenBehavior: deferredUrlFieldAutoOpen,
|
tvTextInputAutoOpenBehavior: deferredUrlFieldAutoOpen,
|
||||||
|
|||||||
@@ -849,6 +849,37 @@ void main() {
|
|||||||
expect(find.byKey(const Key('tv_virtual_keyboard_panel')), findsNothing);
|
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 {
|
testWidgets('Android TV after-first-focus skips initial auto-open and opens on refocus', (tester) async {
|
||||||
TvDetectionService.debugSetAppleTVOverride(null);
|
TvDetectionService.debugSetAppleTVOverride(null);
|
||||||
await TvDetectionService.getInstance(forceTv: true);
|
await TvDetectionService.getInstance(forceTv: true);
|
||||||
|
|||||||
Reference in New Issue
Block a user