diff --git a/lib/services/gamepad_service.dart b/lib/services/gamepad_service.dart index 27efd8ea..e7eacc3e 100644 --- a/lib/services/gamepad_service.dart +++ b/lib/services/gamepad_service.dart @@ -5,6 +5,7 @@ import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:universal_gamepad/universal_gamepad.dart'; +import 'package:window_manager/window_manager.dart'; import '../utils/app_logger.dart'; import '../utils/key_event_simulator.dart' as key_sim; @@ -14,7 +15,7 @@ import '../utils/key_event_simulator.dart' as key_sim; /// Listens to gamepad events from the `universal_gamepad` package and translates /// them into focus navigation actions and key events that integrate with the /// existing keyboard navigation system. -class GamepadService { +class GamepadService with WindowListener { static GamepadService? _instance; StreamSubscription? _subscription; @@ -48,6 +49,9 @@ class GamepadService { // Track button states to prevent repeated events from button holds final Set _pressedButtons = {}; + // Whether the app window is currently focused — ignore gamepad input when false + bool _windowFocused = true; + GamepadService._(); /// Get the singleton instance. @@ -75,6 +79,10 @@ class GamepadService { appLogger.e('GamepadService: Error listing gamepads', error: e); } + // Track window focus so we ignore gamepad input when another app is active + windowManager.addListener(this); + _windowFocused = await windowManager.isFocused(); + _subscription?.cancel(); _subscription = Gamepad.instance.events.listen( _handleGamepadEvent, @@ -88,9 +96,35 @@ class GamepadService { _stopDirectionRepeat(); _subscription?.cancel(); _subscription = null; + windowManager.removeListener(this); Gamepad.instance.dispose(); } + @override + void onWindowFocus() => _windowFocused = true; + + @override + void onWindowBlur() { + _windowFocused = false; + _stopDirectionRepeat(); + + // Send key-up for any face buttons that are mid-hold so widgets + // waiting for the release (e.g. long-press timers) don't get stuck. + if (_pressedButtons.contains(GamepadButton.a)) { + _simulateKeyUp(LogicalKeyboardKey.enter); + } + if (_pressedButtons.contains(GamepadButton.x)) { + _simulateKeyUp(LogicalKeyboardKey.gameButtonX); + } + _pressedButtons.clear(); + + // Reset analog stick state so re-focus doesn't inherit stale direction + _leftStickUp = false; + _leftStickDown = false; + _leftStickLeft = false; + _leftStickRight = false; + } + void _handleGamepadEvent(GamepadEvent event) { switch (event) { case GamepadConnectionEvent e: @@ -103,6 +137,8 @@ class GamepadService { } void _handleButton(GamepadButtonEvent event) { + if (!_windowFocused) return; + // Switch to keyboard mode on any button press if (event.pressed) { onGamepadInput?.call(); @@ -167,6 +203,8 @@ class GamepadService { } void _handleAxis(GamepadAxisEvent event) { + if (!_windowFocused) return; + // Switch to keyboard mode on significant axis input if (event.value.abs() > 0.3) { onGamepadInput?.call();