Unverified Commit 541bff40 authored by Justin McCandless's avatar Justin McCandless Committed by GitHub

Text Editing Movement Keys via Shortcuts (#75032)

Text editing shortcuts involving the arrow keys are no longer handled by RenderEditable's RawKeyboardListener, they use the new Shortcuts setup.  First PR in a plan to port all text editing keyboard handling to shortcuts.
parent 57dc5f29
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle; import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle;
import 'package:flutter/foundation.dart' show defaultTargetPlatform, kIsWeb; import 'package:flutter/foundation.dart' show defaultTargetPlatform;
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
...@@ -1202,7 +1202,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio ...@@ -1202,7 +1202,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio
), ),
); );
final Widget child = Semantics( return Semantics(
enabled: enabled, enabled: enabled,
onTap: !enabled || widget.readOnly ? null : () { onTap: !enabled || widget.readOnly ? null : () {
if (!controller.selection.isValid) { if (!controller.selection.isValid) {
...@@ -1226,13 +1226,5 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio ...@@ -1226,13 +1226,5 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio
), ),
), ),
); );
if (kIsWeb) {
return Shortcuts(
shortcuts: scrollShortcutOverrides,
child: child,
);
}
return child;
} }
} }
...@@ -884,7 +884,7 @@ class _MaterialAppState extends State<MaterialApp> { ...@@ -884,7 +884,7 @@ class _MaterialAppState extends State<MaterialApp> {
child: HeroControllerScope( child: HeroControllerScope(
controller: _heroController, controller: _heroController,
child: result, child: result,
) ),
); );
} }
} }
...@@ -1290,7 +1290,7 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements ...@@ -1290,7 +1290,7 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
semanticsMaxValueLength = null; semanticsMaxValueLength = null;
} }
child = MouseRegion( return MouseRegion(
cursor: effectiveMouseCursor, cursor: effectiveMouseCursor,
onEnter: (PointerEnterEvent event) => _handleHover(true), onEnter: (PointerEnterEvent event) => _handleHover(true),
onExit: (PointerExitEvent event) => _handleHover(false), onExit: (PointerExitEvent event) => _handleHover(false),
...@@ -1317,13 +1317,5 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements ...@@ -1317,13 +1317,5 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
), ),
), ),
); );
if (kIsWeb) {
return Shortcuts(
shortcuts: scrollShortcutOverrides,
child: child,
);
}
return child;
} }
} }
...@@ -652,12 +652,8 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -652,12 +652,8 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
// all the keyboard handling functions assume it is not. // all the keyboard handling functions assume it is not.
assert(selection != null); assert(selection != null);
final bool isWordModifierPressed = isMacOS ? keyEvent.isAltPressed : keyEvent.isControlPressed;
final bool isLineModifierPressed = isMacOS ? keyEvent.isMetaPressed : keyEvent.isAltPressed;
final bool isShortcutModifierPressed = isMacOS ? keyEvent.isMetaPressed : keyEvent.isControlPressed; final bool isShortcutModifierPressed = isMacOS ? keyEvent.isMetaPressed : keyEvent.isControlPressed;
if (_movementKeys.contains(key)) { if (isShortcutModifierPressed && _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 // _handleShortcuts depends on being started in the same stack invocation
// as the _handleKeyEvent method // as the _handleKeyEvent method
_handleShortcuts(key); _handleShortcuts(key);
...@@ -733,176 +729,993 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -733,176 +729,993 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
return 0; return 0;
} }
void _handleMovement( // Return a new selection that has been moved left once.
LogicalKeyboardKey key, { //
required bool wordModifier, // If it can't be moved left, the original TextSelection is returned.
required bool lineModifier, static TextSelection _moveGivenSelectionLeft(TextSelection selection, String text) {
required bool shift, // If the selection is already all the way left, there is nothing to do.
}){ if (selection.isCollapsed && selection.extentOffset <= 0) {
if (wordModifier && lineModifier) { return selection;
// If both modifiers are down, nothing happens on any of the platforms. }
int previousExtent;
if (selection.start != selection.end) {
previousExtent = selection.start;
} else {
previousExtent = previousCharacter(selection.extentOffset, text);
}
final TextSelection newSelection = selection.copyWith(
extentOffset: previousExtent,
);
final int newOffset = newSelection.extentOffset;
return TextSelection.fromPosition(TextPosition(offset: newOffset));
}
// Return a new selection that has been moved right once.
//
// If it can't be moved right, the original TextSelection is returned.
static TextSelection _moveGivenSelectionRight(TextSelection selection, String text) {
// If the selection is already all the way right, there is nothing to do.
if (selection.isCollapsed && selection.extentOffset >= text.length) {
return selection;
}
int nextExtent;
if (selection.start != selection.end) {
nextExtent = selection.end;
} else {
nextExtent = nextCharacter(selection.extentOffset, text);
}
final TextSelection nextSelection = selection.copyWith(extentOffset: nextExtent);
int newOffset = nextSelection.extentOffset;
newOffset = nextSelection.baseOffset > nextSelection.extentOffset
? nextSelection.baseOffset : nextSelection.extentOffset;
return TextSelection.fromPosition(TextPosition(offset: newOffset));
}
// Return the offset at the start of the nearest word to the left of the given
// offset.
static int _getLeftByWord(TextPainter textPainter, int offset, [bool includeWhitespace = true]) {
// If the offset is already all the way left, there is nothing to do.
if (offset <= 0) {
return offset;
}
// If we can just return the start of the text without checking for a word.
if (offset == 1) {
return 0;
}
final String text = textPainter.text!.toPlainText();
final int startPoint = previousCharacter(offset, text, includeWhitespace);
final TextRange word = textPainter.getWordBoundary(TextPosition(offset: startPoint));
return word.start;
}
// Return the offset at the end of the nearest word to the right of the given
// offset.
static int _getRightByWord(TextPainter textPainter, int offset, [bool includeWhitespace = true]) {
// If the selection is already all the way right, there is nothing to do.
final String text = textPainter.text!.toPlainText();
if (offset == text.length) {
return offset;
}
// If we can just return the end of the text without checking for a word.
if (offset == text.length - 1 || offset == text.length) {
return text.length;
}
final int startPoint = includeWhitespace || !_isWhitespace(text.codeUnitAt(offset))
? offset
: nextCharacter(offset, text, includeWhitespace);
final TextRange nextWord = textPainter.getWordBoundary(TextPosition(offset: startPoint));
return nextWord.end;
}
// Return the given TextSelection extended left to the beginning of the
// nearest word.
static TextSelection _extendGivenSelectionLeftByWord(TextPainter textPainter, TextSelection selection, [bool includeWhitespace = true]) {
// If the selection is already all the way left, there is nothing to do.
if (selection.isCollapsed && selection.extentOffset <= 0) {
return selection;
}
final int leftOffset = _getLeftByWord(textPainter, selection.extentOffset, includeWhitespace);
return selection.copyWith(
extentOffset: leftOffset,
);
}
// Return the given TextSelection extended right to the end of the nearest
// word.
static TextSelection _extendGivenSelectionRightByWord(TextPainter textPainter, TextSelection selection, [bool includeWhitespace = true]) {
// If the selection is already all the way right, there is nothing to do.
final String text = textPainter.text!.toPlainText();
if (selection.isCollapsed && selection.extentOffset == text.length) {
return selection;
}
final int rightOffset = _getRightByWord(textPainter, selection.extentOffset, includeWhitespace);
return selection.copyWith(
extentOffset: rightOffset,
);
}
// Return the given TextSelection moved left to the end of the nearest word.
//
// A TextSelection that isn't collapsed will be collapsed and moved from the
// extentOffset.
static TextSelection _moveGivenSelectionLeftByWord(TextPainter textPainter, TextSelection selection, [bool includeWhitespace = true]) {
// If the selection is already all the way left, there is nothing to do.
if (selection.isCollapsed && selection.extentOffset <= 0) {
return selection;
}
final int leftOffset = _getLeftByWord(textPainter, selection.extentOffset, includeWhitespace);
return selection.copyWith(
baseOffset: leftOffset,
extentOffset: leftOffset,
);
}
// Return the given TextSelection moved right to the end of the nearest word.
//
// A TextSelection that isn't collapsed will be collapsed and moved from the
// extentOffset.
static TextSelection _moveGivenSelectionRightByWord(TextPainter textPainter, TextSelection selection, [bool includeWhitespace = true]) {
// If the selection is already all the way right, there is nothing to do.
final String text = textPainter.text!.toPlainText();
if (selection.isCollapsed && selection.extentOffset == text.length) {
return selection;
}
final int rightOffset = _getRightByWord(textPainter, selection.extentOffset, includeWhitespace);
return selection.copyWith(
baseOffset: rightOffset,
extentOffset: rightOffset,
);
}
static TextSelection _extendGivenSelectionLeft(TextSelection selection, String text, [bool includeWhitespace = true]) {
// If the selection is already all the way left, there is nothing to do.
if (selection.extentOffset <= 0) {
return selection;
}
final int previousExtent = previousCharacter(selection.extentOffset, text, includeWhitespace);
return selection.copyWith(extentOffset: previousExtent);
}
static TextSelection _extendGivenSelectionRight(TextSelection selection, String text, [bool includeWhitespace = true]) {
// If the selection is already all the way right, there is nothing to do.
if (selection.extentOffset >= text.length) {
return selection;
}
final int nextExtent = nextCharacter(selection.extentOffset, text, includeWhitespace);
return selection.copyWith(extentOffset: nextExtent);
}
// Extend the current selection to the end of the field.
//
// If selectionEnabled is false, keeps the selection collapsed and moves it to
// the end.
//
// The given [SelectionChangedCause] indicates the cause of this change and
// will be passed to [onSelectionChanged].
//
// See also:
//
// * _extendSelectionToStart
void _extendSelectionToEnd(SelectionChangedCause cause) {
if (selection!.extentOffset == _plainText.length) {
return;
}
if (!selectionEnabled) {
return moveSelectionToEnd(cause);
}
final TextSelection nextSelection = selection!.copyWith(
extentOffset: _plainText.length,
);
_setSelection(nextSelection, cause);
}
// Extend the current selection to the start of the field.
//
// If selectionEnabled is false, keeps the selection collapsed and moves it to
// the start.
//
// The given [SelectionChangedCause] indicates the cause of this change and
// will be passed to [onSelectionChanged].
//
// See also:
//
// * _expandSelectionToEnd
void _extendSelectionToStart(SelectionChangedCause cause) {
if (selection!.extentOffset == 0) {
return; return;
} }
if (!selectionEnabled) {
return moveSelectionToStart(cause);
}
final TextSelection nextSelection = selection!.copyWith(
extentOffset: 0,
);
_setSelection(nextSelection, cause);
}
// Returns the TextPosition above or below the given offset.
TextPosition _getTextPositionVertical(int textOffset, double verticalOffset) {
final Offset caretOffset = _textPainter.getOffsetForCaret(TextPosition(offset: textOffset), _caretPrototype);
final Offset caretOffsetTranslated = caretOffset.translate(0.0, verticalOffset);
return _textPainter.getPositionForOffset(caretOffsetTranslated);
}
// Returns the TextPosition above the given offset into _plainText.
//
// If the offset is already on the first line, the given offset will be
// returned.
TextPosition _getTextPositionAbove(int offset) {
// The caret offset gives a location in the upper left hand corner of
// the caret so the middle of the line above is a half line above that
// point and the line below is 1.5 lines below that point.
final double preferredLineHeight = _textPainter.preferredLineHeight;
final double verticalOffset = -0.5 * preferredLineHeight;
return _getTextPositionVertical(offset, verticalOffset);
}
// Returns the TextPosition below the given offset into _plainText.
//
// If the offset is already on the last line, the given offset will be
// returned.
TextPosition _getTextPositionBelow(int offset) {
// The caret offset gives a location in the upper left hand corner of
// the caret so the middle of the line above is a half line above that
// point and the line below is 1.5 lines below that point.
final double preferredLineHeight = _textPainter.preferredLineHeight;
final double verticalOffset = 1.5 * preferredLineHeight;
return _getTextPositionVertical(offset, verticalOffset);
}
/// Keeping [selection]'s [TextSelection.baseOffset] fixed, move the
/// [TextSelection.extentOffset] down by one line.
///
/// If [selectionEnabled] is false, keeps the selection collapsed and just
/// moves it down.
///
/// {@template flutter.rendering.RenderEditable.cause}
/// The given [SelectionChangedCause] indicates the cause of this change and
/// will be passed to [onSelectionChanged].
/// {@endtemplate}
///
/// See also:
///
/// * [extendSelectionUp], which is same but in the opposite direction.
void extendSelectionDown(SelectionChangedCause cause) {
assert(selection != null); assert(selection != null);
TextSelection newSelection = selection!; // If the selection is collapsed at the end of the field already, then
// nothing happens.
final bool rightArrow = key == LogicalKeyboardKey.arrowRight; if (selection!.isCollapsed && selection!.extentOffset >= _plainText.length) {
final bool leftArrow = key == LogicalKeyboardKey.arrowLeft; return;
final bool upArrow = key == LogicalKeyboardKey.arrowUp; }
final bool downArrow = key == LogicalKeyboardKey.arrowDown; if (!selectionEnabled) {
return moveSelectionDown(cause);
if ((rightArrow || leftArrow) && !(rightArrow && leftArrow)) { }
// Jump to begin/end of word.
if (wordModifier) { final TextPosition positionBelow = _getTextPositionBelow(selection!.extentOffset);
// If control/option is pressed, we will decide which way to look for a late final TextSelection nextSelection;
// word based on which arrow is pressed. if (positionBelow.offset == selection!.extentOffset) {
if (leftArrow) { nextSelection = selection!.copyWith(
// When going left, we want to skip over any whitespace before the word, extentOffset: _plainText.length,
// so we go back to the first non-whitespace before asking for the word );
// boundary, since _selectWordAtOffset finds the word boundaries without _wasSelectingVerticallyWithKeyboard = true;
// including whitespace. } else if (_wasSelectingVerticallyWithKeyboard) {
final int startPoint = previousCharacter(newSelection.extentOffset, _plainText, false); nextSelection = selection!.copyWith(
final TextSelection textSelection = _selectWordAtOffset(TextPosition(offset: startPoint)); extentOffset: _cursorResetLocation,
newSelection = newSelection.copyWith(extentOffset: textSelection.baseOffset); );
_wasSelectingVerticallyWithKeyboard = false;
} else { } else {
// When going right, we want to skip over any whitespace after the word, nextSelection = selection!.copyWith(
// so we go forward to the first non-whitespace character before asking extentOffset: positionBelow.offset,
// for the word bounds, since _selectWordAtOffset finds the word );
// boundaries without including whitespace. _cursorResetLocation = nextSelection.extentOffset;
final int startPoint = nextCharacter(newSelection.extentOffset, _plainText, false); }
final TextSelection textSelection = _selectWordAtOffset(TextPosition(offset: startPoint));
newSelection = newSelection.copyWith(extentOffset: textSelection.extentOffset); _setSelection(nextSelection, cause);
} }
} else if (lineModifier) {
// If control/command is pressed, we will decide which way to expand to /// Expand the current [selection] to the end of the field.
// the beginning/end of the line based on which arrow is pressed. ///
if (leftArrow) { /// The selection will never shrink. The [TextSelection.extentOffset] will
// always be at the end of the field, regardless of the original order of
/// [TextSelection.baseOffset] and [TextSelection.extentOffset].
///
/// If [selectionEnabled] is false, keeps the selection collapsed and moves it
/// to the end.
///
/// {@macro flutter.rendering.RenderEditable.cause}
///
/// See also:
///
/// * [expandSelectionToStart], which is same but in the opposite direction.
void expandSelectionToEnd(SelectionChangedCause cause) {
assert(selection != null);
if (selection!.extentOffset == _plainText.length) {
return;
}
if (!selectionEnabled) {
return moveSelectionToEnd(cause);
}
final int firstOffset = math.max(0, math.min(
selection!.baseOffset,
selection!.extentOffset,
));
final TextSelection nextSelection = TextSelection(
baseOffset: firstOffset,
extentOffset: _plainText.length,
);
_setSelection(nextSelection, cause);
}
/// Keeping [selection]'s [TextSelection.baseOffset] fixed, move the
/// [TextSelection.extentOffset] left.
///
/// If [selectionEnabled] is false, keeps the selection collapsed and moves it
/// left.
///
/// {@macro flutter.rendering.RenderEditable.cause}
///
/// See also:
///
/// * [extendSelectionRight], which is same but in the opposite direction.
void extendSelectionLeft(SelectionChangedCause cause) {
assert(selection != null);
if (!selectionEnabled) {
return moveSelectionLeft(cause);
}
final TextSelection nextSelection = _extendGivenSelectionLeft(
selection!,
_plainText,
);
if (nextSelection == selection) {
return;
}
final int distance = selection!.extentOffset - nextSelection.extentOffset;
_cursorResetLocation -= distance;
_setSelection(nextSelection, cause);
}
/// Extend the current [selection] to the start of
/// [TextSelection.extentOffset]'s line.
///
/// Uses [TextSelection.baseOffset] as a pivot point and doesn't change it.
/// If [TextSelection.extentOffset] is right of [TextSelection.baseOffset],
/// then collapses the selection.
///
/// If [selectionEnabled] is false, keeps the selection collapsed and moves it
/// left by line.
///
/// {@macro flutter.rendering.RenderEditable.cause}
///
/// See also:
///
/// * [extendSelectionRightByLine], which is same but in the opposite
/// direction.
/// * [expandSelectionRightByLine], which strictly grows the selection
/// regardless of the order.
void extendSelectionLeftByLine(SelectionChangedCause cause) {
assert(selection != null);
if (!selectionEnabled) {
return moveSelectionLeftByLine(cause);
}
// When going left, we want to skip over any whitespace before the line, // 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 // so we go back to the first non-whitespace before asking for the line
// bounds, since _selectLineAtOffset finds the line boundaries without // bounds, since _getLineAtOffset finds the line boundaries without
// including whitespace (like the newline). // including whitespace (like the newline).
final int startPoint = previousCharacter(newSelection.extentOffset, _plainText, false); final int startPoint = previousCharacter(selection!.extentOffset, _plainText, false);
final TextSelection textSelection = _selectLineAtOffset(TextPosition(offset: startPoint)); final TextSelection selectedLine = _getLineAtOffset(TextPosition(offset: startPoint));
newSelection = newSelection.copyWith(extentOffset: textSelection.baseOffset);
late final TextSelection nextSelection;
if (selection!.extentOffset > selection!.baseOffset) {
nextSelection = selection!.copyWith(
extentOffset: selection!.baseOffset,
);
} else { } else {
// When going right, we want to skip over any whitespace after the line, nextSelection = selection!.copyWith(
// so we go forward to the first non-whitespace character before asking extentOffset: selectedLine.baseOffset,
// for the line bounds, since _selectLineAtOffset finds the line );
// boundaries without including whitespace (like the newline).
final int startPoint = nextCharacter(newSelection.extentOffset, _plainText, false);
final TextSelection textSelection = _selectLineAtOffset(TextPosition(offset: startPoint));
newSelection = newSelection.copyWith(extentOffset: textSelection.extentOffset);
} }
} else {
// The directional arrows move the TextSelection.extentOffset, while the _setSelection(nextSelection, cause);
// base remains fixed.
if (rightArrow && newSelection.extentOffset < _plainText.length) {
int nextExtent;
if (!shift && !wordModifier && !lineModifier && newSelection.start != newSelection.end) {
nextExtent = newSelection.end;
} else {
nextExtent = nextCharacter(newSelection.extentOffset, _plainText);
} }
final int distance = nextExtent - newSelection.extentOffset;
newSelection = newSelection.copyWith(extentOffset: nextExtent); /// Keeping [selection]'s [TextSelection.baseOffset] fixed, move the
if (shift) { /// [TextSelection.extentOffset] right.
///
/// If [selectionEnabled] is false, keeps the selection collapsed and moves it
/// right.
///
/// {@macro flutter.rendering.RenderEditable.cause}
///
/// See also:
///
/// * [extendSelectionLeft], which is same but in the opposite direction.
void extendSelectionRight(SelectionChangedCause cause) {
assert(selection != null);
if (!selectionEnabled) {
return moveSelectionRight(cause);
}
final TextSelection nextSelection = _extendGivenSelectionRight(
selection!,
_plainText,
);
if (nextSelection == selection) {
return;
}
final int distance = nextSelection.extentOffset - selection!.extentOffset;
_cursorResetLocation += distance; _cursorResetLocation += distance;
_setSelection(nextSelection, cause);
} }
} else if (leftArrow && newSelection.extentOffset > 0) {
int previousExtent; /// Extend the current [selection] to the end of [TextSelection.extentOffset]'s
if (!shift && !wordModifier && !lineModifier && newSelection.start != newSelection.end) { /// line.
previousExtent = newSelection.start; ///
/// Uses [TextSelection.baseOffset] as a pivot point and doesn't change it. If
/// [TextSelection.extentOffset] is left of [TextSelection.baseOffset], then
/// collapses the selection.
///
/// If [selectionEnabled] is false, keeps the selection collapsed and moves it
/// right by line.
///
/// {@macro flutter.rendering.RenderEditable.cause}
///
/// See also:
///
/// * [extendSelectionLeftByLine], which is same but in the opposite
/// direction.
/// * [expandSelectionRightByLine], which strictly grows the selection
/// regardless of the order.
void extendSelectionRightByLine(SelectionChangedCause cause) {
assert(selection != null);
if (!selectionEnabled) {
return moveSelectionRightByLine(cause);
}
final int startPoint = nextCharacter(selection!.extentOffset, _plainText, false);
final TextSelection selectedLine = _getLineAtOffset(TextPosition(offset: startPoint));
late final TextSelection nextSelection;
if (selection!.extentOffset < selection!.baseOffset) {
nextSelection = selection!.copyWith(
extentOffset: selection!.baseOffset,
);
} else { } else {
previousExtent = previousCharacter(newSelection.extentOffset, _plainText); nextSelection = selection!.copyWith(
extentOffset: selectedLine.extentOffset,
);
} }
final int distance = newSelection.extentOffset - previousExtent;
newSelection = newSelection.copyWith(extentOffset: previousExtent); _setSelection(nextSelection, cause);
if (shift) {
_cursorResetLocation -= distance;
} }
/// Keeping [selection]'s [TextSelection.baseOffset] fixed, move the
/// [TextSelection.extentOffset] up by one
/// line.
///
/// If [selectionEnabled] is false, keeps the selection collapsed and moves it
/// up.
///
/// {@macro flutter.rendering.RenderEditable.cause}
///
/// See also:
///
/// * [extendSelectionDown], which is the same but in the opposite
/// direction.
void extendSelectionUp(SelectionChangedCause cause) {
assert(selection != null);
// If the selection is collapsed at the beginning of the field already, then
// nothing happens.
if (selection!.isCollapsed && selection!.extentOffset <= 0.0) {
return;
} }
if (!selectionEnabled) {
return moveSelectionUp(cause);
} }
final TextPosition positionAbove = _getTextPositionAbove(selection!.extentOffset);
late final TextSelection nextSelection;
if (positionAbove.offset == selection!.extentOffset) {
nextSelection = selection!.copyWith(
extentOffset: 0,
);
_wasSelectingVerticallyWithKeyboard = true;
} else if (_wasSelectingVerticallyWithKeyboard) {
nextSelection = selection!.copyWith(
baseOffset: selection!.baseOffset,
extentOffset: _cursorResetLocation,
);
_wasSelectingVerticallyWithKeyboard = false;
} else {
nextSelection = selection!.copyWith(
baseOffset: selection!.baseOffset,
extentOffset: positionAbove.offset,
);
_cursorResetLocation = nextSelection.extentOffset;
} }
// Handles moving the cursor vertically as well as taking care of the _setSelection(nextSelection, cause);
// case where the user moves the cursor to the end or beginning of the text }
// and then back up or down.
if (downArrow || upArrow) { /// Expand the current [selection] to the start of the field.
if (lineModifier) { ///
if (upArrow) { /// The selection will never shrink. The [TextSelection.extentOffset] will
// Extend the selection to the beginning of the field. /// always be at the start of the field, regardless of the original order of
final int upperOffset = math.max(0, math.max( /// [TextSelection.baseOffset] and [TextSelection.extentOffset].
newSelection.baseOffset, ///
newSelection.extentOffset, /// If [selectionEnabled] is false, keeps the selection collapsed and moves it
/// to the start.
///
/// {@macro flutter.rendering.RenderEditable.cause}
///
/// See also:
///
/// * [expandSelectionToEnd], which is the same but in the opposite
/// direction.
void expandSelectionToStart(SelectionChangedCause cause) {
assert(selection != null);
if (selection!.extentOffset == 0) {
return;
}
if (!selectionEnabled) {
return moveSelectionToStart(cause);
}
final int lastOffset = math.max(0, math.max(
selection!.baseOffset,
selection!.extentOffset,
)); ));
newSelection = TextSelection( final TextSelection nextSelection = TextSelection(
baseOffset: shift ? upperOffset : 0, baseOffset: lastOffset,
extentOffset: 0, extentOffset: 0,
); );
_setSelection(nextSelection, cause);
}
/// Expand the current [selection] to the start of the line.
///
/// The selection will never shrink. The upper offset will be expanded to the
/// beginning of its line, and the original order of baseOffset and
/// [TextSelection.extentOffset] will be preserved.
///
/// If [selectionEnabled] is false, keeps the selection collapsed and moves it
/// left by line.
///
/// {@macro flutter.rendering.RenderEditable.cause}
///
/// See also:
///
/// * [expandSelectionRightByLine], which is the same but in the opposite
/// direction.
void expandSelectionLeftByLine(SelectionChangedCause cause) {
assert(selection != null);
if (!selectionEnabled) {
return moveSelectionLeftByLine(cause);
}
final int firstOffset = math.min(selection!.baseOffset, selection!.extentOffset);
final int startPoint = previousCharacter(firstOffset, _plainText, false);
final TextSelection selectedLine = _getLineAtOffset(TextPosition(offset: startPoint));
late final TextSelection nextSelection;
if (selection!.extentOffset <= selection!.baseOffset) {
nextSelection = selection!.copyWith(
extentOffset: selectedLine.baseOffset,
);
} else { } else {
// Extend the selection to the end of the field. nextSelection = selection!.copyWith(
final int lowerOffset = math.max(0, math.min( baseOffset: selectedLine.baseOffset,
newSelection.baseOffset, );
newSelection.extentOffset, }
));
newSelection = TextSelection( _setSelection(nextSelection, cause);
baseOffset: shift ? lowerOffset : _plainText.length, }
extentOffset: _plainText.length,
/// Extend the current [selection] to the previous start of a word.
///
/// {@macro flutter.rendering.RenderEditable.cause}
///
/// By default, includeWhitespace is set to true, meaning that whitespace can
/// be considered a word in itself. If set to false, the selection will be
/// extended past any whitespace and the first word following the whitespace.
///
/// See also:
///
/// * [extendSelectionRightByWord], which is the same but in the opposite
/// direction.
void extendSelectionLeftByWord(SelectionChangedCause cause, [bool includeWhitespace = true]) {
assert(selection != null);
// When the text is obscured, the whole thing is treated as one big word.
if (obscureText) {
return _extendSelectionToStart(cause);
}
assert(_textLayoutLastMaxWidth == constraints.maxWidth &&
_textLayoutLastMinWidth == constraints.minWidth,
'Last width ($_textLayoutLastMinWidth, $_textLayoutLastMaxWidth) not the same as max width constraint (${constraints.minWidth}, ${constraints.maxWidth}).');
final TextSelection nextSelection = _extendGivenSelectionLeftByWord(
_textPainter,
selection!,
includeWhitespace,
); );
if (nextSelection == selection) {
return;
}
_setSelection(nextSelection, cause);
}
/// Extend the current [selection] to the next end of a word.
///
/// {@macro flutter.rendering.RenderEditable.cause}
///
/// By default, includeWhitespace is set to true, meaning that whitespace can
/// be considered a word in itself. If set to false, the selection will be
/// extended past any whitespace and the first word following the whitespace.
///
/// See also:
///
/// * [extendSelectionLeftByWord], which is the same but in the opposite
/// direction.
void extendSelectionRightByWord(SelectionChangedCause cause, [bool includeWhitespace = true]) {
assert(selection != null);
// When the text is obscured, the whole thing is treated as one big word.
if (obscureText) {
return _extendSelectionToEnd(cause);
} }
assert(_textLayoutLastMaxWidth == constraints.maxWidth &&
_textLayoutLastMinWidth == constraints.minWidth,
'Last width ($_textLayoutLastMinWidth, $_textLayoutLastMaxWidth) not the same as max width constraint (${constraints.minWidth}, ${constraints.maxWidth}).');
final TextSelection nextSelection = _extendGivenSelectionRightByWord(
_textPainter,
selection!,
includeWhitespace,
);
if (nextSelection == selection) {
return;
}
_setSelection(nextSelection, cause);
}
/// Expand the current [selection] to the end of the line.
///
/// The selection will never shrink. The lower offset will be expanded to the
/// end of its line and the original order of [TextSelection.baseOffset] and
/// [TextSelection.extentOffset] will be preserved.
///
/// If [selectionEnabled] is false, keeps the selection collapsed and moves it
/// right by line.
///
/// {@macro flutter.rendering.RenderEditable.cause}
///
/// See also:
///
/// * [expandSelectionLeftByLine], which is the same but in the opposite
/// direction.
void expandSelectionRightByLine(SelectionChangedCause cause) {
assert(selection != null);
if (!selectionEnabled) {
return moveSelectionRightByLine(cause);
}
final int lastOffset = math.max(selection!.baseOffset, selection!.extentOffset);
final int startPoint = nextCharacter(lastOffset, _plainText, false);
final TextSelection selectedLine = _getLineAtOffset(TextPosition(offset: startPoint));
late final TextSelection nextSelection;
if (selection!.extentOffset >= selection!.baseOffset) {
nextSelection = selection!.copyWith(
extentOffset: selectedLine.extentOffset,
);
} else { } else {
// The caret offset gives a location in the upper left hand corner of nextSelection = selection!.copyWith(
// the caret so the middle of the line above is a half line above that baseOffset: selectedLine.extentOffset,
// point and the line below is 1.5 lines below that point. );
final double preferredLineHeight = _textPainter.preferredLineHeight; }
final double verticalOffset = upArrow ? -0.5 * preferredLineHeight : 1.5 * preferredLineHeight;
final Offset caretOffset = _textPainter.getOffsetForCaret(TextPosition(offset: newSelection.extentOffset), _caretPrototype); _setSelection(nextSelection, cause);
final Offset caretOffsetTranslated = caretOffset.translate(0.0, verticalOffset); }
final TextPosition position = _textPainter.getPositionForOffset(caretOffsetTranslated);
/// Move the current [selection] to the next line.
// To account for the possibility where the user vertically highlights ///
// all the way to the top or bottom of the text, we hold the previous /// {@macro flutter.rendering.RenderEditable.cause}
// cursor location. This allows us to restore to this position in the ///
// case that the user wants to unhighlight some text. /// See also:
if (position.offset == newSelection.extentOffset) { ///
if (downArrow) { /// * [moveSelectionUp], which is the same but in the opposite direction.
newSelection = newSelection.copyWith(extentOffset: _plainText.length); void moveSelectionDown(SelectionChangedCause cause) {
} else if (upArrow) { assert(selection != null);
newSelection = newSelection.copyWith(extentOffset: 0);
} // If the selection is collapsed at the end of the field already, then
_wasSelectingVerticallyWithKeyboard = shift; // nothing happens.
} else if (_wasSelectingVerticallyWithKeyboard && shift) { if (selection!.isCollapsed && selection!.extentOffset >= _plainText.length) {
newSelection = newSelection.copyWith(extentOffset: _cursorResetLocation); return;
}
final TextPosition positionBelow = _getTextPositionBelow(selection!.extentOffset);
late final TextSelection nextSelection;
if (positionBelow.offset == selection!.extentOffset) {
nextSelection = selection!.copyWith(
baseOffset: _plainText.length,
extentOffset: _plainText.length,
);
_wasSelectingVerticallyWithKeyboard = false; _wasSelectingVerticallyWithKeyboard = false;
} else { } else {
newSelection = newSelection.copyWith(extentOffset: position.offset); nextSelection = TextSelection.fromPosition(positionBelow);
_cursorResetLocation = newSelection.extentOffset; _cursorResetLocation = nextSelection.extentOffset;
}
_setSelection(nextSelection, cause);
}
/// Move the current [selection] left by one character.
///
/// {@macro flutter.rendering.RenderEditable.cause}
///
/// See also:
///
/// * [moveSelectionRight], which is the same but in the opposite direction.
void moveSelectionLeft(SelectionChangedCause cause) {
assert(selection != null);
final TextSelection nextSelection = _moveGivenSelectionLeft(
selection!,
_plainText,
);
if (nextSelection == selection) {
return;
}
_cursorResetLocation -= selection!.extentOffset - nextSelection.extentOffset;
_setSelection(nextSelection, cause);
}
/// Move the current [selection] to the leftmost of the current line.
///
/// {@macro flutter.rendering.RenderEditable.cause}
///
/// See also:
///
/// * [moveSelectionRightByLine], which is the same but in the opposite
/// direction.
void moveSelectionLeftByLine(SelectionChangedCause cause) {
assert(selection != null);
// If the previous character is the edge of a line, don't do anything.
final int previousPoint = previousCharacter(selection!.extentOffset, _plainText, true);
final TextSelection line = _getLineAtOffset(TextPosition(offset: previousPoint));
if (line.extentOffset == previousPoint) {
return;
}
// 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 _getLineAtOffset finds the line boundaries without
// including whitespace (like the newline).
final int startPoint = previousCharacter(selection!.extentOffset, _plainText, false);
final TextSelection selectedLine = _getLineAtOffset(TextPosition(offset: startPoint));
final TextSelection nextSelection = TextSelection.collapsed(
offset: selectedLine.baseOffset,
);
_setSelection(nextSelection, cause);
}
/// Move the current [selection] to the previous start of a word.
///
/// {@macro flutter.rendering.RenderEditable.cause}
///
/// By default, includeWhitespace is set to true, meaning that whitespace can
/// be considered a word in itself. If set to false, the selection will be
/// moved past any whitespace and the first word following the whitespace.
///
/// See also:
///
/// * [moveSelectionRightByWord], which is the same but in the opposite
/// direction.
void moveSelectionLeftByWord(SelectionChangedCause cause, [bool includeWhitespace = true]) {
assert(selection != null);
// When the text is obscured, the whole thing is treated as one big word.
if (obscureText) {
return moveSelectionToStart(cause);
} }
assert(_textLayoutLastMaxWidth == constraints.maxWidth &&
_textLayoutLastMinWidth == constraints.minWidth,
'Last width ($_textLayoutLastMinWidth, $_textLayoutLastMaxWidth) not the same as max width constraint (${constraints.minWidth}, ${constraints.maxWidth}).');
final TextSelection nextSelection = _moveGivenSelectionLeftByWord(
_textPainter,
selection!,
includeWhitespace,
);
if (nextSelection == selection) {
return;
} }
_setSelection(nextSelection, cause);
} }
// Just place the collapsed selection at the end or beginning of the region /// Move the current [selection] to the right by one character.
// if shift isn't down or selection isn't enabled. ///
if (!shift || !selectionEnabled) { /// {@macro flutter.rendering.RenderEditable.cause}
// We want to put the cursor at the correct location depending on which ///
// arrow is used while there is a selection. /// See also:
int newOffset = newSelection.extentOffset; ///
if (!selection!.isCollapsed) { /// * [moveSelectionLeft], which is the same but in the opposite direction.
if (leftArrow) { void moveSelectionRight(SelectionChangedCause cause) {
newOffset = newSelection.baseOffset < newSelection.extentOffset ? newSelection.baseOffset : newSelection.extentOffset; assert(selection != null);
} else if (rightArrow) {
newOffset = newSelection.baseOffset > newSelection.extentOffset ? newSelection.baseOffset : newSelection.extentOffset; final TextSelection nextSelection = _moveGivenSelectionRight(
selection!,
_plainText,
);
if (nextSelection == selection) {
return;
} }
_setSelection(nextSelection, cause);
} }
newSelection = TextSelection.fromPosition(TextPosition(offset: newOffset));
/// Move the current [selection] to the rightmost point of the current line.
///
/// {@macro flutter.rendering.RenderEditable.cause}
///
/// See also:
///
/// * [moveSelectionLeftByLine], which is the same but in the opposite
/// direction.
void moveSelectionRightByLine(SelectionChangedCause cause) {
assert(selection != null);
// If already at the right edge of the line, do nothing.
final TextSelection currentLine = _getLineAtOffset(TextPosition(
offset: selection!.extentOffset,
));
if (currentLine.extentOffset == selection!.extentOffset) {
return;
} }
_setSelection( // When going right, we want to skip over any whitespace after the line,
newSelection, // so we go forward to the first non-whitespace character before asking
SelectionChangedCause.keyboard, // for the line bounds, since _getLineAtOffset finds the line
// boundaries without including whitespace (like the newline).
final int startPoint = nextCharacter(selection!.extentOffset, _plainText, false);
final TextSelection selectedLine = _getLineAtOffset(TextPosition(offset: startPoint));
final TextSelection nextSelection = TextSelection.collapsed(
offset: selectedLine.extentOffset,
); );
_setSelection(nextSelection, cause);
}
/// Move the current [selection] to the next end of a word.
///
/// {@macro flutter.rendering.RenderEditable.cause}
///
/// By default, includeWhitespace is set to true, meaning that whitespace can
/// be considered a word in itself. If set to false, the selection will be
/// moved past any whitespace and the first word following the whitespace.
///
/// See also:
///
/// * [moveSelectionLeftByWord], which is the same but in the opposite
/// direction.
void moveSelectionRightByWord(SelectionChangedCause cause, [bool includeWhitespace = true]) {
assert(selection != null);
// When the text is obscured, the whole thing is treated as one big word.
if (obscureText) {
return moveSelectionToEnd(cause);
}
assert(_textLayoutLastMaxWidth == constraints.maxWidth &&
_textLayoutLastMinWidth == constraints.minWidth,
'Last width ($_textLayoutLastMinWidth, $_textLayoutLastMaxWidth) not the same as max width constraint (${constraints.minWidth}, ${constraints.maxWidth}).');
final TextSelection nextSelection = _moveGivenSelectionRightByWord(
_textPainter,
selection!,
includeWhitespace,
);
if (nextSelection == selection) {
return;
}
_setSelection(nextSelection, cause);
}
/// Move the current [selection] to the end of the field.
///
/// {@macro flutter.rendering.RenderEditable.cause}
///
/// See also:
///
/// * [moveSelectionToStart], which is the same but in the opposite
/// direction.
void moveSelectionToEnd(SelectionChangedCause cause) {
assert(selection != null);
if (selection!.isCollapsed && selection!.extentOffset == _plainText.length) {
return;
}
final TextSelection nextSelection = TextSelection.collapsed(
offset: _plainText.length,
);
_setSelection(nextSelection, cause);
}
/// Move the current [selection] to the start of the field.
///
/// {@macro flutter.rendering.RenderEditable.cause}
///
/// See also:
///
/// * [moveSelectionToEnd], which is the same but in the opposite direction.
void moveSelectionToStart(SelectionChangedCause cause) {
assert(selection != null);
if (selection!.isCollapsed && selection!.extentOffset == 0) {
return;
}
const TextSelection nextSelection = TextSelection.collapsed(offset: 0);
_setSelection(nextSelection, cause);
}
/// Move the current [selection] up by one line.
///
/// {@macro flutter.rendering.RenderEditable.cause}
///
/// See also:
///
/// * [moveSelectionDown], which is the same but in the opposite direction.
void moveSelectionUp(SelectionChangedCause cause) {
assert(selection != null);
// If the selection is collapsed at the beginning of the field already, then
// nothing happens.
if (selection!.isCollapsed && selection!.extentOffset <= 0.0) {
return;
}
final TextPosition positionAbove = _getTextPositionAbove(selection!.extentOffset);
late final TextSelection nextSelection;
if (positionAbove.offset == selection!.extentOffset) {
nextSelection = selection!.copyWith(baseOffset: 0, extentOffset: 0);
_wasSelectingVerticallyWithKeyboard = false;
} else {
nextSelection = selection!.copyWith(
baseOffset: positionAbove.offset,
extentOffset: positionAbove.offset,
);
_cursorResetLocation = nextSelection.extentOffset;
}
_setSelection(nextSelection, cause);
} }
// Handles shortcut functionality including cut, copy, paste and select all // Handles shortcut functionality including cut, copy, paste and select all
...@@ -1150,10 +1963,14 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -1150,10 +1963,14 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
_hasFocus = value; _hasFocus = value;
if (_hasFocus) { if (_hasFocus) {
assert(!_listenerAttached); assert(!_listenerAttached);
// TODO(justinmc): This listener should be ported to Actions and removed.
// https://github.com/flutter/flutter/issues/75004
RawKeyboard.instance.addListener(_handleKeyEvent); RawKeyboard.instance.addListener(_handleKeyEvent);
_listenerAttached = true; _listenerAttached = true;
} else { } else {
assert(_listenerAttached); assert(_listenerAttached);
// TODO(justinmc): This listener should be ported to Actions and removed.
// https://github.com/flutter/flutter/issues/75004
RawKeyboard.instance.removeListener(_handleKeyEvent); RawKeyboard.instance.removeListener(_handleKeyEvent);
_listenerAttached = false; _listenerAttached = false;
} }
...@@ -1752,6 +2569,8 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -1752,6 +2569,8 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
_longPress.dispose(); _longPress.dispose();
_offset.removeListener(markNeedsPaint); _offset.removeListener(markNeedsPaint);
_showCursor.removeListener(_showHideCursor); _showCursor.removeListener(_showHideCursor);
// TODO(justinmc): This listener should be ported to Actions and removed.
// https://github.com/flutter/flutter/issues/75004
if (_listenerAttached) if (_listenerAttached)
RawKeyboard.instance.removeListener(_handleKeyEvent); RawKeyboard.instance.removeListener(_handleKeyEvent);
super.detach(); super.detach();
...@@ -2129,15 +2948,18 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -2129,15 +2948,18 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
assert(from != null); assert(from != null);
_layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth); _layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth);
final TextPosition firstPosition = _textPainter.getPositionForOffset(globalToLocal(from - _paintOffset)); final TextPosition firstPosition = _textPainter.getPositionForOffset(globalToLocal(from - _paintOffset));
final TextSelection firstWord = _selectWordAtOffset(firstPosition); final TextSelection firstWord = _getWordAtOffset(firstPosition);
final TextSelection lastWord = to == null ? final TextSelection lastWord = to == null ?
firstWord : _selectWordAtOffset(_textPainter.getPositionForOffset(globalToLocal(to - _paintOffset))); firstWord : _getWordAtOffset(_textPainter.getPositionForOffset(globalToLocal(to - _paintOffset)));
final TextSelection newSelection = TextSelection(
_setSelection(
TextSelection(
baseOffset: firstWord.base.offset, baseOffset: firstWord.base.offset,
extentOffset: lastWord.extent.offset, extentOffset: lastWord.extent.offset,
affinity: firstWord.affinity, affinity: firstWord.affinity,
),
cause,
); );
_setSelection(newSelection, cause);
} }
/// Move the selection to the beginning or end of a word. /// Move the selection to the beginning or end of a word.
...@@ -2158,7 +2980,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -2158,7 +2980,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
_setSelection(newSelection, cause); _setSelection(newSelection, cause);
} }
TextSelection _selectWordAtOffset(TextPosition position) { TextSelection _getWordAtOffset(TextPosition position) {
assert(_textLayoutLastMaxWidth == constraints.maxWidth && assert(_textLayoutLastMaxWidth == constraints.maxWidth &&
_textLayoutLastMinWidth == constraints.minWidth, _textLayoutLastMinWidth == constraints.minWidth,
'Last width ($_textLayoutLastMinWidth, $_textLayoutLastMaxWidth) not the same as max width constraint (${constraints.minWidth}, ${constraints.maxWidth}).'); 'Last width ($_textLayoutLastMinWidth, $_textLayoutLastMaxWidth) not the same as max width constraint (${constraints.minWidth}, ${constraints.maxWidth}).');
...@@ -2201,7 +3023,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -2201,7 +3023,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
return TextSelection(baseOffset: word.start, extentOffset: word.end); return TextSelection(baseOffset: word.start, extentOffset: word.end);
} }
TextSelection _selectLineAtOffset(TextPosition position) { TextSelection _getLineAtOffset(TextPosition position) {
assert(_textLayoutLastMaxWidth == constraints.maxWidth && assert(_textLayoutLastMaxWidth == constraints.maxWidth &&
_textLayoutLastMinWidth == constraints.minWidth, _textLayoutLastMinWidth == constraints.minWidth,
'Last width ($_textLayoutLastMinWidth, $_textLayoutLastMaxWidth) not the same as max width constraint (${constraints.minWidth}, ${constraints.maxWidth}).'); 'Last width ($_textLayoutLastMinWidth, $_textLayoutLastMaxWidth) not the same as max width constraint (${constraints.minWidth}, ${constraints.maxWidth}).');
......
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:ui' show hashValues, TextAffinity, TextPosition, TextRange; import 'dart:ui' show hashValues, TextAffinity, TextPosition, TextRange;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
......
...@@ -12,6 +12,8 @@ import 'actions.dart'; ...@@ -12,6 +12,8 @@ import 'actions.dart';
import 'banner.dart'; import 'banner.dart';
import 'basic.dart'; import 'basic.dart';
import 'binding.dart'; import 'binding.dart';
import 'default_text_editing_actions.dart';
import 'default_text_editing_shortcuts.dart';
import 'focus_traversal.dart'; import 'focus_traversal.dart';
import 'framework.dart'; import 'framework.dart';
import 'localizations.dart'; import 'localizations.dart';
...@@ -861,6 +863,9 @@ class WidgetsApp extends StatefulWidget { ...@@ -861,6 +863,9 @@ class WidgetsApp extends StatefulWidget {
/// The default map of keyboard shortcuts to intents for the application. /// The default map of keyboard shortcuts to intents for the application.
/// ///
/// By default, this is set to [WidgetsApp.defaultShortcuts]. /// By default, this is set to [WidgetsApp.defaultShortcuts].
///
/// Passing this will not replace [DefaultTextEditingShortcuts]. These can be
/// overridden by using a [Shortcuts] widget lower in the widget tree.
/// {@endtemplate} /// {@endtemplate}
/// ///
/// {@tool snippet} /// {@tool snippet}
...@@ -910,6 +915,9 @@ class WidgetsApp extends StatefulWidget { ...@@ -910,6 +915,9 @@ class WidgetsApp extends StatefulWidget {
/// the [actions] for this app. You may also add to the bindings, or override /// the [actions] for this app. You may also add to the bindings, or override
/// specific bindings for a widget subtree, by adding your own [Actions] /// specific bindings for a widget subtree, by adding your own [Actions]
/// widget. /// widget.
///
/// Passing this will not replace [DefaultTextEditingActions]. These can be
/// overridden by placing an [Actions] widget lower in the widget tree.
/// {@endtemplate} /// {@endtemplate}
/// ///
/// {@tool snippet} /// {@tool snippet}
...@@ -1621,13 +1629,18 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver { ...@@ -1621,13 +1629,18 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
: _locale!; : _locale!;
assert(_debugCheckLocalizations(appLocale)); assert(_debugCheckLocalizations(appLocale));
return RootRestorationScope( return RootRestorationScope(
restorationId: widget.restorationScopeId, restorationId: widget.restorationScopeId,
child: Shortcuts( child: Shortcuts(
shortcuts: widget.shortcuts ?? WidgetsApp.defaultShortcuts,
debugLabel: '<Default WidgetsApp Shortcuts>', debugLabel: '<Default WidgetsApp Shortcuts>',
shortcuts: widget.shortcuts ?? WidgetsApp.defaultShortcuts,
// DefaultTextEditingShortcuts is nested inside Shortcuts so that it can
// fall through to the defaultShortcuts.
child: DefaultTextEditingShortcuts(
child: Actions( child: Actions(
actions: widget.actions ?? WidgetsApp.defaultActions, actions: widget.actions ?? WidgetsApp.defaultActions,
child: DefaultTextEditingActions(
child: FocusTraversalGroup( child: FocusTraversalGroup(
policy: ReadingOrderTraversalPolicy(), policy: ReadingOrderTraversalPolicy(),
child: _MediaQueryFromWindow( child: _MediaQueryFromWindow(
...@@ -1640,6 +1653,8 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver { ...@@ -1640,6 +1653,8 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
), ),
), ),
), ),
),
),
); );
} }
} }
......
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'actions.dart';
import 'editable_text.dart';
import 'framework.dart';
import 'text_editing_action.dart';
import 'text_editing_intents.dart';
/// An [Actions] widget that handles the default text editing behavior for
/// Flutter on the current platform.
///
/// This default behavior can be overridden by placing an [Actions] widget lower
/// in the widget tree than this. See [DefaultTextEditingShortcuts] for an example of
/// remapping keyboard keys to an existing text editing [Intent].
///
/// See also:
///
/// * [DefaultTextEditingShortcuts], which maps keyboard keys to many of the
/// [Intent]s that are handled here.
/// * [WidgetsApp], which creates a DefaultTextEditingShortcuts.
class DefaultTextEditingActions extends Actions{
/// Creates an instance of DefaultTextEditingActions.
DefaultTextEditingActions({
Key? key,
required Widget child,
}) : super(
key: key,
actions: _shortcutsActions,
child: child,
);
// These Intents are triggered by DefaultTextEditingShortcuts. They are included
// regardless of the platform; it's up to DefaultTextEditingShortcuts to decide which
// are called on which platform.
static final Map<Type, Action<Intent>> _shortcutsActions = <Type, Action<Intent>>{
DoNothingAndStopPropagationTextIntent: _DoNothingAndStopPropagationTextAction(),
ExtendSelectionDownTextIntent: _ExtendSelectionDownTextAction(),
ExtendSelectionLeftByLineTextIntent: _ExtendSelectionLeftByLineTextAction(),
ExtendSelectionLeftByWordTextIntent: _ExtendSelectionLeftByWordTextAction(),
ExtendSelectionLeftTextIntent: _ExtendSelectionLeftTextAction(),
ExtendSelectionRightByWordTextIntent: _ExtendSelectionRightByWordTextAction(),
ExtendSelectionRightByLineTextIntent: _ExtendSelectionRightByLineTextAction(),
ExtendSelectionRightTextIntent: _ExtendSelectionRightTextAction(),
ExtendSelectionUpTextIntent: _ExtendSelectionUpTextAction(),
ExpandSelectionLeftByLineTextIntent: _ExpandSelectionLeftByLineTextAction(),
ExpandSelectionRightByLineTextIntent: _ExpandSelectionRightByLineTextAction(),
ExpandSelectionToEndTextIntent: _ExpandSelectionToEndTextAction(),
ExpandSelectionToStartTextIntent: _ExpandSelectionToStartTextAction(),
MoveSelectionDownTextIntent: _MoveSelectionDownTextAction(),
MoveSelectionLeftByLineTextIntent: _MoveSelectionLeftByLineTextAction(),
MoveSelectionLeftByWordTextIntent: _MoveSelectionLeftByWordTextAction(),
MoveSelectionLeftTextIntent: _MoveSelectionLeftTextAction(),
MoveSelectionRightByLineTextIntent: _MoveSelectionRightByLineTextAction(),
MoveSelectionRightByWordTextIntent: _MoveSelectionRightByWordTextAction(),
MoveSelectionRightTextIntent: _MoveSelectionRightTextAction(),
MoveSelectionToEndTextIntent: _MoveSelectionToEndTextAction(),
MoveSelectionToStartTextIntent: _MoveSelectionToStartTextAction(),
MoveSelectionUpTextIntent: _MoveSelectionUpTextAction(),
};
}
// This allows the web engine to handle text editing events natively while using
// the same TextEditingAction logic to only handle events from a
// TextEditingTarget.
class _DoNothingAndStopPropagationTextAction extends TextEditingAction<DoNothingAndStopPropagationTextIntent> {
_DoNothingAndStopPropagationTextAction();
@override
bool consumesKey(Intent intent) => true;
@override
void invoke(DoNothingAndStopPropagationTextIntent intent, [BuildContext? context]) {}
}
class _ExpandSelectionLeftByLineTextAction extends TextEditingAction<ExpandSelectionLeftByLineTextIntent> {
@override
Object? invoke(ExpandSelectionLeftByLineTextIntent intent, [BuildContext? context]) {
textEditingActionTarget!.renderEditable.expandSelectionLeftByLine(SelectionChangedCause.keyboard);
}
}
class _ExpandSelectionRightByLineTextAction extends TextEditingAction<ExpandSelectionRightByLineTextIntent> {
@override
Object? invoke(ExpandSelectionRightByLineTextIntent intent, [BuildContext? context]) {
textEditingActionTarget!.renderEditable.expandSelectionRightByLine(SelectionChangedCause.keyboard);
}
}
class _ExpandSelectionToEndTextAction extends TextEditingAction<ExpandSelectionToEndTextIntent> {
@override
Object? invoke(ExpandSelectionToEndTextIntent intent, [BuildContext? context]) {
textEditingActionTarget!.renderEditable.expandSelectionToEnd(SelectionChangedCause.keyboard);
}
}
class _ExpandSelectionToStartTextAction extends TextEditingAction<ExpandSelectionToStartTextIntent> {
@override
Object? invoke(ExpandSelectionToStartTextIntent intent, [BuildContext? context]) {
textEditingActionTarget!.renderEditable.expandSelectionToStart(SelectionChangedCause.keyboard);
}
}
class _ExtendSelectionDownTextAction extends TextEditingAction<ExtendSelectionDownTextIntent> {
@override
Object? invoke(ExtendSelectionDownTextIntent intent, [BuildContext? context]) {
textEditingActionTarget!.renderEditable.extendSelectionDown(SelectionChangedCause.keyboard);
}
}
class _ExtendSelectionLeftByLineTextAction extends TextEditingAction<ExtendSelectionLeftByLineTextIntent> {
@override
Object? invoke(ExtendSelectionLeftByLineTextIntent intent, [BuildContext? context]) {
textEditingActionTarget!.renderEditable.extendSelectionLeftByLine(SelectionChangedCause.keyboard);
}
}
class _ExtendSelectionLeftByWordTextAction extends TextEditingAction<ExtendSelectionLeftByWordTextIntent> {
@override
Object? invoke(ExtendSelectionLeftByWordTextIntent intent, [BuildContext? context]) {
textEditingActionTarget!.renderEditable.extendSelectionLeftByWord(SelectionChangedCause.keyboard, false);
}
}
class _ExtendSelectionLeftTextAction extends TextEditingAction<ExtendSelectionLeftTextIntent> {
@override
Object? invoke(ExtendSelectionLeftTextIntent intent, [BuildContext? context]) {
textEditingActionTarget!.renderEditable.extendSelectionLeft(SelectionChangedCause.keyboard);
}
}
class _ExtendSelectionRightByLineTextAction extends TextEditingAction<ExtendSelectionRightByLineTextIntent> {
@override
Object? invoke(ExtendSelectionRightByLineTextIntent intent, [BuildContext? context]) {
textEditingActionTarget!.renderEditable.extendSelectionRightByLine(SelectionChangedCause.keyboard);
}
}
class _ExtendSelectionRightByWordTextAction extends TextEditingAction<ExtendSelectionRightByWordTextIntent> {
@override
Object? invoke(ExtendSelectionRightByWordTextIntent intent, [BuildContext? context]) {
textEditingActionTarget!.renderEditable.extendSelectionRightByWord(SelectionChangedCause.keyboard, false);
}
}
class _ExtendSelectionRightTextAction extends TextEditingAction<ExtendSelectionRightTextIntent> {
@override
Object? invoke(ExtendSelectionRightTextIntent intent, [BuildContext? context]) {
textEditingActionTarget!.renderEditable.extendSelectionRight(SelectionChangedCause.keyboard);
}
}
class _ExtendSelectionUpTextAction extends TextEditingAction<ExtendSelectionUpTextIntent> {
@override
Object? invoke(ExtendSelectionUpTextIntent intent, [BuildContext? context]) {
textEditingActionTarget!.renderEditable.extendSelectionUp(SelectionChangedCause.keyboard);
}
}
class _MoveSelectionDownTextAction extends TextEditingAction<MoveSelectionDownTextIntent> {
@override
Object? invoke(MoveSelectionDownTextIntent intent, [BuildContext? context]) {
textEditingActionTarget!.renderEditable.moveSelectionDown(SelectionChangedCause.keyboard);
}
}
class _MoveSelectionLeftTextAction extends TextEditingAction<MoveSelectionLeftTextIntent> {
@override
Object? invoke(MoveSelectionLeftTextIntent intent, [BuildContext? context]) {
textEditingActionTarget!.renderEditable.moveSelectionLeft(SelectionChangedCause.keyboard);
}
}
class _MoveSelectionRightTextAction extends TextEditingAction<MoveSelectionRightTextIntent> {
@override
Object? invoke(MoveSelectionRightTextIntent intent, [BuildContext? context]) {
textEditingActionTarget!.renderEditable.moveSelectionRight(SelectionChangedCause.keyboard);
}
}
class _MoveSelectionUpTextAction extends TextEditingAction<MoveSelectionUpTextIntent> {
@override
Object? invoke(MoveSelectionUpTextIntent intent, [BuildContext? context]) {
textEditingActionTarget!.renderEditable.moveSelectionUp(SelectionChangedCause.keyboard);
}
}
class _MoveSelectionLeftByLineTextAction extends TextEditingAction<MoveSelectionLeftByLineTextIntent> {
@override
Object? invoke(MoveSelectionLeftByLineTextIntent intent, [BuildContext? context]) {
textEditingActionTarget!.renderEditable.moveSelectionLeftByLine(SelectionChangedCause.keyboard);
}
}
class _MoveSelectionLeftByWordTextAction extends TextEditingAction<MoveSelectionLeftByWordTextIntent> {
@override
Object? invoke(MoveSelectionLeftByWordTextIntent intent, [BuildContext? context]) {
textEditingActionTarget!.renderEditable.moveSelectionLeftByWord(SelectionChangedCause.keyboard, false);
}
}
class _MoveSelectionRightByLineTextAction extends TextEditingAction<MoveSelectionRightByLineTextIntent> {
@override
Object? invoke(MoveSelectionRightByLineTextIntent intent, [BuildContext? context]) {
textEditingActionTarget!.renderEditable.moveSelectionRightByLine(SelectionChangedCause.keyboard);
}
}
class _MoveSelectionRightByWordTextAction extends TextEditingAction<MoveSelectionRightByWordTextIntent> {
@override
Object? invoke(MoveSelectionRightByWordTextIntent intent, [BuildContext? context]) {
textEditingActionTarget!.renderEditable.moveSelectionRightByWord(SelectionChangedCause.keyboard, false);
}
}
class _MoveSelectionToEndTextAction extends TextEditingAction<MoveSelectionToEndTextIntent> {
@override
Object? invoke(MoveSelectionToEndTextIntent intent, [BuildContext? context]) {
textEditingActionTarget!.renderEditable.moveSelectionToEnd(SelectionChangedCause.keyboard);
}
}
class _MoveSelectionToStartTextAction extends TextEditingAction<MoveSelectionToStartTextIntent> {
@override
Object? invoke(MoveSelectionToStartTextIntent intent, [BuildContext? context]) {
textEditingActionTarget!.renderEditable.moveSelectionToStart(SelectionChangedCause.keyboard);
}
}
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/services.dart';
import 'package:flutter/foundation.dart';
import 'actions.dart';
import 'framework.dart';
import 'shortcuts.dart';
import 'text_editing_intents.dart';
/// A [Shortcuts] widget with the shortcuts used for the default text editing
/// behavior.
///
/// This default behavior can be overridden by placing a [Shortcuts] widget
/// lower in the widget tree than this. See [DefaultTextEditingActions] for an example
/// of remapping a text editing [Intent] to a custom [Action].
///
/// {@tool snippet}
///
/// This example shows how to use an additional [Shortcuts] widget to override
/// some default text editing keyboard shortcuts to have new behavior. Instead
/// of moving the cursor, alt + up/down will change the focused widget.
///
/// ```dart
/// @override
/// Widget build(BuildContext context) {
/// // If using WidgetsApp or its descendents MaterialApp or CupertinoApp,
/// // then DefaultTextEditingShortcuts is already being inserted into the
/// // widget tree.
/// return DefaultTextEditingShortcuts(
/// child: Center(
/// child: Shortcuts(
/// shortcuts: <LogicalKeySet, Intent>{
/// LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.arrowDown): NextFocusIntent(),
/// LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.arrowUp): PreviousFocusIntent(),
/// },
/// child: Column(
/// children: <Widget>[
/// TextField(
/// decoration: InputDecoration(
/// hintText: 'alt + down moves to the next field.',
/// ),
/// ),
/// TextField(
/// decoration: InputDecoration(
/// hintText: 'And alt + up moves to the previous.',
/// ),
/// ),
/// ],
/// ),
/// ),
/// ),
/// );
/// }
/// ```
/// {@end-tool}
///
/// {@tool snippet}
///
/// This example shows how to use an additional [Shortcuts] widget to override
/// default text editing shortcuts to have completely custom behavior defined by
/// a custom Intent and Action. Here, the up/down arrow keys increment/decrement
/// a counter instead of moving the cursor.
///
/// ```dart
/// class IncrementCounterIntent extends Intent {}
/// class DecrementCounterIntent extends Intent {}
///
/// class MyWidget extends StatefulWidget {
/// MyWidget({ Key? key }) : super(key: key);
///
/// @override
/// MyWidgetState createState() => MyWidgetState();
/// }
///
/// class MyWidgetState extends State<MyWidget> {
///
/// int _counter = 0;
///
/// @override
/// Widget build(BuildContext context) {
/// // If using WidgetsApp or its descendents MaterialApp or CupertinoApp,
/// // then DefaultTextEditingShortcuts is already being inserted into the
/// // widget tree.
/// return DefaultTextEditingShortcuts(
/// child: Center(
/// child: Column(
/// mainAxisAlignment: MainAxisAlignment.center,
/// children: <Widget>[
/// Text(
/// 'You have pushed the button this many times:',
/// ),
/// Text(
/// '$_counter',
/// style: Theme.of(context).textTheme.headline4,
/// ),
/// Shortcuts(
/// shortcuts: <LogicalKeySet, Intent>{
/// LogicalKeySet(LogicalKeyboardKey.arrowUp): IncrementCounterIntent(),
/// LogicalKeySet(LogicalKeyboardKey.arrowDown): DecrementCounterIntent(),
/// },
/// child: Actions(
/// actions: <Type, Action<Intent>>{
/// IncrementCounterIntent: CallbackAction<IncrementCounterIntent>(
/// onInvoke: (IncrementCounterIntent intent) {
/// setState(() {
/// _counter++;
/// });
/// },
/// ),
/// DecrementCounterIntent: CallbackAction<DecrementCounterIntent>(
/// onInvoke: (DecrementCounterIntent intent) {
/// setState(() {
/// _counter--;
/// });
/// },
/// ),
/// },
/// child: TextField(
/// maxLines: 2,
/// decoration: InputDecoration(
/// hintText: 'Up/down increment/decrement here.',
/// ),
/// ),
/// ),
/// ),
/// TextField(
/// maxLines: 2,
/// decoration: InputDecoration(
/// hintText: 'Up/down behave normally here.',
/// ),
/// ),
/// ],
/// ),
/// ),
/// );
/// }
/// }
/// ```
/// {@end-tool}
///
/// See also:
///
/// * [DefaultTextEditingActions], which contains all of the [Action]s that
/// respond to the [Intent]s in these shortcuts with the default text editing
/// behavior.
/// * [WidgetsApp], which creates a DefaultTextEditingShortcuts.
class DefaultTextEditingShortcuts extends Shortcuts {
/// Creates a [Shortcuts] widget that provides the default text editing
/// shortcuts on the current platform.
DefaultTextEditingShortcuts({
Key? key,
required Widget child,
}) : super(
key: key,
debugLabel: '<Default Text Editing Shortcuts>',
shortcuts: _shortcuts,
child: child,
);
static final Map<LogicalKeySet, Intent> _androidShortcuts = <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.arrowDown): const MoveSelectionToEndTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.arrowLeft): const MoveSelectionLeftByLineTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.arrowRight): const MoveSelectionRightByLineTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.arrowUp): const MoveSelectionToStartTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowDown): const ExpandSelectionToEndTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowLeft): const ExpandSelectionLeftByLineTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowRight): const ExpandSelectionRightByLineTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowUp): const ExpandSelectionToStartTextIntent(),
LogicalKeySet(LogicalKeyboardKey.arrowDown): const MoveSelectionDownTextIntent(),
LogicalKeySet(LogicalKeyboardKey.arrowLeft): const MoveSelectionLeftTextIntent(),
LogicalKeySet(LogicalKeyboardKey.arrowRight): const MoveSelectionRightTextIntent(),
LogicalKeySet(LogicalKeyboardKey.arrowUp): const MoveSelectionUpTextIntent(),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.arrowLeft): const MoveSelectionLeftByWordTextIntent(),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.arrowRight): const MoveSelectionRightByWordTextIntent(),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowLeft): const ExtendSelectionLeftByWordTextIntent(),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowRight): const ExtendSelectionRightByWordTextIntent(),
LogicalKeySet(LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowDown): const ExtendSelectionDownTextIntent(),
LogicalKeySet(LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowLeft): const ExtendSelectionLeftTextIntent(),
LogicalKeySet(LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowRight): const ExtendSelectionRightTextIntent(),
LogicalKeySet(LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowUp): const ExtendSelectionUpTextIntent(),
// The following key combinations have no effect on text edition on this
// platform:
// * End
// * Home
// * Meta + arrow down
// * Meta + arrow left
// * Meta + arrow right
// * Meta + arrow up
// * Meta + shift + arrow down
// * Meta + shift + arrow left
// * Meta + shift + arrow right
// * Meta + shift + arrow up
// * Shift + end
// * Shift + home
};
static final Map<LogicalKeySet, Intent> _fuchsiaShortcuts = <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.arrowDown): const MoveSelectionToEndTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.arrowLeft): const MoveSelectionLeftByLineTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.arrowRight): const MoveSelectionRightByLineTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.arrowUp): const MoveSelectionToStartTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowDown): const ExpandSelectionToEndTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowLeft): const ExpandSelectionLeftByLineTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowRight): const ExpandSelectionRightByLineTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowUp): const ExpandSelectionToStartTextIntent(),
LogicalKeySet(LogicalKeyboardKey.arrowDown): const MoveSelectionDownTextIntent(),
LogicalKeySet(LogicalKeyboardKey.arrowLeft): const MoveSelectionLeftTextIntent(),
LogicalKeySet(LogicalKeyboardKey.arrowRight): const MoveSelectionRightTextIntent(),
LogicalKeySet(LogicalKeyboardKey.arrowUp): const MoveSelectionUpTextIntent(),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.arrowLeft): const MoveSelectionLeftByWordTextIntent(),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.arrowRight): const MoveSelectionRightByWordTextIntent(),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowLeft): const ExtendSelectionLeftByWordTextIntent(),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowRight): const ExtendSelectionRightByWordTextIntent(),
LogicalKeySet(LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowDown): const ExtendSelectionDownTextIntent(),
LogicalKeySet(LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowLeft): const ExtendSelectionLeftTextIntent(),
LogicalKeySet(LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowRight): const ExtendSelectionRightTextIntent(),
LogicalKeySet(LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowUp): const ExtendSelectionUpTextIntent(),
// The following key combinations have no effect on text edition on this
// platform:
// * Meta + arrow down
// * End
// * Home
// * Meta + arrow left
// * Meta + arrow right
// * Meta + arrow up
// * Meta + shift + arrow down
// * Meta + shift + arrow left
// * Meta + shift + arrow right
// * Meta + shift + arrow up
// * Shift + end
// * Shift + home
};
static final Map<LogicalKeySet, Intent> _iOSShortcuts = <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.arrowDown): const MoveSelectionToEndTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.arrowLeft): const MoveSelectionLeftByLineTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.arrowRight): const MoveSelectionRightByLineTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.arrowUp): const MoveSelectionToStartTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowDown): const ExpandSelectionToEndTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowLeft): const ExpandSelectionLeftByLineTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowRight): const ExpandSelectionRightByLineTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowUp): const ExpandSelectionToStartTextIntent(),
LogicalKeySet(LogicalKeyboardKey.arrowDown): const MoveSelectionDownTextIntent(),
LogicalKeySet(LogicalKeyboardKey.arrowLeft): const MoveSelectionLeftTextIntent(),
LogicalKeySet(LogicalKeyboardKey.arrowRight): const MoveSelectionRightTextIntent(),
LogicalKeySet(LogicalKeyboardKey.arrowUp): const MoveSelectionUpTextIntent(),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.arrowRight): const MoveSelectionRightByWordTextIntent(),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.arrowLeft): const MoveSelectionLeftByWordTextIntent(),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowLeft): const ExtendSelectionLeftByWordTextIntent(),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowRight): const ExtendSelectionRightByWordTextIntent(),
LogicalKeySet(LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowDown): const ExtendSelectionDownTextIntent(),
LogicalKeySet(LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowLeft): const ExtendSelectionLeftTextIntent(),
LogicalKeySet(LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowRight): const ExtendSelectionRightTextIntent(),
LogicalKeySet(LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowUp): const ExtendSelectionUpTextIntent(),
// The following key combinations have no effect on text edition on this
// platform:
// * Meta + arrow down
// * End
// * Home
// * Meta + arrow left
// * Meta + arrow right
// * Meta + arrow up
// * Meta + shift + arrow down
// * Meta + shift + arrow left
// * Meta + shift + arrow right
// * Meta + shift + arrow up
// * Shift + end
// * Shift + home
};
static final Map<LogicalKeySet, Intent> _linuxShortcuts = <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.arrowDown): const MoveSelectionToEndTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.arrowLeft): const MoveSelectionLeftByLineTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.arrowRight): const MoveSelectionRightByLineTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.arrowUp): const MoveSelectionToStartTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowDown): const ExpandSelectionToEndTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowLeft): const ExpandSelectionLeftByLineTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowRight): const ExpandSelectionRightByLineTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowUp): const ExpandSelectionToStartTextIntent(),
LogicalKeySet(LogicalKeyboardKey.arrowDown): const MoveSelectionDownTextIntent(),
LogicalKeySet(LogicalKeyboardKey.arrowLeft): const MoveSelectionLeftTextIntent(),
LogicalKeySet(LogicalKeyboardKey.arrowRight): const MoveSelectionRightTextIntent(),
LogicalKeySet(LogicalKeyboardKey.arrowUp): const MoveSelectionUpTextIntent(),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.arrowLeft): const MoveSelectionLeftByWordTextIntent(),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.arrowRight): const MoveSelectionRightByWordTextIntent(),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowLeft): const ExtendSelectionLeftByWordTextIntent(),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowRight): const ExtendSelectionRightByWordTextIntent(),
LogicalKeySet(LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowDown): const ExtendSelectionDownTextIntent(),
LogicalKeySet(LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowLeft): const ExtendSelectionLeftTextIntent(),
LogicalKeySet(LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowRight): const ExtendSelectionRightTextIntent(),
LogicalKeySet(LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowUp): const ExtendSelectionUpTextIntent(),
// The following key combinations have no effect on text edition on this
// platform:
// * Meta + arrow down
// * End
// * Home
// * Meta + arrow left
// * Meta + arrow right
// * Meta + arrow up
// * Meta + shift + arrow down
// * Meta + shift + arrow left
// * Meta + shift + arrow right
// * Meta + shift + arrow up
// * Shift + end
// * Shift + home
};
static final Map<LogicalKeySet, Intent> _macShortcuts = <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.arrowDown): const MoveSelectionRightByLineTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.arrowLeft): const MoveSelectionLeftByWordTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.arrowRight): const MoveSelectionRightByWordTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.arrowUp): const MoveSelectionLeftByLineTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowDown): const ExtendSelectionRightByLineTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowLeft): const ExtendSelectionLeftByWordTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowRight): const ExtendSelectionRightByWordTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowUp): const ExtendSelectionLeftByLineTextIntent(),
LogicalKeySet(LogicalKeyboardKey.arrowDown): const MoveSelectionDownTextIntent(),
LogicalKeySet(LogicalKeyboardKey.arrowLeft): const MoveSelectionLeftTextIntent(),
LogicalKeySet(LogicalKeyboardKey.arrowRight): const MoveSelectionRightTextIntent(),
LogicalKeySet(LogicalKeyboardKey.arrowUp): const MoveSelectionUpTextIntent(),
LogicalKeySet(LogicalKeyboardKey.meta, LogicalKeyboardKey.arrowDown): const MoveSelectionToEndTextIntent(),
LogicalKeySet(LogicalKeyboardKey.meta, LogicalKeyboardKey.arrowLeft): const MoveSelectionLeftByLineTextIntent(),
LogicalKeySet(LogicalKeyboardKey.meta, LogicalKeyboardKey.arrowRight): const MoveSelectionRightByLineTextIntent(),
LogicalKeySet(LogicalKeyboardKey.meta, LogicalKeyboardKey.arrowUp): const MoveSelectionToStartTextIntent(),
LogicalKeySet(LogicalKeyboardKey.meta, LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowDown): const ExpandSelectionToEndTextIntent(),
LogicalKeySet(LogicalKeyboardKey.meta, LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowLeft): const ExpandSelectionLeftByLineTextIntent(),
LogicalKeySet(LogicalKeyboardKey.meta, LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowRight): const ExpandSelectionRightByLineTextIntent(),
LogicalKeySet(LogicalKeyboardKey.meta, LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowUp): const ExpandSelectionToStartTextIntent(),
LogicalKeySet(LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowDown): const ExtendSelectionDownTextIntent(),
LogicalKeySet(LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowLeft): const ExtendSelectionLeftTextIntent(),
LogicalKeySet(LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowRight): const ExtendSelectionRightTextIntent(),
LogicalKeySet(LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowUp): const ExtendSelectionUpTextIntent(),
// The following key combinations have no effect on text edition on this
// platform:
// * Control + arrow left
// * Control + arrow right
// * Control + shift + arrow left
// * Control + shift + arrow right
// * End
// * Home
// * Shift + end
// * Shift + home
};
static final Map<LogicalKeySet, Intent> _windowsShortcuts = <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.arrowDown): const MoveSelectionToEndTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.arrowLeft): const MoveSelectionLeftByLineTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.arrowRight): const MoveSelectionRightByLineTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.arrowUp): const MoveSelectionToStartTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowDown): const ExpandSelectionToEndTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowLeft): const ExpandSelectionLeftByLineTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowRight): const ExpandSelectionRightByLineTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowUp): const ExpandSelectionToStartTextIntent(),
LogicalKeySet(LogicalKeyboardKey.arrowDown): const MoveSelectionDownTextIntent(),
LogicalKeySet(LogicalKeyboardKey.arrowLeft): const MoveSelectionLeftTextIntent(),
LogicalKeySet(LogicalKeyboardKey.arrowRight): const MoveSelectionRightTextIntent(),
LogicalKeySet(LogicalKeyboardKey.arrowUp): const MoveSelectionUpTextIntent(),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.arrowLeft): const MoveSelectionLeftByWordTextIntent(),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.arrowRight): const MoveSelectionRightByWordTextIntent(),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowLeft): const ExtendSelectionLeftByWordTextIntent(),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowRight): const ExtendSelectionRightByWordTextIntent(),
LogicalKeySet(LogicalKeyboardKey.end): const MoveSelectionRightByLineTextIntent(),
LogicalKeySet(LogicalKeyboardKey.home): const MoveSelectionLeftByLineTextIntent(),
LogicalKeySet(LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowDown): const ExtendSelectionDownTextIntent(),
LogicalKeySet(LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowLeft): const ExtendSelectionLeftTextIntent(),
LogicalKeySet(LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowRight): const ExtendSelectionRightTextIntent(),
LogicalKeySet(LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowUp): const ExtendSelectionUpTextIntent(),
LogicalKeySet(LogicalKeyboardKey.shift, LogicalKeyboardKey.end): const ExpandSelectionRightByLineTextIntent(),
LogicalKeySet(LogicalKeyboardKey.shift, LogicalKeyboardKey.home): const ExpandSelectionLeftByLineTextIntent(),
// The following key combinations have no effect on text edition on this
// platform:
// * Meta + arrow down
// * Meta + arrow left
// * Meta + arrow right
// * Meta + arrow up
// * Meta + shift + arrow down
// * Meta + shift + arrow left
// * Meta + shift + arrow right
// * Meta + shift + arrow up
};
// Web handles its text selection natively and doesn't use any of these
// shortcuts in Flutter.
static final Map<LogicalKeySet, Intent> _webShortcuts = <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.arrowDown): const DoNothingAndStopPropagationTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.arrowLeft): const DoNothingAndStopPropagationTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.arrowRight): const DoNothingAndStopPropagationTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.arrowUp): const DoNothingAndStopPropagationTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowDown): const DoNothingAndStopPropagationTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowLeft): const DoNothingAndStopPropagationTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowRight): const DoNothingAndStopPropagationTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowUp): const DoNothingAndStopPropagationTextIntent(),
LogicalKeySet(LogicalKeyboardKey.arrowDown): const DoNothingAndStopPropagationTextIntent(),
LogicalKeySet(LogicalKeyboardKey.arrowLeft): const DoNothingAndStopPropagationTextIntent(),
LogicalKeySet(LogicalKeyboardKey.arrowRight): const DoNothingAndStopPropagationTextIntent(),
LogicalKeySet(LogicalKeyboardKey.arrowUp): const DoNothingAndStopPropagationTextIntent(),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.arrowLeft): const DoNothingAndStopPropagationTextIntent(),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.arrowRight): const DoNothingAndStopPropagationTextIntent(),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowLeft): const DoNothingAndStopPropagationTextIntent(),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowRight): const DoNothingAndStopPropagationTextIntent(),
LogicalKeySet(LogicalKeyboardKey.end): const DoNothingAndStopPropagationTextIntent(),
LogicalKeySet(LogicalKeyboardKey.home): const DoNothingAndStopPropagationTextIntent(),
LogicalKeySet(LogicalKeyboardKey.meta, LogicalKeyboardKey.arrowDown): const DoNothingAndStopPropagationTextIntent(),
LogicalKeySet(LogicalKeyboardKey.meta, LogicalKeyboardKey.arrowLeft): const DoNothingAndStopPropagationTextIntent(),
LogicalKeySet(LogicalKeyboardKey.meta, LogicalKeyboardKey.arrowRight): const DoNothingAndStopPropagationTextIntent(),
LogicalKeySet(LogicalKeyboardKey.meta, LogicalKeyboardKey.arrowUp): const DoNothingAndStopPropagationTextIntent(),
LogicalKeySet(LogicalKeyboardKey.meta, LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowDown): const DoNothingAndStopPropagationTextIntent(),
LogicalKeySet(LogicalKeyboardKey.meta, LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowLeft): const DoNothingAndStopPropagationTextIntent(),
LogicalKeySet(LogicalKeyboardKey.meta, LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowRight): const DoNothingAndStopPropagationTextIntent(),
LogicalKeySet(LogicalKeyboardKey.meta, LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowUp): const DoNothingAndStopPropagationTextIntent(),
LogicalKeySet(LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowDown): const DoNothingAndStopPropagationTextIntent(),
LogicalKeySet(LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowLeft): const DoNothingAndStopPropagationTextIntent(),
LogicalKeySet(LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowRight): const DoNothingAndStopPropagationTextIntent(),
LogicalKeySet(LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowUp): const DoNothingAndStopPropagationTextIntent(),
LogicalKeySet(LogicalKeyboardKey.shift, LogicalKeyboardKey.end): const DoNothingAndStopPropagationTextIntent(),
LogicalKeySet(LogicalKeyboardKey.shift, LogicalKeyboardKey.home): const DoNothingAndStopPropagationTextIntent(),
LogicalKeySet(LogicalKeyboardKey.space): const DoNothingAndStopPropagationTextIntent(),
};
static Map<LogicalKeySet, Intent> get _shortcuts {
if (kIsWeb) {
return _webShortcuts;
}
switch (defaultTargetPlatform) {
case TargetPlatform.android:
return _androidShortcuts;
case TargetPlatform.fuchsia:
return _fuchsiaShortcuts;
case TargetPlatform.iOS:
return _iOSShortcuts;
case TargetPlatform.linux:
return _linuxShortcuts;
case TargetPlatform.macOS:
return _macShortcuts;
case TargetPlatform.windows:
return _windowsShortcuts;
}
}
}
...@@ -12,7 +12,6 @@ import 'package:flutter/rendering.dart'; ...@@ -12,7 +12,6 @@ import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'actions.dart';
import 'autofill.dart'; import 'autofill.dart';
import 'automatic_keep_alive.dart'; import 'automatic_keep_alive.dart';
import 'basic.dart'; import 'basic.dart';
...@@ -27,8 +26,8 @@ import 'media_query.dart'; ...@@ -27,8 +26,8 @@ import 'media_query.dart';
import 'scroll_controller.dart'; import 'scroll_controller.dart';
import 'scroll_physics.dart'; import 'scroll_physics.dart';
import 'scrollable.dart'; import 'scrollable.dart';
import 'shortcuts.dart';
import 'text.dart'; import 'text.dart';
import 'text_editing_action.dart';
import 'text_selection.dart'; import 'text_selection.dart';
import 'ticker_provider.dart'; import 'ticker_provider.dart';
...@@ -54,17 +53,6 @@ const Duration _kCursorBlinkWaitForStart = Duration(milliseconds: 150); ...@@ -54,17 +53,6 @@ const Duration _kCursorBlinkWaitForStart = Duration(milliseconds: 150);
// is shown in an obscured text field. // is shown in an obscured text field.
const int _kObscureShowLatestCharCursorTicks = 3; const int _kObscureShowLatestCharCursorTicks = 3;
/// A map used to disable scrolling shortcuts in text fields.
///
/// This is a temporary fix for: https://github.com/flutter/flutter/issues/74191
final Map<LogicalKeySet, Intent> scrollShortcutOverrides = <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.space): DoNothingAndStopPropagationIntent(),
LogicalKeySet(LogicalKeyboardKey.arrowUp): DoNothingAndStopPropagationIntent(),
LogicalKeySet(LogicalKeyboardKey.arrowDown): DoNothingAndStopPropagationIntent(),
LogicalKeySet(LogicalKeyboardKey.arrowLeft): DoNothingAndStopPropagationIntent(),
LogicalKeySet(LogicalKeyboardKey.arrowRight): DoNothingAndStopPropagationIntent(),
};
/// A controller for an editable text field. /// A controller for an editable text field.
/// ///
/// Whenever the user modifies a text field with an associated /// Whenever the user modifies a text field with an associated
...@@ -1491,7 +1479,7 @@ class EditableText extends StatefulWidget { ...@@ -1491,7 +1479,7 @@ class EditableText extends StatefulWidget {
} }
/// State for a [EditableText]. /// State for a [EditableText].
class EditableTextState extends State<EditableText> with AutomaticKeepAliveClientMixin<EditableText>, WidgetsBindingObserver, TickerProviderStateMixin<EditableText>, TextSelectionDelegate implements TextInputClient, AutofillClient { class EditableTextState extends State<EditableText> with AutomaticKeepAliveClientMixin<EditableText>, WidgetsBindingObserver, TickerProviderStateMixin<EditableText>, TextSelectionDelegate implements TextInputClient, AutofillClient, TextEditingActionTarget {
Timer? _cursorTimer; Timer? _cursorTimer;
bool _targetCursorVisibility = false; bool _targetCursorVisibility = false;
final ValueNotifier<bool> _cursorVisibilityNotifier = ValueNotifier<bool>(true); final ValueNotifier<bool> _cursorVisibilityNotifier = ValueNotifier<bool>(true);
...@@ -2472,6 +2460,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -2472,6 +2460,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
/// ///
/// This property is typically used to notify the renderer of input gestures /// This property is typically used to notify the renderer of input gestures
/// when [RenderEditable.ignorePointer] is true. /// when [RenderEditable.ignorePointer] is true.
@override
RenderEditable get renderEditable => _editableKey.currentContext!.findRenderObject()! as RenderEditable; RenderEditable get renderEditable => _editableKey.currentContext!.findRenderObject()! as RenderEditable;
@override @override
......
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart' show RenderEditable;
import 'actions.dart';
import 'editable_text.dart';
import 'focus_manager.dart';
import 'framework.dart';
/// The recipient of a [TextEditingAction].
///
/// TextEditingActions will only be enabled when an implementer of this class is
/// focused.
///
/// See also:
///
/// * [EditableTextState], which implements this and is the most typical
/// target of a TextEditingAction.
abstract class TextEditingActionTarget {
/// The renderer that handles [TextEditingAction]s.
///
/// See also:
///
/// * [EditableTextState.renderEditable], which overrides this.
RenderEditable get renderEditable;
}
/// An [Action] related to editing text.
///
/// Enables itself only when a [TextEditingActionTarget], e.g. [EditableText],
/// is currently focused. The result of this is that when a
/// TextEditingActionTarget is not focused, it will fall through to any
/// non-TextEditingAction that handles the same shortcut. For example,
/// overriding the tab key in [Shortcuts] with a TextEditingAction will only
/// invoke your TextEditingAction when a TextEditingActionTarget is focused,
/// otherwise the default tab behavior will apply.
///
/// The currently focused TextEditingActionTarget is available in the [invoke]
/// method via [textEditingActionTarget].
///
/// See also:
///
/// * [CallbackAction], which is a similar Action type but unrelated to text
/// editing.
abstract class TextEditingAction<T extends Intent> extends ContextAction<T> {
/// Returns the currently focused [TextEditingAction], or null if none is
/// focused.
@protected
TextEditingActionTarget? get textEditingActionTarget {
// If a TextEditingActionTarget is not focused, then ignore this action.
if (primaryFocus?.context == null
|| primaryFocus!.context! is! StatefulElement
|| ((primaryFocus!.context! as StatefulElement).state is! TextEditingActionTarget)) {
return null;
}
return (primaryFocus!.context! as StatefulElement).state as TextEditingActionTarget;
}
@override
bool isEnabled(T intent) {
// The Action is disabled if there is no focused TextEditingActionTarget, or
// if the platform is web, because web lets the browser handle text editing.
return !kIsWeb && textEditingActionTarget != null;
}
}
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'actions.dart';
/// An [Intent] to send the event straight to the engine, but only if a
/// TextEditingTarget is focused.
///
/// {@template flutter.widgets.TextEditingIntents.seeAlso}
/// See also:
///
/// * [DefaultTextEditingActions], which responds to this [Intent].
/// * [DefaultTextEditingShortcuts], which triggers this [Intent].
/// {@endtemplate}
class DoNothingAndStopPropagationTextIntent extends Intent{
/// Creates an instance of DoNothingAndStopPropagationTextIntent.
const DoNothingAndStopPropagationTextIntent();
}
/// An [Intent] to expand the selection left to the start/end of the current
/// line.
///
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
class ExpandSelectionLeftByLineTextIntent extends Intent {
/// Creates an instance of ExpandSelectionLeftByLineTextIntent.
const ExpandSelectionLeftByLineTextIntent();
}
/// An [Intent] to expand the selection right to the start/end of the current
/// field.
///
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
class ExpandSelectionRightByLineTextIntent extends Intent{
/// Creates an instance of ExpandSelectionRightByLineTextIntent.
const ExpandSelectionRightByLineTextIntent();
}
/// An [Intent] to expand the selection to the end of the field.
///
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
class ExpandSelectionToEndTextIntent extends Intent{
/// Creates an instance of ExpandSelectionToEndTextIntent.
const ExpandSelectionToEndTextIntent();
}
/// An [Intent] to expand the selection to the start of the field.
///
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
class ExpandSelectionToStartTextIntent extends Intent{
/// Creates an instance of ExpandSelectionToStartTextIntent.
const ExpandSelectionToStartTextIntent();
}
/// An [Intent] to extend the selection down by one line.
///
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
class ExtendSelectionDownTextIntent extends Intent{
/// Creates an instance of ExtendSelectionDownTextIntent.
const ExtendSelectionDownTextIntent();
}
/// An [Intent] to extend the selection left to the start/end of the current
/// line.
///
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
class ExtendSelectionLeftByLineTextIntent extends Intent{
/// Creates an instance of ExtendSelectionLeftByLineTextIntent.
const ExtendSelectionLeftByLineTextIntent();
}
/// An [Intent] to extend the selection left past the nearest word.
///
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
class ExtendSelectionLeftByWordTextIntent extends Intent{
/// Creates an instance of ExtendSelectionLeftByWordTextIntent.
const ExtendSelectionLeftByWordTextIntent();
}
/// An [Intent] to extend the selection left by one character.
/// platform for the shift + arrow-left key event.
///
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
class ExtendSelectionLeftTextIntent extends Intent{
/// Creates an instance of ExtendSelectionLeftTextIntent.
const ExtendSelectionLeftTextIntent();
}
/// An [Intent] to extend the selection right to the start/end of the current
/// line.
///
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
class ExtendSelectionRightByLineTextIntent extends Intent{
/// Creates an instance of ExtendSelectionRightByLineTextIntent.
const ExtendSelectionRightByLineTextIntent();
}
/// An [Intent] to extend the selection right past the nearest word.
///
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
class ExtendSelectionRightByWordTextIntent extends Intent{
/// Creates an instance of ExtendSelectionRightByWordTextIntent.
const ExtendSelectionRightByWordTextIntent();
}
/// An [Intent] to extend the selection right by one character.
///
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
class ExtendSelectionRightTextIntent extends Intent{
/// Creates an instance of ExtendSelectionRightTextIntent.
const ExtendSelectionRightTextIntent();
}
/// An [Intent] to extend the selection up by one line.
///
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
class ExtendSelectionUpTextIntent extends Intent{
/// Creates an instance of ExtendSelectionUpTextIntent.
const ExtendSelectionUpTextIntent();
}
/// An [Intent] to move the selection down by one line.
///
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
class MoveSelectionDownTextIntent extends Intent{
/// Creates an instance of MoveSelectionDownTextIntent.
const MoveSelectionDownTextIntent();
}
/// An [Intent] to move the selection left by one line.
///
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
class MoveSelectionLeftByLineTextIntent extends Intent{
/// Creates an instance of MoveSelectionLeftByLineTextIntent.
const MoveSelectionLeftByLineTextIntent();
}
/// An [Intent] to move the selection left past the nearest word.
///
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
class MoveSelectionLeftByWordTextIntent extends Intent{
/// Creates an instance of MoveSelectionLeftByWordTextIntent.
const MoveSelectionLeftByWordTextIntent();
}
/// An [Intent] to move the selection left by one character.
///
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
class MoveSelectionLeftTextIntent extends Intent{
/// Creates an instance of MoveSelectionLeftTextIntent.
const MoveSelectionLeftTextIntent();
}
/// An [Intent] to move the selection to the start of the field.
///
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
class MoveSelectionToStartTextIntent extends Intent{
/// Creates an instance of MoveSelectionToStartTextIntent.
const MoveSelectionToStartTextIntent();
}
/// An [Intent] to move the selection right by one line.
///
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
class MoveSelectionRightByLineTextIntent extends Intent{
/// Creates an instance of MoveSelectionRightByLineTextIntent.
const MoveSelectionRightByLineTextIntent();
}
/// An [Intent] to move the selection right past the nearest word.
///
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
class MoveSelectionRightByWordTextIntent extends Intent{
/// Creates an instance of MoveSelectionRightByWordTextIntent.
const MoveSelectionRightByWordTextIntent();
}
/// An [Intent] to move the selection right by one character.
///
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
class MoveSelectionRightTextIntent extends Intent{
/// Creates an instance of MoveSelectionRightTextIntent.
const MoveSelectionRightTextIntent();
}
/// An [Intent] to move the selection to the end of the field.
///
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
class MoveSelectionToEndTextIntent extends Intent{
/// Creates an instance of MoveSelectionToEndTextIntent.
const MoveSelectionToEndTextIntent();
}
/// An [Intent] to move the selection up by one character.
///
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
class MoveSelectionUpTextIntent extends Intent{
/// Creates an instance of MoveSelectionUpTextIntent.
const MoveSelectionUpTextIntent();
}
...@@ -194,6 +194,9 @@ abstract class TextSelectionControls { ...@@ -194,6 +194,9 @@ abstract class TextSelectionControls {
return delegate.selectAllEnabled && delegate.textEditingValue.text.isNotEmpty && delegate.textEditingValue.selection.isCollapsed; return delegate.selectAllEnabled && delegate.textEditingValue.text.isNotEmpty && delegate.textEditingValue.selection.isCollapsed;
} }
// TODO(justinmc): This and other methods should be ported to Actions and
// removed, along with their keyboard shortcut equivalents.
// https://github.com/flutter/flutter/issues/75004
/// Copy the current selection of the text field managed by the given /// Copy the current selection of the text field managed by the given
/// `delegate` to the [Clipboard]. Then, remove the selected text from the /// `delegate` to the [Clipboard]. Then, remove the selected text from the
/// text field and hide the toolbar. /// text field and hide the toolbar.
......
...@@ -33,6 +33,8 @@ export 'src/widgets/bottom_navigation_bar_item.dart'; ...@@ -33,6 +33,8 @@ export 'src/widgets/bottom_navigation_bar_item.dart';
export 'src/widgets/color_filter.dart'; export 'src/widgets/color_filter.dart';
export 'src/widgets/container.dart'; export 'src/widgets/container.dart';
export 'src/widgets/debug.dart'; export 'src/widgets/debug.dart';
export 'src/widgets/default_text_editing_actions.dart';
export 'src/widgets/default_text_editing_shortcuts.dart';
export 'src/widgets/desktop_text_selection_toolbar_layout_delegate.dart'; export 'src/widgets/desktop_text_selection_toolbar_layout_delegate.dart';
export 'src/widgets/dismissible.dart'; export 'src/widgets/dismissible.dart';
export 'src/widgets/disposable_build_context.dart'; export 'src/widgets/disposable_build_context.dart';
...@@ -116,6 +118,8 @@ export 'src/widgets/spacer.dart'; ...@@ -116,6 +118,8 @@ export 'src/widgets/spacer.dart';
export 'src/widgets/status_transitions.dart'; export 'src/widgets/status_transitions.dart';
export 'src/widgets/table.dart'; export 'src/widgets/table.dart';
export 'src/widgets/text.dart'; export 'src/widgets/text.dart';
export 'src/widgets/text_editing_action.dart';
export 'src/widgets/text_editing_intents.dart';
export 'src/widgets/text_selection.dart'; export 'src/widgets/text_selection.dart';
export 'src/widgets/text_selection_toolbar_layout_delegate.dart'; export 'src/widgets/text_selection_toolbar_layout_delegate.dart';
export 'src/widgets/texture.dart'; export 'src/widgets/texture.dart';
......
...@@ -186,11 +186,18 @@ void main() { ...@@ -186,11 +186,18 @@ void main() {
' _FocusTraversalGroupMarker\n' ' _FocusTraversalGroupMarker\n'
' FocusTraversalGroup\n' ' FocusTraversalGroup\n'
' _ActionsMarker\n' ' _ActionsMarker\n'
' DefaultTextEditingActions\n'
' _ActionsMarker\n'
' Actions\n' ' Actions\n'
' _ShortcutsMarker\n' ' _ShortcutsMarker\n'
' Semantics\n' ' Semantics\n'
' _FocusMarker\n' ' _FocusMarker\n'
' Focus\n' ' Focus\n'
' DefaultTextEditingShortcuts\n'
' _ShortcutsMarker\n'
' Semantics\n'
' _FocusMarker\n'
' Focus\n'
' Shortcuts\n' ' Shortcuts\n'
' UnmanagedRestorationScope\n' ' UnmanagedRestorationScope\n'
' RestorationScope\n' ' RestorationScope\n'
......
...@@ -78,6 +78,8 @@ Widget overlayWithEntry(OverlayEntry entry) { ...@@ -78,6 +78,8 @@ Widget overlayWithEntry(OverlayEntry entry) {
WidgetsLocalizationsDelegate(), WidgetsLocalizationsDelegate(),
MaterialLocalizationsDelegate(), MaterialLocalizationsDelegate(),
], ],
child: DefaultTextEditingShortcuts(
child: DefaultTextEditingActions(
child: Directionality( child: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: MediaQuery( child: MediaQuery(
...@@ -89,6 +91,8 @@ Widget overlayWithEntry(OverlayEntry entry) { ...@@ -89,6 +91,8 @@ Widget overlayWithEntry(OverlayEntry entry) {
), ),
), ),
), ),
),
),
); );
} }
......
...@@ -412,58 +412,6 @@ void main() { ...@@ -412,58 +412,6 @@ void main() {
expect(editable, paintsExactlyCountTimes(#drawRect, 1)); expect(editable, paintsExactlyCountTimes(#drawRect, 1));
}); });
test('ignore key event from web platform', () async {
final TextSelectionDelegate delegate = FakeEditableTextState();
final ViewportOffset viewportOffset = ViewportOffset.zero();
late TextSelection currentSelection;
final RenderEditable editable = RenderEditable(
backgroundCursorColor: Colors.grey,
selectionColor: Colors.black,
textDirection: TextDirection.ltr,
cursorColor: Colors.red,
offset: viewportOffset,
// This makes the scroll axis vertical.
maxLines: 2,
textSelectionDelegate: delegate,
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {
currentSelection = selection;
},
startHandleLayerLink: LayerLink(),
endHandleLayerLink: LayerLink(),
text: const TextSpan(
text: 'test\ntest',
style: TextStyle(
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
),
),
selection: const TextSelection.collapsed(
offset: 4,
),
);
layout(editable);
editable.hasFocus = true;
expect(
editable,
paints..paragraph(offset: Offset.zero),
);
editable.selectPositionAt(from: Offset.zero, cause: SelectionChangedCause.tap);
editable.selection = const TextSelection.collapsed(offset: 0);
pumpFrame();
if(kIsWeb) {
await simulateKeyDownEvent(LogicalKeyboardKey.arrowRight, platform: 'web');
expect(currentSelection.isCollapsed, true);
expect(currentSelection.baseOffset, 0);
} else {
await simulateKeyDownEvent(LogicalKeyboardKey.arrowRight, platform: 'android');
expect(currentSelection.isCollapsed, true);
expect(currentSelection.baseOffset, 1);
}
});
test('selects correct place with offsets', () { test('selects correct place with offsets', () {
final TextSelectionDelegate delegate = FakeEditableTextState(); final TextSelectionDelegate delegate = FakeEditableTextState();
final ViewportOffset viewportOffset = ViewportOffset.zero(); final ViewportOffset viewportOffset = ViewportOffset.zero();
...@@ -748,6 +696,88 @@ void main() { ...@@ -748,6 +696,88 @@ void main() {
expect(editable.maxScrollExtent, equals(10)); expect(editable.maxScrollExtent, equals(10));
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/42772 }, skip: isBrowser); // https://github.com/flutter/flutter/issues/42772
test('moveSelectionLeft/RightByLine stays on the current line', () async {
const String text = 'one two three\n\nfour five six';
final TextSelectionDelegate delegate = FakeEditableTextState()
..textEditingValue = const TextEditingValue(
text: text,
selection: TextSelection.collapsed(offset: 0),
);
final ViewportOffset viewportOffset = ViewportOffset.zero();
late TextSelection currentSelection;
final RenderEditable editable = RenderEditable(
backgroundCursorColor: Colors.grey,
selectionColor: Colors.black,
textDirection: TextDirection.ltr,
cursorColor: Colors.red,
offset: viewportOffset,
textSelectionDelegate: delegate,
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {
renderObject.selection = selection;
currentSelection = selection;
},
startHandleLayerLink: LayerLink(),
endHandleLayerLink: LayerLink(),
text: const TextSpan(
text: text,
style: TextStyle(
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
),
),
selection: const TextSelection.collapsed(
offset: 0,
),
);
layout(editable);
editable.hasFocus = true;
editable.selectPositionAt(from: Offset.zero, cause: SelectionChangedCause.tap);
editable.selection = const TextSelection.collapsed(offset: 0);
pumpFrame();
// Move to the end of the first line.
editable.moveSelectionRightByLine(SelectionChangedCause.keyboard);
expect(currentSelection.isCollapsed, true);
expect(currentSelection.baseOffset, 13);
// RenderEditable relies on its parent that passes onSelectionChanged to set
// the selection.
// Try moveSelectionRightByLine again and nothing happens because we're
// already at the end of a line.
editable.moveSelectionRightByLine(SelectionChangedCause.keyboard);
expect(currentSelection.isCollapsed, true);
expect(currentSelection.baseOffset, 13);
// Move back to the start of the line.
editable.moveSelectionLeftByLine(SelectionChangedCause.keyboard);
expect(currentSelection.isCollapsed, true);
expect(currentSelection.baseOffset, 0);
// Trying moveSelectionLeftByLine does nothing at the leftmost of the field.
editable.moveSelectionLeftByLine(SelectionChangedCause.keyboard);
expect(currentSelection.isCollapsed, true);
expect(currentSelection.baseOffset, 0);
// Move the selection to the empty line.
editable.moveSelectionRightByLine(SelectionChangedCause.keyboard);
expect(currentSelection.isCollapsed, true);
expect(currentSelection.baseOffset, 13);
editable.moveSelectionRight(SelectionChangedCause.keyboard);
expect(currentSelection.isCollapsed, true);
expect(currentSelection.baseOffset, 14);
// Neither moveSelectionLeftByLine nor moveSelectionRightByLine do anything
// here, because we're at both the beginning and end of the line.
editable.moveSelectionLeftByLine(SelectionChangedCause.keyboard);
expect(currentSelection.isCollapsed, true);
expect(currentSelection.baseOffset, 14);
editable.moveSelectionRightByLine(SelectionChangedCause.keyboard);
expect(currentSelection.isCollapsed, true);
expect(currentSelection.baseOffset, 14);
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021
test('arrow keys and delete handle simple text correctly', () async { test('arrow keys and delete handle simple text correctly', () async {
final TextSelectionDelegate delegate = FakeEditableTextState() final TextSelectionDelegate delegate = FakeEditableTextState()
..textEditingValue = const TextEditingValue( ..textEditingValue = const TextEditingValue(
...@@ -756,6 +786,7 @@ void main() { ...@@ -756,6 +786,7 @@ void main() {
); );
final ViewportOffset viewportOffset = ViewportOffset.zero(); final ViewportOffset viewportOffset = ViewportOffset.zero();
late TextSelection currentSelection; late TextSelection currentSelection;
final RenderEditable editable = RenderEditable( final RenderEditable editable = RenderEditable(
backgroundCursorColor: Colors.grey, backgroundCursorColor: Colors.grey,
selectionColor: Colors.black, selectionColor: Colors.black,
...@@ -764,6 +795,7 @@ void main() { ...@@ -764,6 +795,7 @@ void main() {
offset: viewportOffset, offset: viewportOffset,
textSelectionDelegate: delegate, textSelectionDelegate: delegate,
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) { onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {
renderObject.selection = selection;
currentSelection = selection; currentSelection = selection;
}, },
startHandleLayerLink: LayerLink(), startHandleLayerLink: LayerLink(),
...@@ -786,13 +818,11 @@ void main() { ...@@ -786,13 +818,11 @@ void main() {
editable.selection = const TextSelection.collapsed(offset: 0); editable.selection = const TextSelection.collapsed(offset: 0);
pumpFrame(); pumpFrame();
await simulateKeyDownEvent(LogicalKeyboardKey.arrowRight, platform: 'android'); editable.moveSelectionRight(SelectionChangedCause.keyboard);
await simulateKeyUpEvent(LogicalKeyboardKey.arrowRight, platform: 'android');
expect(currentSelection.isCollapsed, true); expect(currentSelection.isCollapsed, true);
expect(currentSelection.baseOffset, 1); expect(currentSelection.baseOffset, 1);
await simulateKeyDownEvent(LogicalKeyboardKey.arrowLeft, platform: 'android'); editable.moveSelectionLeft(SelectionChangedCause.keyboard);
await simulateKeyUpEvent(LogicalKeyboardKey.arrowLeft, platform: 'android');
expect(currentSelection.isCollapsed, true); expect(currentSelection.isCollapsed, true);
expect(currentSelection.baseOffset, 0); expect(currentSelection.baseOffset, 0);
...@@ -817,6 +847,7 @@ void main() { ...@@ -817,6 +847,7 @@ void main() {
offset: viewportOffset, offset: viewportOffset,
textSelectionDelegate: delegate, textSelectionDelegate: delegate,
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) { onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {
renderObject.selection = selection;
currentSelection = selection; currentSelection = selection;
}, },
startHandleLayerLink: LayerLink(), startHandleLayerLink: LayerLink(),
...@@ -838,17 +869,13 @@ void main() { ...@@ -838,17 +869,13 @@ void main() {
editable.selection = const TextSelection.collapsed(offset: 4); editable.selection = const TextSelection.collapsed(offset: 4);
pumpFrame(); pumpFrame();
await simulateKeyDownEvent(LogicalKeyboardKey.arrowRight, platform: 'android'); editable.moveSelectionRight(SelectionChangedCause.keyboard);
await simulateKeyUpEvent(LogicalKeyboardKey.arrowRight, platform: 'android');
expect(currentSelection.isCollapsed, true); expect(currentSelection.isCollapsed, true);
expect(currentSelection.baseOffset, 6); expect(currentSelection.baseOffset, 6);
editable.selection = currentSelection;
await simulateKeyDownEvent(LogicalKeyboardKey.arrowLeft, platform: 'android'); editable.moveSelectionLeft(SelectionChangedCause.keyboard);
await simulateKeyUpEvent(LogicalKeyboardKey.arrowLeft, platform: 'android');
expect(currentSelection.isCollapsed, true); expect(currentSelection.isCollapsed, true);
expect(currentSelection.baseOffset, 4); expect(currentSelection.baseOffset, 4);
editable.selection = currentSelection;
await simulateKeyDownEvent(LogicalKeyboardKey.delete, platform: 'android'); await simulateKeyDownEvent(LogicalKeyboardKey.delete, platform: 'android');
await simulateKeyUpEvent(LogicalKeyboardKey.delete, platform: 'android'); await simulateKeyUpEvent(LogicalKeyboardKey.delete, platform: 'android');
...@@ -871,6 +898,7 @@ void main() { ...@@ -871,6 +898,7 @@ void main() {
offset: viewportOffset, offset: viewportOffset,
textSelectionDelegate: delegate, textSelectionDelegate: delegate,
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) { onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {
renderObject.selection = selection;
currentSelection = selection; currentSelection = selection;
}, },
startHandleLayerLink: LayerLink(), startHandleLayerLink: LayerLink(),
...@@ -892,17 +920,13 @@ void main() { ...@@ -892,17 +920,13 @@ void main() {
editable.selection = const TextSelection.collapsed(offset: 4); editable.selection = const TextSelection.collapsed(offset: 4);
pumpFrame(); pumpFrame();
await simulateKeyDownEvent(LogicalKeyboardKey.arrowRight, platform: 'android'); editable.moveSelectionRight(SelectionChangedCause.keyboard);
await simulateKeyUpEvent(LogicalKeyboardKey.arrowRight, platform: 'android');
expect(currentSelection.isCollapsed, true); expect(currentSelection.isCollapsed, true);
expect(currentSelection.baseOffset, 12); expect(currentSelection.baseOffset, 12);
editable.selection = currentSelection;
await simulateKeyDownEvent(LogicalKeyboardKey.arrowLeft, platform: 'android'); editable.moveSelectionLeft(SelectionChangedCause.keyboard);
await simulateKeyUpEvent(LogicalKeyboardKey.arrowLeft, platform: 'android');
expect(currentSelection.isCollapsed, true); expect(currentSelection.isCollapsed, true);
expect(currentSelection.baseOffset, 4); expect(currentSelection.baseOffset, 4);
editable.selection = currentSelection;
await simulateKeyDownEvent(LogicalKeyboardKey.delete, platform: 'android'); await simulateKeyDownEvent(LogicalKeyboardKey.delete, platform: 'android');
await simulateKeyUpEvent(LogicalKeyboardKey.delete, platform: 'android'); await simulateKeyUpEvent(LogicalKeyboardKey.delete, platform: 'android');
...@@ -921,6 +945,7 @@ void main() { ...@@ -921,6 +945,7 @@ void main() {
offset: viewportOffset, offset: viewportOffset,
textSelectionDelegate: delegate, textSelectionDelegate: delegate,
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) { onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {
renderObject.selection = selection;
currentSelection = selection; currentSelection = selection;
}, },
startHandleLayerLink: LayerLink(), startHandleLayerLink: LayerLink(),
...@@ -943,13 +968,11 @@ void main() { ...@@ -943,13 +968,11 @@ void main() {
editable.selection = const TextSelection.collapsed(offset: 0); editable.selection = const TextSelection.collapsed(offset: 0);
pumpFrame(); pumpFrame();
await simulateKeyDownEvent(LogicalKeyboardKey.arrowRight, platform: 'android'); editable.moveSelectionRight(SelectionChangedCause.keyboard);
await simulateKeyUpEvent(LogicalKeyboardKey.arrowRight, platform: 'android');
expect(currentSelection.isCollapsed, true); expect(currentSelection.isCollapsed, true);
expect(currentSelection.baseOffset, 2); expect(currentSelection.baseOffset, 2);
await simulateKeyDownEvent(LogicalKeyboardKey.arrowLeft, platform: 'android'); editable.moveSelectionLeft(SelectionChangedCause.keyboard);
await simulateKeyUpEvent(LogicalKeyboardKey.arrowLeft, platform: 'android');
expect(currentSelection.isCollapsed, true); expect(currentSelection.isCollapsed, true);
expect(currentSelection.baseOffset, 0); expect(currentSelection.baseOffset, 0);
...@@ -998,19 +1021,14 @@ void main() { ...@@ -998,19 +1021,14 @@ void main() {
editable.selection = const TextSelection.collapsed(offset: 0); editable.selection = const TextSelection.collapsed(offset: 0);
pumpFrame(); pumpFrame();
await simulateKeyDownEvent(LogicalKeyboardKey.arrowRight, platform: 'android'); editable.moveSelectionRight(SelectionChangedCause.keyboard);
await simulateKeyUpEvent(LogicalKeyboardKey.arrowRight, platform: 'android'); editable.moveSelectionRight(SelectionChangedCause.keyboard);
await simulateKeyDownEvent(LogicalKeyboardKey.arrowRight, platform: 'android'); editable.moveSelectionRight(SelectionChangedCause.keyboard);
await simulateKeyUpEvent(LogicalKeyboardKey.arrowRight, platform: 'android'); editable.moveSelectionRight(SelectionChangedCause.keyboard);
await simulateKeyDownEvent(LogicalKeyboardKey.arrowRight, platform: 'android');
await simulateKeyUpEvent(LogicalKeyboardKey.arrowRight, platform: 'android');
await simulateKeyDownEvent(LogicalKeyboardKey.arrowRight, platform: 'android');
await simulateKeyUpEvent(LogicalKeyboardKey.arrowRight, platform: 'android');
expect(editable.selection?.isCollapsed, true); expect(editable.selection?.isCollapsed, true);
expect(editable.selection?.baseOffset, 4); expect(editable.selection?.baseOffset, 4);
await simulateKeyDownEvent(LogicalKeyboardKey.arrowLeft, platform: 'android'); editable.moveSelectionLeft(SelectionChangedCause.keyboard);
await simulateKeyUpEvent(LogicalKeyboardKey.arrowLeft, platform: 'android');
expect(editable.selection?.isCollapsed, true); expect(editable.selection?.isCollapsed, true);
expect(editable.selection?.baseOffset, 3); expect(editable.selection?.baseOffset, 3);
...@@ -1031,6 +1049,7 @@ void main() { ...@@ -1031,6 +1049,7 @@ void main() {
offset: viewportOffset, offset: viewportOffset,
textSelectionDelegate: delegate, textSelectionDelegate: delegate,
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) { onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {
renderObject.selection = selection;
currentSelection = selection; currentSelection = selection;
}, },
startHandleLayerLink: LayerLink(), startHandleLayerLink: LayerLink(),
...@@ -1050,32 +1069,28 @@ void main() { ...@@ -1050,32 +1069,28 @@ void main() {
editable.selection = const TextSelection(baseOffset: 2, extentOffset: 4); editable.selection = const TextSelection(baseOffset: 2, extentOffset: 4);
pumpFrame(); pumpFrame();
await simulateKeyDownEvent(LogicalKeyboardKey.arrowRight); editable.moveSelectionRight(SelectionChangedCause.keyboard);
await simulateKeyUpEvent(LogicalKeyboardKey.arrowRight);
expect(currentSelection.isCollapsed, true); expect(currentSelection.isCollapsed, true);
expect(currentSelection.baseOffset, 4); expect(currentSelection.baseOffset, 4);
editable.selection = const TextSelection(baseOffset: 4, extentOffset: 2); editable.selection = const TextSelection(baseOffset: 4, extentOffset: 2);
pumpFrame(); pumpFrame();
await simulateKeyDownEvent(LogicalKeyboardKey.arrowRight); editable.moveSelectionRight(SelectionChangedCause.keyboard);
await simulateKeyUpEvent(LogicalKeyboardKey.arrowRight);
expect(currentSelection.isCollapsed, true); expect(currentSelection.isCollapsed, true);
expect(currentSelection.baseOffset, 4); expect(currentSelection.baseOffset, 4);
editable.selection = const TextSelection(baseOffset: 2, extentOffset: 4); editable.selection = const TextSelection(baseOffset: 2, extentOffset: 4);
pumpFrame(); pumpFrame();
await simulateKeyDownEvent(LogicalKeyboardKey.arrowLeft); editable.moveSelectionLeft(SelectionChangedCause.keyboard);
await simulateKeyUpEvent(LogicalKeyboardKey.arrowLeft);
expect(currentSelection.isCollapsed, true); expect(currentSelection.isCollapsed, true);
expect(currentSelection.baseOffset, 2); expect(currentSelection.baseOffset, 2);
editable.selection = const TextSelection(baseOffset: 4, extentOffset: 2); editable.selection = const TextSelection(baseOffset: 4, extentOffset: 2);
pumpFrame(); pumpFrame();
await simulateKeyDownEvent(LogicalKeyboardKey.arrowLeft); editable.moveSelectionLeft(SelectionChangedCause.keyboard);
await simulateKeyUpEvent(LogicalKeyboardKey.arrowLeft);
expect(currentSelection.isCollapsed, true); expect(currentSelection.isCollapsed, true);
expect(currentSelection.baseOffset, 2); expect(currentSelection.baseOffset, 2);
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/58068 }, skip: isBrowser); // https://github.com/flutter/flutter/issues/58068
...@@ -1092,6 +1107,7 @@ void main() { ...@@ -1092,6 +1107,7 @@ void main() {
offset: viewportOffset, offset: viewportOffset,
textSelectionDelegate: delegate, textSelectionDelegate: delegate,
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) { onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {
renderObject.selection = selection;
currentSelection = selection; currentSelection = selection;
}, },
startHandleLayerLink: LayerLink(), startHandleLayerLink: LayerLink(),
...@@ -1111,10 +1127,7 @@ void main() { ...@@ -1111,10 +1127,7 @@ void main() {
editable.selection = const TextSelection(baseOffset: 2, extentOffset: 4); editable.selection = const TextSelection(baseOffset: 2, extentOffset: 4);
pumpFrame(); pumpFrame();
await simulateKeyDownEvent(LogicalKeyboardKey.shift); editable.extendSelectionRight(SelectionChangedCause.keyboard);
await simulateKeyDownEvent(LogicalKeyboardKey.arrowRight);
await simulateKeyUpEvent(LogicalKeyboardKey.arrowRight);
await simulateKeyUpEvent(LogicalKeyboardKey.shift);
expect(currentSelection.isCollapsed, false); expect(currentSelection.isCollapsed, false);
expect(currentSelection.baseOffset, 2); expect(currentSelection.baseOffset, 2);
expect(currentSelection.extentOffset, 5); expect(currentSelection.extentOffset, 5);
...@@ -1122,10 +1135,7 @@ void main() { ...@@ -1122,10 +1135,7 @@ void main() {
editable.selection = const TextSelection(baseOffset: 4, extentOffset: 2); editable.selection = const TextSelection(baseOffset: 4, extentOffset: 2);
pumpFrame(); pumpFrame();
await simulateKeyDownEvent(LogicalKeyboardKey.shift); editable.extendSelectionRight(SelectionChangedCause.keyboard);
await simulateKeyDownEvent(LogicalKeyboardKey.arrowRight);
await simulateKeyUpEvent(LogicalKeyboardKey.arrowRight);
await simulateKeyUpEvent(LogicalKeyboardKey.shift);
expect(currentSelection.isCollapsed, false); expect(currentSelection.isCollapsed, false);
expect(currentSelection.baseOffset, 4); expect(currentSelection.baseOffset, 4);
expect(currentSelection.extentOffset, 3); expect(currentSelection.extentOffset, 3);
...@@ -1133,10 +1143,7 @@ void main() { ...@@ -1133,10 +1143,7 @@ void main() {
editable.selection = const TextSelection(baseOffset: 2, extentOffset: 4); editable.selection = const TextSelection(baseOffset: 2, extentOffset: 4);
pumpFrame(); pumpFrame();
await simulateKeyDownEvent(LogicalKeyboardKey.shift); editable.extendSelectionLeft(SelectionChangedCause.keyboard);
await simulateKeyDownEvent(LogicalKeyboardKey.arrowLeft);
await simulateKeyUpEvent(LogicalKeyboardKey.arrowLeft);
await simulateKeyUpEvent(LogicalKeyboardKey.shift);
expect(currentSelection.isCollapsed, false); expect(currentSelection.isCollapsed, false);
expect(currentSelection.baseOffset, 2); expect(currentSelection.baseOffset, 2);
expect(currentSelection.extentOffset, 3); expect(currentSelection.extentOffset, 3);
...@@ -1144,10 +1151,7 @@ void main() { ...@@ -1144,10 +1151,7 @@ void main() {
editable.selection = const TextSelection(baseOffset: 4, extentOffset: 2); editable.selection = const TextSelection(baseOffset: 4, extentOffset: 2);
pumpFrame(); pumpFrame();
await simulateKeyDownEvent(LogicalKeyboardKey.shift); editable.extendSelectionLeft(SelectionChangedCause.keyboard);
await simulateKeyDownEvent(LogicalKeyboardKey.arrowLeft);
await simulateKeyUpEvent(LogicalKeyboardKey.arrowLeft);
await simulateKeyUpEvent(LogicalKeyboardKey.shift);
expect(currentSelection.isCollapsed, false); expect(currentSelection.isCollapsed, false);
expect(currentSelection.baseOffset, 4); expect(currentSelection.baseOffset, 4);
expect(currentSelection.extentOffset, 1); expect(currentSelection.extentOffset, 1);
...@@ -1165,6 +1169,7 @@ void main() { ...@@ -1165,6 +1169,7 @@ void main() {
offset: viewportOffset, offset: viewportOffset,
textSelectionDelegate: delegate, textSelectionDelegate: delegate,
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) { onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {
renderObject.selection = selection;
currentSelection = selection; currentSelection = selection;
}, },
startHandleLayerLink: LayerLink(), startHandleLayerLink: LayerLink(),
...@@ -1187,34 +1192,26 @@ void main() { ...@@ -1187,34 +1192,26 @@ void main() {
await simulateKeyDownEvent(LogicalKeyboardKey.shift); await simulateKeyDownEvent(LogicalKeyboardKey.shift);
await simulateKeyDownEvent(LogicalKeyboardKey.arrowRight); editable.moveSelectionRight(SelectionChangedCause.keyboard);
await simulateKeyUpEvent(LogicalKeyboardKey.arrowRight);
expect(currentSelection.isCollapsed, true); expect(currentSelection.isCollapsed, true);
expect(currentSelection.baseOffset, 3); expect(currentSelection.baseOffset, 3);
editable.selection = currentSelection;
await simulateKeyDownEvent(LogicalKeyboardKey.arrowLeft); editable.moveSelectionLeft(SelectionChangedCause.keyboard);
await simulateKeyUpEvent(LogicalKeyboardKey.arrowLeft);
expect(currentSelection.isCollapsed, true); expect(currentSelection.isCollapsed, true);
expect(currentSelection.baseOffset, 2); expect(currentSelection.baseOffset, 2);
editable.selection = currentSelection;
final LogicalKeyboardKey wordModifier = final LogicalKeyboardKey wordModifier =
Platform.isMacOS ? LogicalKeyboardKey.alt : LogicalKeyboardKey.control; Platform.isMacOS ? LogicalKeyboardKey.alt : LogicalKeyboardKey.control;
await simulateKeyDownEvent(wordModifier); await simulateKeyDownEvent(wordModifier);
await simulateKeyDownEvent(LogicalKeyboardKey.arrowRight); editable.moveSelectionRightByWord(SelectionChangedCause.keyboard);
await simulateKeyUpEvent(LogicalKeyboardKey.arrowRight);
expect(currentSelection.isCollapsed, true); expect(currentSelection.isCollapsed, true);
expect(currentSelection.baseOffset, 6); expect(currentSelection.baseOffset, 6);
editable.selection = currentSelection;
await simulateKeyDownEvent(LogicalKeyboardKey.arrowLeft); editable.moveSelectionLeftByWord(SelectionChangedCause.keyboard);
await simulateKeyUpEvent(LogicalKeyboardKey.arrowLeft);
expect(currentSelection.isCollapsed, true); expect(currentSelection.isCollapsed, true);
expect(currentSelection.baseOffset, 0); expect(currentSelection.baseOffset, 0);
editable.selection = currentSelection;
await simulateKeyUpEvent(wordModifier); await simulateKeyUpEvent(wordModifier);
await simulateKeyUpEvent(LogicalKeyboardKey.shift); await simulateKeyUpEvent(LogicalKeyboardKey.shift);
......
...@@ -39,7 +39,7 @@ void main() { ...@@ -39,7 +39,7 @@ void main() {
expect(find.byKey(key), findsOneWidget); expect(find.byKey(key), findsOneWidget);
}); });
testWidgets('WidgetsApp can override default key bindings', (WidgetTester tester) async { testWidgets('WidgetsApp default key bindings', (WidgetTester tester) async {
bool? checked = false; bool? checked = false;
final GlobalKey key = GlobalKey(); final GlobalKey key = GlobalKey();
await tester.pumpWidget( await tester.pumpWidget(
...@@ -64,9 +64,12 @@ void main() { ...@@ -64,9 +64,12 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
// Default key mapping worked. // Default key mapping worked.
expect(checked, isTrue); expect(checked, isTrue);
checked = false; });
testWidgets('WidgetsApp can override default key bindings', (WidgetTester tester) async {
final TestAction action = TestAction(); final TestAction action = TestAction();
bool? checked = false;
final GlobalKey key = GlobalKey();
await tester.pumpWidget( await tester.pumpWidget(
WidgetsApp( WidgetsApp(
key: key, key: key,
......
...@@ -4181,7 +4181,7 @@ void main() { ...@@ -4181,7 +4181,7 @@ void main() {
reason: 'on $platform', reason: 'on $platform',
); );
// Select to the beginning of the line. // Select to the beginning of the first line.
await sendKeys( await sendKeys(
tester, tester,
<LogicalKeyboardKey>[ <LogicalKeyboardKey>[
...@@ -4196,8 +4196,8 @@ void main() { ...@@ -4196,8 +4196,8 @@ void main() {
selection, selection,
equals( equals(
const TextSelection( const TextSelection(
baseOffset: 20, baseOffset: 0,
extentOffset: 55, extentOffset: 72,
affinity: TextAffinity.downstream, affinity: TextAffinity.downstream,
), ),
), ),
...@@ -4562,25 +4562,12 @@ void main() { ...@@ -4562,25 +4562,12 @@ void main() {
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 { testWidgets('keyboard text selection works', (WidgetTester tester) async {
await testTextEditing(tester, platform: 'linux'); final String targetPlatform = defaultTargetPlatform.toString();
// On web, using keyboard for selection is handled by the browser. final String platform = targetPlatform.substring(targetPlatform.indexOf('.') + 1).toLowerCase();
}, skip: kIsWeb); await testTextEditing(tester, platform: platform);
testWidgets('keyboard text selection works as expected on android', (WidgetTester tester) async {
await testTextEditing(tester, platform: 'android');
// On web, using keyboard for selection is handled by the browser.
}, skip: kIsWeb);
testWidgets('keyboard text selection works as expected on fuchsia', (WidgetTester tester) async {
await testTextEditing(tester, platform: 'fuchsia');
// On web, using keyboard for selection is handled by the browser. // On web, using keyboard for selection is handled by the browser.
}, skip: kIsWeb); }, skip: kIsWeb, variant: TargetPlatformVariant.all());
testWidgets('keyboard text selection works as expected on macos', (WidgetTester tester) async {
await testTextEditing(tester, platform: 'macos');
// On web, using keyboard for selection is handled by the browser.
}, skip: kIsWeb);
testWidgets('keyboard shortcuts respect read-only', (WidgetTester tester) async { testWidgets('keyboard shortcuts respect read-only', (WidgetTester tester) async {
final String platform = describeEnum(defaultTargetPlatform).toLowerCase(); final String platform = describeEnum(defaultTargetPlatform).toLowerCase();
...@@ -7182,6 +7169,172 @@ void main() { ...@@ -7182,6 +7169,172 @@ void main() {
expect(tester.takeException(), null); expect(tester.takeException(), null);
}); });
testWidgets('can change behavior by overriding text editing shortcuts', (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: Shortcuts(
shortcuts: <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.arrowLeft): const MoveSelectionRightTextIntent(),
},
child: EditableText(
maxLines: 10,
controller: controller,
showSelectionHandles: true,
autofocus: true,
focusNode: focusNode,
style: Typography.material2018(platform: TargetPlatform.android).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.
// The right arrow key moves to the right as usual.
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
await tester.pump();
expect(controller.selection.isCollapsed, isTrue);
expect(controller.selection.baseOffset, 1);
// And the left arrow also moves to the right due to the Shortcuts override.
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
await tester.pump();
expect(controller.selection.isCollapsed, isTrue);
expect(controller.selection.baseOffset, 2);
// On web, using keyboard for selection is handled by the browser.
}, skip: kIsWeb);
testWidgets('can change behavior by overriding text editing actions', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(text: testText);
controller.selection = const TextSelection(
baseOffset: 0,
extentOffset: 0,
affinity: TextAffinity.upstream,
);
late final bool myIntentWasCalled;
await tester.pumpWidget(MaterialApp(
home: Align(
alignment: Alignment.topLeft,
child: SizedBox(
width: 400,
child: Actions(
actions: <Type, Action<Intent>>{
MoveSelectionLeftTextIntent: _MyMoveSelectionRightTextAction(
onInvoke: () {
myIntentWasCalled = true;
},
),
},
child: EditableText(
maxLines: 10,
controller: controller,
showSelectionHandles: true,
autofocus: true,
focusNode: focusNode,
style: Typography.material2018(platform: TargetPlatform.android).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.
// The right arrow key moves to the right as usual.
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
await tester.pump();
expect(controller.selection.isCollapsed, isTrue);
expect(controller.selection.baseOffset, 1);
// And the left arrow also moves to the right due to the Actions override.
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
await tester.pump();
expect(controller.selection.isCollapsed, isTrue);
expect(controller.selection.baseOffset, 2);
expect(myIntentWasCalled, isTrue);
// On web, using keyboard for selection is handled by the browser.
}, skip: kIsWeb);
testWidgets('ignore key event from web platform', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'test\ntest',
);
controller.selection = const TextSelection(
baseOffset: 0,
extentOffset: 0,
affinity: TextAffinity.upstream,
);
bool myIntentWasCalled = false;
await tester.pumpWidget(MaterialApp(
home: Align(
alignment: Alignment.topLeft,
child: SizedBox(
width: 400,
child: Actions(
actions: <Type, Action<Intent>>{
MoveSelectionRightTextIntent: _MyMoveSelectionRightTextAction(
onInvoke: () {
myIntentWasCalled = true;
},
),
},
child: EditableText(
maxLines: 10,
controller: controller,
showSelectionHandles: true,
autofocus: true,
focusNode: focusNode,
style: Typography.material2018(platform: TargetPlatform.android).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.
if (kIsWeb) {
await simulateKeyDownEvent(LogicalKeyboardKey.arrowRight, platform: 'web');
await tester.pump();
expect(myIntentWasCalled, isFalse);
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 0);
} else {
await simulateKeyDownEvent(LogicalKeyboardKey.arrowRight, platform: 'android');
await tester.pump();
expect(myIntentWasCalled, isTrue);
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 1);
}
});
} }
class UnsettableController extends TextEditingController { class UnsettableController extends TextEditingController {
...@@ -7410,3 +7563,17 @@ class _AccentColorTextEditingController extends TextEditingController { ...@@ -7410,3 +7563,17 @@ class _AccentColorTextEditingController extends TextEditingController {
return super.buildTextSpan(context: context, style: TextStyle(color: color), withComposing: withComposing); return super.buildTextSpan(context: context, style: TextStyle(color: color), withComposing: withComposing);
} }
} }
class _MyMoveSelectionRightTextAction extends TextEditingAction<Intent> {
_MyMoveSelectionRightTextAction({
required this.onInvoke,
}) : super();
final VoidCallback onInvoke;
@override
Object? invoke(Intent intent, [BuildContext? context]) {
textEditingActionTarget!.renderEditable.moveSelectionRight(SelectionChangedCause.keyboard);
onInvoke();
}
}
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