fix(player): keep the remote on the player surface after a window switch (#1797)
Returning to the desktop window with the chrome still up left arrow keys navigating the OSD instead of seeking: the first press seeked and silently moved focus onto Play/Pause, and every press after that walked the buttons. A window blur drops Flutter's primary focus to the root scope, so the player screen's reclaim parks it on its own node. The only handoff back down to the controls was the chrome visible->hidden transition, so with the OSD up nothing reclaimed it -- hence the reported workarounds of letting the controls hide, or moving the pointer off the player and back. Pointer exit normally hides the chrome and masks this, which is why it only shows when the pointer stays over the player while another window takes focus. Hand the surface back on window re-activation, next to the existing hide-path claim, and rename the helper since it is no longer hidden-chrome specific. The claim runs synchronously because a platform callback is not guaranteed to be followed by a frame; the screen's reclaim re-tests hasFocus when it runs, so the two no longer compete. Also gate the screen's self-heal so a directional key no longer pulls focus into the OSD when "Video Player Navigation" is off -- Tab and select keep their path in, which the ungated return value would otherwise consume with nowhere to go.
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:plezy/providers/playback_state_provider.dart';
|
||||
import 'package:plezy/screens/video_player_screen.dart';
|
||||
import 'package:plezy/services/settings_service.dart';
|
||||
import 'package:plezy/utils/platform_detector.dart';
|
||||
import 'package:plezy/widgets/video_controls/player_chrome_controller.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../test_helpers/media_items.dart';
|
||||
import '../../test_helpers/mock_player_channels.dart';
|
||||
import '../../test_helpers/prefs.dart';
|
||||
|
||||
/// Regression coverage for #1797: while the screen node holds primary focus,
|
||||
/// its self-heal answers an actionable key by raising the chrome onto the
|
||||
/// Play/Pause button. With "Video Player Navigation" off, arrows are playback
|
||||
/// shortcuts and must not be turned into a focus jump — but Tab is the
|
||||
/// deliberate way into the OSD and must keep working.
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
setUp(() async {
|
||||
resetSharedPreferencesForTest();
|
||||
SettingsService.resetForTesting();
|
||||
await SettingsService.getInstance();
|
||||
TvDetectionService.debugSetAppleTVOverride(false);
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
TvDetectionService.debugSetAppleTVOverride(null);
|
||||
});
|
||||
|
||||
testWidgets('an arrow is left to the playback shortcuts when player navigation is off', (tester) async {
|
||||
final target = await _selfHealTargetFor(tester, LogicalKeyboardKey.arrowLeft);
|
||||
|
||||
expect(target, isNull, reason: 'an arrow must seek, not pull focus onto Play/Pause');
|
||||
});
|
||||
|
||||
testWidgets('Tab still walks into the player controls when player navigation is off', (tester) async {
|
||||
final target = await _selfHealTargetFor(tester, LogicalKeyboardKey.tab);
|
||||
|
||||
expect(
|
||||
target,
|
||||
PlayerChromeFocusTarget.playPause,
|
||||
reason: 'Tab is the deliberate way into the OSD and must keep reaching it',
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// Sends [key] to a freshly opened player route — whose screen node still owns
|
||||
/// primary focus, exactly as after a window re-activation — and reports the
|
||||
/// focus target its self-heal queued on the chrome, if any.
|
||||
Future<PlayerChromeFocusTarget?> _selfHealTargetFor(WidgetTester tester, LogicalKeyboardKey key) async {
|
||||
final screenKey = GlobalKey<VideoPlayerScreenState>();
|
||||
PlayerChromeFocusTarget? target;
|
||||
|
||||
await withMockPlayerChannels(
|
||||
methodChannelName: 'com.plezy/mpv_player',
|
||||
eventChannelName: 'com.plezy/mpv_player/events',
|
||||
testBody: () async {
|
||||
await tester.pumpWidget(
|
||||
ChangeNotifierProvider(
|
||||
create: (_) => PlaybackStateProvider(),
|
||||
child: MaterialApp(
|
||||
home: VideoPlayerScreen(
|
||||
key: screenKey,
|
||||
metadata: testMediaItem(title: 'Self-heal keys'),
|
||||
isOffline: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
final chrome = screenKey.currentState!.chromeController;
|
||||
// Drain anything the route queued while opening, so the assertion can
|
||||
// only see what this key press produced.
|
||||
chrome.takeFocusTarget();
|
||||
|
||||
await tester.sendKeyDownEvent(key);
|
||||
await tester.pump();
|
||||
await tester.sendKeyUpEvent(key);
|
||||
await tester.pump();
|
||||
|
||||
target = chrome.takeFocusTarget();
|
||||
await tester.pumpWidget(const SizedBox.shrink());
|
||||
},
|
||||
);
|
||||
|
||||
return target;
|
||||
}
|
||||
Reference in New Issue
Block a user