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