Skip to content

Commit 916b2aa

Browse files
authored
Linux and Windows right clicking text behavior (#101588)
1 parent 8ffb1d2 commit 916b2aa

File tree

6 files changed

+654
-17
lines changed

6 files changed

+654
-17
lines changed

packages/flutter/lib/src/widgets/text_selection.dart

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1615,14 +1615,29 @@ class TextSelectionGestureDetectorBuilder {
16151615
/// By default, selects the word if possible and shows the toolbar.
16161616
@protected
16171617
void onSecondaryTap() {
1618-
if (delegate.selectionEnabled) {
1619-
if (!_lastSecondaryTapWasOnSelection) {
1620-
renderEditable.selectWord(cause: SelectionChangedCause.tap);
1621-
}
1622-
if (shouldShowSelectionToolbar) {
1623-
editableText.hideToolbar();
1624-
editableText.showToolbar();
1625-
}
1618+
if (!delegate.selectionEnabled) {
1619+
return;
1620+
}
1621+
switch (defaultTargetPlatform) {
1622+
case TargetPlatform.iOS:
1623+
case TargetPlatform.macOS:
1624+
if (!_lastSecondaryTapWasOnSelection) {
1625+
renderEditable.selectWord(cause: SelectionChangedCause.tap);
1626+
}
1627+
if (shouldShowSelectionToolbar) {
1628+
editableText.hideToolbar();
1629+
editableText.showToolbar();
1630+
}
1631+
break;
1632+
case TargetPlatform.android:
1633+
case TargetPlatform.fuchsia:
1634+
case TargetPlatform.linux:
1635+
case TargetPlatform.windows:
1636+
if (!renderEditable.hasFocus) {
1637+
renderEditable.selectPosition(cause: SelectionChangedCause.tap);
1638+
}
1639+
editableText.toggleToolbar();
1640+
break;
16261641
}
16271642
}
16281643

packages/flutter/test/cupertino/text_field_test.dart

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5496,4 +5496,87 @@ void main() {
54965496
expect(controller.selection.baseOffset, 23);
54975497
expect(controller.selection.extentOffset, 14);
54985498
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.linux, TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.windows }));
5499+
5500+
// Regression test for https://github.com/flutter/flutter/issues/101587.
5501+
testWidgets('Right clicking menu behavior', (WidgetTester tester) async {
5502+
final TextEditingController controller = TextEditingController(
5503+
text: 'blah1 blah2',
5504+
);
5505+
await tester.pumpWidget(
5506+
CupertinoApp(
5507+
home: Center(
5508+
child: CupertinoTextField(
5509+
controller: controller,
5510+
),
5511+
),
5512+
),
5513+
);
5514+
5515+
// Initially, the menu is not shown and there is no selection.
5516+
expect(find.byType(CupertinoButton), findsNothing);
5517+
expect(controller.selection, const TextSelection(baseOffset: -1, extentOffset: -1));
5518+
5519+
final Offset midBlah1 = textOffsetToPosition(tester, 2);
5520+
final Offset midBlah2 = textOffsetToPosition(tester, 8);
5521+
5522+
// Right click the second word.
5523+
final TestGesture gesture = await tester.startGesture(
5524+
midBlah2,
5525+
kind: PointerDeviceKind.mouse,
5526+
buttons: kSecondaryMouseButton,
5527+
);
5528+
addTearDown(gesture.removePointer);
5529+
await tester.pump();
5530+
await gesture.up();
5531+
await tester.pumpAndSettle();
5532+
5533+
switch (defaultTargetPlatform) {
5534+
case TargetPlatform.iOS:
5535+
case TargetPlatform.macOS:
5536+
expect(controller.selection, const TextSelection(baseOffset: 6, extentOffset: 11));
5537+
expect(find.text('Cut'), findsOneWidget);
5538+
expect(find.text('Copy'), findsOneWidget);
5539+
expect(find.text('Paste'), findsOneWidget);
5540+
break;
5541+
5542+
case TargetPlatform.android:
5543+
case TargetPlatform.fuchsia:
5544+
case TargetPlatform.linux:
5545+
case TargetPlatform.windows:
5546+
expect(controller.selection, const TextSelection.collapsed(offset: 8));
5547+
expect(find.text('Cut'), findsNothing);
5548+
expect(find.text('Copy'), findsNothing);
5549+
expect(find.text('Paste'), findsOneWidget);
5550+
break;
5551+
}
5552+
5553+
// Right click the first word.
5554+
await gesture.down(midBlah1);
5555+
await tester.pump();
5556+
await gesture.up();
5557+
await tester.pumpAndSettle();
5558+
5559+
switch (defaultTargetPlatform) {
5560+
case TargetPlatform.iOS:
5561+
case TargetPlatform.macOS:
5562+
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 5));
5563+
expect(find.text('Cut'), findsOneWidget);
5564+
expect(find.text('Copy'), findsOneWidget);
5565+
expect(find.text('Paste'), findsOneWidget);
5566+
break;
5567+
5568+
case TargetPlatform.android:
5569+
case TargetPlatform.fuchsia:
5570+
case TargetPlatform.linux:
5571+
case TargetPlatform.windows:
5572+
expect(controller.selection, const TextSelection.collapsed(offset: 8));
5573+
expect(find.text('Cut'), findsNothing);
5574+
expect(find.text('Copy'), findsNothing);
5575+
expect(find.text('Paste'), findsNothing);
5576+
break;
5577+
}
5578+
},
5579+
variant: TargetPlatformVariant.all(),
5580+
skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu.
5581+
);
54995582
}

packages/flutter/test/material/text_field_test.dart

Lines changed: 233 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@ void main() {
241241
await tester.pump();
242242
await gesture.up();
243243
await tester.pumpAndSettle();
244+
244245
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 5));
245246
expect(find.text('Cut'), findsOneWidget);
246247
expect(find.text('Copy'), findsOneWidget);
@@ -282,7 +283,151 @@ void main() {
282283
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 0));
283284
expect(find.byType(CupertinoButton), findsNothing);
284285
},
285-
variant: TargetPlatformVariant.desktop(),
286+
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.macOS }),
287+
skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu.
288+
);
289+
290+
testWidgets('can use the desktop cut/copy/paste buttons on Windows and Linux', (WidgetTester tester) async {
291+
final TextEditingController controller = TextEditingController(
292+
text: 'blah1 blah2',
293+
);
294+
await tester.pumpWidget(
295+
MaterialApp(
296+
home: Material(
297+
child: TextField(
298+
controller: controller,
299+
),
300+
),
301+
),
302+
);
303+
304+
// Initially, the menu is not shown and there is no selection.
305+
expect(find.byType(CupertinoButton), findsNothing);
306+
expect(controller.selection, const TextSelection(baseOffset: -1, extentOffset: -1));
307+
308+
final Offset midBlah1 = textOffsetToPosition(tester, 2);
309+
310+
// Right clicking shows the menu.
311+
TestGesture gesture = await tester.startGesture(
312+
midBlah1,
313+
kind: PointerDeviceKind.mouse,
314+
buttons: kSecondaryMouseButton,
315+
);
316+
await tester.pump();
317+
await gesture.up();
318+
await gesture.removePointer();
319+
await tester.pumpAndSettle();
320+
expect(controller.selection, const TextSelection.collapsed(offset: 2));
321+
expect(find.text('Cut'), findsNothing);
322+
expect(find.text('Copy'), findsNothing);
323+
expect(find.text('Paste'), findsOneWidget);
324+
expect(find.text('Select all'), findsOneWidget);
325+
326+
// Double tap to select the first word, then right click to show the menu.
327+
final Offset startBlah1 = textOffsetToPosition(tester, 0);
328+
gesture = await tester.startGesture(
329+
startBlah1,
330+
kind: PointerDeviceKind.mouse,
331+
);
332+
await tester.pump();
333+
await gesture.up();
334+
await tester.pump(const Duration(milliseconds: 100));
335+
await gesture.down(startBlah1);
336+
await tester.pump();
337+
await gesture.up();
338+
await gesture.removePointer();
339+
await tester.pumpAndSettle();
340+
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 5));
341+
expect(find.text('Cut'), findsNothing);
342+
expect(find.text('Copy'), findsNothing);
343+
expect(find.text('Paste'), findsNothing);
344+
expect(find.text('Select all'), findsNothing);
345+
gesture = await tester.startGesture(
346+
midBlah1,
347+
kind: PointerDeviceKind.mouse,
348+
buttons: kSecondaryMouseButton,
349+
);
350+
await tester.pump();
351+
await gesture.up();
352+
await gesture.removePointer();
353+
await tester.pumpAndSettle();
354+
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 5));
355+
expect(find.text('Cut'), findsOneWidget);
356+
expect(find.text('Copy'), findsOneWidget);
357+
expect(find.text('Paste'), findsOneWidget);
358+
359+
// Copy the first word.
360+
await tester.tap(find.text('Copy'));
361+
await tester.pumpAndSettle();
362+
expect(controller.text, 'blah1 blah2');
363+
expect(controller.selection, const TextSelection(baseOffset: 5, extentOffset: 5));
364+
expect(find.byType(CupertinoButton), findsNothing);
365+
366+
// Paste it at the end.
367+
gesture = await tester.startGesture(
368+
textOffsetToPosition(tester, controller.text.length),
369+
kind: PointerDeviceKind.mouse,
370+
);
371+
await tester.pump();
372+
await gesture.up();
373+
await gesture.removePointer();
374+
expect(controller.selection, const TextSelection.collapsed(offset: 11, affinity: TextAffinity.upstream));
375+
gesture = await tester.startGesture(
376+
textOffsetToPosition(tester, controller.text.length),
377+
kind: PointerDeviceKind.mouse,
378+
buttons: kSecondaryMouseButton,
379+
);
380+
await tester.pump();
381+
await gesture.up();
382+
await gesture.removePointer();
383+
await tester.pumpAndSettle();
384+
expect(controller.selection, const TextSelection.collapsed(offset: 11, affinity: TextAffinity.upstream));
385+
expect(find.text('Cut'), findsNothing);
386+
expect(find.text('Copy'), findsNothing);
387+
expect(find.text('Paste'), findsOneWidget);
388+
await tester.tap(find.text('Paste'));
389+
await tester.pumpAndSettle();
390+
expect(controller.text, 'blah1 blah2blah1');
391+
expect(controller.selection, const TextSelection.collapsed(offset: 16));
392+
393+
// Cut the first word.
394+
gesture = await tester.startGesture(
395+
midBlah1,
396+
kind: PointerDeviceKind.mouse,
397+
);
398+
await tester.pump();
399+
await gesture.up();
400+
await tester.pump(const Duration(milliseconds: 100));
401+
await gesture.down(startBlah1);
402+
await tester.pump();
403+
await gesture.up();
404+
await gesture.removePointer();
405+
await tester.pumpAndSettle();
406+
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 5));
407+
expect(find.text('Cut'), findsNothing);
408+
expect(find.text('Copy'), findsNothing);
409+
expect(find.text('Paste'), findsNothing);
410+
expect(find.text('Select all'), findsNothing);
411+
gesture = await tester.startGesture(
412+
textOffsetToPosition(tester, controller.text.length),
413+
kind: PointerDeviceKind.mouse,
414+
buttons: kSecondaryMouseButton,
415+
);
416+
await tester.pump();
417+
await gesture.up();
418+
await gesture.removePointer();
419+
await tester.pumpAndSettle();
420+
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 5));
421+
expect(find.text('Cut'), findsOneWidget);
422+
expect(find.text('Copy'), findsOneWidget);
423+
expect(find.text('Paste'), findsOneWidget);
424+
await tester.tap(find.text('Cut'));
425+
await tester.pumpAndSettle();
426+
expect(controller.text, ' blah2blah1');
427+
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 0));
428+
expect(find.byType(CupertinoButton), findsNothing);
429+
},
430+
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.linux, TargetPlatform.windows }),
286431
skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu.
287432
);
288433

@@ -10057,9 +10202,9 @@ void main() {
1005710202
await tester.pumpAndSettle();
1005810203

1005910204
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
10060-
expect(find.text('Copy'), findsOneWidget);
10205+
expect(find.text('Paste'), findsOneWidget);
1006110206

10062-
await gesture.moveTo(tester.getCenter(find.text('Copy')));
10207+
await gesture.moveTo(tester.getCenter(find.text('Paste')));
1006310208
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic);
1006410209
},
1006510210
variant: TargetPlatformVariant.desktop(),
@@ -11093,4 +11238,89 @@ void main() {
1109311238
expect(controller.selection.baseOffset, 23);
1109411239
expect(controller.selection.extentOffset, 14);
1109511240
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.linux, TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.windows }));
11241+
11242+
// Regression test for https://github.com/flutter/flutter/issues/101587.
11243+
testWidgets('Right clicking menu behavior', (WidgetTester tester) async {
11244+
final TextEditingController controller = TextEditingController(
11245+
text: 'blah1 blah2',
11246+
);
11247+
await tester.pumpWidget(
11248+
MaterialApp(
11249+
home: Material(
11250+
child: TextField(
11251+
controller: controller,
11252+
),
11253+
),
11254+
),
11255+
);
11256+
11257+
// Initially, the menu is not shown and there is no selection.
11258+
expect(find.byType(CupertinoButton), findsNothing);
11259+
expect(controller.selection, const TextSelection(baseOffset: -1, extentOffset: -1));
11260+
11261+
final Offset midBlah1 = textOffsetToPosition(tester, 2);
11262+
final Offset midBlah2 = textOffsetToPosition(tester, 8);
11263+
11264+
// Right click the second word.
11265+
final TestGesture gesture = await tester.startGesture(
11266+
midBlah2,
11267+
kind: PointerDeviceKind.mouse,
11268+
buttons: kSecondaryMouseButton,
11269+
);
11270+
addTearDown(gesture.removePointer);
11271+
await tester.pump();
11272+
await gesture.up();
11273+
await tester.pumpAndSettle();
11274+
11275+
switch (defaultTargetPlatform) {
11276+
case TargetPlatform.iOS:
11277+
case TargetPlatform.macOS:
11278+
expect(controller.selection, const TextSelection(baseOffset: 6, extentOffset: 11));
11279+
expect(find.text('Cut'), findsOneWidget);
11280+
expect(find.text('Copy'), findsOneWidget);
11281+
expect(find.text('Paste'), findsOneWidget);
11282+
break;
11283+
11284+
case TargetPlatform.android:
11285+
case TargetPlatform.fuchsia:
11286+
case TargetPlatform.linux:
11287+
case TargetPlatform.windows:
11288+
expect(controller.selection, const TextSelection.collapsed(offset: 8));
11289+
expect(find.text('Cut'), findsNothing);
11290+
expect(find.text('Copy'), findsNothing);
11291+
expect(find.text('Paste'), findsOneWidget);
11292+
expect(find.text('Select all'), findsOneWidget);
11293+
break;
11294+
}
11295+
11296+
// Right click the first word.
11297+
await gesture.down(midBlah1);
11298+
await tester.pump();
11299+
await gesture.up();
11300+
await tester.pumpAndSettle();
11301+
11302+
switch (defaultTargetPlatform) {
11303+
case TargetPlatform.iOS:
11304+
case TargetPlatform.macOS:
11305+
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 5));
11306+
expect(find.text('Cut'), findsOneWidget);
11307+
expect(find.text('Copy'), findsOneWidget);
11308+
expect(find.text('Paste'), findsOneWidget);
11309+
break;
11310+
11311+
case TargetPlatform.android:
11312+
case TargetPlatform.fuchsia:
11313+
case TargetPlatform.linux:
11314+
case TargetPlatform.windows:
11315+
expect(controller.selection, const TextSelection.collapsed(offset: 8));
11316+
expect(find.text('Cut'), findsNothing);
11317+
expect(find.text('Copy'), findsNothing);
11318+
expect(find.text('Paste'), findsNothing);
11319+
expect(find.text('Select all'), findsNothing);
11320+
break;
11321+
}
11322+
},
11323+
variant: TargetPlatformVariant.all(),
11324+
skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu.
11325+
);
1109611326
}

0 commit comments

Comments
 (0)