From 916b2aa76b30c6fbe1fc0447949eba7ef76a702e Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Thu, 14 Apr 2022 17:44:06 -0700 Subject: [PATCH] Linux and Windows right clicking text behavior (#101588) --- .../lib/src/widgets/text_selection.dart | 31 ++- .../test/cupertino/text_field_test.dart | 83 ++++++ .../test/material/text_field_test.dart | 236 ++++++++++++++++- .../test/material/text_form_field_test.dart | 245 +++++++++++++++++- .../test/widgets/selectable_text_test.dart | 4 +- .../test/widgets/text_selection_test.dart | 72 ++++- 6 files changed, 654 insertions(+), 17 deletions(-) diff --git a/packages/flutter/lib/src/widgets/text_selection.dart b/packages/flutter/lib/src/widgets/text_selection.dart index ba498f4991..a21fede1fc 100644 --- a/packages/flutter/lib/src/widgets/text_selection.dart +++ b/packages/flutter/lib/src/widgets/text_selection.dart @@ -1615,14 +1615,29 @@ class TextSelectionGestureDetectorBuilder { /// By default, selects the word if possible and shows the toolbar. @protected void onSecondaryTap() { - if (delegate.selectionEnabled) { - if (!_lastSecondaryTapWasOnSelection) { - renderEditable.selectWord(cause: SelectionChangedCause.tap); - } - if (shouldShowSelectionToolbar) { - editableText.hideToolbar(); - editableText.showToolbar(); - } + if (!delegate.selectionEnabled) { + return; + } + switch (defaultTargetPlatform) { + case TargetPlatform.iOS: + case TargetPlatform.macOS: + if (!_lastSecondaryTapWasOnSelection) { + renderEditable.selectWord(cause: SelectionChangedCause.tap); + } + if (shouldShowSelectionToolbar) { + editableText.hideToolbar(); + editableText.showToolbar(); + } + break; + case TargetPlatform.android: + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + case TargetPlatform.windows: + if (!renderEditable.hasFocus) { + renderEditable.selectPosition(cause: SelectionChangedCause.tap); + } + editableText.toggleToolbar(); + break; } } diff --git a/packages/flutter/test/cupertino/text_field_test.dart b/packages/flutter/test/cupertino/text_field_test.dart index c7510c7259..fc4daba64e 100644 --- a/packages/flutter/test/cupertino/text_field_test.dart +++ b/packages/flutter/test/cupertino/text_field_test.dart @@ -5496,4 +5496,87 @@ void main() { expect(controller.selection.baseOffset, 23); expect(controller.selection.extentOffset, 14); }, variant: const TargetPlatformVariant({ TargetPlatform.linux, TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.windows })); + + // Regression test for https://github.com/flutter/flutter/issues/101587. + testWidgets('Right clicking menu behavior', (WidgetTester tester) async { + final TextEditingController controller = TextEditingController( + text: 'blah1 blah2', + ); + await tester.pumpWidget( + CupertinoApp( + home: Center( + child: CupertinoTextField( + controller: controller, + ), + ), + ), + ); + + // Initially, the menu is not shown and there is no selection. + expect(find.byType(CupertinoButton), findsNothing); + expect(controller.selection, const TextSelection(baseOffset: -1, extentOffset: -1)); + + final Offset midBlah1 = textOffsetToPosition(tester, 2); + final Offset midBlah2 = textOffsetToPosition(tester, 8); + + // Right click the second word. + final TestGesture gesture = await tester.startGesture( + midBlah2, + kind: PointerDeviceKind.mouse, + buttons: kSecondaryMouseButton, + ); + addTearDown(gesture.removePointer); + await tester.pump(); + await gesture.up(); + await tester.pumpAndSettle(); + + switch (defaultTargetPlatform) { + case TargetPlatform.iOS: + case TargetPlatform.macOS: + expect(controller.selection, const TextSelection(baseOffset: 6, extentOffset: 11)); + expect(find.text('Cut'), findsOneWidget); + expect(find.text('Copy'), findsOneWidget); + expect(find.text('Paste'), findsOneWidget); + break; + + case TargetPlatform.android: + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + case TargetPlatform.windows: + expect(controller.selection, const TextSelection.collapsed(offset: 8)); + expect(find.text('Cut'), findsNothing); + expect(find.text('Copy'), findsNothing); + expect(find.text('Paste'), findsOneWidget); + break; + } + + // Right click the first word. + await gesture.down(midBlah1); + await tester.pump(); + await gesture.up(); + await tester.pumpAndSettle(); + + switch (defaultTargetPlatform) { + case TargetPlatform.iOS: + case TargetPlatform.macOS: + expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 5)); + expect(find.text('Cut'), findsOneWidget); + expect(find.text('Copy'), findsOneWidget); + expect(find.text('Paste'), findsOneWidget); + break; + + case TargetPlatform.android: + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + case TargetPlatform.windows: + expect(controller.selection, const TextSelection.collapsed(offset: 8)); + expect(find.text('Cut'), findsNothing); + expect(find.text('Copy'), findsNothing); + expect(find.text('Paste'), findsNothing); + break; + } + }, + variant: TargetPlatformVariant.all(), + skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu. + ); } diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart index 143cee6ad2..3d00a1c9a7 100644 --- a/packages/flutter/test/material/text_field_test.dart +++ b/packages/flutter/test/material/text_field_test.dart @@ -241,6 +241,7 @@ void main() { await tester.pump(); await gesture.up(); await tester.pumpAndSettle(); + expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 5)); expect(find.text('Cut'), findsOneWidget); expect(find.text('Copy'), findsOneWidget); @@ -282,7 +283,151 @@ void main() { expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 0)); expect(find.byType(CupertinoButton), findsNothing); }, - variant: TargetPlatformVariant.desktop(), + variant: const TargetPlatformVariant({ TargetPlatform.macOS }), + skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu. + ); + + testWidgets('can use the desktop cut/copy/paste buttons on Windows and Linux', (WidgetTester tester) async { + final TextEditingController controller = TextEditingController( + text: 'blah1 blah2', + ); + await tester.pumpWidget( + MaterialApp( + home: Material( + child: TextField( + controller: controller, + ), + ), + ), + ); + + // Initially, the menu is not shown and there is no selection. + expect(find.byType(CupertinoButton), findsNothing); + expect(controller.selection, const TextSelection(baseOffset: -1, extentOffset: -1)); + + final Offset midBlah1 = textOffsetToPosition(tester, 2); + + // Right clicking shows the menu. + TestGesture gesture = await tester.startGesture( + midBlah1, + kind: PointerDeviceKind.mouse, + buttons: kSecondaryMouseButton, + ); + await tester.pump(); + await gesture.up(); + await gesture.removePointer(); + await tester.pumpAndSettle(); + expect(controller.selection, const TextSelection.collapsed(offset: 2)); + expect(find.text('Cut'), findsNothing); + expect(find.text('Copy'), findsNothing); + expect(find.text('Paste'), findsOneWidget); + expect(find.text('Select all'), findsOneWidget); + + // Double tap to select the first word, then right click to show the menu. + final Offset startBlah1 = textOffsetToPosition(tester, 0); + gesture = await tester.startGesture( + startBlah1, + kind: PointerDeviceKind.mouse, + ); + await tester.pump(); + await gesture.up(); + await tester.pump(const Duration(milliseconds: 100)); + await gesture.down(startBlah1); + await tester.pump(); + await gesture.up(); + await gesture.removePointer(); + await tester.pumpAndSettle(); + expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 5)); + expect(find.text('Cut'), findsNothing); + expect(find.text('Copy'), findsNothing); + expect(find.text('Paste'), findsNothing); + expect(find.text('Select all'), findsNothing); + gesture = await tester.startGesture( + midBlah1, + kind: PointerDeviceKind.mouse, + buttons: kSecondaryMouseButton, + ); + await tester.pump(); + await gesture.up(); + await gesture.removePointer(); + await tester.pumpAndSettle(); + expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 5)); + expect(find.text('Cut'), findsOneWidget); + expect(find.text('Copy'), findsOneWidget); + expect(find.text('Paste'), findsOneWidget); + + // Copy the first word. + await tester.tap(find.text('Copy')); + await tester.pumpAndSettle(); + expect(controller.text, 'blah1 blah2'); + expect(controller.selection, const TextSelection(baseOffset: 5, extentOffset: 5)); + expect(find.byType(CupertinoButton), findsNothing); + + // Paste it at the end. + gesture = await tester.startGesture( + textOffsetToPosition(tester, controller.text.length), + kind: PointerDeviceKind.mouse, + ); + await tester.pump(); + await gesture.up(); + await gesture.removePointer(); + expect(controller.selection, const TextSelection.collapsed(offset: 11, affinity: TextAffinity.upstream)); + gesture = await tester.startGesture( + textOffsetToPosition(tester, controller.text.length), + kind: PointerDeviceKind.mouse, + buttons: kSecondaryMouseButton, + ); + await tester.pump(); + await gesture.up(); + await gesture.removePointer(); + await tester.pumpAndSettle(); + expect(controller.selection, const TextSelection.collapsed(offset: 11, affinity: TextAffinity.upstream)); + expect(find.text('Cut'), findsNothing); + expect(find.text('Copy'), findsNothing); + expect(find.text('Paste'), findsOneWidget); + await tester.tap(find.text('Paste')); + await tester.pumpAndSettle(); + expect(controller.text, 'blah1 blah2blah1'); + expect(controller.selection, const TextSelection.collapsed(offset: 16)); + + // Cut the first word. + gesture = await tester.startGesture( + midBlah1, + kind: PointerDeviceKind.mouse, + ); + await tester.pump(); + await gesture.up(); + await tester.pump(const Duration(milliseconds: 100)); + await gesture.down(startBlah1); + await tester.pump(); + await gesture.up(); + await gesture.removePointer(); + await tester.pumpAndSettle(); + expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 5)); + expect(find.text('Cut'), findsNothing); + expect(find.text('Copy'), findsNothing); + expect(find.text('Paste'), findsNothing); + expect(find.text('Select all'), findsNothing); + gesture = await tester.startGesture( + textOffsetToPosition(tester, controller.text.length), + kind: PointerDeviceKind.mouse, + buttons: kSecondaryMouseButton, + ); + await tester.pump(); + await gesture.up(); + await gesture.removePointer(); + await tester.pumpAndSettle(); + expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 5)); + expect(find.text('Cut'), findsOneWidget); + expect(find.text('Copy'), findsOneWidget); + expect(find.text('Paste'), findsOneWidget); + await tester.tap(find.text('Cut')); + await tester.pumpAndSettle(); + expect(controller.text, ' blah2blah1'); + expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 0)); + expect(find.byType(CupertinoButton), findsNothing); + }, + variant: const TargetPlatformVariant({ TargetPlatform.linux, TargetPlatform.windows }), skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu. ); @@ -10057,9 +10202,9 @@ void main() { await tester.pumpAndSettle(); expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text); - expect(find.text('Copy'), findsOneWidget); + expect(find.text('Paste'), findsOneWidget); - await gesture.moveTo(tester.getCenter(find.text('Copy'))); + await gesture.moveTo(tester.getCenter(find.text('Paste'))); expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic); }, variant: TargetPlatformVariant.desktop(), @@ -11093,4 +11238,89 @@ void main() { expect(controller.selection.baseOffset, 23); expect(controller.selection.extentOffset, 14); }, variant: const TargetPlatformVariant({ TargetPlatform.linux, TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.windows })); + + // Regression test for https://github.com/flutter/flutter/issues/101587. + testWidgets('Right clicking menu behavior', (WidgetTester tester) async { + final TextEditingController controller = TextEditingController( + text: 'blah1 blah2', + ); + await tester.pumpWidget( + MaterialApp( + home: Material( + child: TextField( + controller: controller, + ), + ), + ), + ); + + // Initially, the menu is not shown and there is no selection. + expect(find.byType(CupertinoButton), findsNothing); + expect(controller.selection, const TextSelection(baseOffset: -1, extentOffset: -1)); + + final Offset midBlah1 = textOffsetToPosition(tester, 2); + final Offset midBlah2 = textOffsetToPosition(tester, 8); + + // Right click the second word. + final TestGesture gesture = await tester.startGesture( + midBlah2, + kind: PointerDeviceKind.mouse, + buttons: kSecondaryMouseButton, + ); + addTearDown(gesture.removePointer); + await tester.pump(); + await gesture.up(); + await tester.pumpAndSettle(); + + switch (defaultTargetPlatform) { + case TargetPlatform.iOS: + case TargetPlatform.macOS: + expect(controller.selection, const TextSelection(baseOffset: 6, extentOffset: 11)); + expect(find.text('Cut'), findsOneWidget); + expect(find.text('Copy'), findsOneWidget); + expect(find.text('Paste'), findsOneWidget); + break; + + case TargetPlatform.android: + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + case TargetPlatform.windows: + expect(controller.selection, const TextSelection.collapsed(offset: 8)); + expect(find.text('Cut'), findsNothing); + expect(find.text('Copy'), findsNothing); + expect(find.text('Paste'), findsOneWidget); + expect(find.text('Select all'), findsOneWidget); + break; + } + + // Right click the first word. + await gesture.down(midBlah1); + await tester.pump(); + await gesture.up(); + await tester.pumpAndSettle(); + + switch (defaultTargetPlatform) { + case TargetPlatform.iOS: + case TargetPlatform.macOS: + expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 5)); + expect(find.text('Cut'), findsOneWidget); + expect(find.text('Copy'), findsOneWidget); + expect(find.text('Paste'), findsOneWidget); + break; + + case TargetPlatform.android: + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + case TargetPlatform.windows: + expect(controller.selection, const TextSelection.collapsed(offset: 8)); + expect(find.text('Cut'), findsNothing); + expect(find.text('Copy'), findsNothing); + expect(find.text('Paste'), findsNothing); + expect(find.text('Select all'), findsNothing); + break; + } + }, + variant: TargetPlatformVariant.all(), + skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu. + ); } diff --git a/packages/flutter/test/material/text_form_field_test.dart b/packages/flutter/test/material/text_form_field_test.dart index 2eb7790193..c07ae84a5a 100644 --- a/packages/flutter/test/material/text_form_field_test.dart +++ b/packages/flutter/test/material/text_form_field_test.dart @@ -98,7 +98,153 @@ void main() { expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 0)); expect(find.byType(CupertinoButton), findsNothing); }, - variant: TargetPlatformVariant.desktop(), + variant: const TargetPlatformVariant({ TargetPlatform.macOS }), + skip: kIsWeb, // [intended] we don't supply the cut/copy/paste buttons on the web. + ); + + testWidgets('can use the desktop cut/copy/paste buttons on Windows and Linux', (WidgetTester tester) async { + final TextEditingController controller = TextEditingController( + text: 'blah1 blah2', + ); + await tester.pumpWidget( + MaterialApp( + home: Material( + child: Center( + child: TextFormField( + controller: controller, + ), + ), + ), + ), + ); + + // Initially, the menu is not shown and there is no selection. + expect(find.byType(CupertinoButton), findsNothing); + expect(controller.selection, const TextSelection(baseOffset: -1, extentOffset: -1)); + + final Offset midBlah1 = textOffsetToPosition(tester, 2); + + // Right clicking shows the menu. + TestGesture gesture = await tester.startGesture( + midBlah1, + kind: PointerDeviceKind.mouse, + buttons: kSecondaryMouseButton, + ); + await tester.pump(); + await gesture.up(); + await gesture.removePointer(); + await tester.pumpAndSettle(); + expect(controller.selection, const TextSelection.collapsed(offset: 2)); + expect(find.text('Cut'), findsNothing); + expect(find.text('Copy'), findsNothing); + expect(find.text('Paste'), findsOneWidget); + expect(find.text('Select all'), findsOneWidget); + + // Double tap to select the first word, then right click to show the menu. + final Offset startBlah1 = textOffsetToPosition(tester, 0); + gesture = await tester.startGesture( + startBlah1, + kind: PointerDeviceKind.mouse, + ); + await tester.pump(); + await gesture.up(); + await tester.pump(const Duration(milliseconds: 100)); + await gesture.down(startBlah1); + await tester.pump(); + await gesture.up(); + await gesture.removePointer(); + await tester.pumpAndSettle(); + expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 5)); + expect(find.text('Cut'), findsNothing); + expect(find.text('Copy'), findsNothing); + expect(find.text('Paste'), findsNothing); + expect(find.text('Select all'), findsNothing); + gesture = await tester.startGesture( + midBlah1, + kind: PointerDeviceKind.mouse, + buttons: kSecondaryMouseButton, + ); + await tester.pump(); + await gesture.up(); + await gesture.removePointer(); + await tester.pumpAndSettle(); + expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 5)); + expect(find.text('Cut'), findsOneWidget); + expect(find.text('Copy'), findsOneWidget); + expect(find.text('Paste'), findsOneWidget); + + // Copy the first word. + await tester.tap(find.text('Copy')); + await tester.pumpAndSettle(); + expect(controller.text, 'blah1 blah2'); + expect(controller.selection, const TextSelection(baseOffset: 5, extentOffset: 5)); + expect(find.byType(CupertinoButton), findsNothing); + + // Paste it at the end. + gesture = await tester.startGesture( + textOffsetToPosition(tester, controller.text.length), + kind: PointerDeviceKind.mouse, + ); + await tester.pump(); + await gesture.up(); + await gesture.removePointer(); + expect(controller.selection, const TextSelection.collapsed(offset: 11, affinity: TextAffinity.upstream)); + gesture = await tester.startGesture( + textOffsetToPosition(tester, controller.text.length), + kind: PointerDeviceKind.mouse, + buttons: kSecondaryMouseButton, + ); + await tester.pump(); + await gesture.up(); + await gesture.removePointer(); + await tester.pumpAndSettle(); + expect(controller.selection, const TextSelection.collapsed(offset: 11, affinity: TextAffinity.upstream)); + expect(find.text('Cut'), findsNothing); + expect(find.text('Copy'), findsNothing); + expect(find.text('Paste'), findsOneWidget); + await tester.tap(find.text('Paste')); + await tester.pumpAndSettle(); + expect(controller.text, 'blah1 blah2blah1'); + expect(controller.selection, const TextSelection.collapsed(offset: 16)); + + // Cut the first word. + gesture = await tester.startGesture( + midBlah1, + kind: PointerDeviceKind.mouse, + ); + await tester.pump(); + await gesture.up(); + await tester.pump(const Duration(milliseconds: 100)); + await gesture.down(startBlah1); + await tester.pump(); + await gesture.up(); + await gesture.removePointer(); + await tester.pumpAndSettle(); + expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 5)); + expect(find.text('Cut'), findsNothing); + expect(find.text('Copy'), findsNothing); + expect(find.text('Paste'), findsNothing); + expect(find.text('Select all'), findsNothing); + gesture = await tester.startGesture( + textOffsetToPosition(tester, controller.text.length), + kind: PointerDeviceKind.mouse, + buttons: kSecondaryMouseButton, + ); + await tester.pump(); + await gesture.up(); + await gesture.removePointer(); + await tester.pumpAndSettle(); + expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 5)); + expect(find.text('Cut'), findsOneWidget); + expect(find.text('Copy'), findsOneWidget); + expect(find.text('Paste'), findsOneWidget); + await tester.tap(find.text('Cut')); + await tester.pumpAndSettle(); + expect(controller.text, ' blah2blah1'); + expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 0)); + expect(find.byType(CupertinoButton), findsNothing); + }, + variant: const TargetPlatformVariant({ TargetPlatform.linux, TargetPlatform.windows }), skip: kIsWeb, // [intended] we don't supply the cut/copy/paste buttons on the web. ); @@ -171,6 +317,16 @@ void main() { final Offset midBlah1 = textOffsetToPosition(tester, 2); + // Make a selection. + await tester.tapAt(midBlah1); + await tester.pump(); + await tester.sendKeyDownEvent(LogicalKeyboardKey.shift); + await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft); + await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft); + await tester.sendKeyUpEvent(LogicalKeyboardKey.shift); + await tester.pump(); + expect(controller.selection, const TextSelection(baseOffset: 2, extentOffset: 0)); + // Right clicking shows the menu. final TestGesture gesture = await tester.startGesture( midBlah1, @@ -181,7 +337,6 @@ void main() { await tester.pump(); await gesture.up(); await tester.pumpAndSettle(); - expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 11)); expect(find.text('Copy'), findsNothing); expect(find.text('Cut'), findsNothing); expect(find.text('Paste'), findsOneWidget); @@ -882,4 +1037,90 @@ void main() { await gesture.moveTo(center); }); + // Regression test for https://github.com/flutter/flutter/issues/101587. + testWidgets('Right clicking menu behavior', (WidgetTester tester) async { + final TextEditingController controller = TextEditingController( + text: 'blah1 blah2', + ); + await tester.pumpWidget( + MaterialApp( + home: Material( + child: Center( + child: TextFormField( + controller: controller, + ), + ), + ), + ), + ); + + // Initially, the menu is not shown and there is no selection. + expect(find.byType(CupertinoButton), findsNothing); + expect(controller.selection, const TextSelection(baseOffset: -1, extentOffset: -1)); + + final Offset midBlah1 = textOffsetToPosition(tester, 2); + final Offset midBlah2 = textOffsetToPosition(tester, 8); + + // Right click the second word. + final TestGesture gesture = await tester.startGesture( + midBlah2, + kind: PointerDeviceKind.mouse, + buttons: kSecondaryMouseButton, + ); + addTearDown(gesture.removePointer); + await tester.pump(); + await gesture.up(); + await tester.pumpAndSettle(); + + switch (defaultTargetPlatform) { + case TargetPlatform.iOS: + case TargetPlatform.macOS: + expect(controller.selection, const TextSelection(baseOffset: 6, extentOffset: 11)); + expect(find.text('Cut'), findsOneWidget); + expect(find.text('Copy'), findsOneWidget); + expect(find.text('Paste'), findsOneWidget); + break; + + case TargetPlatform.android: + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + case TargetPlatform.windows: + expect(controller.selection, const TextSelection.collapsed(offset: 8)); + expect(find.text('Cut'), findsNothing); + expect(find.text('Copy'), findsNothing); + expect(find.text('Paste'), findsOneWidget); + expect(find.text('Select all'), findsOneWidget); + break; + } + + // Right click the first word. + await gesture.down(midBlah1); + await tester.pump(); + await gesture.up(); + await tester.pumpAndSettle(); + + switch (defaultTargetPlatform) { + case TargetPlatform.iOS: + case TargetPlatform.macOS: + expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 5)); + expect(find.text('Cut'), findsOneWidget); + expect(find.text('Copy'), findsOneWidget); + expect(find.text('Paste'), findsOneWidget); + break; + + case TargetPlatform.android: + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + case TargetPlatform.windows: + expect(controller.selection, const TextSelection.collapsed(offset: 8)); + expect(find.text('Cut'), findsNothing); + expect(find.text('Copy'), findsNothing); + expect(find.text('Paste'), findsNothing); + expect(find.text('Select all'), findsNothing); + break; + } + }, + variant: TargetPlatformVariant.all(), + skip: kIsWeb, // [intended] we don't supply the cut/copy/paste buttons on the web. + ); } diff --git a/packages/flutter/test/widgets/selectable_text_test.dart b/packages/flutter/test/widgets/selectable_text_test.dart index 63ecb8de78..45b848bb3b 100644 --- a/packages/flutter/test/widgets/selectable_text_test.dart +++ b/packages/flutter/test/widgets/selectable_text_test.dart @@ -4613,8 +4613,8 @@ void main() { await tester.pump(); await gesture.up(); await tester.pumpAndSettle(); - expect(newSelection!.baseOffset, 4); - expect(newSelection!.extentOffset, 7); + expect(newSelection!.isCollapsed, isTrue); + expect(newSelection!.baseOffset, 5); newSelection = null; await tester.tap(find.text('Select all')); diff --git a/packages/flutter/test/widgets/text_selection_test.dart b/packages/flutter/test/widgets/text_selection_test.dart index 139d80afa7..312101fa0e 100644 --- a/packages/flutter/test/widgets/text_selection_test.dart +++ b/packages/flutter/test/widgets/text_selection_test.dart @@ -455,7 +455,7 @@ void main() { expect(renderEditable.selectPositionAtCalled, isTrue); }); - testWidgets('TextSelectionGestureDetectorBuilder right click', (WidgetTester tester) async { + testWidgets('TextSelectionGestureDetectorBuilder right click Apple platforms', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/80119 await pumpTextSelectionGestureDetectorBuilder(tester); @@ -496,7 +496,60 @@ void main() { await gesture.up(); await tester.pump(); expect(renderEditable.selectWordCalled, isFalse); - }); + }, + variant: const TargetPlatformVariant({ TargetPlatform.iOS, TargetPlatform.macOS }), + ); + + testWidgets('TextSelectionGestureDetectorBuilder right click non-Apple platforms', (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/80119 + await pumpTextSelectionGestureDetectorBuilder(tester); + + final FakeRenderEditable renderEditable = tester.renderObject(find.byType(FakeEditable)); + renderEditable.text = const TextSpan(text: 'one two three four five six seven'); + await tester.pump(); + + final TestGesture gesture = await tester.createGesture( + pointer: 0, + kind: PointerDeviceKind.mouse, + buttons: kSecondaryButton, + ); + addTearDown(gesture.removePointer); + + // Get the location of the 10th character + final Offset charLocation = renderEditable + .getLocalRectForCaret(const TextPosition(offset: 10)).center; + final Offset globalCharLocation = charLocation + tester.getTopLeft(find.byType(FakeEditable)); + + // Right clicking on an unfocused field should place the cursor, not select + // the word. + await gesture.down(globalCharLocation); + await gesture.up(); + await tester.pump(); + expect(renderEditable.selectWordCalled, isFalse); + expect(renderEditable.selectPositionCalled, isTrue); + + // Right clicking on a focused field with selection shouldn't change the + // selection. + renderEditable.selectPositionCalled = false; + renderEditable.selection = const TextSelection(baseOffset: 3, extentOffset: 20); + renderEditable.hasFocus = true; + await gesture.down(globalCharLocation); + await gesture.up(); + await tester.pump(); + expect(renderEditable.selectWordCalled, isFalse); + expect(renderEditable.selectPositionCalled, isFalse); + + // Right clicking on a focused field with a reverse (right to left) + // selection shouldn't change the selection. + renderEditable.selection = const TextSelection(baseOffset: 20, extentOffset: 3); + await gesture.down(globalCharLocation); + await gesture.up(); + await tester.pump(); + expect(renderEditable.selectWordCalled, isFalse); + expect(renderEditable.selectPositionCalled, isFalse); + }, + variant: const TargetPlatformVariant({ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux, TargetPlatform.windows }), + ); testWidgets('test TextSelectionGestureDetectorBuilder tap', (WidgetTester tester) async { await pumpTextSelectionGestureDetectorBuilder(tester); @@ -1083,6 +1136,11 @@ class FakeEditableTextState extends EditableTextState { return true; } + @override + void toggleToolbar() { + return; + } + @override Widget build(BuildContext context) { super.build(context); @@ -1144,11 +1202,21 @@ class FakeRenderEditable extends RenderEditable { selectPositionAtTo = to; } + bool selectPositionCalled = false; + @override + void selectPosition({ required SelectionChangedCause cause }) { + selectPositionCalled = true; + return super.selectPosition(cause: cause); + } + bool selectWordCalled = false; @override void selectWord({ required SelectionChangedCause cause }) { selectWordCalled = true; } + + @override + bool hasFocus = false; } class CustomTextSelectionControls extends TextSelectionControls {