mirror of
https://github.com/flutter/flutter
synced 2024-09-13 13:21:36 +00:00
Selection area right click behavior should match native (#128224)
This change updates `SelectableRegion`s right-click gesture to match native platform behavior. Before: Right-click gesture selects word at position and opens context menu (All Platforms) After: - Linux, toggles context menu on/off, and collapses selection when click was not on an active selection (uncollapsed). - Windows, Android, Fuchsia, shows context menu at right-clicked position (unless the click is at an active selection). - macOS, toggles the context menu if right click was at the same position as the previous / or selects word at position and opens context menu. - iOS, selects word at position and opens context menu. This change also prevents the `copy` menu button from being shown when there is a collapsed selection (nothing to copy). Fixes #117561
This commit is contained in:
parent
c40baf47c5
commit
b36ef583fb
|
@ -157,6 +157,7 @@ class _RenderSelectableAdapter extends RenderProxyBox with Selectable, Selection
|
|||
hasContent: true,
|
||||
startSelectionPoint: isReversed ? secondSelectionPoint : firstSelectionPoint,
|
||||
endSelectionPoint: isReversed ? firstSelectionPoint : secondSelectionPoint,
|
||||
selectionRects: <Rect>[selectionRect],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1335,6 +1335,14 @@ class _SelectableFragment with Selectable, ChangeNotifier implements TextLayoutM
|
|||
: paragraph._getOffsetForPosition(TextPosition(offset: selectionEnd));
|
||||
final bool flipHandles = isReversed != (TextDirection.rtl == paragraph.textDirection);
|
||||
final Matrix4 paragraphToFragmentTransform = getTransformToParagraph()..invert();
|
||||
final TextSelection selection = TextSelection(
|
||||
baseOffset: selectionStart,
|
||||
extentOffset: selectionEnd,
|
||||
);
|
||||
final List<Rect> selectionRects = <Rect>[];
|
||||
for (final TextBox textBox in paragraph.getBoxesForSelection(selection)) {
|
||||
selectionRects.add(textBox.toRect());
|
||||
}
|
||||
return SelectionGeometry(
|
||||
startSelectionPoint: SelectionPoint(
|
||||
localPosition: MatrixUtils.transformPoint(paragraphToFragmentTransform, startOffsetInParagraphCoordinates),
|
||||
|
@ -1346,6 +1354,7 @@ class _SelectableFragment with Selectable, ChangeNotifier implements TextLayoutM
|
|||
lineHeight: paragraph._textPainter.preferredLineHeight,
|
||||
handleType: flipHandles ? TextSelectionHandleType.left : TextSelectionHandleType.right,
|
||||
),
|
||||
selectionRects: selectionRects,
|
||||
status: _textSelectionStart!.offset == _textSelectionEnd!.offset
|
||||
? SelectionStatus.collapsed
|
||||
: SelectionStatus.uncollapsed,
|
||||
|
|
|
@ -576,8 +576,8 @@ enum SelectionStatus {
|
|||
/// The geometry of the current selection.
|
||||
///
|
||||
/// This includes details such as the locations of the selection start and end,
|
||||
/// line height, etc. This information is used for drawing selection controls
|
||||
/// for mobile platforms.
|
||||
/// line height, the rects that encompass the selection, etc. This information
|
||||
/// is used for drawing selection controls for mobile platforms.
|
||||
///
|
||||
/// The positions in geometry are in local coordinates of the [SelectionHandler]
|
||||
/// or [Selectable].
|
||||
|
@ -590,6 +590,7 @@ class SelectionGeometry {
|
|||
const SelectionGeometry({
|
||||
this.startSelectionPoint,
|
||||
this.endSelectionPoint,
|
||||
this.selectionRects = const <Rect>[],
|
||||
required this.status,
|
||||
required this.hasContent,
|
||||
}) : assert((startSelectionPoint == null && endSelectionPoint == null) || status != SelectionStatus.none);
|
||||
|
@ -627,6 +628,10 @@ class SelectionGeometry {
|
|||
/// The status of ongoing selection in the [Selectable] or [SelectionHandler].
|
||||
final SelectionStatus status;
|
||||
|
||||
/// The rects in the local coordinates of the containing [Selectable] that
|
||||
/// represent the selection if there is any.
|
||||
final List<Rect> selectionRects;
|
||||
|
||||
/// Whether there is any selectable content in the [Selectable] or
|
||||
/// [SelectionHandler].
|
||||
final bool hasContent;
|
||||
|
@ -638,12 +643,14 @@ class SelectionGeometry {
|
|||
SelectionGeometry copyWith({
|
||||
SelectionPoint? startSelectionPoint,
|
||||
SelectionPoint? endSelectionPoint,
|
||||
List<Rect>? selectionRects,
|
||||
SelectionStatus? status,
|
||||
bool? hasContent,
|
||||
}) {
|
||||
return SelectionGeometry(
|
||||
startSelectionPoint: startSelectionPoint ?? this.startSelectionPoint,
|
||||
endSelectionPoint: endSelectionPoint ?? this.endSelectionPoint,
|
||||
selectionRects: selectionRects ?? this.selectionRects,
|
||||
status: status ?? this.status,
|
||||
hasContent: hasContent ?? this.hasContent,
|
||||
);
|
||||
|
@ -660,6 +667,7 @@ class SelectionGeometry {
|
|||
return other is SelectionGeometry
|
||||
&& other.startSelectionPoint == startSelectionPoint
|
||||
&& other.endSelectionPoint == endSelectionPoint
|
||||
&& other.selectionRects == selectionRects
|
||||
&& other.status == status
|
||||
&& other.hasContent == hasContent;
|
||||
}
|
||||
|
@ -669,6 +677,7 @@ class SelectionGeometry {
|
|||
return Object.hash(
|
||||
startSelectionPoint,
|
||||
endSelectionPoint,
|
||||
selectionRects,
|
||||
status,
|
||||
hasContent,
|
||||
);
|
||||
|
|
|
@ -263,7 +263,7 @@ class SelectableRegion extends StatefulWidget {
|
|||
required final VoidCallback onCopy,
|
||||
required final VoidCallback onSelectAll,
|
||||
}) {
|
||||
final bool canCopy = selectionGeometry.hasSelection;
|
||||
final bool canCopy = selectionGeometry.status == SelectionStatus.uncollapsed;
|
||||
final bool canSelectAll = selectionGeometry.hasContent;
|
||||
|
||||
// Determine which buttons will appear so that the order and total number is
|
||||
|
@ -489,12 +489,62 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
|
|||
_updateSelectedContentIfNeeded();
|
||||
}
|
||||
|
||||
bool _positionIsOnActiveSelection({required Offset globalPosition}) {
|
||||
for (final Rect selectionRect in _selectionDelegate.value.selectionRects) {
|
||||
final Matrix4 transform = _selectable!.getTransformTo(null);
|
||||
final Rect globalRect = MatrixUtils.transformRect(transform, selectionRect);
|
||||
if (globalRect.contains(globalPosition)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void _handleRightClickDown(TapDownDetails details) {
|
||||
final Offset? previousSecondaryTapDownPosition = lastSecondaryTapDownPosition;
|
||||
final bool toolbarIsVisible = _selectionOverlay?.toolbarIsVisible ?? false;
|
||||
lastSecondaryTapDownPosition = details.globalPosition;
|
||||
widget.focusNode.requestFocus();
|
||||
_selectWordAt(offset: details.globalPosition);
|
||||
_showHandles();
|
||||
_showToolbar(location: details.globalPosition);
|
||||
switch (defaultTargetPlatform) {
|
||||
case TargetPlatform.android:
|
||||
case TargetPlatform.fuchsia:
|
||||
case TargetPlatform.windows:
|
||||
// If lastSecondaryTapDownPosition is within the current selection then
|
||||
// keep the current selection, if not then collapse it.
|
||||
final bool lastSecondaryTapDownPositionWasOnActiveSelection = _positionIsOnActiveSelection(globalPosition: details.globalPosition);
|
||||
if (!lastSecondaryTapDownPositionWasOnActiveSelection) {
|
||||
_selectStartTo(offset: lastSecondaryTapDownPosition!);
|
||||
_selectEndTo(offset: lastSecondaryTapDownPosition!);
|
||||
}
|
||||
_showHandles();
|
||||
_showToolbar(location: lastSecondaryTapDownPosition);
|
||||
case TargetPlatform.iOS:
|
||||
_selectWordAt(offset: lastSecondaryTapDownPosition!);
|
||||
_showHandles();
|
||||
_showToolbar(location: lastSecondaryTapDownPosition);
|
||||
case TargetPlatform.macOS:
|
||||
if (previousSecondaryTapDownPosition == lastSecondaryTapDownPosition && toolbarIsVisible) {
|
||||
hideToolbar();
|
||||
return;
|
||||
}
|
||||
_selectWordAt(offset: lastSecondaryTapDownPosition!);
|
||||
_showHandles();
|
||||
_showToolbar(location: lastSecondaryTapDownPosition);
|
||||
case TargetPlatform.linux:
|
||||
if (toolbarIsVisible) {
|
||||
hideToolbar();
|
||||
return;
|
||||
}
|
||||
// If lastSecondaryTapDownPosition is within the current selection then
|
||||
// keep the current selection, if not then collapse it.
|
||||
final bool lastSecondaryTapDownPositionWasOnActiveSelection = _positionIsOnActiveSelection(globalPosition: details.globalPosition);
|
||||
if (!lastSecondaryTapDownPositionWasOnActiveSelection) {
|
||||
_selectStartTo(offset: lastSecondaryTapDownPosition!);
|
||||
_selectEndTo(offset: lastSecondaryTapDownPosition!);
|
||||
}
|
||||
_showHandles();
|
||||
_showToolbar(location: lastSecondaryTapDownPosition);
|
||||
}
|
||||
_updateSelectedContentIfNeeded();
|
||||
}
|
||||
|
||||
|
@ -1770,9 +1820,30 @@ abstract class MultiSelectableSelectionContainerDelegate extends SelectionContai
|
|||
}
|
||||
}
|
||||
|
||||
// Need to collect selection rects from selectables ranging from the
|
||||
// currentSelectionStartIndex to the currentSelectionEndIndex.
|
||||
final List<Rect> selectionRects = <Rect>[];
|
||||
final Rect? drawableArea = hasSize ? Rect
|
||||
.fromLTWH(0, 0, containerSize.width, containerSize.height) : null;
|
||||
for (int index = currentSelectionStartIndex; index <= currentSelectionEndIndex; index++) {
|
||||
final List<Rect> currSelectableSelectionRects = selectables[index].value.selectionRects;
|
||||
final List<Rect> selectionRectsWithinDrawableArea = currSelectableSelectionRects.map((Rect selectionRect) {
|
||||
final Matrix4 transform = getTransformFrom(selectables[index]);
|
||||
final Rect localRect = MatrixUtils.transformRect(transform, selectionRect);
|
||||
if (drawableArea != null) {
|
||||
return drawableArea.intersect(localRect);
|
||||
}
|
||||
return localRect;
|
||||
}).where((Rect selectionRect) {
|
||||
return selectionRect.isFinite && !selectionRect.isEmpty;
|
||||
}).toList();
|
||||
selectionRects.addAll(selectionRectsWithinDrawableArea);
|
||||
}
|
||||
|
||||
return SelectionGeometry(
|
||||
startSelectionPoint: startPoint,
|
||||
endSelectionPoint: endPoint,
|
||||
selectionRects: selectionRects,
|
||||
status: startGeometry != endGeometry
|
||||
? SelectionStatus.uncollapsed
|
||||
: startGeometry.status,
|
||||
|
|
|
@ -562,15 +562,13 @@ class TextSelectionOverlay {
|
|||
/// Whether the handles are currently visible.
|
||||
bool get handlesAreVisible => _selectionOverlay._handles != null && handlesVisible;
|
||||
|
||||
/// Whether the toolbar is currently visible.
|
||||
///
|
||||
/// Includes both the text selection toolbar and the spell check menu.
|
||||
/// {@macro flutter.widgets.SelectionOverlay.toolbarIsVisible}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [spellCheckToolbarIsVisible], which is only whether the spell check menu
|
||||
/// specifically is visible.
|
||||
bool get toolbarIsVisible => _selectionOverlay._toolbarIsVisible;
|
||||
bool get toolbarIsVisible => _selectionOverlay.toolbarIsVisible;
|
||||
|
||||
/// Whether the magnifier is currently visible.
|
||||
bool get magnifierIsVisible => _selectionOverlay._magnifierController.shown;
|
||||
|
@ -984,7 +982,12 @@ class SelectionOverlay {
|
|||
/// {@macro flutter.widgets.magnifier.TextMagnifierConfiguration.details}
|
||||
final TextMagnifierConfiguration magnifierConfiguration;
|
||||
|
||||
bool get _toolbarIsVisible {
|
||||
/// {@template flutter.widgets.SelectionOverlay.toolbarIsVisible}
|
||||
/// Whether the toolbar is currently visible.
|
||||
///
|
||||
/// Includes both the text selection toolbar and the spell check menu.
|
||||
/// {@endtemplate}
|
||||
bool get toolbarIsVisible {
|
||||
return selectionControls is TextSelectionHandleControls
|
||||
? _contextMenuController.isShown || _spellCheckToolbarController.isShown
|
||||
: _toolbar != null || _spellCheckToolbarController.isShown;
|
||||
|
@ -1001,7 +1004,7 @@ class SelectionOverlay {
|
|||
/// [MagnifierController.shown].
|
||||
/// {@endtemplate}
|
||||
void showMagnifier(MagnifierInfo initialMagnifierInfo) {
|
||||
if (_toolbarIsVisible) {
|
||||
if (toolbarIsVisible) {
|
||||
hideToolbar();
|
||||
}
|
||||
|
||||
|
|
|
@ -560,6 +560,403 @@ void main() {
|
|||
await gesture.up();
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'right-click mouse can select word at position on Apple platforms',
|
||||
(WidgetTester tester) async {
|
||||
Set<ContextMenuButtonType> buttonTypes = <ContextMenuButtonType>{};
|
||||
final UniqueKey toolbarKey = UniqueKey();
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: SelectableRegion(
|
||||
focusNode: FocusNode(),
|
||||
selectionControls: materialTextSelectionHandleControls,
|
||||
contextMenuBuilder: (
|
||||
BuildContext context,
|
||||
SelectableRegionState selectableRegionState,
|
||||
) {
|
||||
buttonTypes = selectableRegionState.contextMenuButtonItems
|
||||
.map((ContextMenuButtonItem buttonItem) => buttonItem.type)
|
||||
.toSet();
|
||||
return SizedBox.shrink(key: toolbarKey);
|
||||
},
|
||||
child: const Center(
|
||||
child: Text('How are you'),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(buttonTypes.isEmpty, true);
|
||||
expect(find.byKey(toolbarKey), findsNothing);
|
||||
|
||||
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, buttons: kSecondaryMouseButton);
|
||||
addTearDown(gesture.removePointer);
|
||||
await tester.pump();
|
||||
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3));
|
||||
|
||||
await gesture.up();
|
||||
await tester.pump();
|
||||
|
||||
expect(buttonTypes, contains(ContextMenuButtonType.copy));
|
||||
expect(buttonTypes, contains(ContextMenuButtonType.selectAll));
|
||||
expect(find.byKey(toolbarKey), findsOneWidget);
|
||||
|
||||
await gesture.down(textOffsetToPosition(paragraph, 6));
|
||||
await tester.pump();
|
||||
expect(paragraph.selections[0], const TextSelection(baseOffset: 4, extentOffset: 7));
|
||||
|
||||
await gesture.up();
|
||||
await tester.pump();
|
||||
|
||||
expect(buttonTypes, contains(ContextMenuButtonType.copy));
|
||||
expect(buttonTypes, contains(ContextMenuButtonType.selectAll));
|
||||
expect(find.byKey(toolbarKey), findsOneWidget);
|
||||
|
||||
await gesture.down(textOffsetToPosition(paragraph, 9));
|
||||
await tester.pump();
|
||||
expect(paragraph.selections[0], const TextSelection(baseOffset: 8, extentOffset: 11));
|
||||
|
||||
await gesture.up();
|
||||
await tester.pump();
|
||||
|
||||
expect(buttonTypes, contains(ContextMenuButtonType.copy));
|
||||
expect(buttonTypes, contains(ContextMenuButtonType.selectAll));
|
||||
expect(find.byKey(toolbarKey), findsOneWidget);
|
||||
|
||||
// Clear selection.
|
||||
await tester.tapAt(textOffsetToPosition(paragraph, 1));
|
||||
await tester.pump();
|
||||
expect(paragraph.selections.isEmpty, true);
|
||||
expect(find.byKey(toolbarKey), findsNothing);
|
||||
},
|
||||
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
|
||||
skip: kIsWeb, // [intended] Web uses its native context menu.
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'right-click mouse at the same position as previous right-click toggles the context menu on macOS',
|
||||
(WidgetTester tester) async {
|
||||
Set<ContextMenuButtonType> buttonTypes = <ContextMenuButtonType>{};
|
||||
final UniqueKey toolbarKey = UniqueKey();
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: SelectableRegion(
|
||||
focusNode: FocusNode(),
|
||||
selectionControls: materialTextSelectionHandleControls,
|
||||
contextMenuBuilder: (
|
||||
BuildContext context,
|
||||
SelectableRegionState selectableRegionState,
|
||||
) {
|
||||
buttonTypes = selectableRegionState.contextMenuButtonItems
|
||||
.map((ContextMenuButtonItem buttonItem) => buttonItem.type)
|
||||
.toSet();
|
||||
return SizedBox.shrink(key: toolbarKey);
|
||||
},
|
||||
child: const Center(
|
||||
child: Text('How are you'),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(buttonTypes.isEmpty, true);
|
||||
expect(find.byKey(toolbarKey), findsNothing);
|
||||
|
||||
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, buttons: kSecondaryMouseButton);
|
||||
addTearDown(gesture.removePointer);
|
||||
await tester.pump();
|
||||
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3));
|
||||
|
||||
await gesture.up();
|
||||
await tester.pump();
|
||||
|
||||
expect(buttonTypes, contains(ContextMenuButtonType.copy));
|
||||
expect(buttonTypes, contains(ContextMenuButtonType.selectAll));
|
||||
expect(find.byKey(toolbarKey), findsOneWidget);
|
||||
|
||||
await gesture.down(textOffsetToPosition(paragraph, 2));
|
||||
await tester.pump();
|
||||
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3));
|
||||
|
||||
await gesture.up();
|
||||
await tester.pump();
|
||||
|
||||
// Right-click at same position will toggle the context menu off.
|
||||
expect(buttonTypes, contains(ContextMenuButtonType.copy));
|
||||
expect(buttonTypes, contains(ContextMenuButtonType.selectAll));
|
||||
expect(find.byKey(toolbarKey), findsNothing);
|
||||
|
||||
await gesture.down(textOffsetToPosition(paragraph, 9));
|
||||
await tester.pump();
|
||||
expect(paragraph.selections[0], const TextSelection(baseOffset: 8, extentOffset: 11));
|
||||
|
||||
await gesture.up();
|
||||
await tester.pump();
|
||||
|
||||
expect(buttonTypes, contains(ContextMenuButtonType.copy));
|
||||
expect(buttonTypes, contains(ContextMenuButtonType.selectAll));
|
||||
expect(find.byKey(toolbarKey), findsOneWidget);
|
||||
|
||||
await gesture.down(textOffsetToPosition(paragraph, 9));
|
||||
await tester.pump();
|
||||
expect(paragraph.selections[0], const TextSelection(baseOffset: 8, extentOffset: 11));
|
||||
|
||||
await gesture.up();
|
||||
await tester.pump();
|
||||
|
||||
// Right-click at same position will toggle the context menu off.
|
||||
expect(buttonTypes, contains(ContextMenuButtonType.copy));
|
||||
expect(buttonTypes, contains(ContextMenuButtonType.selectAll));
|
||||
expect(find.byKey(toolbarKey), findsNothing);
|
||||
|
||||
await gesture.down(textOffsetToPosition(paragraph, 6));
|
||||
await tester.pump();
|
||||
expect(paragraph.selections[0], const TextSelection(baseOffset: 4, extentOffset: 7));
|
||||
|
||||
await gesture.up();
|
||||
await tester.pump();
|
||||
|
||||
expect(buttonTypes, contains(ContextMenuButtonType.copy));
|
||||
expect(buttonTypes, contains(ContextMenuButtonType.selectAll));
|
||||
expect(find.byKey(toolbarKey), findsOneWidget);
|
||||
|
||||
// Clear selection.
|
||||
await tester.tapAt(textOffsetToPosition(paragraph, 1));
|
||||
await tester.pump();
|
||||
expect(paragraph.selections.isEmpty, true);
|
||||
expect(find.byKey(toolbarKey), findsNothing);
|
||||
},
|
||||
variant: TargetPlatformVariant.only(TargetPlatform.macOS),
|
||||
skip: kIsWeb, // [intended] Web uses its native context menu.
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'right-click mouse shows the context menu at position on Android, Fucshia, and Windows',
|
||||
(WidgetTester tester) async {
|
||||
Set<ContextMenuButtonType> buttonTypes = <ContextMenuButtonType>{};
|
||||
final UniqueKey toolbarKey = UniqueKey();
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: SelectableRegion(
|
||||
focusNode: FocusNode(),
|
||||
selectionControls: materialTextSelectionHandleControls,
|
||||
contextMenuBuilder: (
|
||||
BuildContext context,
|
||||
SelectableRegionState selectableRegionState,
|
||||
) {
|
||||
buttonTypes = selectableRegionState.contextMenuButtonItems
|
||||
.map((ContextMenuButtonItem buttonItem) => buttonItem.type)
|
||||
.toSet();
|
||||
return SizedBox.shrink(key: toolbarKey);
|
||||
},
|
||||
child: const Center(
|
||||
child: Text('How are you'),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(buttonTypes.isEmpty, true);
|
||||
expect(find.byKey(toolbarKey), findsNothing);
|
||||
|
||||
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, buttons: kSecondaryMouseButton);
|
||||
addTearDown(gesture.removePointer);
|
||||
await tester.pump();
|
||||
// Selection is collapsed so none is reported.
|
||||
expect(paragraph.selections.isEmpty, true);
|
||||
|
||||
await gesture.up();
|
||||
await tester.pump();
|
||||
|
||||
expect(buttonTypes.length, 1);
|
||||
expect(buttonTypes, contains(ContextMenuButtonType.selectAll));
|
||||
expect(find.byKey(toolbarKey), findsOneWidget);
|
||||
|
||||
await gesture.down(textOffsetToPosition(paragraph, 6));
|
||||
await tester.pump();
|
||||
expect(paragraph.selections.isEmpty, true);
|
||||
|
||||
await gesture.up();
|
||||
await tester.pump();
|
||||
|
||||
expect(buttonTypes.length, 1);
|
||||
expect(buttonTypes, contains(ContextMenuButtonType.selectAll));
|
||||
expect(find.byKey(toolbarKey), findsOneWidget);
|
||||
|
||||
await gesture.down(textOffsetToPosition(paragraph, 9));
|
||||
await tester.pump();
|
||||
expect(paragraph.selections.isEmpty, true);
|
||||
|
||||
await gesture.up();
|
||||
await tester.pump();
|
||||
|
||||
expect(buttonTypes.length, 1);
|
||||
expect(buttonTypes, contains(ContextMenuButtonType.selectAll));
|
||||
expect(find.byKey(toolbarKey), findsOneWidget);
|
||||
|
||||
// Clear selection.
|
||||
await tester.tapAt(textOffsetToPosition(paragraph, 1));
|
||||
await tester.pump();
|
||||
expect(paragraph.selections.isEmpty, true);
|
||||
expect(find.byKey(toolbarKey), findsNothing);
|
||||
|
||||
// Create an uncollapsed selection by dragging.
|
||||
final TestGesture dragGesture = await tester.startGesture(textOffsetToPosition(paragraph, 0), kind: PointerDeviceKind.mouse);
|
||||
addTearDown(dragGesture.removePointer);
|
||||
await tester.pump();
|
||||
await dragGesture.moveTo(textOffsetToPosition(paragraph, 5));
|
||||
await tester.pump();
|
||||
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 5));
|
||||
await dragGesture.up();
|
||||
await tester.pump();
|
||||
|
||||
// Right click on previous selection should not collapse the selection.
|
||||
await gesture.down(textOffsetToPosition(paragraph, 2));
|
||||
await tester.pump();
|
||||
await gesture.up();
|
||||
await tester.pump();
|
||||
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 5));
|
||||
expect(find.byKey(toolbarKey), findsOneWidget);
|
||||
|
||||
// Right click anywhere outside previous selection should collapse the
|
||||
// selection.
|
||||
await gesture.down(textOffsetToPosition(paragraph, 7));
|
||||
await tester.pump();
|
||||
await gesture.up();
|
||||
await tester.pump();
|
||||
expect(paragraph.selections.isEmpty, true);
|
||||
expect(find.byKey(toolbarKey), findsOneWidget);
|
||||
|
||||
// Clear selection.
|
||||
await tester.tapAt(textOffsetToPosition(paragraph, 1));
|
||||
await tester.pump();
|
||||
expect(paragraph.selections.isEmpty, true);
|
||||
expect(find.byKey(toolbarKey), findsNothing);
|
||||
},
|
||||
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.windows }),
|
||||
skip: kIsWeb, // [intended] Web uses its native context menu.
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'right-click mouse toggles the context menu on Linux',
|
||||
(WidgetTester tester) async {
|
||||
Set<ContextMenuButtonType> buttonTypes = <ContextMenuButtonType>{};
|
||||
final UniqueKey toolbarKey = UniqueKey();
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: SelectableRegion(
|
||||
focusNode: FocusNode(),
|
||||
selectionControls: materialTextSelectionHandleControls,
|
||||
contextMenuBuilder: (
|
||||
BuildContext context,
|
||||
SelectableRegionState selectableRegionState,
|
||||
) {
|
||||
buttonTypes = selectableRegionState.contextMenuButtonItems
|
||||
.map((ContextMenuButtonItem buttonItem) => buttonItem.type)
|
||||
.toSet();
|
||||
return SizedBox.shrink(key: toolbarKey);
|
||||
},
|
||||
child: const Center(
|
||||
child: Text('How are you'),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(buttonTypes.isEmpty, true);
|
||||
expect(find.byKey(toolbarKey), findsNothing);
|
||||
|
||||
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, buttons: kSecondaryMouseButton);
|
||||
addTearDown(gesture.removePointer);
|
||||
await tester.pump();
|
||||
// Selection is collapsed so none is reported.
|
||||
expect(paragraph.selections.isEmpty, true);
|
||||
|
||||
await gesture.up();
|
||||
await tester.pump();
|
||||
|
||||
// Context menu toggled on.
|
||||
expect(buttonTypes.length, 1);
|
||||
expect(buttonTypes, contains(ContextMenuButtonType.selectAll));
|
||||
expect(find.byKey(toolbarKey), findsOneWidget);
|
||||
|
||||
await gesture.down(textOffsetToPosition(paragraph, 6));
|
||||
await tester.pump();
|
||||
expect(paragraph.selections.isEmpty, true);
|
||||
|
||||
await gesture.up();
|
||||
await tester.pump();
|
||||
|
||||
// Context menu toggled off.
|
||||
expect(find.byKey(toolbarKey), findsNothing);
|
||||
|
||||
await gesture.down(textOffsetToPosition(paragraph, 9));
|
||||
await tester.pump();
|
||||
expect(paragraph.selections.isEmpty, true);
|
||||
|
||||
await gesture.up();
|
||||
await tester.pump();
|
||||
|
||||
// Context menu toggled on.
|
||||
expect(buttonTypes.length, 1);
|
||||
expect(buttonTypes, contains(ContextMenuButtonType.selectAll));
|
||||
expect(find.byKey(toolbarKey), findsOneWidget);
|
||||
|
||||
// Clear selection.
|
||||
await tester.tapAt(textOffsetToPosition(paragraph, 1));
|
||||
await tester.pump();
|
||||
expect(paragraph.selections.isEmpty, true);
|
||||
expect(find.byKey(toolbarKey), findsNothing);
|
||||
|
||||
final TestGesture dragGesture = await tester.startGesture(textOffsetToPosition(paragraph, 0), kind: PointerDeviceKind.mouse);
|
||||
addTearDown(dragGesture.removePointer);
|
||||
await tester.pump();
|
||||
await dragGesture.moveTo(textOffsetToPosition(paragraph, 5));
|
||||
await tester.pump();
|
||||
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 5));
|
||||
await dragGesture.up();
|
||||
await tester.pump();
|
||||
|
||||
// Right click on previous selection should not collapse the selection.
|
||||
await gesture.down(textOffsetToPosition(paragraph, 2));
|
||||
await tester.pump();
|
||||
await gesture.up();
|
||||
await tester.pump();
|
||||
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 5));
|
||||
expect(find.byKey(toolbarKey), findsOneWidget);
|
||||
|
||||
// Right click anywhere outside previous selection should first toggle the context
|
||||
// menu off.
|
||||
await gesture.down(textOffsetToPosition(paragraph, 7));
|
||||
await tester.pump();
|
||||
await gesture.up();
|
||||
await tester.pump();
|
||||
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 5));
|
||||
expect(find.byKey(toolbarKey), findsNothing);
|
||||
|
||||
// Right click again should collapse the selection and toggle the context
|
||||
// menu on.
|
||||
await gesture.down(textOffsetToPosition(paragraph, 7));
|
||||
await tester.pump();
|
||||
await gesture.up();
|
||||
await tester.pump();
|
||||
expect(paragraph.selections.isEmpty, true);
|
||||
expect(find.byKey(toolbarKey), findsOneWidget);
|
||||
|
||||
// Clear selection.
|
||||
await tester.tapAt(textOffsetToPosition(paragraph, 1));
|
||||
await tester.pump();
|
||||
expect(paragraph.selections.isEmpty, true);
|
||||
expect(find.byKey(toolbarKey), findsNothing);
|
||||
},
|
||||
variant: TargetPlatformVariant.only(TargetPlatform.linux),
|
||||
skip: kIsWeb, // [intended] Web uses its native context menu.
|
||||
);
|
||||
|
||||
testWidgets('can copy a selection made with the mouse', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
|
@ -808,6 +1205,7 @@ void main() {
|
|||
// Should select "Hello".
|
||||
expect(paragraph.selections[0], const TextSelection(baseOffset: 124, extentOffset: 129));
|
||||
},
|
||||
variant: TargetPlatformVariant.only(TargetPlatform.macOS),
|
||||
skip: isBrowser, // https://github.com/flutter/flutter/issues/61020
|
||||
);
|
||||
|
||||
|
|
Loading…
Reference in a new issue