Unverified Commit 8b46014e authored by Justin McCandless's avatar Justin McCandless Committed by GitHub

Mac cmd + shift + left/right (#95948)

parent 06515fe0
......@@ -238,8 +238,8 @@ class TextSelection extends TextRange {
/// [TextSelection.extentOffset] to the given [TextPosition].
///
/// In some cases, the [TextSelection.baseOffset] and
/// [TextSelection.extentOffset] may flip during this operation, or the size
/// of the selection may shrink.
/// [TextSelection.extentOffset] may flip during this operation, and/or the
/// size of the selection may shrink.
///
/// ## Difference with [expandTo]
/// In contrast with this method, [expandTo] is strictly growth; the
......
......@@ -307,8 +307,8 @@ class DefaultTextEditingShortcuts extends Shortcuts {
const SingleActivator(LogicalKeyboardKey.arrowUp, meta: true): const ExtendSelectionToDocumentBoundaryIntent(forward: false, collapseSelection: true),
const SingleActivator(LogicalKeyboardKey.arrowDown, meta: true): const ExtendSelectionToDocumentBoundaryIntent(forward: true, collapseSelection: true),
const SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true, meta: true): const ExtendSelectionToLineBreakIntent(forward: false, collapseSelection: false),
const SingleActivator(LogicalKeyboardKey.arrowRight, shift: true, meta: true): const ExtendSelectionToLineBreakIntent(forward: true, collapseSelection: false),
const SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true, meta: true): const ExpandSelectionToLineBreakIntent(forward: false),
const SingleActivator(LogicalKeyboardKey.arrowRight, shift: true, meta: true): const ExpandSelectionToLineBreakIntent(forward: true),
const SingleActivator(LogicalKeyboardKey.arrowUp, shift: true, meta: true): const ExtendSelectionToDocumentBoundaryIntent(forward: false, collapseSelection: false),
const SingleActivator(LogicalKeyboardKey.arrowDown, shift: true, meta: true): const ExtendSelectionToDocumentBoundaryIntent(forward: true, collapseSelection: false),
......
......@@ -2931,6 +2931,30 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
late final _UpdateTextSelectionToAdjacentLineAction<ExtendSelectionVerticallyToAdjacentLineIntent> _adjacentLineAction = _UpdateTextSelectionToAdjacentLineAction<ExtendSelectionVerticallyToAdjacentLineIntent>(this);
void _expandSelection(ExpandSelectionToLineBreakIntent intent) {
final _TextBoundary textBoundary = _linebreak(intent);
final TextSelection textBoundarySelection = textBoundary.textEditingValue.selection;
if (!textBoundarySelection.isValid) {
return;
}
final bool inOrder = textBoundarySelection.baseOffset <= textBoundarySelection.extentOffset;
final bool towardsExtent = intent.forward == inOrder;
final TextPosition position = towardsExtent
? textBoundarySelection.extent
: textBoundarySelection.base;
final TextPosition newExtent = intent.forward
? textBoundary.getTrailingTextBoundaryAt(position)
: textBoundary.getLeadingTextBoundaryAt(position);
final TextSelection newSelection = textBoundarySelection.expandTo(newExtent, textBoundarySelection.isCollapsed);
userUpdateTextEditingValue(
_value.copyWith(selection: newSelection),
SelectionChangedCause.keyboard,
);
}
late final Map<Type, Action<Intent>> _actions = <Type, Action<Intent>>{
DoNothingAndStopPropagationTextIntent: DoNothingAction(consumesKey: false),
ReplaceTextIntent: _replaceTextAction,
......@@ -2946,6 +2970,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
ExtendSelectionByCharacterIntent: _makeOverridable(_UpdateTextSelectionAction<ExtendSelectionByCharacterIntent>(this, false, _characterBoundary,)),
ExtendSelectionToNextWordBoundaryIntent: _makeOverridable(_UpdateTextSelectionAction<ExtendSelectionToNextWordBoundaryIntent>(this, true, _nextWordBoundary)),
ExtendSelectionToLineBreakIntent: _makeOverridable(_UpdateTextSelectionAction<ExtendSelectionToLineBreakIntent>(this, true, _linebreak)),
ExpandSelectionToLineBreakIntent: _makeOverridable(CallbackAction<ExpandSelectionToLineBreakIntent>(onInvoke: _expandSelection)),
ExtendSelectionVerticallyToAdjacentLineIntent: _makeOverridable(_adjacentLineAction),
ExtendSelectionToDocumentBoundaryIntent: _makeOverridable(_UpdateTextSelectionAction<ExtendSelectionToDocumentBoundaryIntent>(this, true, _documentBoundary)),
ExtendSelectionToNextWordBoundaryOrCaretLocationIntent: _makeOverridable(_ExtendSelectionOrCaretPositionAction(this, _nextWordBoundary)),
......
......@@ -92,7 +92,7 @@ abstract class DirectionalCaretMovementIntent extends DirectionalTextEditingInte
final bool collapseAtReversal;
}
/// Expands, or moves the current selection from the current
/// Extends, or moves the current selection from the current
/// [TextSelection.extent] position to the previous or the next character
/// boundary.
class ExtendSelectionByCharacterIntent extends DirectionalCaretMovementIntent {
......@@ -103,7 +103,7 @@ class ExtendSelectionByCharacterIntent extends DirectionalCaretMovementIntent {
}) : super(forward, collapseSelection);
}
/// Expands, or moves the current selection from the current
/// Extends, or moves the current selection from the current
/// [TextSelection.extent] position to the previous or the next word
/// boundary.
class ExtendSelectionToNextWordBoundaryIntent extends DirectionalCaretMovementIntent {
......@@ -114,7 +114,7 @@ class ExtendSelectionToNextWordBoundaryIntent extends DirectionalCaretMovementIn
}) : super(forward, collapseSelection);
}
/// Expands, or moves the current selection from the current
/// Extends, or moves the current selection from the current
/// [TextSelection.extent] position to the previous or the next word
/// boundary, or the [TextSelection.base] position if it's closer in the move
/// direction.
......@@ -124,7 +124,7 @@ class ExtendSelectionToNextWordBoundaryIntent extends DirectionalCaretMovementIn
/// when the order of [TextSelection.base] and [TextSelection.extent] would
/// reverse.
///
/// This is typically only used on macOS.
/// This is typically only used on MacOS.
class ExtendSelectionToNextWordBoundaryOrCaretLocationIntent extends DirectionalTextEditingIntent {
/// Creates an [ExtendSelectionToNextWordBoundaryOrCaretLocationIntent].
const ExtendSelectionToNextWordBoundaryOrCaretLocationIntent({
......@@ -132,9 +132,33 @@ class ExtendSelectionToNextWordBoundaryOrCaretLocationIntent extends Directional
}) : super(forward);
}
/// Expands, or moves the current selection from the current
/// Expands the current selection to the closest line break in the direction
/// given by [forward].
///
/// Either the base or extent can move, whichever is closer to the line break.
/// The selection will never shrink.
///
/// This behavior is common on MacOS.
///
/// See also:
///
/// [ExtendSelectionToLineBreakIntent], which is similar but always moves the
/// extent.
class ExpandSelectionToLineBreakIntent extends DirectionalTextEditingIntent {
/// Creates an [ExpandSelectionToLineBreakIntent].
const ExpandSelectionToLineBreakIntent({
required bool forward,
}) : super(forward);
}
/// Extends, or moves the current selection from the current
/// [TextSelection.extent] position to the closest line break in the direction
/// given by [forward].
///
/// See also:
///
/// [ExpandSelectionToLineBreakIntent], which is similar but always increases
/// the size of the selection.
class ExtendSelectionToLineBreakIntent extends DirectionalCaretMovementIntent {
/// Creates an [ExtendSelectionToLineBreakIntent].
const ExtendSelectionToLineBreakIntent({
......@@ -145,7 +169,7 @@ class ExtendSelectionToLineBreakIntent extends DirectionalCaretMovementIntent {
super(forward, collapseSelection, collapseAtReversal);
}
/// Expands, or moves the current selection from the current
/// Extends, or moves the current selection from the current
/// [TextSelection.extent] position to the closest position on the adjacent
/// line.
class ExtendSelectionVerticallyToAdjacentLineIntent extends DirectionalCaretMovementIntent {
......@@ -156,7 +180,7 @@ class ExtendSelectionVerticallyToAdjacentLineIntent extends DirectionalCaretMove
}) : super(forward, collapseSelection);
}
/// Expands, or moves the current selection from the current
/// Extends, or moves the current selection from the current
/// [TextSelection.extent] position to the start or the end of the document.
class ExtendSelectionToDocumentBoundaryIntent extends DirectionalCaretMovementIntent {
/// Creates an [ExtendSelectionToDocumentBoundaryIntent].
......
......@@ -4682,6 +4682,13 @@ void main() {
targetPlatform: defaultTargetPlatform,
);
switch (defaultTargetPlatform) {
// These platforms extend by line.
case TargetPlatform.iOS:
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
expect(
selection,
equals(
......@@ -4693,6 +4700,23 @@ void main() {
),
reason: 'on $platform',
);
break;
// Mac expands by line.
case TargetPlatform.macOS:
expect(
selection,
equals(
const TextSelection(
baseOffset: 20,
extentOffset: 54,
affinity: TextAffinity.upstream,
),
),
reason: 'on $platform',
);
break;
}
// Select All
await sendKeys(
......@@ -8502,7 +8526,7 @@ void main() {
expect(controller.selection.baseOffset, 17);
expect(controller.selection.extentOffset, 24);
// Multiple expandLeftByLine shortcuts only move ot the start of the line
// Multiple expandLeftByLine shortcuts only move to the start of the line
// and not to the previous line.
await sendKeys(
tester,
......@@ -8516,8 +8540,23 @@ void main() {
targetPlatform: defaultTargetPlatform,
);
expect(controller.selection.isCollapsed, false);
switch (defaultTargetPlatform) {
// These platforms extend by line.
case TargetPlatform.iOS:
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
expect(controller.selection.baseOffset, 17);
expect(controller.selection.extentOffset, 15);
break;
// Mac expands by line.
case TargetPlatform.macOS:
expect(controller.selection.baseOffset, 15);
expect(controller.selection.extentOffset, 24);
break;
}
// Set the caret to the end of a line.
controller.selection = const TextSelection(
......@@ -8599,6 +8638,343 @@ void main() {
// On web, using keyboard for selection is handled by the browser.
}, variant: TargetPlatformVariant.all(), skip: kIsWeb); // [intended]
testWidgets("Mac's expand by line behavior on multiple lines", (WidgetTester tester) async {
const String multilineText = 'word word word\nword word\nword'; // 15 + 10 + 4;
final TextEditingController controller = TextEditingController(text: multilineText);
// word word word
// wo|rd word
// w|ord
controller.selection = const TextSelection(
baseOffset: 17,
extentOffset: 26,
affinity: TextAffinity.upstream,
);
await tester.pumpWidget(MaterialApp(
home: Align(
alignment: Alignment.topLeft,
child: SizedBox(
width: 400,
child: EditableText(
maxLines: 10,
controller: controller,
autofocus: true,
focusNode: focusNode,
style: Typography.material2018().black.subtitle1!,
cursorColor: Colors.blue,
backgroundCursorColor: Colors.grey,
keyboardType: TextInputType.text,
),
),
),
));
await tester.pump(); // Wait for autofocus to take effect.
expect(controller.selection.isCollapsed, false);
expect(controller.selection.baseOffset, 17);
expect(controller.selection.extentOffset, 26);
// Expanding right to the end of the line moves the extent on the second
// selected line.
await sendKeys(
tester,
<LogicalKeyboardKey>[
LogicalKeyboardKey.arrowRight,
],
shift: true,
lineModifier: true,
targetPlatform: defaultTargetPlatform,
);
expect(controller.selection.isCollapsed, false);
expect(controller.selection.baseOffset, 17);
expect(controller.selection.extentOffset, 29);
// Expanding right again does nothing.
await sendKeys(
tester,
<LogicalKeyboardKey>[
LogicalKeyboardKey.arrowRight,
LogicalKeyboardKey.arrowRight,
LogicalKeyboardKey.arrowRight,
],
shift: true,
lineModifier: true,
targetPlatform: defaultTargetPlatform,
);
expect(controller.selection.isCollapsed, false);
expect(controller.selection.baseOffset, 17);
expect(controller.selection.extentOffset, 29);
// Expanding left by line moves the base on the first selected line to the
// beginning of that line.
await sendKeys(
tester,
<LogicalKeyboardKey>[
LogicalKeyboardKey.arrowLeft,
],
shift: true,
lineModifier: true,
targetPlatform: defaultTargetPlatform,
);
expect(controller.selection.isCollapsed, false);
expect(controller.selection.baseOffset, 15);
expect(controller.selection.extentOffset, 29);
// Expanding left again does nothing.
await sendKeys(
tester,
<LogicalKeyboardKey>[
LogicalKeyboardKey.arrowLeft,
LogicalKeyboardKey.arrowLeft,
LogicalKeyboardKey.arrowLeft,
],
shift: true,
lineModifier: true,
targetPlatform: defaultTargetPlatform,
);
expect(controller.selection.isCollapsed, false);
expect(controller.selection.baseOffset, 15);
expect(controller.selection.extentOffset, 29);
},
// On web, using keyboard for selection is handled by the browser.
skip: kIsWeb, // [intended]
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.macOS })
);
testWidgets("Mac's expand extent position", (WidgetTester tester) async {
const String testText = 'Now is the time for all good people to come to the aid of their country';
final TextEditingController controller = TextEditingController(text: testText);
// Start the selection in the middle somewhere.
controller.selection = const TextSelection.collapsed(offset: 10);
await tester.pumpWidget(MaterialApp(
home: Align(
alignment: Alignment.topLeft,
child: SizedBox(
width: 400,
child: EditableText(
maxLines: 10,
controller: controller,
autofocus: true,
focusNode: focusNode,
style: Typography.material2018().black.subtitle1!,
cursorColor: Colors.blue,
backgroundCursorColor: Colors.grey,
keyboardType: TextInputType.text,
),
),
),
));
await tester.pump(); // Wait for autofocus to take effect.
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 10);
// With cursor in the middle of the line, cmd + left. Left end is the extent.
await sendKeys(
tester,
<LogicalKeyboardKey>[
LogicalKeyboardKey.arrowLeft,
],
lineModifier: true,
shift: true,
targetPlatform: defaultTargetPlatform,
);
expect(
controller.selection,
equals(
const TextSelection(
baseOffset: 10,
extentOffset: 0,
affinity: TextAffinity.upstream,
),
),
);
// With cursor in the middle of the line, cmd + right. Right end is the extent.
controller.selection = const TextSelection.collapsed(offset: 10);
await tester.pump();
await sendKeys(
tester,
<LogicalKeyboardKey>[
LogicalKeyboardKey.arrowRight,
],
lineModifier: true,
shift: true,
targetPlatform: defaultTargetPlatform,
);
expect(
controller.selection,
equals(
const TextSelection(
baseOffset: 10,
extentOffset: 29,
affinity: TextAffinity.upstream,
),
),
);
// With cursor in the middle of the line, cmd + left then cmd + right. Left end is the extent.
controller.selection = const TextSelection.collapsed(offset: 10);
await tester.pump();
await sendKeys(
tester,
<LogicalKeyboardKey>[
LogicalKeyboardKey.arrowLeft,
],
lineModifier: true,
shift: true,
targetPlatform: defaultTargetPlatform,
);
await tester.pump();
await sendKeys(
tester,
<LogicalKeyboardKey>[
LogicalKeyboardKey.arrowRight,
],
lineModifier: true,
shift: true,
targetPlatform: defaultTargetPlatform,
);
expect(
controller.selection,
equals(
const TextSelection(
baseOffset: 29,
extentOffset: 0,
affinity: TextAffinity.upstream,
),
),
);
// With cursor in the middle of the line, cmd + right then cmd + left. Right end is the extent.
controller.selection = const TextSelection.collapsed(offset: 10);
await tester.pump();
await sendKeys(
tester,
<LogicalKeyboardKey>[
LogicalKeyboardKey.arrowRight,
],
lineModifier: true,
shift: true,
targetPlatform: defaultTargetPlatform,
);
await tester.pump();
await sendKeys(
tester,
<LogicalKeyboardKey>[
LogicalKeyboardKey.arrowLeft,
],
lineModifier: true,
shift: true,
targetPlatform: defaultTargetPlatform,
);
expect(
controller.selection,
equals(
const TextSelection(
baseOffset: 0,
extentOffset: 29,
affinity: TextAffinity.upstream,
),
),
);
// With an RTL selection in the middle of the line, cmd + left. Left end is the extent.
controller.selection = const TextSelection(baseOffset: 12, extentOffset: 8);
await tester.pump();
await sendKeys(
tester,
<LogicalKeyboardKey>[
LogicalKeyboardKey.arrowLeft,
],
lineModifier: true,
shift: true,
targetPlatform: defaultTargetPlatform,
);
expect(
controller.selection,
equals(
const TextSelection(
baseOffset: 12,
extentOffset: 0,
affinity: TextAffinity.upstream,
),
),
);
// With an RTL selection in the middle of the line, cmd + right. Left end is the extent.
controller.selection = const TextSelection(baseOffset: 12, extentOffset: 8);
await tester.pump();
await sendKeys(
tester,
<LogicalKeyboardKey>[
LogicalKeyboardKey.arrowRight,
],
lineModifier: true,
shift: true,
targetPlatform: defaultTargetPlatform,
);
expect(
controller.selection,
equals(
const TextSelection(
baseOffset: 29,
extentOffset: 8,
affinity: TextAffinity.upstream,
),
),
);
// With an LTR selection in the middle of the line, cmd + right. Right end is the extent.
controller.selection = const TextSelection(baseOffset: 8, extentOffset: 12);
await tester.pump();
await sendKeys(
tester,
<LogicalKeyboardKey>[
LogicalKeyboardKey.arrowRight,
],
lineModifier: true,
shift: true,
targetPlatform: defaultTargetPlatform,
);
expect(
controller.selection,
equals(
const TextSelection(
baseOffset: 8,
extentOffset: 29,
affinity: TextAffinity.upstream,
),
),
);
// With an LTR selection in the middle of the line, cmd + left. Right end is the extent.
controller.selection = const TextSelection(baseOffset: 8, extentOffset: 12);
await tester.pump();
await sendKeys(
tester,
<LogicalKeyboardKey>[
LogicalKeyboardKey.arrowLeft,
],
lineModifier: true,
shift: true,
targetPlatform: defaultTargetPlatform,
);
expect(
controller.selection,
equals(
const TextSelection(
baseOffset: 0,
extentOffset: 12,
affinity: TextAffinity.upstream,
),
),
);
},
// On web, using keyboard for selection is handled by the browser.
skip: kIsWeb, // [intended]
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.macOS })
);
testWidgets('expanding selection to start/end', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(text: 'word word word');
// word wo|rd| word
......
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