Unverified Commit 21158d83 authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Add command key bindings to macOS text editing and fix selection. (#44130)

This adds support for the command key for text selection/editing on macOS. I had ported the text editing code (in #42879), but forgot to add support for the command key itself. This also adds a test that tests the text editing on multiple platforms instead of just testing Android.

There appears to still be a bug (filed #44135) where we're losing key events sometimes on macOS, leaving some keys "stuck" on, but this PR at least allows the right key combinations to be used.
parent a7367b65
......@@ -816,15 +816,15 @@ class TextPainter {
/// <http://www.unicode.org/reports/tr29/#Word_Boundaries>.
TextRange getWordBoundary(TextPosition position) {
assert(!_needsLayout);
// TODO(gspencergoog): remove the List<int>-based code when the engine API
// returns a TextRange instead of a List<int>.
final dynamic boundary = _paragraph.getWordBoundary(position);
if (boundary is List<int>) {
final List<int> indices = boundary;
return TextRange(start: indices[0], end: indices[1]);
}
final TextRange range = boundary;
return range;
return _paragraph.getWordBoundary(position);
}
/// Returns the text range of the line at the given offset.
///
/// The newline, if any, is included in the range.
TextRange getLineBoundary(TextPosition position) {
assert(!_needsLayout);
return _paragraph.getLineBoundary(position);
}
/// Returns the full list of [LineMetrics] that describe in detail the various
......
......@@ -97,6 +97,47 @@ class TextSelectionPoint {
}
}
// Check if the given code unit is a white space or separator
// character.
//
// Includes newline characters from ASCII and separators from the
// [unicode separator category](https://www.compart.com/en/unicode/category/Zs)
// TODO(gspencergoog): replace when we expose this ICU information.
bool _isWhitespace(int codeUnit) {
switch (codeUnit) {
case 0x9: // horizontal tab
case 0xA: // line feed
case 0xB: // vertical tab
case 0xC: // form feed
case 0xD: // carriage return
case 0x1C: // file separator
case 0x1D: // group separator
case 0x1E: // record separator
case 0x1F: // unit separator
case 0x20: // space
case 0xA0: // no-break space
case 0x1680: // ogham space mark
case 0x2000: // en quad
case 0x2001: // em quad
case 0x2002: // en space
case 0x2003: // em space
case 0x2004: // three-per-em space
case 0x2005: // four-er-em space
case 0x2006: // six-per-em space
case 0x2007: // figure space
case 0x2008: // punctuation space
case 0x2009: // thin space
case 0x200A: // hair space
case 0x202F: // narrow no-break space
case 0x205F: // medium mathematical space
case 0x3000: // ideographic space
break;
default:
return false;
}
return true;
}
/// Displays some text in a scrollable container with a potentially blinking
/// cursor and with gesture recognizers.
///
......@@ -400,10 +441,18 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
static final Set<LogicalKeyboardKey> _modifierKeys = <LogicalKeyboardKey>{
LogicalKeyboardKey.shift,
LogicalKeyboardKey.control,
LogicalKeyboardKey.alt,
};
static final Set<LogicalKeyboardKey> _macOsModifierKeys = <LogicalKeyboardKey>{
LogicalKeyboardKey.shift,
LogicalKeyboardKey.meta,
LogicalKeyboardKey.alt,
};
static final Set<LogicalKeyboardKey> _interestingKeys = <LogicalKeyboardKey>{
..._modifierKeys,
..._macOsModifierKeys,
..._nonModifierKeys,
};
......@@ -414,12 +463,12 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
void _handleKeyEvent(RawKeyEvent keyEvent) {
if (keyEvent is! RawKeyDownEvent || onSelectionChanged == null)
return;
final Set<LogicalKeyboardKey> keysPressed = LogicalKeyboardKey.collapseSynonyms(RawKeyboard.instance.keysPressed);
final LogicalKeyboardKey key = keyEvent.logicalKey;
final bool isMacOS = keyEvent.data is RawKeyEventDataMacOs;
if (!_nonModifierKeys.contains(key) ||
keysPressed.difference(_modifierKeys).length > 1 ||
keysPressed.difference(isMacOS ? _macOsModifierKeys : _modifierKeys).length > 1 ||
keysPressed.difference(_interestingKeys).isNotEmpty) {
// If the most recently pressed key isn't a non-modifier key, or more than
// one non-modifier key is down, or keys other than the ones we're interested in
......@@ -427,9 +476,12 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
return;
}
final bool isWordModifierPressed = isMacOS ? keyEvent.isAltPressed : keyEvent.isControlPressed;
final bool isLineModifierPressed = isMacOS ? keyEvent.isMetaPressed : keyEvent.isAltPressed;
final bool isShortcutModifierPressed = isMacOS ? keyEvent.isMetaPressed : keyEvent.isControlPressed;
if (_movementKeys.contains(key)) {
_handleMovement(key, control: keyEvent.isControlPressed, shift: keyEvent.isShiftPressed);
} else if (keyEvent.isControlPressed && _shortcutKeys.contains(key)) {
_handleMovement(key, wordModifier: isWordModifierPressed, lineModifier: isLineModifierPressed, shift: keyEvent.isShiftPressed);
} else if (isShortcutModifierPressed && _shortcutKeys.contains(key)) {
// _handleShortcuts depends on being started in the same stack invocation
// as the _handleKeyEvent method
_handleShortcuts(key);
......@@ -440,9 +492,15 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
void _handleMovement(
LogicalKeyboardKey key, {
@required bool control,
@required bool wordModifier,
@required bool lineModifier,
@required bool shift,
}) {
if (wordModifier && lineModifier) {
// If both modifiers are down, nothing happens on any of the platforms.
return;
}
TextSelection newSelection = selection;
final bool rightArrow = key == LogicalKeyboardKey.arrowRight;
......@@ -450,34 +508,80 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
final bool upArrow = key == LogicalKeyboardKey.arrowUp;
final bool downArrow = key == LogicalKeyboardKey.arrowDown;
// Because the user can use multiple keys to change how they select, the
// new offset variable is threaded through these four functions and
// potentially changes after each one.
if (control) {
// If control is pressed, we will decide which way to look for a word
// based on which arrow is pressed.
if (leftArrow && newSelection.extentOffset > 2) {
final TextSelection textSelection = _selectWordAtOffset(TextPosition(offset: newSelection.extentOffset - 2));
newSelection = newSelection.copyWith(extentOffset: textSelection.baseOffset + 1);
} else if (rightArrow && newSelection.extentOffset < text.toPlainText().length - 2) {
final TextSelection textSelection = _selectWordAtOffset(TextPosition(offset: newSelection.extentOffset + 1));
newSelection = newSelection.copyWith(extentOffset: textSelection.extentOffset - 1);
}
}
// Set the new offset to be +/- 1 depending on which arrow is pressed
// If shift is down, we also want to update the previous cursor location
if (rightArrow && newSelection.extentOffset < text.toPlainText().length) {
// Find the previous non-whitespace character
int previousNonWhitespace(int extent) {
int result = math.max(extent - 1, 0);
while (result > 0 && _isWhitespace(_plainText.codeUnitAt(result))) {
result -= 1;
}
return result;
}
int nextNonWhitespace(int extent) {
int result = math.min(extent + 1, _plainText.length);
while (result < _plainText.length && _isWhitespace(_plainText.codeUnitAt(result))) {
result += 1;
}
return result;
}
if ((rightArrow || leftArrow) && !(rightArrow && leftArrow)) {
// Jump to begin/end of word.
if (wordModifier) {
// If control/option is pressed, we will decide which way to look for a
// word based on which arrow is pressed.
if (leftArrow) {
// When going left, we want to skip over any whitespace before the word,
// so we go back to the first non-whitespace before asking for the word
// boundary, since _selectWordAtOffset finds the word boundaries without
// including whitespace.
final int startPoint = previousNonWhitespace(newSelection.extentOffset);
final TextSelection textSelection = _selectWordAtOffset(TextPosition(offset: startPoint));
newSelection = newSelection.copyWith(extentOffset: textSelection.baseOffset);
} else {
// When going right, we want to skip over any whitespace after the word,
// so we go forward to the first non-whitespace character before asking
// for the word bounds, since _selectWordAtOffset finds the word
// boundaries without including whitespace.
final int startPoint = nextNonWhitespace(newSelection.extentOffset);
final TextSelection textSelection = _selectWordAtOffset(TextPosition(offset: startPoint));
newSelection = newSelection.copyWith(extentOffset: textSelection.extentOffset);
}
} else if (lineModifier) {
// If control/command is pressed, we will decide which way to expand to
// the beginning/end of the line based on which arrow is pressed.
if (leftArrow) {
// When going left, we want to skip over any whitespace before the line,
// so we go back to the first non-whitespace before asking for the line
// bounds, since _selectLineAtOffset finds the line boundaries without
// including whitespace (like the newline).
final int startPoint = previousNonWhitespace(newSelection.extentOffset);
final TextSelection textSelection = _selectLineAtOffset(TextPosition(offset: startPoint));
newSelection = newSelection.copyWith(extentOffset: textSelection.baseOffset);
} else {
// When going right, we want to skip over any whitespace after the line,
// so we go forward to the first non-whitespace character before asking
// for the line bounds, since _selectLineAtOffset finds the line
// boundaries without including whitespace (like the newline).
final int startPoint = nextNonWhitespace(newSelection.extentOffset);
final TextSelection textSelection = _selectLineAtOffset(TextPosition(offset: startPoint));
newSelection = newSelection.copyWith(extentOffset: textSelection.extentOffset);
}
} else {
if (rightArrow && newSelection.extentOffset < _plainText.length) {
newSelection = newSelection.copyWith(extentOffset: newSelection.extentOffset + 1);
if (shift) {
_cursorResetLocation += 1;
}
}
if (leftArrow && newSelection.extentOffset > 0) {
} else if (leftArrow && newSelection.extentOffset > 0) {
newSelection = newSelection.copyWith(extentOffset: newSelection.extentOffset - 1);
if (shift) {
_cursorResetLocation -= 1;
}
}
}
}
// Handles moving the cursor vertically as well as taking care of the
// case where the user moves the cursor to the end or beginning of the text
// and then back up or down.
......@@ -498,7 +602,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
// case that the user wants to unhighlight some text.
if (position.offset == newSelection.extentOffset) {
if (downArrow) {
newSelection = newSelection.copyWith(extentOffset: text.toPlainText().length);
newSelection = newSelection.copyWith(extentOffset: _plainText.length);
} else if (upArrow) {
newSelection = newSelection.copyWith(extentOffset: 0);
}
......@@ -512,20 +616,24 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
}
}
// Just place the collapsed selection at the new position if shift isn't down.
// Just place the collapsed selection at the end or beginning of the region
// if shift isn't down.
if (!shift) {
// We want to put the cursor at the correct location depending on which
// arrow is used while there is a selection.
int newOffset = newSelection.extentOffset;
if (!selection.isCollapsed) {
if (leftArrow)
if (leftArrow) {
newOffset = newSelection.baseOffset < newSelection.extentOffset ? newSelection.baseOffset : newSelection.extentOffset;
else if (rightArrow)
} else if (rightArrow) {
newOffset = newSelection.baseOffset > newSelection.extentOffset ? newSelection.baseOffset : newSelection.extentOffset;
}
}
newSelection = TextSelection.fromPosition(TextPosition(offset: newOffset));
}
// Update the text selection delegate so that the engine knows what we did.
textSelectionDelegate.textEditingValue = textSelectionDelegate.textEditingValue.copyWith(selection: newSelection);
_handleSelectionChange(
newSelection,
SelectionChangedCause.keyboard,
......@@ -533,22 +641,22 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
}
// Handles shortcut functionality including cut, copy, paste and select all
// using control + (X, C, V, A).
// using control/command + (X, C, V, A).
Future<void> _handleShortcuts(LogicalKeyboardKey key) async {
assert(_shortcutKeys.contains(key), 'shortcut key $key not recognized.');
if (key == LogicalKeyboardKey.keyC) {
if (!selection.isCollapsed) {
Clipboard.setData(
ClipboardData(text: selection.textInside(text.toPlainText())));
ClipboardData(text: selection.textInside(_plainText)));
}
return;
}
if (key == LogicalKeyboardKey.keyX) {
if (!selection.isCollapsed) {
Clipboard.setData(ClipboardData(text: selection.textInside(text.toPlainText())));
Clipboard.setData(ClipboardData(text: selection.textInside(_plainText)));
textSelectionDelegate.textEditingValue = TextEditingValue(
text: selection.textBefore(text.toPlainText())
+ selection.textAfter(text.toPlainText()),
text: selection.textBefore(_plainText)
+ selection.textAfter(_plainText),
selection: TextSelection.collapsed(offset: selection.start),
);
}
......@@ -584,15 +692,15 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
}
void _handleDelete() {
if (selection.textAfter(text.toPlainText()).isNotEmpty) {
if (selection.textAfter(_plainText).isNotEmpty) {
textSelectionDelegate.textEditingValue = TextEditingValue(
text: selection.textBefore(text.toPlainText())
+ selection.textAfter(text.toPlainText()).substring(1),
text: selection.textBefore(_plainText)
+ selection.textAfter(_plainText).substring(1),
selection: TextSelection.collapsed(offset: selection.start),
);
} else {
textSelectionDelegate.textEditingValue = TextEditingValue(
text: selection.textBefore(text.toPlainText()),
text: selection.textBefore(_plainText),
selection: TextSelection.collapsed(offset: selection.start),
);
}
......@@ -617,6 +725,13 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
_textLayoutLastMinWidth = null;
}
// Retuns a cached plain text version of the text in the painter.
String _cachedPlainText;
String get _plainText {
_cachedPlainText ??= _textPainter.text.toPlainText();
return _cachedPlainText;
}
/// The text to display.
TextSpan get text => _textPainter.text;
final TextPainter _textPainter;
......@@ -624,6 +739,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
if (_textPainter.text == value)
return;
_textPainter.text = value;
_cachedPlainText = null;
markNeedsTextLayout();
markNeedsSemanticsUpdate();
}
......@@ -1013,8 +1129,8 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
config
..value = obscureText
? obscuringCharacter * text.toPlainText().length
: text.toPlainText()
? obscuringCharacter * _plainText.length
: _plainText
..isObscured = obscureText
..isMultiline = _isMultiline
..textDirection = textDirection
......@@ -1124,41 +1240,13 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
// Check if the given text range only contains white space or separator
// characters.
//
// newline characters from ascii and separators from the
// Includes newline characters from ASCII and separators from the
// [unicode separator category](https://www.compart.com/en/unicode/category/Zs)
// TODO(jonahwilliams): replace when we expose this ICU information.
bool _onlyWhitespace(TextRange range) {
for (int i = range.start; i < range.end; i++) {
final int codeUnit = text.codeUnitAt(i);
switch (codeUnit) {
case 0x9: // horizontal tab
case 0xA: // line feed
case 0xB: // vertical tab
case 0xC: // form feed
case 0xD: // carriage return
case 0x1C: // file separator
case 0x1D: // group separator
case 0x1E: // record separator
case 0x1F: // unit separator
case 0x20: // space
case 0xA0: // no-break space
case 0x1680: // ogham space mark
case 0x2000: // en quad
case 0x2001: // em quad
case 0x2002: // en space
case 0x2003: // em space
case 0x2004: // three-per-em space
case 0x2005: // four-er-em space
case 0x2006: // six-per-em space
case 0x2007: // figure space
case 0x2008: // punctuation space
case 0x2009: // thin space
case 0x200A: // hair space
case 0x202F: // narrow no-break space
case 0x205F: // medium mathematical space
case 0x3000: // ideographic space
break;
default:
if (!_isWhitespace(codeUnit)) {
return false;
}
}
......@@ -1338,7 +1426,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
// Set the height based on the content.
if (width == double.infinity) {
final String text = _textPainter.text.toPlainText();
final String text = _plainText;
int lines = 1;
for (int index = 0; index < text.length; index += 1) {
if (text.codeUnitAt(index) == 0x0A) // count explicit line breaks
......@@ -1550,11 +1638,25 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
return TextSelection.fromPosition(position);
// If text is obscured, the entire sentence should be treated as one word.
if (obscureText) {
return TextSelection(baseOffset: 0, extentOffset: text.toPlainText().length);
return TextSelection(baseOffset: 0, extentOffset: _plainText.length);
}
return TextSelection(baseOffset: word.start, extentOffset: word.end);
}
TextSelection _selectLineAtOffset(TextPosition position) {
assert(_textLayoutLastMaxWidth == constraints.maxWidth &&
_textLayoutLastMinWidth == constraints.minWidth,
'Last width ($_textLayoutLastMinWidth, $_textLayoutLastMaxWidth) not the same as max width constraint (${constraints.minWidth}, ${constraints.maxWidth}).');
final TextRange line = _textPainter.getLineBoundary(position);
if (position.offset >= line.end)
return TextSelection.fromPosition(position);
// If text is obscured, the entire string should be treated as one line.
if (obscureText) {
return TextSelection(baseOffset: 0, extentOffset: _plainText.length);
}
return TextSelection(baseOffset: line.start, extentOffset: line.end);
}
Rect _caretPrototype;
void _layoutText({ double minWidth = 0.0, double maxWidth = double.infinity }) {
......
......@@ -2975,12 +2975,66 @@ void main() {
expect(controller.selection.extent.offset, 5);
}, skip: isBrowser);
testWidgets('keyboard text selection works as expected', (WidgetTester tester) async {
// Text with two separate words to select.
const String testText = 'Now is the time for\n'
'all good people\n'
'to come to the aid\n'
'of their country.';
Future<void> sendKeys(
WidgetTester tester,
List<LogicalKeyboardKey> keys, {
bool shift = false,
bool wordModifier = false,
bool lineModifier = false,
bool shortcutModifier = false,
String platform,
}) async {
if (shift) {
await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft, platform: platform);
}
if (shortcutModifier) {
await tester.sendKeyDownEvent(
platform == 'macos' ? LogicalKeyboardKey.metaLeft : LogicalKeyboardKey.controlLeft,
platform: platform);
}
if (wordModifier) {
await tester.sendKeyDownEvent(
platform == 'macos' ? LogicalKeyboardKey.altLeft : LogicalKeyboardKey.controlLeft,
platform: platform);
}
if (lineModifier) {
await tester.sendKeyDownEvent(
platform == 'macos' ? LogicalKeyboardKey.metaLeft : LogicalKeyboardKey.altLeft,
platform: platform);
}
for (LogicalKeyboardKey key in keys) {
await tester.sendKeyEvent(key, platform: platform);
await tester.pump();
}
if (lineModifier) {
await tester.sendKeyUpEvent(
platform == 'macos' ? LogicalKeyboardKey.metaLeft : LogicalKeyboardKey.altLeft,
platform: platform);
}
if (wordModifier) {
await tester.sendKeyUpEvent(
platform == 'macos' ? LogicalKeyboardKey.altLeft : LogicalKeyboardKey.controlLeft,
platform: platform);
}
if (shortcutModifier) {
await tester.sendKeyUpEvent(
platform == 'macos' ? LogicalKeyboardKey.metaLeft : LogicalKeyboardKey.controlLeft,
platform: platform);
}
if (shift) {
await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft, platform: platform);
}
if (shift || wordModifier || lineModifier) {
await tester.pump();
}
}
Future<void> testTextEditing(WidgetTester tester, {String platform}) async {
final TextEditingController controller = TextEditingController(text: testText);
controller.selection = const TextSelection(
baseOffset: 0,
......@@ -3017,149 +3071,192 @@ void main() {
await tester.pump(); // Wait for autofocus to take effect.
Future<void> sendKeys(List<LogicalKeyboardKey> keys, {bool shift = false, bool control = false}) async {
if (shift) {
await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
}
if (control) {
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
}
for (LogicalKeyboardKey key in keys) {
await tester.sendKeyEvent(key);
await tester.pump();
}
if (control) {
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
}
if (shift) {
await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft);
}
if (shift || control) {
await tester.pump();
}
}
// Select a few characters using shift right arrow
await sendKeys(
tester,
<LogicalKeyboardKey>[
LogicalKeyboardKey.arrowRight,
LogicalKeyboardKey.arrowRight,
LogicalKeyboardKey.arrowRight,
],
shift: true,
platform: platform,
);
expect(cause, equals(SelectionChangedCause.keyboard));
expect(selection, equals(const TextSelection(
expect(cause, equals(SelectionChangedCause.keyboard), reason: 'on $platform');
expect(
selection,
equals(
const TextSelection(
baseOffset: 0,
extentOffset: 3,
affinity: TextAffinity.upstream,
)));
),
),
reason: 'on $platform',
);
// Select fewer characters using shift left arrow
await sendKeys(
tester,
<LogicalKeyboardKey>[
LogicalKeyboardKey.arrowLeft,
LogicalKeyboardKey.arrowLeft,
LogicalKeyboardKey.arrowLeft,
],
shift: true,
platform: platform,
);
expect(selection, equals(const TextSelection(
expect(
selection,
equals(
const TextSelection(
baseOffset: 0,
extentOffset: 0,
affinity: TextAffinity.upstream,
)));
),
),
reason: 'on $platform',
);
// Try to select before the first character, nothing should change.
await sendKeys(
tester,
<LogicalKeyboardKey>[
LogicalKeyboardKey.arrowLeft,
],
shift: true,
platform: platform,
);
expect(selection, equals(const TextSelection(
expect(
selection,
equals(
const TextSelection(
baseOffset: 0,
extentOffset: 0,
affinity: TextAffinity.upstream,
)));
),
),
reason: 'on $platform',
);
// Select the first two words.
await sendKeys(
tester,
<LogicalKeyboardKey>[
LogicalKeyboardKey.arrowRight,
LogicalKeyboardKey.arrowRight,
],
shift: true,
control: true,
wordModifier: true,
platform: platform,
);
expect(selection, equals(const TextSelection(
expect(
selection,
equals(
const TextSelection(
baseOffset: 0,
extentOffset: 6,
affinity: TextAffinity.upstream,
)));
),
),
reason: 'on $platform',
);
// Unselect the second word.
await sendKeys(
tester,
<LogicalKeyboardKey>[
LogicalKeyboardKey.arrowLeft,
],
shift: true,
control: true,
wordModifier: true,
platform: platform,
);
expect(selection, equals(const TextSelection(
expect(
selection,
equals(
const TextSelection(
baseOffset: 0,
extentOffset: 4,
affinity: TextAffinity.upstream,
)));
),
),
reason: 'on $platform',
);
// Select the next line.
await sendKeys(
tester,
<LogicalKeyboardKey>[
LogicalKeyboardKey.arrowDown,
],
shift: true,
platform: platform,
);
expect(selection, equals(const TextSelection(
expect(
selection,
equals(
const TextSelection(
baseOffset: 0,
extentOffset: 20,
affinity: TextAffinity.upstream,
)));
),
),
reason: 'on $platform',
);
// Move forward one character to reset the selection.
await sendKeys(
tester,
<LogicalKeyboardKey>[
LogicalKeyboardKey.arrowRight,
],
platform: platform,
);
expect(selection, equals(const TextSelection(
expect(
selection,
equals(
const TextSelection(
baseOffset: 21,
extentOffset: 21,
affinity: TextAffinity.downstream,
)));
),
),
reason: 'on $platform',
);
// Select the next line.
await sendKeys(
tester,
<LogicalKeyboardKey>[
LogicalKeyboardKey.arrowDown,
],
shift: true,
platform: platform,
);
expect(selection, equals(const TextSelection(
expect(
selection,
equals(
const TextSelection(
baseOffset: 21,
extentOffset: 40,
affinity: TextAffinity.downstream,
)));
),
),
reason: 'on $platform',
);
// Select to the end of the string by going down.
await sendKeys(
tester,
<LogicalKeyboardKey>[
LogicalKeyboardKey.arrowDown,
LogicalKeyboardKey.arrowDown,
......@@ -3167,166 +3264,323 @@ void main() {
LogicalKeyboardKey.arrowDown,
],
shift: true,
platform: platform,
);
expect(selection, equals(const TextSelection(
expect(
selection,
equals(
const TextSelection(
baseOffset: 21,
extentOffset: testText.length,
affinity: TextAffinity.downstream,
)));
),
),
reason: 'on $platform',
);
// Go back up one line to set selection up to part of the last line.
await sendKeys(
tester,
<LogicalKeyboardKey>[
LogicalKeyboardKey.arrowUp,
],
shift: true,
platform: platform,
);
expect(selection, equals(const TextSelection(
expect(
selection,
equals(
const TextSelection(
baseOffset: 21,
extentOffset: 58,
affinity: TextAffinity.downstream,
)));
),
),
reason: 'on $platform',
);
// Select to the end of the selection.
await sendKeys(
tester,
<LogicalKeyboardKey>[
LogicalKeyboardKey.arrowRight,
],
lineModifier: true,
shift: true,
platform: platform,
);
expect(
selection,
equals(
const TextSelection(
baseOffset: 21,
extentOffset: 72,
affinity: TextAffinity.downstream,
),
),
reason: 'on $platform',
);
// Select to the beginning of the line.
await sendKeys(
tester,
<LogicalKeyboardKey>[
LogicalKeyboardKey.arrowLeft,
],
lineModifier: true,
shift: true,
platform: platform,
);
expect(
selection,
equals(
const TextSelection(
baseOffset: 21,
extentOffset: 55,
affinity: TextAffinity.downstream,
),
),
reason: 'on $platform',
);
// Select All
await sendKeys(
tester,
<LogicalKeyboardKey>[
LogicalKeyboardKey.keyA,
],
control: true,
shortcutModifier: true,
platform: platform,
);
expect(selection, equals(const TextSelection(
expect(
selection,
equals(
const TextSelection(
baseOffset: 0,
extentOffset: testText.length,
affinity: TextAffinity.downstream,
)));
),
),
reason: 'on $platform',
);
// Jump to beginning of selection.
await sendKeys(
tester,
<LogicalKeyboardKey>[
LogicalKeyboardKey.arrowLeft,
],
platform: platform,
);
expect(selection, equals(const TextSelection(
expect(
selection,
equals(
const TextSelection(
baseOffset: 0,
extentOffset: 0,
affinity: TextAffinity.downstream,
)));
),
),
reason: 'on $platform',
);
// Jump forward three words.
await sendKeys(
tester,
<LogicalKeyboardKey>[
LogicalKeyboardKey.arrowRight,
LogicalKeyboardKey.arrowRight,
LogicalKeyboardKey.arrowRight,
],
control: true,
wordModifier: true,
platform: platform,
);
expect(selection, equals(const TextSelection(
expect(
selection,
equals(
const TextSelection(
baseOffset: 10,
extentOffset: 10,
affinity: TextAffinity.downstream,
)));
),
),
reason: 'on $platform',
);
// Select some characters backward.
await sendKeys(
tester,
<LogicalKeyboardKey>[
LogicalKeyboardKey.arrowLeft,
LogicalKeyboardKey.arrowLeft,
LogicalKeyboardKey.arrowLeft,
],
shift: true,
platform: platform,
);
expect(selection, equals(const TextSelection(
expect(
selection,
equals(
const TextSelection(
baseOffset: 10,
extentOffset: 7,
affinity: TextAffinity.downstream,
)));
),
),
reason: 'on $platform',
);
// Select a word backward.
await sendKeys(
tester,
<LogicalKeyboardKey>[
LogicalKeyboardKey.arrowLeft,
],
shift: true,
control: true,
wordModifier: true,
platform: platform,
);
expect(selection, equals(const TextSelection(
expect(
selection,
equals(
const TextSelection(
baseOffset: 10,
extentOffset: 4,
affinity: TextAffinity.downstream,
)));
expect(controller.text, equals(testText));
),
),
reason: 'on $platform',
);
expect(controller.text, equals(testText), reason: 'on $platform');
// Cut
await sendKeys(
tester,
<LogicalKeyboardKey>[
LogicalKeyboardKey.keyX,
],
control: true,
shortcutModifier: true,
platform: platform,
);
expect(selection, equals(const TextSelection(
expect(
selection,
equals(
const TextSelection(
baseOffset: 10,
extentOffset: 4,
affinity: TextAffinity.downstream,
)));
expect(controller.text, equals('Now time for\n'
),
),
reason: 'on $platform',
);
expect(
controller.text,
equals('Now time for\n'
'all good people\n'
'to come to the aid\n'
'of their country.'));
expect((await Clipboard.getData(Clipboard.kTextPlain)).text, equals('is the'));
'of their country.'),
reason: 'on $platform',
);
expect(
(await Clipboard.getData(Clipboard.kTextPlain)).text,
equals('is the'),
reason: 'on $platform',
);
// Paste
await sendKeys(
tester,
<LogicalKeyboardKey>[
LogicalKeyboardKey.keyV,
],
control: true,
shortcutModifier: true,
platform: platform,
);
expect(selection, equals(const TextSelection(
expect(
selection,
equals(
const TextSelection(
baseOffset: 10,
extentOffset: 4,
affinity: TextAffinity.downstream,
)));
expect(controller.text, equals(testText));
),
),
reason: 'on $platform',
);
expect(controller.text, equals(testText), reason: 'on $platform');
// Copy All
await sendKeys(
tester,
<LogicalKeyboardKey>[
LogicalKeyboardKey.keyA,
LogicalKeyboardKey.keyC,
],
control: true,
shortcutModifier: true,
platform: platform,
);
expect(selection, equals(const TextSelection(
expect(
selection,
equals(
const TextSelection(
baseOffset: 0,
extentOffset: testText.length,
affinity: TextAffinity.downstream,
)));
expect(controller.text, equals(testText));
),
),
reason: 'on $platform',
);
expect(controller.text, equals(testText), reason: 'on $platform');
expect((await Clipboard.getData(Clipboard.kTextPlain)).text, equals(testText));
// Delete
await sendKeys(
tester,
<LogicalKeyboardKey>[
LogicalKeyboardKey.delete,
],
platform: platform,
);
expect(selection, equals(const TextSelection(
expect(
selection,
equals(
const TextSelection(
baseOffset: 0,
extentOffset: 72,
affinity: TextAffinity.downstream,
)));
expect(controller.text, isEmpty);
),
),
reason: 'on $platform',
);
expect(controller.text, isEmpty, reason: 'on $platform');
}
testWidgets('keyboard text selection works as expected on linux', (WidgetTester tester) async {
await testTextEditing(tester, platform: 'linux');
});
testWidgets('keyboard text selection works as expected on android', (WidgetTester tester) async {
await testTextEditing(tester, platform: 'android');
});
testWidgets('keyboard text selection works as expected on fuchsia', (WidgetTester tester) async {
await testTextEditing(tester, platform: 'fuchsia');
});
testWidgets('keyboard text selection works as expected on macos', (WidgetTester tester) async {
await testTextEditing(tester, platform: 'macos');
});
// Regression test for https://github.com/flutter/flutter/issues/31287
......
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