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
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();
}
......
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