fix(tv): pause gamepad during native text input
This commit is contained in:
@@ -3,6 +3,7 @@ import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import '../services/gamepad_service.dart';
|
||||
import '../utils/platform_detector.dart';
|
||||
import '../widgets/tv_virtual_keyboard.dart';
|
||||
import 'dpad_navigator.dart';
|
||||
@@ -24,6 +25,7 @@ class _NativeTvTextInputFocusBridge {
|
||||
}
|
||||
|
||||
if (!PlatformDetector.isTV() || PlatformDetector.isAppleTV()) {
|
||||
_focusedTokens.clear();
|
||||
_lastSentFocused = false;
|
||||
return;
|
||||
}
|
||||
@@ -31,6 +33,7 @@ class _NativeTvTextInputFocusBridge {
|
||||
final anyFocused = _focusedTokens.isNotEmpty;
|
||||
if (_lastSentFocused == anyFocused) return;
|
||||
_lastSentFocused = anyFocused;
|
||||
unawaited(GamepadService.setNativeTextInputFocused(anyFocused));
|
||||
unawaited(_sendFocused(anyFocused));
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
@@ -150,6 +151,10 @@ class GamepadService with WindowListener {
|
||||
// Whether the app window is currently focused — ignore gamepad input when false
|
||||
bool _windowFocused = true;
|
||||
bool _nativeKeyHandlerRegistered = false;
|
||||
bool _nativeTextInputFocused = false;
|
||||
|
||||
@visibleForTesting
|
||||
static Future<void> Function(bool focused)? debugNativeTextInputFocusHandler;
|
||||
|
||||
GamepadService._({GamepadDuplicateInputGuard? duplicateInputGuard})
|
||||
: _duplicateInputGuard = duplicateInputGuard ?? GamepadDuplicateInputGuard(enabled: () => Platform.isWindows);
|
||||
@@ -159,6 +164,10 @@ class GamepadService with WindowListener {
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
static Future<void> setNativeTextInputFocused(bool focused) {
|
||||
return instance._setNativeTextInputFocused(focused);
|
||||
}
|
||||
|
||||
/// Start listening to gamepad events.
|
||||
/// Only active on desktop platforms (macOS, Windows, Linux).
|
||||
static bool get _isDesktop => PlatformDetector.isDesktopOS();
|
||||
@@ -255,6 +264,34 @@ class GamepadService with WindowListener {
|
||||
return _duplicateInputGuard.handleNativeKeyEvent(event);
|
||||
}
|
||||
|
||||
Future<void> _setNativeTextInputFocused(bool focused) async {
|
||||
if (_nativeTextInputFocused == focused) return;
|
||||
_nativeTextInputFocused = focused;
|
||||
|
||||
if (focused) {
|
||||
_stopDirectionRepeat();
|
||||
_pressedButtons.clear();
|
||||
_suppressedButtons.clear();
|
||||
_duplicateInputGuard.clear();
|
||||
}
|
||||
|
||||
final debugHandler = debugNativeTextInputFocusHandler;
|
||||
if (debugHandler != null) {
|
||||
await debugHandler(focused);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (focused) {
|
||||
await Gamepad.instance.pause();
|
||||
} else {
|
||||
await Gamepad.instance.resume();
|
||||
}
|
||||
} catch (e) {
|
||||
appLogger.e('GamepadService: Failed to ${focused ? "pause" : "resume"} for native text input', error: e);
|
||||
}
|
||||
}
|
||||
|
||||
void _handleGamepadEvent(GamepadEvent event) {
|
||||
switch (event) {
|
||||
case final GamepadConnectionEvent e:
|
||||
|
||||
+2
-2
@@ -1288,10 +1288,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: universal_gamepad
|
||||
sha256: "3ada9b26e3b3471adc414fd50b8a27d2f890301d9d4cfd7a46732988a9f143d0"
|
||||
sha256: "1373d86676b938f27497a58f61cdf3d3a3e61b849b69c006774182fd7c0f550b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.5"
|
||||
version: "1.5.6"
|
||||
url_launcher:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
||||
+1
-1
@@ -44,7 +44,7 @@ dependencies:
|
||||
path: wakelock_plus
|
||||
path_provider: ^2.1.0
|
||||
path: ^1.9.0
|
||||
universal_gamepad: ^1.5.0
|
||||
universal_gamepad: ^1.5.6
|
||||
drift: ^2.31.0
|
||||
sqlite3_flutter_libs: ^0.5.42
|
||||
crypto: ^3.0.7
|
||||
|
||||
@@ -4,12 +4,14 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:plezy/focus/focusable_text_field.dart';
|
||||
import 'package:plezy/services/gamepad_service.dart';
|
||||
import 'package:plezy/utils/platform_detector.dart';
|
||||
|
||||
void main() {
|
||||
tearDown(() {
|
||||
TvDetectionService.debugSetAppleTVOverride(null);
|
||||
TvDetectionService.setForceTVSync(false);
|
||||
GamepadService.debugNativeTextInputFocusHandler = null;
|
||||
});
|
||||
|
||||
testWidgets('tab traversal focuses the text form field', (tester) async {
|
||||
@@ -343,6 +345,10 @@ void main() {
|
||||
TvDetectionService.setForceTVSync(true);
|
||||
const channel = MethodChannel('com.plezy/text_input');
|
||||
final calls = <MethodCall>[];
|
||||
final gamepadFocusStates = <bool>[];
|
||||
GamepadService.debugNativeTextInputFocusHandler = (focused) async {
|
||||
gamepadFocusStates.add(focused);
|
||||
};
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(channel, (call) async {
|
||||
calls.add(call);
|
||||
return null;
|
||||
@@ -377,6 +383,7 @@ void main() {
|
||||
|
||||
expect(calls.last.method, 'setNativeTextInputFocused');
|
||||
expect(calls.last.arguments, isTrue);
|
||||
expect(gamepadFocusStates, [true]);
|
||||
|
||||
otherFocusNode.requestFocus();
|
||||
await tester.pump();
|
||||
@@ -384,6 +391,7 @@ void main() {
|
||||
|
||||
expect(calls.last.method, 'setNativeTextInputFocused');
|
||||
expect(calls.last.arguments, isFalse);
|
||||
expect(gamepadFocusStates, [true, false]);
|
||||
});
|
||||
|
||||
testWidgets('Android TV physical keyboard still uses field navigation', (tester) async {
|
||||
|
||||
Reference in New Issue
Block a user