SelectionArea's selection should not be cleared on loss of window focus (#148067)
This change fixes an issue where SelectionArea would clear its selection when the application window lost focus by first checking if the application is running. This is needed because `FocusManager` is aware of the application lifecycle as of https://github.com/flutter/flutter/pull/142930 , and triggers a focus lost if the application is not active. Also fixes an issue where the `FocusManager` was not being reset on tests at the right time, causing it always to build with `TargetPlatform.android` as its context.
This commit is contained in:
parent
722c8d62fd
commit
5890a2fc73
@ -1873,6 +1873,34 @@ class FocusManager with DiagnosticableTreeMixin, ChangeNotifier {
|
|||||||
}());
|
}());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Enables this [FocusManager] to listen to changes of the application
|
||||||
|
/// lifecycle if it does not already have an application lifecycle listener
|
||||||
|
/// active, and the current platform is detected as [kIsWeb] or non-Android.
|
||||||
|
///
|
||||||
|
/// Typically, the application lifecycle listener for this [FocusManager] is
|
||||||
|
/// setup at construction, but sometimes it is necessary to manually initialize
|
||||||
|
/// it when the [FocusManager] does not have the relevant platform context in
|
||||||
|
/// [defaultTargetPlatform] at the time of construction. This can happen in
|
||||||
|
/// a test environment where the [BuildOwner] which initializes its own
|
||||||
|
/// [FocusManager], may not have the accurate platform context during its
|
||||||
|
/// initialization. In this case it is necessary for the test framework to call
|
||||||
|
/// this method after it has set up the test variant for a given test, so the
|
||||||
|
/// [FocusManager] can accurately listen to application lifecycle changes, if
|
||||||
|
/// supported.
|
||||||
|
@visibleForTesting
|
||||||
|
void listenToApplicationLifecycleChangesIfSupported() {
|
||||||
|
if (_appLifecycleListener == null && (kIsWeb || defaultTargetPlatform != TargetPlatform.android)) {
|
||||||
|
// It appears that some Android keyboard implementations can cause
|
||||||
|
// app lifecycle state changes: adding this listener would cause the
|
||||||
|
// text field to unfocus as the user is trying to type.
|
||||||
|
//
|
||||||
|
// Until this is resolved, we won't be adding the listener to Android apps.
|
||||||
|
// https://github.com/flutter/flutter/pull/142930#issuecomment-1981750069
|
||||||
|
_appLifecycleListener = _AppLifecycleListener(_appLifecycleChange);
|
||||||
|
WidgetsBinding.instance.addObserver(_appLifecycleListener!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<DiagnosticsNode> debugDescribeChildren() {
|
List<DiagnosticsNode> debugDescribeChildren() {
|
||||||
return <DiagnosticsNode>[
|
return <DiagnosticsNode>[
|
||||||
|
@ -454,7 +454,16 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
|
|||||||
if (kIsWeb) {
|
if (kIsWeb) {
|
||||||
PlatformSelectableRegionContextMenu.detach(_selectionDelegate);
|
PlatformSelectableRegionContextMenu.detach(_selectionDelegate);
|
||||||
}
|
}
|
||||||
_clearSelection();
|
if (SchedulerBinding.instance.lifecycleState == AppLifecycleState.resumed) {
|
||||||
|
// We should only clear the selection when this SelectableRegion loses
|
||||||
|
// focus while the application is currently running. It is possible
|
||||||
|
// that the application is not currently running, for example on desktop
|
||||||
|
// platforms, clicking on a different window switches the focus to
|
||||||
|
// the new window causing the Flutter application to go inactive. In this
|
||||||
|
// case we want to retain the selection so it remains when we return to
|
||||||
|
// the Flutter application.
|
||||||
|
_clearSelection();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (kIsWeb) {
|
if (kIsWeb) {
|
||||||
PlatformSelectableRegionContextMenu.attach(_selectionDelegate);
|
PlatformSelectableRegionContextMenu.attach(_selectionDelegate);
|
||||||
|
@ -416,7 +416,7 @@ void main() {
|
|||||||
|
|
||||||
await setAppLifecycleState(AppLifecycleState.resumed);
|
await setAppLifecycleState(AppLifecycleState.resumed);
|
||||||
expect(focusNode.hasPrimaryFocus, isTrue);
|
expect(focusNode.hasPrimaryFocus, isTrue);
|
||||||
});
|
}, variant: TargetPlatformVariant.desktop());
|
||||||
|
|
||||||
testWidgets('Node is removed completely even if app is paused.', (WidgetTester tester) async {
|
testWidgets('Node is removed completely even if app is paused.', (WidgetTester tester) async {
|
||||||
Future<void> setAppLifecycleState(AppLifecycleState state) async {
|
Future<void> setAppLifecycleState(AppLifecycleState state) async {
|
||||||
|
@ -548,6 +548,54 @@ void main() {
|
|||||||
}, variant: TargetPlatformVariant.all());
|
}, variant: TargetPlatformVariant.all());
|
||||||
|
|
||||||
group('SelectionArea integration', () {
|
group('SelectionArea integration', () {
|
||||||
|
testWidgets('selection is not cleared when app loses focus on desktop', (WidgetTester tester) async {
|
||||||
|
Future<void> setAppLifecycleState(AppLifecycleState state) async {
|
||||||
|
final ByteData? message = const StringCodec().encodeMessage(state.toString());
|
||||||
|
await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||||
|
.handlePlatformMessage('flutter/lifecycle', message, (_) {});
|
||||||
|
}
|
||||||
|
final FocusNode focusNode = FocusNode();
|
||||||
|
final GlobalKey selectableKey = GlobalKey();
|
||||||
|
addTearDown(focusNode.dispose);
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: SelectableRegion(
|
||||||
|
key: selectableKey,
|
||||||
|
focusNode: focusNode,
|
||||||
|
selectionControls: materialTextSelectionControls,
|
||||||
|
child: const Center(
|
||||||
|
child: Text('How are you'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await setAppLifecycleState(AppLifecycleState.resumed);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you'), matching: find.byType(RichText)));
|
||||||
|
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph, 2), kind: PointerDeviceKind.mouse);
|
||||||
|
addTearDown(gesture.removePointer);
|
||||||
|
await tester.pump();
|
||||||
|
await gesture.up();
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
await gesture.down(textOffsetToPosition(paragraph, 2));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3));
|
||||||
|
|
||||||
|
await gesture.up();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3));
|
||||||
|
expect(focusNode.hasFocus, isTrue);
|
||||||
|
|
||||||
|
// Setting the app lifecycle state to AppLifecycleState.inactive to simulate
|
||||||
|
// a lose of window focus.
|
||||||
|
await setAppLifecycleState(AppLifecycleState.inactive);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(focusNode.hasFocus, isFalse);
|
||||||
|
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3));
|
||||||
|
}, variant: TargetPlatformVariant.desktop());
|
||||||
|
|
||||||
testWidgets('mouse can select single text on desktop platforms', (WidgetTester tester) async {
|
testWidgets('mouse can select single text on desktop platforms', (WidgetTester tester) async {
|
||||||
final FocusNode focusNode = FocusNode();
|
final FocusNode focusNode = FocusNode();
|
||||||
addTearDown(focusNode.dispose);
|
addTearDown(focusNode.dispose);
|
||||||
|
@ -254,6 +254,14 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
|
|||||||
_testTextInput.register();
|
_testTextInput.register();
|
||||||
}
|
}
|
||||||
CustomSemanticsAction.resetForTests(); // ignore: invalid_use_of_visible_for_testing_member
|
CustomSemanticsAction.resetForTests(); // ignore: invalid_use_of_visible_for_testing_member
|
||||||
|
_enableFocusManagerLifecycleAwarenessIfSupported();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _enableFocusManagerLifecycleAwarenessIfSupported() {
|
||||||
|
if (buildOwner == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
buildOwner!.focusManager.listenToApplicationLifecycleChangesIfSupported(); // ignore: invalid_use_of_visible_for_testing_member
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -174,11 +174,11 @@ void testWidgets(
|
|||||||
test_package.addTearDown(binding.postTest);
|
test_package.addTearDown(binding.postTest);
|
||||||
return binding.runTest(
|
return binding.runTest(
|
||||||
() async {
|
() async {
|
||||||
binding.reset(); // TODO(ianh): the binding should just do this itself in _runTest
|
|
||||||
debugResetSemanticsIdCounter();
|
debugResetSemanticsIdCounter();
|
||||||
Object? memento;
|
Object? memento;
|
||||||
try {
|
try {
|
||||||
memento = await variant.setUp(value);
|
memento = await variant.setUp(value);
|
||||||
|
binding.reset(); // TODO(ianh): the binding should just do this itself in _runTest
|
||||||
maybeSetupLeakTrackingForTest(experimentalLeakTesting, combinedDescription);
|
maybeSetupLeakTrackingForTest(experimentalLeakTesting, combinedDescription);
|
||||||
await callback(tester);
|
await callback(tester);
|
||||||
} finally {
|
} finally {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user