@@ -1026,9 +1026,23 @@ class TextSelectionGestureDetectorBuilder {
1026
1026
// The viewport offset pixels of the [RenderEditable] at the last drag start.
1027
1027
double _dragStartViewportOffset = 0.0 ;
1028
1028
1029
+ // Returns true iff either shift key is currently down.
1030
+ bool get _isShiftPressed {
1031
+ return HardwareKeyboard .instance.logicalKeysPressed
1032
+ .any (< LogicalKeyboardKey > {
1033
+ LogicalKeyboardKey .shiftLeft,
1034
+ LogicalKeyboardKey .shiftRight,
1035
+ }.contains);
1036
+ }
1037
+
1029
1038
// True iff a tap + shift has been detected but the tap has not yet come up.
1030
1039
bool _isShiftTapping = false ;
1031
1040
1041
+ // For a shift + tap + drag gesture, the TextSelection at the point of the
1042
+ // tap. Mac uses this value to reset to the original selection when an
1043
+ // inversion of the base and offset happens.
1044
+ TextSelection ? _shiftTapDragSelection;
1045
+
1032
1046
/// Handler for [TextSelectionGestureDetector.onTapDown] .
1033
1047
///
1034
1048
/// By default, it forwards the tap to [RenderEditable.handleTapDown] and sets
@@ -1050,12 +1064,7 @@ class TextSelectionGestureDetectorBuilder {
1050
1064
|| kind == PointerDeviceKind .stylus;
1051
1065
1052
1066
// Handle shift + click selection if needed.
1053
- final bool isShiftPressed = HardwareKeyboard .instance.logicalKeysPressed
1054
- .any (< LogicalKeyboardKey > {
1055
- LogicalKeyboardKey .shiftLeft,
1056
- LogicalKeyboardKey .shiftRight,
1057
- }.contains);
1058
- if (isShiftPressed && renderEditable.selection? .baseOffset != null ) {
1067
+ if (_isShiftPressed && renderEditable.selection? .baseOffset != null ) {
1059
1068
_isShiftTapping = true ;
1060
1069
switch (defaultTargetPlatform) {
1061
1070
case TargetPlatform .iOS:
@@ -1290,10 +1299,27 @@ class TextSelectionGestureDetectorBuilder {
1290
1299
|| kind == PointerDeviceKind .touch
1291
1300
|| kind == PointerDeviceKind .stylus;
1292
1301
1293
- renderEditable.selectPositionAt (
1294
- from: details.globalPosition,
1295
- cause: SelectionChangedCause .drag,
1296
- );
1302
+ if (_isShiftPressed && renderEditable.selection != null && renderEditable.selection! .isValid) {
1303
+ _isShiftTapping = true ;
1304
+ switch (defaultTargetPlatform) {
1305
+ case TargetPlatform .iOS:
1306
+ case TargetPlatform .macOS:
1307
+ _expandSelection (details.globalPosition, SelectionChangedCause .drag);
1308
+ break ;
1309
+ case TargetPlatform .android:
1310
+ case TargetPlatform .fuchsia:
1311
+ case TargetPlatform .linux:
1312
+ case TargetPlatform .windows:
1313
+ _extendSelection (details.globalPosition, SelectionChangedCause .drag);
1314
+ break ;
1315
+ }
1316
+ _shiftTapDragSelection = renderEditable.selection;
1317
+ } else {
1318
+ renderEditable.selectPositionAt (
1319
+ from: details.globalPosition,
1320
+ cause: SelectionChangedCause .drag,
1321
+ );
1322
+ }
1297
1323
1298
1324
_dragStartViewportOffset = renderEditable.offset.pixels;
1299
1325
}
@@ -1312,28 +1338,77 @@ class TextSelectionGestureDetectorBuilder {
1312
1338
if (! delegate.selectionEnabled)
1313
1339
return ;
1314
1340
1315
- // Adjust the drag start offset for possible viewport offset changes.
1316
- final Offset startOffset = renderEditable.maxLines == 1
1317
- ? Offset (renderEditable.offset.pixels - _dragStartViewportOffset, 0.0 )
1318
- : Offset (0.0 , renderEditable.offset.pixels - _dragStartViewportOffset);
1341
+ if (! _isShiftTapping) {
1342
+ // Adjust the drag start offset for possible viewport offset changes.
1343
+ final Offset startOffset = renderEditable.maxLines == 1
1344
+ ? Offset (renderEditable.offset.pixels - _dragStartViewportOffset, 0.0 )
1345
+ : Offset (0.0 , renderEditable.offset.pixels - _dragStartViewportOffset);
1319
1346
1320
- renderEditable.selectPositionAt (
1321
- from: startDetails.globalPosition - startOffset,
1322
- to: updateDetails.globalPosition,
1323
- cause: SelectionChangedCause .drag,
1324
- );
1347
+ return renderEditable.selectPositionAt (
1348
+ from: startDetails.globalPosition - startOffset,
1349
+ to: updateDetails.globalPosition,
1350
+ cause: SelectionChangedCause .drag,
1351
+ );
1352
+ }
1353
+
1354
+ if (_shiftTapDragSelection! .isCollapsed
1355
+ || (defaultTargetPlatform != TargetPlatform .iOS
1356
+ && defaultTargetPlatform != TargetPlatform .macOS)) {
1357
+ return _extendSelection (updateDetails.globalPosition, SelectionChangedCause .drag);
1358
+ }
1359
+
1360
+ // If the drag inverts the selection, Mac and iOS revert to the initial
1361
+ // selection.
1362
+ final TextSelection selection = editableText.textEditingValue.selection;
1363
+ final TextPosition nextExtent = renderEditable.getPositionForPoint (updateDetails.globalPosition);
1364
+ final bool isShiftTapDragSelectionForward =
1365
+ _shiftTapDragSelection! .baseOffset < _shiftTapDragSelection! .extentOffset;
1366
+ final bool isInverted = isShiftTapDragSelectionForward
1367
+ ? nextExtent.offset < _shiftTapDragSelection! .baseOffset
1368
+ : nextExtent.offset > _shiftTapDragSelection! .baseOffset;
1369
+ if (isInverted && selection.baseOffset == _shiftTapDragSelection! .baseOffset) {
1370
+ editableText.userUpdateTextEditingValue (
1371
+ editableText.textEditingValue.copyWith (
1372
+ selection: TextSelection (
1373
+ baseOffset: _shiftTapDragSelection! .extentOffset,
1374
+ extentOffset: nextExtent.offset,
1375
+ ),
1376
+ ),
1377
+ SelectionChangedCause .drag,
1378
+ );
1379
+ } else if (! isInverted
1380
+ && nextExtent.offset != _shiftTapDragSelection! .baseOffset
1381
+ && selection.baseOffset != _shiftTapDragSelection! .baseOffset) {
1382
+ editableText.userUpdateTextEditingValue (
1383
+ editableText.textEditingValue.copyWith (
1384
+ selection: TextSelection (
1385
+ baseOffset: _shiftTapDragSelection! .baseOffset,
1386
+ extentOffset: nextExtent.offset,
1387
+ ),
1388
+ ),
1389
+ SelectionChangedCause .drag,
1390
+ );
1391
+ } else {
1392
+ _extendSelection (updateDetails.globalPosition, SelectionChangedCause .drag);
1393
+ }
1325
1394
}
1326
1395
1327
1396
/// Handler for [TextSelectionGestureDetector.onDragSelectionEnd] .
1328
1397
///
1329
- /// By default, it services as place holder to enable subclass override.
1398
+ /// By default, it simply cleans up the state used for handling certain
1399
+ /// built-in behaviors.
1330
1400
///
1331
1401
/// See also:
1332
1402
///
1333
1403
/// * [TextSelectionGestureDetector.onDragSelectionEnd] , which triggers this
1334
1404
/// callback.
1335
1405
@protected
1336
- void onDragSelectionEnd (DragEndDetails details) {/* Subclass should override this method if needed. */ }
1406
+ void onDragSelectionEnd (DragEndDetails details) {
1407
+ if (_isShiftTapping) {
1408
+ _isShiftTapping = false ;
1409
+ _shiftTapDragSelection = null ;
1410
+ }
1411
+ }
1337
1412
1338
1413
/// Returns a [TextSelectionGestureDetector] configured with the handlers
1339
1414
/// provided by this builder.
0 commit comments