Unverified Commit b36ef583 authored by Renzo Olivares's avatar Renzo Olivares Committed by GitHub

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
parent c40baf47
...@@ -157,6 +157,7 @@ class _RenderSelectableAdapter extends RenderProxyBox with Selectable, Selection ...@@ -157,6 +157,7 @@ class _RenderSelectableAdapter extends RenderProxyBox with Selectable, Selection
hasContent: true, hasContent: true,
startSelectionPoint: isReversed ? secondSelectionPoint : firstSelectionPoint, startSelectionPoint: isReversed ? secondSelectionPoint : firstSelectionPoint,
endSelectionPoint: isReversed ? firstSelectionPoint : secondSelectionPoint, endSelectionPoint: isReversed ? firstSelectionPoint : secondSelectionPoint,
selectionRects: <Rect>[selectionRect],
); );
} }
} }
......
...@@ -1335,6 +1335,14 @@ class _SelectableFragment with Selectable, ChangeNotifier implements TextLayoutM ...@@ -1335,6 +1335,14 @@ class _SelectableFragment with Selectable, ChangeNotifier implements TextLayoutM
: paragraph._getOffsetForPosition(TextPosition(offset: selectionEnd)); : paragraph._getOffsetForPosition(TextPosition(offset: selectionEnd));
final bool flipHandles = isReversed != (TextDirection.rtl == paragraph.textDirection); final bool flipHandles = isReversed != (TextDirection.rtl == paragraph.textDirection);
final Matrix4 paragraphToFragmentTransform = getTransformToParagraph()..invert(); 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( return SelectionGeometry(
startSelectionPoint: SelectionPoint( startSelectionPoint: SelectionPoint(
localPosition: MatrixUtils.transformPoint(paragraphToFragmentTransform, startOffsetInParagraphCoordinates), localPosition: MatrixUtils.transformPoint(paragraphToFragmentTransform, startOffsetInParagraphCoordinates),
...@@ -1346,6 +1354,7 @@ class _SelectableFragment with Selectable, ChangeNotifier implements TextLayoutM ...@@ -1346,6 +1354,7 @@ class _SelectableFragment with Selectable, ChangeNotifier implements TextLayoutM
lineHeight: paragraph._textPainter.preferredLineHeight, lineHeight: paragraph._textPainter.preferredLineHeight,
handleType: flipHandles ? TextSelectionHandleType.left : TextSelectionHandleType.right, handleType: flipHandles ? TextSelectionHandleType.left : TextSelectionHandleType.right,
), ),
selectionRects: selectionRects,
status: _textSelectionStart!.offset == _textSelectionEnd!.offset status: _textSelectionStart!.offset == _textSelectionEnd!.offset
? SelectionStatus.collapsed ? SelectionStatus.collapsed
: SelectionStatus.uncollapsed, : SelectionStatus.uncollapsed,
......
...@@ -576,8 +576,8 @@ enum SelectionStatus { ...@@ -576,8 +576,8 @@ enum SelectionStatus {
/// The geometry of the current selection. /// The geometry of the current selection.
/// ///
/// This includes details such as the locations of the selection start and end, /// This includes details such as the locations of the selection start and end,
/// line height, etc. This information is used for drawing selection controls /// line height, the rects that encompass the selection, etc. This information
/// for mobile platforms. /// is used for drawing selection controls for mobile platforms.
/// ///
/// The positions in geometry are in local coordinates of the [SelectionHandler] /// The positions in geometry are in local coordinates of the [SelectionHandler]
/// or [Selectable]. /// or [Selectable].
...@@ -590,6 +590,7 @@ class SelectionGeometry { ...@@ -590,6 +590,7 @@ class SelectionGeometry {
const SelectionGeometry({ const SelectionGeometry({
this.startSelectionPoint, this.startSelectionPoint,
this.endSelectionPoint, this.endSelectionPoint,
this.selectionRects = const <Rect>[],
required this.status, required this.status,
required this.hasContent, required this.hasContent,
}) : assert((startSelectionPoint == null && endSelectionPoint == null) || status != SelectionStatus.none); }) : assert((startSelectionPoint == null && endSelectionPoint == null) || status != SelectionStatus.none);
...@@ -627,6 +628,10 @@ class SelectionGeometry { ...@@ -627,6 +628,10 @@ class SelectionGeometry {
/// The status of ongoing selection in the [Selectable] or [SelectionHandler]. /// The status of ongoing selection in the [Selectable] or [SelectionHandler].
final SelectionStatus status; 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 /// Whether there is any selectable content in the [Selectable] or
/// [SelectionHandler]. /// [SelectionHandler].
final bool hasContent; final bool hasContent;
...@@ -638,12 +643,14 @@ class SelectionGeometry { ...@@ -638,12 +643,14 @@ class SelectionGeometry {
SelectionGeometry copyWith({ SelectionGeometry copyWith({
SelectionPoint? startSelectionPoint, SelectionPoint? startSelectionPoint,
SelectionPoint? endSelectionPoint, SelectionPoint? endSelectionPoint,
List<Rect>? selectionRects,
SelectionStatus? status, SelectionStatus? status,
bool? hasContent, bool? hasContent,
}) { }) {
return SelectionGeometry( return SelectionGeometry(
startSelectionPoint: startSelectionPoint ?? this.startSelectionPoint, startSelectionPoint: startSelectionPoint ?? this.startSelectionPoint,
endSelectionPoint: endSelectionPoint ?? this.endSelectionPoint, endSelectionPoint: endSelectionPoint ?? this.endSelectionPoint,
selectionRects: selectionRects ?? this.selectionRects,
status: status ?? this.status, status: status ?? this.status,
hasContent: hasContent ?? this.hasContent, hasContent: hasContent ?? this.hasContent,
); );
...@@ -660,6 +667,7 @@ class SelectionGeometry { ...@@ -660,6 +667,7 @@ class SelectionGeometry {
return other is SelectionGeometry return other is SelectionGeometry
&& other.startSelectionPoint == startSelectionPoint && other.startSelectionPoint == startSelectionPoint
&& other.endSelectionPoint == endSelectionPoint && other.endSelectionPoint == endSelectionPoint
&& other.selectionRects == selectionRects
&& other.status == status && other.status == status
&& other.hasContent == hasContent; && other.hasContent == hasContent;
} }
...@@ -669,6 +677,7 @@ class SelectionGeometry { ...@@ -669,6 +677,7 @@ class SelectionGeometry {
return Object.hash( return Object.hash(
startSelectionPoint, startSelectionPoint,
endSelectionPoint, endSelectionPoint,
selectionRects,
status, status,
hasContent, hasContent,
); );
......
...@@ -263,7 +263,7 @@ class SelectableRegion extends StatefulWidget { ...@@ -263,7 +263,7 @@ class SelectableRegion extends StatefulWidget {
required final VoidCallback onCopy, required final VoidCallback onCopy,
required final VoidCallback onSelectAll, required final VoidCallback onSelectAll,
}) { }) {
final bool canCopy = selectionGeometry.hasSelection; final bool canCopy = selectionGeometry.status == SelectionStatus.uncollapsed;
final bool canSelectAll = selectionGeometry.hasContent; final bool canSelectAll = selectionGeometry.hasContent;
// Determine which buttons will appear so that the order and total number is // Determine which buttons will appear so that the order and total number is
...@@ -489,12 +489,62 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe ...@@ -489,12 +489,62 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
_updateSelectedContentIfNeeded(); _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) { void _handleRightClickDown(TapDownDetails details) {
final Offset? previousSecondaryTapDownPosition = lastSecondaryTapDownPosition;
final bool toolbarIsVisible = _selectionOverlay?.toolbarIsVisible ?? false;
lastSecondaryTapDownPosition = details.globalPosition; lastSecondaryTapDownPosition = details.globalPosition;
widget.focusNode.requestFocus(); widget.focusNode.requestFocus();
_selectWordAt(offset: 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(); _showHandles();
_showToolbar(location: details.globalPosition); _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(); _updateSelectedContentIfNeeded();
} }
...@@ -1770,9 +1820,30 @@ abstract class MultiSelectableSelectionContainerDelegate extends SelectionContai ...@@ -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( return SelectionGeometry(
startSelectionPoint: startPoint, startSelectionPoint: startPoint,
endSelectionPoint: endPoint, endSelectionPoint: endPoint,
selectionRects: selectionRects,
status: startGeometry != endGeometry status: startGeometry != endGeometry
? SelectionStatus.uncollapsed ? SelectionStatus.uncollapsed
: startGeometry.status, : startGeometry.status,
......
...@@ -562,15 +562,13 @@ class TextSelectionOverlay { ...@@ -562,15 +562,13 @@ class TextSelectionOverlay {
/// Whether the handles are currently visible. /// Whether the handles are currently visible.
bool get handlesAreVisible => _selectionOverlay._handles != null && handlesVisible; bool get handlesAreVisible => _selectionOverlay._handles != null && handlesVisible;
/// Whether the toolbar is currently visible. /// {@macro flutter.widgets.SelectionOverlay.toolbarIsVisible}
///
/// Includes both the text selection toolbar and the spell check menu.
/// ///
/// See also: /// See also:
/// ///
/// * [spellCheckToolbarIsVisible], which is only whether the spell check menu /// * [spellCheckToolbarIsVisible], which is only whether the spell check menu
/// specifically is visible. /// specifically is visible.
bool get toolbarIsVisible => _selectionOverlay._toolbarIsVisible; bool get toolbarIsVisible => _selectionOverlay.toolbarIsVisible;
/// Whether the magnifier is currently visible. /// Whether the magnifier is currently visible.
bool get magnifierIsVisible => _selectionOverlay._magnifierController.shown; bool get magnifierIsVisible => _selectionOverlay._magnifierController.shown;
...@@ -984,7 +982,12 @@ class SelectionOverlay { ...@@ -984,7 +982,12 @@ class SelectionOverlay {
/// {@macro flutter.widgets.magnifier.TextMagnifierConfiguration.details} /// {@macro flutter.widgets.magnifier.TextMagnifierConfiguration.details}
final TextMagnifierConfiguration magnifierConfiguration; 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 return selectionControls is TextSelectionHandleControls
? _contextMenuController.isShown || _spellCheckToolbarController.isShown ? _contextMenuController.isShown || _spellCheckToolbarController.isShown
: _toolbar != null || _spellCheckToolbarController.isShown; : _toolbar != null || _spellCheckToolbarController.isShown;
...@@ -1001,7 +1004,7 @@ class SelectionOverlay { ...@@ -1001,7 +1004,7 @@ class SelectionOverlay {
/// [MagnifierController.shown]. /// [MagnifierController.shown].
/// {@endtemplate} /// {@endtemplate}
void showMagnifier(MagnifierInfo initialMagnifierInfo) { void showMagnifier(MagnifierInfo initialMagnifierInfo) {
if (_toolbarIsVisible) { if (toolbarIsVisible) {
hideToolbar(); hideToolbar();
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment