@@ -352,7 +352,8 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
352
352
_showToolbar (location: details.globalPosition);
353
353
}
354
354
} else {
355
- _clearSelection ();
355
+ hideToolbar ();
356
+ _collapseSelectionAt (offset: details.globalPosition);
356
357
}
357
358
};
358
359
instance.onSecondaryTapDown = _handleRightClickDown;
@@ -472,6 +473,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
472
473
(TapAndPanGestureRecognizer instance) {
473
474
instance
474
475
..onTapDown = _startNewMouseSelectionGesture
476
+ ..onTapUp = _handleMouseTapUp
475
477
..onDragStart = _handleMouseDragStart
476
478
..onDragUpdate = _handleMouseDragUpdate
477
479
..onDragEnd = _handleMouseDragEnd
@@ -498,7 +500,17 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
498
500
case 1 :
499
501
widget.focusNode.requestFocus ();
500
502
hideToolbar ();
501
- _clearSelection ();
503
+ switch (defaultTargetPlatform) {
504
+ case TargetPlatform .android:
505
+ case TargetPlatform .fuchsia:
506
+ case TargetPlatform .iOS:
507
+ // On mobile platforms the selection is set on tap up.
508
+ break ;
509
+ case TargetPlatform .macOS:
510
+ case TargetPlatform .linux:
511
+ case TargetPlatform .windows:
512
+ _collapseSelectionAt (offset: details.globalPosition);
513
+ }
502
514
case 2 :
503
515
_selectWordAt (offset: details.globalPosition);
504
516
}
@@ -528,6 +540,24 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
528
540
_updateSelectedContentIfNeeded ();
529
541
}
530
542
543
+ void _handleMouseTapUp (TapDragUpDetails details) {
544
+ switch (_getEffectiveConsecutiveTapCount (details.consecutiveTapCount)) {
545
+ case 1 :
546
+ switch (defaultTargetPlatform) {
547
+ case TargetPlatform .android:
548
+ case TargetPlatform .fuchsia:
549
+ case TargetPlatform .iOS:
550
+ _collapseSelectionAt (offset: details.globalPosition);
551
+ case TargetPlatform .macOS:
552
+ case TargetPlatform .linux:
553
+ case TargetPlatform .windows:
554
+ // On desktop platforms the selection is set on tap down.
555
+ break ;
556
+ }
557
+ }
558
+ _updateSelectedContentIfNeeded ();
559
+ }
560
+
531
561
void _updateSelectedContentIfNeeded () {
532
562
if (_lastSelectedContent? .plainText != _selectable? .getSelectedContent ()? .plainText) {
533
563
_lastSelectedContent = _selectable? .getSelectedContent ();
@@ -586,8 +616,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
586
616
// keep the current selection, if not then collapse it.
587
617
final bool lastSecondaryTapDownPositionWasOnActiveSelection = _positionIsOnActiveSelection (globalPosition: details.globalPosition);
588
618
if (! lastSecondaryTapDownPositionWasOnActiveSelection) {
589
- _selectStartTo (offset: lastSecondaryTapDownPosition! );
590
- _selectEndTo (offset: lastSecondaryTapDownPosition! );
619
+ _collapseSelectionAt (offset: lastSecondaryTapDownPosition! );
591
620
}
592
621
_showHandles ();
593
622
_showToolbar (location: lastSecondaryTapDownPosition);
@@ -612,8 +641,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
612
641
// keep the current selection, if not then collapse it.
613
642
final bool lastSecondaryTapDownPositionWasOnActiveSelection = _positionIsOnActiveSelection (globalPosition: details.globalPosition);
614
643
if (! lastSecondaryTapDownPositionWasOnActiveSelection) {
615
- _selectStartTo (offset: lastSecondaryTapDownPosition! );
616
- _selectEndTo (offset: lastSecondaryTapDownPosition! );
644
+ _collapseSelectionAt (offset: lastSecondaryTapDownPosition! );
617
645
}
618
646
_showHandles ();
619
647
_showToolbar (location: lastSecondaryTapDownPosition);
@@ -925,8 +953,9 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
925
953
/// See also:
926
954
/// * [_selectStartTo] , which sets or updates selection start edge.
927
955
/// * [_finalizeSelection] , which stops the `continuous` updates.
928
- /// * [_clearSelection] , which clear the ongoing selection.
956
+ /// * [_clearSelection] , which clears the ongoing selection.
929
957
/// * [_selectWordAt] , which selects a whole word at the location.
958
+ /// * [_collapseSelectionAt] , which collapses the selection at the location.
930
959
/// * [selectAll] , which selects the entire content.
931
960
void _selectEndTo ({required Offset offset, bool continuous = false , TextGranularity ? textGranularity}) {
932
961
if (! continuous) {
@@ -964,8 +993,9 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
964
993
/// See also:
965
994
/// * [_selectEndTo] , which sets or updates selection end edge.
966
995
/// * [_finalizeSelection] , which stops the `continuous` updates.
967
- /// * [_clearSelection] , which clear the ongoing selection.
996
+ /// * [_clearSelection] , which clears the ongoing selection.
968
997
/// * [_selectWordAt] , which selects a whole word at the location.
998
+ /// * [_collapseSelectionAt] , which collapses the selection at the location.
969
999
/// * [selectAll] , which selects the entire content.
970
1000
void _selectStartTo ({required Offset offset, bool continuous = false , TextGranularity ? textGranularity}) {
971
1001
if (! continuous) {
@@ -978,6 +1008,20 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
978
1008
}
979
1009
}
980
1010
1011
+ /// Collapses the selection at the given `offset` location.
1012
+ ///
1013
+ /// See also:
1014
+ /// * [_selectStartTo] , which sets or updates selection start edge.
1015
+ /// * [_selectEndTo] , which sets or updates selection end edge.
1016
+ /// * [_finalizeSelection] , which stops the `continuous` updates.
1017
+ /// * [_clearSelection] , which clears the ongoing selection.
1018
+ /// * [_selectWordAt] , which selects a whole word at the location.
1019
+ /// * [selectAll] , which selects the entire content.
1020
+ void _collapseSelectionAt ({required Offset offset}) {
1021
+ _selectStartTo (offset: offset);
1022
+ _selectEndTo (offset: offset);
1023
+ }
1024
+
981
1025
/// Selects a whole word at the `offset` location.
982
1026
///
983
1027
/// If the whole word is already in the current selection, selection won't
@@ -991,7 +1035,8 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
991
1035
/// * [_selectStartTo] , which sets or updates selection start edge.
992
1036
/// * [_selectEndTo] , which sets or updates selection end edge.
993
1037
/// * [_finalizeSelection] , which stops the `continuous` updates.
994
- /// * [_clearSelection] , which clear the ongoing selection.
1038
+ /// * [_clearSelection] , which clears the ongoing selection.
1039
+ /// * [_collapseSelectionAt] , which collapses the selection at the location.
995
1040
/// * [selectAll] , which selects the entire content.
996
1041
void _selectWordAt ({required Offset offset}) {
997
1042
// There may be other selection ongoing.
@@ -1881,7 +1926,7 @@ abstract class MultiSelectableSelectionContainerDelegate extends SelectionContai
1881
1926
1882
1927
SelectionPoint ? startPoint;
1883
1928
if (startGeometry.startSelectionPoint != null ) {
1884
- final Matrix4 startTransform = getTransformFrom (selectables[startIndexWalker]);
1929
+ final Matrix4 startTransform = getTransformFrom (selectables[startIndexWalker]);
1885
1930
final Offset start = MatrixUtils .transformPoint (startTransform, startGeometry.startSelectionPoint! .localPosition);
1886
1931
// It can be NaN if it is detached or off-screen.
1887
1932
if (start.isFinite) {
@@ -1902,7 +1947,7 @@ abstract class MultiSelectableSelectionContainerDelegate extends SelectionContai
1902
1947
}
1903
1948
SelectionPoint ? endPoint;
1904
1949
if (endGeometry.endSelectionPoint != null ) {
1905
- final Matrix4 endTransform = getTransformFrom (selectables[endIndexWalker]);
1950
+ final Matrix4 endTransform = getTransformFrom (selectables[endIndexWalker]);
1906
1951
final Offset end = MatrixUtils .transformPoint (endTransform, endGeometry.endSelectionPoint! .localPosition);
1907
1952
// It can be NaN if it is detached or off-screen.
1908
1953
if (end.isFinite) {
@@ -1986,8 +2031,8 @@ abstract class MultiSelectableSelectionContainerDelegate extends SelectionContai
1986
2031
final Rect ? drawableArea = hasSize ? Rect
1987
2032
.fromLTWH (0 , 0 , containerSize.width, containerSize.height)
1988
2033
.inflate (_kSelectionHandleDrawableAreaPadding) : null ;
1989
- final bool hideStartHandle = value.startSelectionPoint == null || drawableArea == null || ! drawableArea.contains (value.startSelectionPoint! .localPosition);
1990
- final bool hideEndHandle = value.endSelectionPoint == null || drawableArea == null || ! drawableArea.contains (value.endSelectionPoint! .localPosition);
2034
+ final bool hideStartHandle = value.startSelectionPoint == null || drawableArea == null || ! drawableArea.contains (value.startSelectionPoint! .localPosition);
2035
+ final bool hideEndHandle = value.endSelectionPoint == null || drawableArea == null || ! drawableArea.contains (value.endSelectionPoint! .localPosition);
1991
2036
effectiveStartHandle = hideStartHandle ? null : _startHandleLayer;
1992
2037
effectiveEndHandle = hideEndHandle ? null : _endHandleLayer;
1993
2038
}
@@ -2047,6 +2092,34 @@ abstract class MultiSelectableSelectionContainerDelegate extends SelectionContai
2047
2092
);
2048
2093
}
2049
2094
2095
+ // Clears the selection on all selectables not in the range of
2096
+ // currentSelectionStartIndex..currentSelectionEndIndex.
2097
+ //
2098
+ // If one of the edges does not exist, then this method will clear the selection
2099
+ // in all selectables except the existing edge.
2100
+ //
2101
+ // If neither of the edges exist this method immediately returns.
2102
+ void _flushInactiveSelections () {
2103
+ if (currentSelectionStartIndex == - 1 && currentSelectionEndIndex == - 1 ) {
2104
+ return ;
2105
+ }
2106
+ if (currentSelectionStartIndex == - 1 || currentSelectionEndIndex == - 1 ) {
2107
+ final int skipIndex = currentSelectionStartIndex == - 1 ? currentSelectionEndIndex : currentSelectionStartIndex;
2108
+ selectables
2109
+ .where ((Selectable target) => target != selectables[skipIndex])
2110
+ .forEach ((Selectable target) => dispatchSelectionEventToChild (target, const ClearSelectionEvent ()));
2111
+ return ;
2112
+ }
2113
+ final int skipStart = min (currentSelectionStartIndex, currentSelectionEndIndex);
2114
+ final int skipEnd = max (currentSelectionStartIndex, currentSelectionEndIndex);
2115
+ for (int index = 0 ; index < selectables.length; index += 1 ) {
2116
+ if (index >= skipStart && index <= skipEnd) {
2117
+ continue ;
2118
+ }
2119
+ dispatchSelectionEventToChild (selectables[index], const ClearSelectionEvent ());
2120
+ }
2121
+ }
2122
+
2050
2123
/// Selects all contents of all selectables.
2051
2124
@protected
2052
2125
SelectionResult handleSelectAll (SelectAllSelectionEvent event) {
@@ -2290,7 +2363,7 @@ abstract class MultiSelectableSelectionContainerDelegate extends SelectionContai
2290
2363
bool hasFoundEdgeIndex = false ;
2291
2364
SelectionResult ? result;
2292
2365
for (int index = 0 ; index < selectables.length && ! hasFoundEdgeIndex; index += 1 ) {
2293
- final Selectable child = selectables[index];
2366
+ final Selectable child = selectables[index];
2294
2367
final SelectionResult childResult = dispatchSelectionEventToChild (child, event);
2295
2368
switch (childResult) {
2296
2369
case SelectionResult .next:
@@ -2323,6 +2396,7 @@ abstract class MultiSelectableSelectionContainerDelegate extends SelectionContai
2323
2396
} else {
2324
2397
currentSelectionStartIndex = newIndex;
2325
2398
}
2399
+ _flushInactiveSelections ();
2326
2400
// The result can only be null if the loop went through the entire list
2327
2401
// without any of the selection returned end or previous. In this case, the
2328
2402
// caller of this method needs to find the next selectable in their list.
@@ -2345,13 +2419,39 @@ abstract class MultiSelectableSelectionContainerDelegate extends SelectionContai
2345
2419
return true ;
2346
2420
}());
2347
2421
SelectionResult ? finalResult;
2348
- int newIndex = isEnd ? currentSelectionEndIndex : currentSelectionStartIndex;
2422
+ // Determines if the edge being adjusted is within the current viewport.
2423
+ // - If so, we begin the search for the new selection edge position at the
2424
+ // currentSelectionEndIndex/currentSelectionStartIndex.
2425
+ // - If not, we attempt to locate the new selection edge starting from
2426
+ // the opposite end.
2427
+ // - If neither edge is in the current viewport, the search for the new
2428
+ // selection edge position begins at 0.
2429
+ //
2430
+ // This can happen when there is a scrollable child and the edge being adjusted
2431
+ // has been scrolled out of view.
2432
+ final bool isCurrentEdgeWithinViewport = isEnd ? _selectionGeometry.endSelectionPoint != null : _selectionGeometry.startSelectionPoint != null ;
2433
+ final bool isOppositeEdgeWithinViewport = isEnd ? _selectionGeometry.startSelectionPoint != null : _selectionGeometry.endSelectionPoint != null ;
2434
+ int newIndex = switch ((isEnd, isCurrentEdgeWithinViewport, isOppositeEdgeWithinViewport)) {
2435
+ (true , true , true ) => currentSelectionEndIndex,
2436
+ (true , true , false ) => currentSelectionEndIndex,
2437
+ (true , false , true ) => currentSelectionStartIndex,
2438
+ (true , false , false ) => 0 ,
2439
+ (false , true , true ) => currentSelectionStartIndex,
2440
+ (false , true , false ) => currentSelectionStartIndex,
2441
+ (false , false , true ) => currentSelectionEndIndex,
2442
+ (false , false , false ) => 0 ,
2443
+ };
2349
2444
bool ? forward;
2350
2445
late SelectionResult currentSelectableResult;
2351
- // This loop sends the selection event to the
2352
- // currentSelectionEndIndex/currentSelectionStartIndex to determine the
2353
- // direction of the search. If the result is `SelectionResult.next`, this
2354
- // loop look backward. Otherwise, it looks forward.
2446
+ // This loop sends the selection event to one of the following to determine
2447
+ // the direction of the search.
2448
+ // - currentSelectionEndIndex/currentSelectionStartIndex if the current edge
2449
+ // is in the current viewport.
2450
+ // - The opposite edge index if the current edge is not in the current viewport.
2451
+ // - Index 0 if neither edge is in the current viewport.
2452
+ //
2453
+ // If the result is `SelectionResult.next`, this loop look backward.
2454
+ // Otherwise, it looks forward.
2355
2455
//
2356
2456
// The terminate condition are:
2357
2457
// 1. the selectable returns end, pending, none.
@@ -2391,6 +2491,7 @@ abstract class MultiSelectableSelectionContainerDelegate extends SelectionContai
2391
2491
} else {
2392
2492
currentSelectionStartIndex = newIndex;
2393
2493
}
2494
+ _flushInactiveSelections ();
2394
2495
return finalResult! ;
2395
2496
}
2396
2497
}
0 commit comments