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.
diff --git a/packages/flutter/lib/src/painting/text_painter.dart b/packages/flutter/lib/src/painting/text_painter.dart
index b6197a2..0fb9861 100644
--- a/packages/flutter/lib/src/painting/text_painter.dart
+++ b/packages/flutter/lib/src/painting/text_painter.dart
@@ -816,15 +816,15 @@
/// <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
diff --git a/packages/flutter/lib/src/rendering/editable.dart b/packages/flutter/lib/src/rendering/editable.dart
index 2a3cd83..9893216 100644
--- a/packages/flutter/lib/src/rendering/editable.dart
+++ b/packages/flutter/lib/src/rendering/editable.dart
@@ -97,6 +97,47 @@
}
}
+// 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 @@
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 @@
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 @@
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 @@
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 @@
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);
+ // 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;
+ }
+ } else if (leftArrow && newSelection.extentOffset > 0) {
+ newSelection = newSelection.copyWith(extentOffset: newSelection.extentOffset - 1);
+ if (shift) {
+ _cursorResetLocation -= 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) {
- newSelection = newSelection.copyWith(extentOffset: newSelection.extentOffset + 1);
- if (shift) {
- _cursorResetLocation += 1;
- }
- }
- 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 @@
// 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 @@
}
}
- // 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 @@
}
// 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 @@
}
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 @@
_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 @@
if (_textPainter.text == value)
return;
_textPainter.text = value;
+ _cachedPlainText = null;
markNeedsTextLayout();
markNeedsSemanticsUpdate();
}
@@ -1013,8 +1129,8 @@
config
..value = obscureText
- ? obscuringCharacter * text.toPlainText().length
- : text.toPlainText()
+ ? obscuringCharacter * _plainText.length
+ : _plainText
..isObscured = obscureText
..isMultiline = _isMultiline
..textDirection = textDirection
@@ -1124,42 +1240,14 @@
// 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:
- return false;
+ if (!_isWhitespace(codeUnit)) {
+ return false;
}
}
return true;
@@ -1338,7 +1426,7 @@
// 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 @@
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 }) {
diff --git a/packages/flutter/test/widgets/editable_text_test.dart b/packages/flutter/test/widgets/editable_text_test.dart
index cee5cf3..ac74d7c 100644
--- a/packages/flutter/test/widgets/editable_text_test.dart
+++ b/packages/flutter/test/widgets/editable_text_test.dart
@@ -2975,12 +2975,66 @@
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.';
+ 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 @@
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(
- baseOffset: 0,
- extentOffset: 3,
- affinity: TextAffinity.upstream,
- )));
+ 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(
- baseOffset: 0,
- extentOffset: 0,
- affinity: TextAffinity.upstream,
- )));
+ 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(
- baseOffset: 0,
- extentOffset: 0,
- affinity: TextAffinity.upstream,
- )));
+ 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(
- baseOffset: 0,
- extentOffset: 6,
- affinity: TextAffinity.upstream,
- )));
+ 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(
- baseOffset: 0,
- extentOffset: 4,
- affinity: TextAffinity.upstream,
- )));
+ 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(
- baseOffset: 0,
- extentOffset: 20,
- affinity: TextAffinity.upstream,
- )));
+ 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(
- baseOffset: 21,
- extentOffset: 21,
- affinity: TextAffinity.downstream,
- )));
+ 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(
- baseOffset: 21,
- extentOffset: 40,
- affinity: TextAffinity.downstream,
- )));
+ 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 @@
LogicalKeyboardKey.arrowDown,
],
shift: true,
+ platform: platform,
);
- expect(selection, equals(const TextSelection(
- baseOffset: 21,
- extentOffset: testText.length,
- affinity: TextAffinity.downstream,
- )));
+ 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(
- baseOffset: 21,
- extentOffset: 58,
- affinity: TextAffinity.downstream,
- )));
+ 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(
- baseOffset: 0,
- extentOffset: testText.length,
- affinity: TextAffinity.downstream,
- )));
+ 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(
- baseOffset: 0,
- extentOffset: 0,
- affinity: TextAffinity.downstream,
- )));
+ 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(
- baseOffset: 10,
- extentOffset: 10,
- affinity: TextAffinity.downstream,
- )));
+ 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(
- baseOffset: 10,
- extentOffset: 7,
- affinity: TextAffinity.downstream,
- )));
+ 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(
- baseOffset: 10,
- extentOffset: 4,
- affinity: TextAffinity.downstream,
- )));
- expect(controller.text, equals(testText));
+ expect(
+ selection,
+ equals(
+ const TextSelection(
+ baseOffset: 10,
+ extentOffset: 4,
+ affinity: TextAffinity.downstream,
+ ),
+ ),
+ 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(
- baseOffset: 10,
- extentOffset: 4,
- affinity: TextAffinity.downstream,
- )));
- 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'));
+ expect(
+ selection,
+ equals(
+ const TextSelection(
+ baseOffset: 10,
+ extentOffset: 4,
+ affinity: TextAffinity.downstream,
+ ),
+ ),
+ reason: 'on $platform',
+ );
+ expect(
+ controller.text,
+ equals('Now time for\n'
+ 'all good people\n'
+ 'to come to the aid\n'
+ '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(
- baseOffset: 10,
- extentOffset: 4,
- affinity: TextAffinity.downstream,
- )));
- expect(controller.text, equals(testText));
+ expect(
+ selection,
+ equals(
+ const TextSelection(
+ baseOffset: 10,
+ extentOffset: 4,
+ affinity: TextAffinity.downstream,
+ ),
+ ),
+ 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(
- baseOffset: 0,
- extentOffset: testText.length,
- affinity: TextAffinity.downstream,
- )));
- expect(controller.text, equals(testText));
+ expect(
+ selection,
+ equals(
+ const TextSelection(
+ baseOffset: 0,
+ extentOffset: testText.length,
+ affinity: TextAffinity.downstream,
+ ),
+ ),
+ 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(
- baseOffset: 0,
- extentOffset: 72,
- affinity: TextAffinity.downstream,
- )));
- expect(controller.text, isEmpty);
+ expect(
+ selection,
+ equals(
+ const TextSelection(
+ baseOffset: 0,
+ extentOffset: 72,
+ affinity: TextAffinity.downstream,
+ ),
+ ),
+ 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