Unverified Commit c42b36f6 authored by Justin McCandless's avatar Justin McCandless Committed by GitHub

Windows/Linux keyboard shortcuts at a wordwrap (#96323)

parent ac708f1c
......@@ -317,8 +317,10 @@ class DefaultTextEditingShortcuts extends Shortcuts {
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),
const SingleActivator(LogicalKeyboardKey.home, shift: true): const ExtendSelectionToDocumentBoundaryIntent(forward: false, collapseSelection: false),
const SingleActivator(LogicalKeyboardKey.end, shift: true): const ExtendSelectionToDocumentBoundaryIntent(forward: true, collapseSelection: false),
const SingleActivator(LogicalKeyboardKey.home): const ScrollToDocumentBoundaryIntent(forward: false),
const SingleActivator(LogicalKeyboardKey.end): const ScrollToDocumentBoundaryIntent(forward: true),
const SingleActivator(LogicalKeyboardKey.home, shift: true): const ExpandSelectionToDocumentBoundaryIntent(forward: false),
const SingleActivator(LogicalKeyboardKey.end, shift: true): const ExpandSelectionToDocumentBoundaryIntent(forward: true),
const SingleActivator(LogicalKeyboardKey.keyX, meta: true): const CopySelectionTextIntent.cut(SelectionChangedCause.keyboard),
const SingleActivator(LogicalKeyboardKey.keyC, meta: true): CopySelectionTextIntent.copy,
......@@ -349,10 +351,10 @@ class DefaultTextEditingShortcuts extends Shortcuts {
// * Meta + backspace
static final Map<ShortcutActivator, Intent> _windowsShortcuts = <ShortcutActivator, Intent>{
..._commonShortcuts,
const SingleActivator(LogicalKeyboardKey.home): const ExtendSelectionToLineBreakIntent(forward: false, collapseSelection: true),
const SingleActivator(LogicalKeyboardKey.end): const ExtendSelectionToLineBreakIntent(forward: true, collapseSelection: true),
const SingleActivator(LogicalKeyboardKey.home, shift: true): const ExtendSelectionToLineBreakIntent(forward: false, collapseSelection: false),
const SingleActivator(LogicalKeyboardKey.end, shift: true): const ExtendSelectionToLineBreakIntent(forward: true, collapseSelection: false),
const SingleActivator(LogicalKeyboardKey.home): const ExtendSelectionToLineBreakIntent(forward: false, collapseSelection: true, continuesAtWrap: true),
const SingleActivator(LogicalKeyboardKey.end): const ExtendSelectionToLineBreakIntent(forward: true, collapseSelection: true, continuesAtWrap: true),
const SingleActivator(LogicalKeyboardKey.home, shift: true): const ExtendSelectionToLineBreakIntent(forward: false, collapseSelection: false, continuesAtWrap: true),
const SingleActivator(LogicalKeyboardKey.end, shift: true): const ExtendSelectionToLineBreakIntent(forward: true, collapseSelection: false, continuesAtWrap: true),
const SingleActivator(LogicalKeyboardKey.home, control: true): const ExtendSelectionToDocumentBoundaryIntent(forward: false, collapseSelection: true),
const SingleActivator(LogicalKeyboardKey.end, control: true): const ExtendSelectionToDocumentBoundaryIntent(forward: true, collapseSelection: true),
const SingleActivator(LogicalKeyboardKey.home, shift: true, control: true): const ExtendSelectionToDocumentBoundaryIntent(forward: false, collapseSelection: false),
......
......@@ -3069,7 +3069,18 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
}
late final Action<ReplaceTextIntent> _replaceTextAction = CallbackAction<ReplaceTextIntent>(onInvoke: _replaceText);
// Scrolls either to the beginning or end of the document depending on the
// intent's `forward` parameter.
void _scrollToDocumentBoundary(ScrollToDocumentBoundaryIntent intent) {
if (intent.forward) {
bringIntoView(TextPosition(offset: _value.text.length));
} else {
bringIntoView(const TextPosition(offset: 0));
}
}
void _updateSelection(UpdateSelectionIntent intent) {
bringIntoView(intent.newSelection.extent);
userUpdateTextEditingValue(
intent.currentTextEditingValue.copyWith(selection: intent.newSelection),
intent.cause,
......@@ -3079,28 +3090,38 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
late final _UpdateTextSelectionToAdjacentLineAction<ExtendSelectionVerticallyToAdjacentLineIntent> _adjacentLineAction = _UpdateTextSelectionToAdjacentLineAction<ExtendSelectionVerticallyToAdjacentLineIntent>(this);
void _expandSelection(ExpandSelectionToLineBreakIntent intent) {
void _expandSelectionToDocumentBoundary(ExpandSelectionToDocumentBoundaryIntent intent) {
final _TextBoundary textBoundary = _documentBoundary(intent);
_expandSelection(intent.forward, textBoundary, true);
}
void _expandSelectionToLinebreak(ExpandSelectionToLineBreakIntent intent) {
final _TextBoundary textBoundary = _linebreak(intent);
_expandSelection(intent.forward, textBoundary);
}
void _expandSelection(bool forward, _TextBoundary textBoundary, [bool extentAtIndex = false]) {
final TextSelection textBoundarySelection = textBoundary.textEditingValue.selection;
if (!textBoundarySelection.isValid) {
return;
}
final bool inOrder = textBoundarySelection.baseOffset <= textBoundarySelection.extentOffset;
final bool towardsExtent = intent.forward == inOrder;
final bool towardsExtent = forward == inOrder;
final TextPosition position = towardsExtent
? textBoundarySelection.extent
: textBoundarySelection.base;
final TextPosition newExtent = intent.forward
final TextPosition newExtent = forward
? textBoundary.getTrailingTextBoundaryAt(position)
: textBoundary.getLeadingTextBoundaryAt(position);
final TextSelection newSelection = textBoundarySelection.expandTo(newExtent, textBoundarySelection.isCollapsed);
final TextSelection newSelection = textBoundarySelection.expandTo(newExtent, textBoundarySelection.isCollapsed || extentAtIndex);
userUpdateTextEditingValue(
_value.copyWith(selection: newSelection),
SelectionChangedCause.keyboard,
);
bringIntoView(newSelection.extent);
}
late final Map<Type, Action<Intent>> _actions = <Type, Action<Intent>>{
......@@ -3118,10 +3139,12 @@ 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)),
ExpandSelectionToLineBreakIntent: _makeOverridable(CallbackAction<ExpandSelectionToLineBreakIntent>(onInvoke: _expandSelectionToLinebreak)),
ExpandSelectionToDocumentBoundaryIntent: _makeOverridable(CallbackAction<ExpandSelectionToDocumentBoundaryIntent>(onInvoke: _expandSelectionToDocumentBoundary)),
ExtendSelectionVerticallyToAdjacentLineIntent: _makeOverridable(_adjacentLineAction),
ExtendSelectionToDocumentBoundaryIntent: _makeOverridable(_UpdateTextSelectionAction<ExtendSelectionToDocumentBoundaryIntent>(this, true, _documentBoundary)),
ExtendSelectionToNextWordBoundaryOrCaretLocationIntent: _makeOverridable(_ExtendSelectionOrCaretPositionAction(this, _nextWordBoundary)),
ScrollToDocumentBoundaryIntent: _makeOverridable(CallbackAction<ScrollToDocumentBoundaryIntent>(onInvoke: _scrollToDocumentBoundary)),
// Copy Paste
SelectAllTextIntent: _makeOverridable(_SelectAllAction(this)),
......@@ -3763,7 +3786,10 @@ class _WordBoundary extends _TextBoundary {
// interpreted as caret locations because [TextPainter.getLineAtOffset] is
// text-affinity-aware.
class _LineBreak extends _TextBoundary {
const _LineBreak(this.textLayout, this.textEditingValue);
const _LineBreak(
this.textLayout,
this.textEditingValue,
);
final TextLayoutMetrics textLayout;
......@@ -3776,6 +3802,7 @@ class _LineBreak extends _TextBoundary {
offset: textLayout.getLineAtOffset(position).start,
);
}
@override
TextPosition getTrailingTextBoundaryAt(TextPosition position) {
return TextPosition(
......@@ -3945,12 +3972,39 @@ class _DeleteTextAction<T extends DirectionalTextEditingIntent> extends ContextA
}
class _UpdateTextSelectionAction<T extends DirectionalCaretMovementIntent> extends ContextAction<T> {
_UpdateTextSelectionAction(this.state, this.ignoreNonCollapsedSelection, this.getTextBoundariesForIntent);
_UpdateTextSelectionAction(
this.state,
this.ignoreNonCollapsedSelection,
this.getTextBoundariesForIntent,
);
final EditableTextState state;
final bool ignoreNonCollapsedSelection;
final _TextBoundary Function(T intent) getTextBoundariesForIntent;
static const int NEWLINE_CODE_UNIT = 10;
// Returns true iff the given position is at a wordwrap boundary in the
// upstream position.
bool _isAtWordwrapUpstream(TextPosition position) {
final TextPosition end = TextPosition(
offset: state.renderEditable.getLineAtOffset(position).end,
affinity: TextAffinity.upstream,
);
return end == position && end.offset != state.textEditingValue.text.length
&& state.textEditingValue.text.codeUnitAt(position.offset) != NEWLINE_CODE_UNIT;
}
// Returns true iff the given position at a wordwrap boundary in the
// downstream position.
bool _isAtWordwrapDownstream(TextPosition position) {
final TextPosition start = TextPosition(
offset: state.renderEditable.getLineAtOffset(position).start,
);
return start == position && start.offset != 0
&& state.textEditingValue.text.codeUnitAt(position.offset - 1) != NEWLINE_CODE_UNIT;
}
@override
Object? invoke(T intent, [BuildContext? context]) {
final TextSelection selection = state._value.selection;
......@@ -3986,7 +4040,23 @@ class _UpdateTextSelectionAction<T extends DirectionalCaretMovementIntent> exten
);
}
final TextPosition extent = textBoundarySelection.extent;
TextPosition extent = textBoundarySelection.extent;
// If continuesAtWrap is true extent and is at the relevant wordwrap, then
// move it just to the other side of the wordwrap.
if (intent.continuesAtWrap) {
if (intent.forward && _isAtWordwrapUpstream(extent)) {
extent = TextPosition(
offset: extent.offset,
);
} else if (!intent.forward && _isAtWordwrapDownstream(extent)) {
extent = TextPosition(
offset: extent.offset,
affinity: TextAffinity.upstream,
);
}
}
final TextPosition newExtent = intent.forward
? textBoundary.getTrailingTextBoundaryAt(extent)
: textBoundary.getLeadingTextBoundaryAt(extent);
......
......@@ -20,7 +20,9 @@ class DoNothingAndStopPropagationTextIntent extends Intent {
/// direction of the current caret location.
abstract class DirectionalTextEditingIntent extends Intent {
/// Creates a [DirectionalTextEditingIntent].
const DirectionalTextEditingIntent(this.forward);
const DirectionalTextEditingIntent(
this.forward,
);
/// Whether the input field, if applicable, should perform the text editing
/// operation from the current caret location towards the end of the document.
......@@ -65,7 +67,10 @@ abstract class DirectionalCaretMovementIntent extends DirectionalTextEditingInte
const DirectionalCaretMovementIntent(
bool forward,
this.collapseSelection,
[this.collapseAtReversal = false]
[
this.collapseAtReversal = false,
this.continuesAtWrap = false,
]
) : assert(!collapseSelection || !collapseAtReversal),
super(forward);
......@@ -90,6 +95,14 @@ abstract class DirectionalCaretMovementIntent extends DirectionalTextEditingInte
///
/// Cannot be true when collapseSelection is true.
final bool collapseAtReversal;
/// Whether or not to continue to the next line at a wordwrap.
///
/// If true, when an [Intent] to go to the beginning/end of a wordwrapped line
/// is received and the selection is already at the beginning/end of the line,
/// then the selection will be moved to the next/previous line. If false, the
/// selection will remain at the wordwrap.
final bool continuesAtWrap;
}
/// Extends, or moves the current selection from the current
......@@ -132,6 +145,23 @@ class ExtendSelectionToNextWordBoundaryOrCaretLocationIntent extends Directional
}) : super(forward);
}
/// Expands the current selection to the document boundary in the direction
/// given by [forward].
///
/// Unlike [ExpandSelectionToLineBreakIntent], the extent will be moved, which
/// matches the behavior on MacOS.
///
/// See also:
///
/// [ExtendSelectionToDocumentBoundaryIntent], which is similar but always
/// moves the extent.
class ExpandSelectionToDocumentBoundaryIntent extends DirectionalTextEditingIntent {
/// Creates an [ExpandSelectionToDocumentBoundaryIntent].
const ExpandSelectionToDocumentBoundaryIntent({
required bool forward,
}) : super(forward);
}
/// Expands the current selection to the closest line break in the direction
/// given by [forward].
///
......@@ -165,8 +195,9 @@ class ExtendSelectionToLineBreakIntent extends DirectionalCaretMovementIntent {
required bool forward,
required bool collapseSelection,
bool collapseAtReversal = false,
bool continuesAtWrap = false,
}) : assert(!collapseSelection || !collapseAtReversal),
super(forward, collapseSelection, collapseAtReversal);
super(forward, collapseSelection, collapseAtReversal, continuesAtWrap);
}
/// Extends, or moves the current selection from the current
......@@ -182,6 +213,11 @@ class ExtendSelectionVerticallyToAdjacentLineIntent extends DirectionalCaretMove
/// Extends, or moves the current selection from the current
/// [TextSelection.extent] position to the start or the end of the document.
///
/// See also:
///
/// [ExtendSelectionToDocumentBoundaryIntent], which is similar but always
/// increases the size of the selection.
class ExtendSelectionToDocumentBoundaryIntent extends DirectionalCaretMovementIntent {
/// Creates an [ExtendSelectionToDocumentBoundaryIntent].
const ExtendSelectionToDocumentBoundaryIntent({
......@@ -190,6 +226,15 @@ class ExtendSelectionToDocumentBoundaryIntent extends DirectionalCaretMovementIn
}) : super(forward, collapseSelection);
}
/// Scrolls to the beginning or end of the document depending on the [forward]
/// parameter.
class ScrollToDocumentBoundaryIntent extends DirectionalTextEditingIntent {
/// Creates a [ScrollToDocumentBoundaryIntent].
const ScrollToDocumentBoundaryIntent({
required bool forward,
}) : super(forward);
}
/// An [Intent] to select everything in the field.
class SelectAllTextIntent extends Intent {
/// Creates an instance of [SelectAllTextIntent].
......
......@@ -5934,67 +5934,976 @@ void main() {
targetPlatform: defaultTargetPlatform,
);
final int afterHomeOffset;
final int afterEndOffset;
final TextAffinity afterEndAffinity;
switch (defaultTargetPlatform) {
// These platforms don't handle home/end at all.
// These platforms don't move the selection with home/end at all.
case TargetPlatform.android:
case TargetPlatform.iOS:
case TargetPlatform.fuchsia:
case TargetPlatform.macOS:
afterHomeOffset = 23;
afterEndOffset = 23;
afterEndAffinity = TextAffinity.downstream;
expect(
selection,
equals(
const TextSelection.collapsed(
offset: 23,
),
),
reason: 'on $platform',
);
break;
// These platforms go to the line start/end.
case TargetPlatform.linux:
case TargetPlatform.windows:
expect(
selection,
equals(
const TextSelection.collapsed(
offset: 20,
),
),
reason: 'on $platform',
);
break;
}
expect(controller.text, equals(testText), reason: 'on $platform');
await sendKeys(
tester,
<LogicalKeyboardKey>[
LogicalKeyboardKey.end,
],
targetPlatform: defaultTargetPlatform,
);
switch (defaultTargetPlatform) {
// These platforms don't move the selection with home/end at all.
case TargetPlatform.android:
case TargetPlatform.iOS:
case TargetPlatform.fuchsia:
case TargetPlatform.macOS:
expect(
selection,
equals(
const TextSelection.collapsed(
offset: 23,
),
),
reason: 'on $platform',
);
break;
// These platforms go to the line start/end.
case TargetPlatform.linux:
case TargetPlatform.windows:
expect(
selection,
equals(
const TextSelection.collapsed(
offset: 35,
affinity: TextAffinity.upstream,
),
),
reason: 'on $platform',
);
break;
}
expect(controller.text, equals(testText), reason: 'on $platform');
},
skip: kIsWeb, // [intended] on web these keys are handled by the browser.
variant: TargetPlatformVariant.all(),
);
testWidgets('home keys and wordwraps', (WidgetTester tester) async {
final String targetPlatformString = defaultTargetPlatform.toString();
final String platform = targetPlatformString.substring(targetPlatformString.indexOf('.') + 1).toLowerCase();
const String testText = 'Now is the time for all good people to come to the aid of their country. Now is the time for all good people to come to the aid of their country.';
final TextEditingController controller = TextEditingController(text: testText);
controller.selection = const TextSelection(
baseOffset: 0,
extentOffset: 0,
affinity: TextAffinity.upstream,
);
late TextSelection selection;
late SelectionChangedCause cause;
await tester.pumpWidget(MaterialApp(
home: Align(
alignment: Alignment.topLeft,
child: SizedBox(
width: 400,
child: EditableText(
maxLines: 10,
controller: controller,
showSelectionHandles: true,
autofocus: true,
focusNode: FocusNode(),
style: Typography.material2018().black.subtitle1!,
cursorColor: Colors.blue,
backgroundCursorColor: Colors.grey,
selectionControls: materialTextSelectionControls,
keyboardType: TextInputType.text,
textAlign: TextAlign.right,
onSelectionChanged: (TextSelection newSelection, SelectionChangedCause? newCause) {
selection = newSelection;
cause = newCause!;
},
),
),
),
));
await tester.pump(); // Wait for autofocus to take effect.
// Move near the middle of the document.
await sendKeys(
tester,
<LogicalKeyboardKey>[
LogicalKeyboardKey.arrowDown,
LogicalKeyboardKey.arrowRight,
LogicalKeyboardKey.arrowRight,
LogicalKeyboardKey.arrowRight,
],
targetPlatform: defaultTargetPlatform,
);
expect(cause, equals(SelectionChangedCause.keyboard), reason: 'on $platform');
expect(
selection,
equals(
const TextSelection.collapsed(
offset: 32,
),
),
reason: 'on $platform',
);
await sendKeys(
tester,
<LogicalKeyboardKey>[
LogicalKeyboardKey.home,
],
targetPlatform: defaultTargetPlatform,
);
switch (defaultTargetPlatform) {
// These platforms don't move the selection with home/end at all.
case TargetPlatform.android:
case TargetPlatform.iOS:
case TargetPlatform.fuchsia:
case TargetPlatform.macOS:
expect(
selection,
equals(
const TextSelection.collapsed(
offset: 32,
),
),
reason: 'on $platform',
);
break;
// These platforms go to the line start/end.
case TargetPlatform.linux:
case TargetPlatform.windows:
expect(
selection,
equals(
const TextSelection.collapsed(
offset: 29,
),
),
reason: 'on $platform',
);
break;
}
expect(controller.text, equals(testText), reason: 'on $platform');
await sendKeys(
tester,
<LogicalKeyboardKey>[
LogicalKeyboardKey.home,
],
targetPlatform: defaultTargetPlatform,
);
switch (defaultTargetPlatform) {
// These platforms don't move the selection with home/end at all still.
case TargetPlatform.android:
case TargetPlatform.iOS:
case TargetPlatform.fuchsia:
case TargetPlatform.macOS:
expect(
selection,
equals(
const TextSelection.collapsed(
offset: 32,
),
),
reason: 'on $platform',
);
break;
// Linux does nothing at a wordwrap with subsequent presses.
case TargetPlatform.linux:
expect(
selection,
equals(
const TextSelection.collapsed(
offset: 29,
),
),
reason: 'on $platform',
);
break;
// Windows jumps to the previous wordwrapped line.
case TargetPlatform.windows:
expect(
selection,
equals(
const TextSelection.collapsed(
offset: 0,
),
),
reason: 'on $platform',
);
break;
}
},
skip: kIsWeb, // [intended] on web these keys are handled by the browser.
variant: TargetPlatformVariant.all(),
);
testWidgets('end keys and wordwraps', (WidgetTester tester) async {
final String targetPlatformString = defaultTargetPlatform.toString();
final String platform = targetPlatformString.substring(targetPlatformString.indexOf('.') + 1).toLowerCase();
const String testText = 'Now is the time for all good people to come to the aid of their country. Now is the time for all good people to come to the aid of their country.';
final TextEditingController controller = TextEditingController(text: testText);
controller.selection = const TextSelection(
baseOffset: 0,
extentOffset: 0,
affinity: TextAffinity.upstream,
);
late TextSelection selection;
late SelectionChangedCause cause;
await tester.pumpWidget(MaterialApp(
home: Align(
alignment: Alignment.topLeft,
child: SizedBox(
width: 400,
child: EditableText(
maxLines: 10,
controller: controller,
showSelectionHandles: true,
autofocus: true,
focusNode: FocusNode(),
style: Typography.material2018().black.subtitle1!,
cursorColor: Colors.blue,
backgroundCursorColor: Colors.grey,
selectionControls: materialTextSelectionControls,
keyboardType: TextInputType.text,
textAlign: TextAlign.right,
onSelectionChanged: (TextSelection newSelection, SelectionChangedCause? newCause) {
selection = newSelection;
cause = newCause!;
},
),
),
),
));
await tester.pump(); // Wait for autofocus to take effect.
// Move near the middle of the document.
await sendKeys(
tester,
<LogicalKeyboardKey>[
LogicalKeyboardKey.arrowDown,
LogicalKeyboardKey.arrowRight,
LogicalKeyboardKey.arrowRight,
LogicalKeyboardKey.arrowRight,
],
targetPlatform: defaultTargetPlatform,
);
expect(cause, equals(SelectionChangedCause.keyboard), reason: 'on $platform');
expect(
selection,
equals(
const TextSelection.collapsed(
offset: 32,
),
),
reason: 'on $platform',
);
await sendKeys(
tester,
<LogicalKeyboardKey>[
LogicalKeyboardKey.end,
],
targetPlatform: defaultTargetPlatform,
);
switch (defaultTargetPlatform) {
// These platforms don't move the selection with home/end at all.
case TargetPlatform.android:
case TargetPlatform.iOS:
case TargetPlatform.fuchsia:
case TargetPlatform.macOS:
expect(
selection,
equals(
const TextSelection.collapsed(
offset: 32,
),
),
reason: 'on $platform',
);
break;
// These platforms go to the line start/end.
case TargetPlatform.linux:
case TargetPlatform.windows:
expect(
selection,
equals(
const TextSelection.collapsed(
offset: 58,
affinity: TextAffinity.upstream,
),
),
reason: 'on $platform',
);
break;
}
expect(controller.text, equals(testText), reason: 'on $platform');
await sendKeys(
tester,
<LogicalKeyboardKey>[
LogicalKeyboardKey.end,
],
targetPlatform: defaultTargetPlatform,
);
switch (defaultTargetPlatform) {
// These platforms don't move the selection with home/end at all still.
case TargetPlatform.android:
case TargetPlatform.iOS:
case TargetPlatform.fuchsia:
case TargetPlatform.macOS:
expect(
selection,
equals(
const TextSelection.collapsed(
offset: 32,
),
),
reason: 'on $platform',
);
break;
// Linux does nothing at a wordwrap with subsequent presses.
case TargetPlatform.linux:
expect(
selection,
equals(
const TextSelection.collapsed(
offset: 58,
affinity: TextAffinity.upstream,
),
),
reason: 'on $platform',
);
break;
// Windows jumps to the next wordwrapped line.
case TargetPlatform.windows:
expect(
selection,
equals(
const TextSelection.collapsed(
offset: 84,
affinity: TextAffinity.upstream,
),
),
reason: 'on $platform',
);
break;
}
},
skip: kIsWeb, // [intended] on web these keys are handled by the browser.
variant: TargetPlatformVariant.all(),
);
testWidgets('shift + home/end keys', (WidgetTester tester) async {
final String targetPlatformString = defaultTargetPlatform.toString();
final String platform = targetPlatformString.substring(targetPlatformString.indexOf('.') + 1).toLowerCase();
final TextEditingController controller = TextEditingController(text: testText);
controller.selection = const TextSelection(
baseOffset: 0,
extentOffset: 0,
affinity: TextAffinity.upstream,
);
late TextSelection selection;
late SelectionChangedCause cause;
await tester.pumpWidget(MaterialApp(
home: Align(
alignment: Alignment.topLeft,
child: SizedBox(
width: 400,
child: EditableText(
maxLines: 10,
controller: controller,
showSelectionHandles: true,
autofocus: true,
focusNode: FocusNode(),
style: Typography.material2018().black.subtitle1!,
cursorColor: Colors.blue,
backgroundCursorColor: Colors.grey,
selectionControls: materialTextSelectionControls,
keyboardType: TextInputType.text,
textAlign: TextAlign.right,
onSelectionChanged: (TextSelection newSelection, SelectionChangedCause? newCause) {
selection = newSelection;
cause = newCause!;
},
),
),
),
));
await tester.pump();
// Move near the middle of the document.
await sendKeys(
tester,
<LogicalKeyboardKey>[
LogicalKeyboardKey.arrowDown,
LogicalKeyboardKey.arrowRight,
LogicalKeyboardKey.arrowRight,
LogicalKeyboardKey.arrowRight,
],
targetPlatform: defaultTargetPlatform,
);
expect(cause, equals(SelectionChangedCause.keyboard), reason: 'on $platform');
expect(
selection,
equals(
const TextSelection.collapsed(
offset: 23,
),
),
reason: 'on $platform',
);
await sendKeys(
tester,
<LogicalKeyboardKey>[
LogicalKeyboardKey.home,
],
shift: true,
targetPlatform: defaultTargetPlatform,
);
expect(controller.text, equals(testText), reason: 'on $platform');
final TextSelection selectionAfterHome = selection;
// Move back to position 23.
controller.selection = const TextSelection.collapsed(
offset: 23,
);
await tester.pump();
await sendKeys(
tester,
<LogicalKeyboardKey>[
LogicalKeyboardKey.end,
],
shift: true,
targetPlatform: defaultTargetPlatform,
);
expect(controller.text, equals(testText), reason: 'on $platform');
final TextSelection selectionAfterEnd = selection;
switch (defaultTargetPlatform) {
// These platforms don't handle shift + home/end at all.
case TargetPlatform.android:
case TargetPlatform.iOS:
case TargetPlatform.fuchsia:
expect(
selectionAfterHome,
equals(
const TextSelection(
baseOffset: 23,
extentOffset: 23,
),
),
reason: 'on $platform',
);
expect(
selectionAfterEnd,
equals(
const TextSelection(
baseOffset: 23,
extentOffset: 23,
),
),
reason: 'on $platform',
);
break;
// Linux extends to the line start/end.
case TargetPlatform.linux:
expect(
selectionAfterHome,
equals(
const TextSelection(
baseOffset: 23,
extentOffset: 20,
),
),
reason: 'on $platform',
);
expect(
selectionAfterEnd,
equals(
const TextSelection(
baseOffset: 23,
extentOffset: 35,
affinity: TextAffinity.upstream,
),
),
reason: 'on $platform',
);
break;
// Windows expands to the line start/end.
case TargetPlatform.windows:
expect(
selectionAfterHome,
equals(
const TextSelection(
baseOffset: 23,
extentOffset: 20,
),
),
reason: 'on $platform',
);
expect(
selectionAfterEnd,
equals(
const TextSelection(
baseOffset: 23,
extentOffset: 35,
),
),
reason: 'on $platform',
);
break;
// Mac goes to the start/end of the document.
case TargetPlatform.macOS:
expect(
selectionAfterHome,
equals(
const TextSelection(
baseOffset: 23,
extentOffset: 0,
affinity: TextAffinity.upstream,
),
),
reason: 'on $platform',
);
expect(
selectionAfterEnd,
equals(
const TextSelection(
baseOffset: 23,
extentOffset: 72,
),
),
reason: 'on $platform',
);
break;
}
},
skip: kIsWeb, // [intended] on web these keys are handled by the browser.
variant: TargetPlatformVariant.all(),
);
testWidgets('shift + home/end keys (Windows only)', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(text: testText);
controller.selection = const TextSelection(
baseOffset: 0,
extentOffset: 0,
affinity: TextAffinity.upstream,
);
await tester.pumpWidget(MaterialApp(
home: Align(
alignment: Alignment.topLeft,
child: SizedBox(
width: 400,
child: EditableText(
maxLines: 10,
controller: controller,
showSelectionHandles: true,
autofocus: true,
focusNode: FocusNode(),
style: Typography.material2018().black.subtitle1!,
cursorColor: Colors.blue,
backgroundCursorColor: Colors.grey,
selectionControls: materialTextSelectionControls,
keyboardType: TextInputType.text,
textAlign: TextAlign.right,
),
),
),
));
await tester.pump();
// Move the selection away from the start so it can invert.
await sendKeys(
tester,
<LogicalKeyboardKey>[
LogicalKeyboardKey.arrowRight,
LogicalKeyboardKey.arrowRight,
LogicalKeyboardKey.arrowRight,
LogicalKeyboardKey.arrowRight,
],
targetPlatform: defaultTargetPlatform,
);
await tester.pump();
expect(
controller.selection,
equals(const TextSelection.collapsed(
offset: 4,
)),
);
// Press shift + end and extend the selection to the end of the line.
await sendKeys(
tester,
<LogicalKeyboardKey>[
LogicalKeyboardKey.end,
],
shift: true,
targetPlatform: defaultTargetPlatform,
);
await tester.pump();
expect(
controller.selection,
equals(const TextSelection(
baseOffset: 4,
extentOffset: 19,
affinity: TextAffinity.upstream,
)),
);
// Press shift + home and the selection inverts and extends to the start, it
// does not collapse and stop at the inversion.
await sendKeys(
tester,
<LogicalKeyboardKey>[
LogicalKeyboardKey.home,
],
shift: true,
targetPlatform: defaultTargetPlatform,
);
await tester.pump();
expect(
controller.selection,
equals(const TextSelection(
baseOffset: 4,
extentOffset: 0,
)),
);
// Press shift + end again and the selection inverts and extends to the end,
// again it does not stop at the inversion.
await sendKeys(
tester,
<LogicalKeyboardKey>[
LogicalKeyboardKey.end,
],
shift: true,
targetPlatform: defaultTargetPlatform,
);
await tester.pump();
expect(
controller.selection,
equals(const TextSelection(
baseOffset: 4,
extentOffset: 19,
)),
);
},
skip: kIsWeb, // [intended] on web these keys are handled by the browser.
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.windows })
);
testWidgets('home/end keys scrolling (Mac only)', (WidgetTester tester) async {
const String testText = 'Now is the time for all good people to come to the aid of their country. Now is the time for all good people to come to the aid of their country.';
final TextEditingController controller = TextEditingController(text: testText);
controller.selection = const TextSelection(
baseOffset: 0,
extentOffset: 0,
affinity: TextAffinity.upstream,
);
await tester.pumpWidget(MaterialApp(
home: Align(
alignment: Alignment.topLeft,
child: SizedBox(
width: 400,
child: EditableText(
maxLines: 10,
controller: controller,
showSelectionHandles: true,
autofocus: true,
focusNode: FocusNode(),
style: Typography.material2018().black.subtitle1!,
cursorColor: Colors.blue,
backgroundCursorColor: Colors.grey,
selectionControls: materialTextSelectionControls,
keyboardType: TextInputType.text,
textAlign: TextAlign.right,
),
),
),
));
await tester.pump(); // Wait for autofocus to take effect.
final Scrollable scrollable = tester.widget<Scrollable>(find.byType(Scrollable));
expect(scrollable.controller!.offset, 0.0);
// Scroll to the end of the document with the end key.
await sendKeys(
tester,
<LogicalKeyboardKey>[
LogicalKeyboardKey.end,
],
targetPlatform: defaultTargetPlatform,
);
final double maxScrollExtent = scrollable.controller!.position.maxScrollExtent;
expect(scrollable.controller!.offset, maxScrollExtent);
// Scroll back to the beginning of the document with the home key.
await sendKeys(
tester,
<LogicalKeyboardKey>[
LogicalKeyboardKey.home,
],
targetPlatform: defaultTargetPlatform,
);
expect(scrollable.controller!.offset, 0.0);
},
skip: kIsWeb, // [intended] on web these keys are handled by the browser.
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.macOS })
);
testWidgets('shift + home keys and wordwraps', (WidgetTester tester) async {
final String targetPlatformString = defaultTargetPlatform.toString();
final String platform = targetPlatformString.substring(targetPlatformString.indexOf('.') + 1).toLowerCase();
const String testText = 'Now is the time for all good people to come to the aid of their country. Now is the time for all good people to come to the aid of their country.';
final TextEditingController controller = TextEditingController(text: testText);
controller.selection = const TextSelection(
baseOffset: 0,
extentOffset: 0,
affinity: TextAffinity.upstream,
);
late TextSelection selection;
late SelectionChangedCause cause;
await tester.pumpWidget(MaterialApp(
home: Align(
alignment: Alignment.topLeft,
child: SizedBox(
width: 400,
child: EditableText(
maxLines: 10,
controller: controller,
showSelectionHandles: true,
autofocus: true,
focusNode: FocusNode(),
style: Typography.material2018().black.subtitle1!,
cursorColor: Colors.blue,
backgroundCursorColor: Colors.grey,
selectionControls: materialTextSelectionControls,
keyboardType: TextInputType.text,
textAlign: TextAlign.right,
onSelectionChanged: (TextSelection newSelection, SelectionChangedCause? newCause) {
selection = newSelection;
cause = newCause!;
},
),
),
),
));
await tester.pump(); // Wait for autofocus to take effect.
// Move near the middle of the document.
await sendKeys(
tester,
<LogicalKeyboardKey>[
LogicalKeyboardKey.arrowDown,
LogicalKeyboardKey.arrowRight,
LogicalKeyboardKey.arrowRight,
LogicalKeyboardKey.arrowRight,
],
targetPlatform: defaultTargetPlatform,
);
expect(cause, equals(SelectionChangedCause.keyboard), reason: 'on $platform');
expect(
selection,
equals(
const TextSelection.collapsed(
offset: 32,
),
),
reason: 'on $platform',
);
await sendKeys(
tester,
<LogicalKeyboardKey>[
LogicalKeyboardKey.home,
],
shift: true,
targetPlatform: defaultTargetPlatform,
);
switch (defaultTargetPlatform) {
// These platforms don't move the selection with shift + home/end at all.
case TargetPlatform.android:
case TargetPlatform.iOS:
case TargetPlatform.fuchsia:
expect(
selection,
equals(
const TextSelection.collapsed(
offset: 32,
),
),
reason: 'on $platform',
);
break;
// Mac selects to the start of the document.
case TargetPlatform.macOS:
expect(
selection,
equals(
const TextSelection(
baseOffset: 32,
extentOffset: 0,
),
),
reason: 'on $platform',
);
break;
// These platforms go to the line start/end.
// These platforms select to the line start.
case TargetPlatform.linux:
case TargetPlatform.windows:
afterHomeOffset = 20;
afterEndOffset = 35;
afterEndAffinity = TextAffinity.upstream;
expect(
selection,
equals(
const TextSelection(
baseOffset: 32,
extentOffset: 29,
),
),
reason: 'on $platform',
);
break;
}
expect(
selection,
equals(
TextSelection.collapsed(
offset: afterHomeOffset,
),
),
reason: 'on $platform',
);
expect(controller.text, equals(testText), reason: 'on $platform');
await sendKeys(
tester,
<LogicalKeyboardKey>[
LogicalKeyboardKey.end,
LogicalKeyboardKey.home,
],
shift: true,
targetPlatform: defaultTargetPlatform,
);
expect(
selection,
equals(
TextSelection.collapsed(
offset: afterEndOffset,
affinity: afterEndAffinity,
),
),
reason: 'on $platform',
);
expect(controller.text, equals(testText), reason: 'on $platform');
switch (defaultTargetPlatform) {
// These platforms don't move the selection with home/end at all still.
case TargetPlatform.android:
case TargetPlatform.iOS:
case TargetPlatform.fuchsia:
expect(
selection,
equals(
const TextSelection.collapsed(
offset: 32,
),
),
reason: 'on $platform',
);
break;
// Mac selects to the start of the document.
case TargetPlatform.macOS:
expect(
selection,
equals(
const TextSelection(
baseOffset: 32,
extentOffset: 0,
),
),
reason: 'on $platform',
);
break;
// Linux does nothing at a wordwrap with subsequent presses.
case TargetPlatform.linux:
expect(
selection,
equals(
const TextSelection(
baseOffset: 32,
extentOffset: 29,
),
),
reason: 'on $platform',
);
break;
// Windows jumps to the previous wordwrapped line.
case TargetPlatform.windows:
expect(
selection,
equals(
const TextSelection(
baseOffset: 32,
extentOffset: 0,
),
),
reason: 'on $platform',
);
break;
}
},
skip: kIsWeb, // [intended] on web these keys are handled by the browser.
variant: TargetPlatformVariant.all(),
);
testWidgets('shift + home/end keys', (WidgetTester tester) async {
testWidgets('shift + end keys and wordwraps', (WidgetTester tester) async {
final String targetPlatformString = defaultTargetPlatform.toString();
final String platform = targetPlatformString.substring(targetPlatformString.indexOf('.') + 1).toLowerCase();
const String testText = 'Now is the time for all good people to come to the aid of their country. Now is the time for all good people to come to the aid of their country.';
final TextEditingController controller = TextEditingController(text: testText);
controller.selection = const TextSelection(
baseOffset: 0,
......@@ -6029,7 +6938,7 @@ void main() {
),
));
await tester.pump();
await tester.pump(); // Wait for autofocus to take effect.
// Move near the middle of the document.
await sendKeys(
......@@ -6048,30 +6957,12 @@ void main() {
selection,
equals(
const TextSelection.collapsed(
offset: 23,
offset: 32,
),
),
reason: 'on $platform',
);
await sendKeys(
tester,
<LogicalKeyboardKey>[
LogicalKeyboardKey.home,
],
shift: true,
targetPlatform: defaultTargetPlatform,
);
expect(controller.text, equals(testText), reason: 'on $platform');
final TextSelection selectionAfterHome = selection;
// Move back to position 23.
controller.selection = const TextSelection.collapsed(
offset: 23,
);
await tester.pump();
await sendKeys(
tester,
<LogicalKeyboardKey>[
......@@ -6081,104 +6972,117 @@ void main() {
targetPlatform: defaultTargetPlatform,
);
expect(controller.text, equals(testText), reason: 'on $platform');
final TextSelection selectionAfterEnd = selection;
switch (defaultTargetPlatform) {
// These platforms don't handle shift + home/end at all.
// These platforms don't move the selection with home/end at all.
case TargetPlatform.android:
case TargetPlatform.iOS:
case TargetPlatform.fuchsia:
expect(
selectionAfterHome,
selection,
equals(
const TextSelection(
baseOffset: 23,
extentOffset: 23,
const TextSelection.collapsed(
offset: 32,
),
),
reason: 'on $platform',
);
break;
// Mac selects to the end of the document.
case TargetPlatform.macOS:
expect(
selectionAfterEnd,
selection,
equals(
const TextSelection(
baseOffset: 23,
extentOffset: 23,
baseOffset: 32,
extentOffset: 145,
),
),
reason: 'on $platform',
);
break;
// Linux extends to the line start/end.
// These platforms select to the line end.
case TargetPlatform.linux:
case TargetPlatform.windows:
expect(
selectionAfterHome,
equals(
const TextSelection(
baseOffset: 23,
extentOffset: 20,
),
),
reason: 'on $platform',
);
expect(
selectionAfterEnd,
selection,
equals(
const TextSelection(
baseOffset: 23,
extentOffset: 35,
baseOffset: 32,
extentOffset: 58,
affinity: TextAffinity.upstream,
),
),
reason: 'on $platform',
);
break;
}
expect(controller.text, equals(testText), reason: 'on $platform');
// Windows expands to the line start/end.
case TargetPlatform.windows:
await sendKeys(
tester,
<LogicalKeyboardKey>[
LogicalKeyboardKey.end,
],
shift: true,
targetPlatform: defaultTargetPlatform,
);
switch (defaultTargetPlatform) {
// These platforms don't move the selection with home/end at all still.
case TargetPlatform.android:
case TargetPlatform.iOS:
case TargetPlatform.fuchsia:
expect(
selectionAfterHome,
selection,
equals(
const TextSelection(
baseOffset: 23,
extentOffset: 20,
const TextSelection.collapsed(
offset: 32,
),
),
reason: 'on $platform',
);
break;
// Mac stays at the end of the document.
case TargetPlatform.macOS:
expect(
selectionAfterEnd,
selection,
equals(
const TextSelection(
baseOffset: 23,
extentOffset: 35,
baseOffset: 32,
extentOffset: 145,
),
),
reason: 'on $platform',
);
break;
// Mac goes to the start/end of the document.
case TargetPlatform.macOS:
// Linux does nothing at a wordwrap with subsequent presses.
case TargetPlatform.linux:
expect(
selectionAfterHome,
selection,
equals(
const TextSelection(
baseOffset: 23,
extentOffset: 0,
baseOffset: 32,
extentOffset: 58,
affinity: TextAffinity.upstream,
),
),
reason: 'on $platform',
);
break;
// Windows jumps to the previous wordwrapped line.
case TargetPlatform.windows:
expect(
selectionAfterEnd,
selection,
equals(
const TextSelection(
baseOffset: 23,
extentOffset: 72,
baseOffset: 32,
extentOffset: 84,
affinity: TextAffinity.upstream,
),
),
reason: 'on $platform',
......@@ -6190,13 +7094,15 @@ void main() {
variant: TargetPlatformVariant.all(),
);
testWidgets('shift + home/end keys (Windows only)', (WidgetTester tester) async {
testWidgets('shift + home/end keys to document boundary (Mac only)', (WidgetTester tester) async {
const String testText = 'Now is the time for all good people to come to the aid of their country. Now is the time for all good people to come to the aid of their country.';
final TextEditingController controller = TextEditingController(text: testText);
controller.selection = const TextSelection(
baseOffset: 0,
extentOffset: 0,
affinity: TextAffinity.upstream,
);
late TextSelection selection;
await tester.pumpWidget(MaterialApp(
home: Align(
alignment: Alignment.topLeft,
......@@ -6214,53 +7120,40 @@ void main() {
selectionControls: materialTextSelectionControls,
keyboardType: TextInputType.text,
textAlign: TextAlign.right,
onSelectionChanged: (TextSelection newSelection, SelectionChangedCause? newCause) {
selection = newSelection;
},
),
),
),
));
await tester.pump();
await tester.pump(); // Wait for autofocus to take effect.
// Move the selection away from the start so it can invert.
final Scrollable scrollable = tester.widget<Scrollable>(find.byType(Scrollable));
expect(scrollable.controller!.offset, 0.0);
// Move near the middle of the document.
await sendKeys(
tester,
<LogicalKeyboardKey>[
LogicalKeyboardKey.arrowDown,
LogicalKeyboardKey.arrowRight,
LogicalKeyboardKey.arrowRight,
LogicalKeyboardKey.arrowRight,
LogicalKeyboardKey.arrowRight,
],
targetPlatform: defaultTargetPlatform,
);
await tester.pump();
expect(
controller.selection,
equals(const TextSelection.collapsed(
offset: 4,
)),
);
// Press shift + end and extend the selection to the end of the line.
await sendKeys(
tester,
<LogicalKeyboardKey>[
LogicalKeyboardKey.end,
],
shift: true,
targetPlatform: defaultTargetPlatform,
);
await tester.pump();
expect(
controller.selection,
equals(const TextSelection(
baseOffset: 4,
extentOffset: 19,
affinity: TextAffinity.upstream,
)),
selection,
equals(
const TextSelection.collapsed(
offset: 32,
),
),
);
// Press shift + home and the selection inverts and extends to the start, it
// does not collapse and stop at the inversion.
// Expand to the start of the document with the home key.
await sendKeys(
tester,
<LogicalKeyboardKey>[
......@@ -6269,17 +7162,18 @@ void main() {
shift: true,
targetPlatform: defaultTargetPlatform,
);
await tester.pump();
expect(scrollable.controller!.offset, 0.0);
expect(
controller.selection,
equals(const TextSelection(
baseOffset: 4,
extentOffset: 0,
)),
selection,
equals(
const TextSelection(
baseOffset: 32,
extentOffset: 0,
),
),
);
// Press shift + end again and the selection inverts and extends to the end,
// again it does not stop at the inversion.
// Expand to the end of the document with the end key.
await sendKeys(
tester,
<LogicalKeyboardKey>[
......@@ -6288,17 +7182,20 @@ void main() {
shift: true,
targetPlatform: defaultTargetPlatform,
);
await tester.pump();
final double maxScrollExtent = scrollable.controller!.position.maxScrollExtent;
expect(scrollable.controller!.offset, maxScrollExtent);
expect(
controller.selection,
equals(const TextSelection(
baseOffset: 4,
extentOffset: 19,
)),
selection,
equals(
const TextSelection(
baseOffset: 0,
extentOffset: 145,
),
),
);
},
skip: kIsWeb, // [intended] on web these keys are handled by the browser.
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.windows })
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.macOS })
);
testWidgets('control + home/end keys (Windows only)', (WidgetTester tester) async {
......@@ -9533,7 +10430,7 @@ void main() {
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.macOS })
);
testWidgets('expanding selection to start/end', (WidgetTester tester) async {
testWidgets('expanding selection to start/end single line', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(text: 'word word word');
// word wo|rd| word
controller.selection = const TextSelection(
......@@ -9583,7 +10480,7 @@ void main() {
controller.selection,
equals(
const TextSelection(
baseOffset: 7,
baseOffset: 9,
extentOffset: 0,
affinity: TextAffinity.upstream,
),
......@@ -9595,7 +10492,7 @@ void main() {
await sendKeys(
tester,
<LogicalKeyboardKey>[
LogicalKeyboardKey.home,
LogicalKeyboardKey.end,
],
shift: true,
targetPlatform: defaultTargetPlatform,
......@@ -9606,8 +10503,8 @@ void main() {
controller.selection,
equals(
const TextSelection(
baseOffset: 7,
extentOffset: 0,
baseOffset: 0,
extentOffset: 14,
affinity: TextAffinity.upstream,
),
),
......
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