mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
Move text editing Action
s to EditableTextState
(#90684)
This commit is contained in:
parent
035dfc87c5
commit
ffcd32ebb6
@ -18,7 +18,7 @@ void main() {
|
||||
}
|
||||
|
||||
// This implements a custom phone number input field that handles the
|
||||
// [DeleteTextIntent] intent.
|
||||
// [DeleteCharacterIntent] intent.
|
||||
class DigitInput extends StatefulWidget {
|
||||
const DigitInput({
|
||||
Key? key,
|
||||
@ -38,9 +38,9 @@ class DigitInput extends StatefulWidget {
|
||||
}
|
||||
|
||||
class DigitInputState extends State<DigitInput> {
|
||||
late final Action<DeleteTextIntent> _deleteTextAction =
|
||||
CallbackAction<DeleteTextIntent>(
|
||||
onInvoke: (DeleteTextIntent intent) {
|
||||
late final Action<DeleteCharacterIntent> _deleteTextAction =
|
||||
CallbackAction<DeleteCharacterIntent>(
|
||||
onInvoke: (DeleteCharacterIntent intent) {
|
||||
// For simplicity we delete everything in the section.
|
||||
widget.controller.clear();
|
||||
},
|
||||
@ -50,8 +50,8 @@ class DigitInputState extends State<DigitInput> {
|
||||
Widget build(BuildContext context) {
|
||||
return Actions(
|
||||
actions: <Type, Action<Intent>>{
|
||||
// Make the default `DeleteTextIntent` handler overridable.
|
||||
DeleteTextIntent: Action<DeleteTextIntent>.overridable(
|
||||
// Make the default `DeleteCharacterIntent` handler overridable.
|
||||
DeleteCharacterIntent: Action<DeleteCharacterIntent>.overridable(
|
||||
defaultAction: _deleteTextAction, context: context),
|
||||
},
|
||||
child: TextField(
|
||||
@ -79,12 +79,12 @@ class SimpleUSPhoneNumberEntry extends StatefulWidget {
|
||||
_SimpleUSPhoneNumberEntryState();
|
||||
}
|
||||
|
||||
class _DeleteDigit extends Action<DeleteTextIntent> {
|
||||
class _DeleteDigit extends Action<DeleteCharacterIntent> {
|
||||
_DeleteDigit(this.state);
|
||||
|
||||
final _SimpleUSPhoneNumberEntryState state;
|
||||
@override
|
||||
Object? invoke(DeleteTextIntent intent) {
|
||||
Object? invoke(DeleteCharacterIntent intent) {
|
||||
assert(callingAction != null);
|
||||
callingAction?.invoke(intent);
|
||||
|
||||
@ -116,7 +116,7 @@ class _SimpleUSPhoneNumberEntryState extends State<SimpleUSPhoneNumberEntry> {
|
||||
Widget build(BuildContext context) {
|
||||
return Actions(
|
||||
actions: <Type, Action<Intent>>{
|
||||
DeleteTextIntent: _DeleteDigit(this),
|
||||
DeleteCharacterIntent: _DeleteDigit(this),
|
||||
},
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
|
@ -206,6 +206,7 @@ class TextPainter {
|
||||
/// in framework will automatically invoke this method.
|
||||
void markNeedsLayout() {
|
||||
_paragraph = null;
|
||||
_lineMetricsCache = null;
|
||||
_previousCaretPosition = null;
|
||||
_previousCaretPrototype = null;
|
||||
}
|
||||
@ -975,6 +976,7 @@ class TextPainter {
|
||||
return _paragraph!.getLineBoundary(position);
|
||||
}
|
||||
|
||||
List<ui.LineMetrics>? _lineMetricsCache;
|
||||
/// Returns the full list of [LineMetrics] that describe in detail the various
|
||||
/// metrics of each laid out line.
|
||||
///
|
||||
@ -992,6 +994,6 @@ class TextPainter {
|
||||
/// should be invalidated upon the next successful [layout].
|
||||
List<ui.LineMetrics> computeLineMetrics() {
|
||||
assert(!_debugNeedsLayout);
|
||||
return _paragraph!.computeLineMetrics();
|
||||
return _lineMetricsCache ??= _paragraph!.computeLineMetrics();
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
import 'dart:collection';
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui' as ui show TextBox, BoxHeightStyle, BoxWidthStyle, PlaceholderAlignment;
|
||||
import 'dart:ui' as ui show TextBox, BoxHeightStyle, BoxWidthStyle, PlaceholderAlignment, LineMetrics;
|
||||
|
||||
import 'package:characters/characters.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
@ -77,6 +77,154 @@ class TextSelectionPoint {
|
||||
}
|
||||
}
|
||||
|
||||
/// The consecutive sequence of [TextPosition]s that the caret should move to
|
||||
/// when the user navigates the paragraph using the upward arrow key or the
|
||||
/// downward arrow key.
|
||||
///
|
||||
/// {@template flutter.rendering.RenderEditable.verticalArrowKeyMovement}
|
||||
/// When the user presses the upward arrow key or the downward arrow key, on
|
||||
/// many platforms (macOS for instance), the caret will move to the previous
|
||||
/// line or the next line, while maintaining its original horizontal location.
|
||||
/// When it encounters a shorter line, the caret moves to the closest horizontal
|
||||
/// location within that line, and restores the original horizontal location
|
||||
/// when a long enough line is encountered.
|
||||
///
|
||||
/// Additionally, the caret will move to the beginning of the document if the
|
||||
/// upward arrow key is pressed and the caret is already on the first line. If
|
||||
/// the downward arrow key is pressed next, the caret will restore its original
|
||||
/// horizontal location and move to the second line. Similarly the caret moves
|
||||
/// to the end of the document if the downward arrow key is pressed when it's
|
||||
/// already on the last line.
|
||||
///
|
||||
/// Consider a left-aligned paragraph:
|
||||
/// aa|
|
||||
/// a
|
||||
/// aaa
|
||||
/// where the caret was initially placed at the end of the first line. Pressing
|
||||
/// the downward arrow key once will move the caret to the end of the second
|
||||
/// line, and twice the arrow key moves to the third line after the second "a"
|
||||
/// on that line. Pressing the downward arrow key again, the caret will move to
|
||||
/// the end of the third line (the end of the document). Pressing the upward
|
||||
/// arrow key in this state will result in the caret moving to the end of the
|
||||
/// second line.
|
||||
///
|
||||
/// Vertical caret runs are typically interrupted when the layout of the text
|
||||
/// changes (including when the text itself changes), or when the selection is
|
||||
/// changed by other input events or programmatically (for example, when the
|
||||
/// user pressed the left arrow key).
|
||||
/// {@endtemplate}
|
||||
///
|
||||
/// The [movePrevious] method moves the caret location (which is
|
||||
/// [VerticalCaretMovementRun.current]) to the previous line, and in case
|
||||
/// the caret is already on the first line, the method does nothing and returns
|
||||
/// false. Similarly the [moveNext] method moves the caret to the next line, and
|
||||
/// returns false if the caret is already on the last line.
|
||||
///
|
||||
/// If the underlying paragraph's layout changes, [isValid] becomes false and
|
||||
/// the [VerticalCaretMovementRun] must not be used. The [isValid] property must
|
||||
/// be checked before calling [movePrevious] and [moveNext], or accessing
|
||||
/// [current].
|
||||
class VerticalCaretMovementRun extends BidirectionalIterator<TextPosition> {
|
||||
VerticalCaretMovementRun._(
|
||||
this._editable,
|
||||
this._lineMetrics,
|
||||
this._currentTextPosition,
|
||||
this._currentLine,
|
||||
this._currentOffset,
|
||||
);
|
||||
|
||||
Offset _currentOffset;
|
||||
int _currentLine;
|
||||
TextPosition _currentTextPosition;
|
||||
|
||||
final List<ui.LineMetrics> _lineMetrics;
|
||||
final RenderEditable _editable;
|
||||
|
||||
bool _isValid = true;
|
||||
/// Whether this [VerticalCaretMovementRun] can still continue.
|
||||
///
|
||||
/// A [VerticalCaretMovementRun] run is valid if the underlying text layout
|
||||
/// hasn't changed.
|
||||
///
|
||||
/// The [current] value and the [movePrevious] and [moveNext] methods must not
|
||||
/// be accessed when [isValid] is false.
|
||||
bool get isValid {
|
||||
if (!_isValid) {
|
||||
return false;
|
||||
}
|
||||
final List<ui.LineMetrics> newLineMetrics = _editable._textPainter.computeLineMetrics();
|
||||
// Use the implementation detail of the computeLineMetrics method to figure
|
||||
// out if the current text layout has been invalidated.
|
||||
if (!identical(newLineMetrics, _lineMetrics)) {
|
||||
_isValid = false;
|
||||
}
|
||||
return _isValid;
|
||||
}
|
||||
|
||||
// Computes the vertical distance from the `from` line's bottom to the `to`
|
||||
// lines top.
|
||||
double _lineDistance(int from, int to) {
|
||||
double lineHeight = 0;
|
||||
for (int index = from + 1; index < to; index += 1) {
|
||||
lineHeight += _lineMetrics[index].height;
|
||||
}
|
||||
return lineHeight;
|
||||
}
|
||||
|
||||
final Map<int, MapEntry<Offset, TextPosition>> _positionCache = <int, MapEntry<Offset, TextPosition>>{};
|
||||
|
||||
MapEntry<Offset, TextPosition> _getTextPositionForLine(int lineNumber) {
|
||||
assert(isValid);
|
||||
assert(lineNumber >= 0);
|
||||
final MapEntry<Offset, TextPosition>? cachedPosition = _positionCache[lineNumber];
|
||||
if (cachedPosition != null) {
|
||||
return cachedPosition;
|
||||
}
|
||||
assert(lineNumber != _currentLine);
|
||||
final double distanceY = lineNumber > _currentLine
|
||||
? _lineMetrics[_currentLine].descent + _lineMetrics[lineNumber].ascent + _lineDistance(_currentLine, lineNumber)
|
||||
: - _lineMetrics[_currentLine].ascent - _lineMetrics[lineNumber].descent - _lineDistance(lineNumber, _currentLine);
|
||||
|
||||
final Offset newOffset = _currentOffset.translate(0, distanceY);
|
||||
final TextPosition closestPosition = _editable._textPainter.getPositionForOffset(newOffset);
|
||||
final MapEntry<Offset, TextPosition> position = MapEntry<Offset, TextPosition>(newOffset, closestPosition);
|
||||
_positionCache[lineNumber] = position;
|
||||
return position;
|
||||
}
|
||||
|
||||
@override
|
||||
TextPosition get current {
|
||||
assert(isValid);
|
||||
return _currentTextPosition;
|
||||
}
|
||||
|
||||
@override
|
||||
bool moveNext() {
|
||||
assert(isValid);
|
||||
if (_currentLine + 1 >= _lineMetrics.length) {
|
||||
return false;
|
||||
}
|
||||
final MapEntry<Offset, TextPosition> position = _getTextPositionForLine(_currentLine + 1);
|
||||
_currentLine += 1;
|
||||
_currentOffset = position.key;
|
||||
_currentTextPosition = position.value;
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
bool movePrevious() {
|
||||
assert(isValid);
|
||||
if (_currentLine <= 0) {
|
||||
return false;
|
||||
}
|
||||
final MapEntry<Offset, TextPosition> position = _getTextPositionForLine(_currentLine - 1);
|
||||
_currentLine -= 1;
|
||||
_currentOffset = position.key;
|
||||
_currentTextPosition = position.value;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Displays some text in a scrollable container with a potentially blinking
|
||||
/// cursor and with gesture recognizers.
|
||||
///
|
||||
@ -2266,6 +2414,49 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
|
||||
_caretPainter.showRegularCaret = _resetFloatingCursorAnimationValue == null;
|
||||
}
|
||||
|
||||
MapEntry<int, Offset> _lineNumberFor(TextPosition startPosition, List<ui.LineMetrics> metrics) {
|
||||
// TODO(LongCatIsLooong): include line boundaries information in
|
||||
// ui.LineMetrics, then we can get rid of this.
|
||||
final Offset offset = _textPainter.getOffsetForCaret(startPosition, Rect.zero);
|
||||
int line = 0;
|
||||
double accumulatedHeight = 0;
|
||||
for (final ui.LineMetrics lineMetrics in metrics) {
|
||||
if (accumulatedHeight + lineMetrics.height > offset.dy) {
|
||||
return MapEntry<int, Offset>(line, Offset(offset.dx, lineMetrics.baseline));
|
||||
}
|
||||
line += 1;
|
||||
accumulatedHeight += lineMetrics.height;
|
||||
}
|
||||
assert(false, 'unable to find the line for $startPosition');
|
||||
return MapEntry<int, Offset>(math.max(0, metrics.length - 1), Offset(offset.dx, accumulatedHeight));
|
||||
}
|
||||
|
||||
/// Starts a [VerticalCaretMovementRun] at the given location in the text, for
|
||||
/// handling consecutive vertical caret movements.
|
||||
///
|
||||
/// This can be used to handle consecutive upward/downward arrow key movements
|
||||
/// in an input field.
|
||||
///
|
||||
/// {@macro flutter.rendering.RenderEditable.verticalArrowKeyMovement}
|
||||
///
|
||||
/// The [VerticalCaretMovementRun.isValid] property indicates whether the text
|
||||
/// layout has changed and the vertical caret run is invalidated.
|
||||
///
|
||||
/// The caller should typically discard a [VerticalCaretMovementRun] when
|
||||
/// its [VerticalCaretMovementRun.isValid] becomes false, or on other
|
||||
/// occasions where the vertical caret run should be interrupted.
|
||||
VerticalCaretMovementRun startVerticalCaretMovement(TextPosition startPosition) {
|
||||
final List<ui.LineMetrics> metrics = _textPainter.computeLineMetrics();
|
||||
final MapEntry<int, Offset> currentLine = _lineNumberFor(startPosition, metrics);
|
||||
return VerticalCaretMovementRun._(
|
||||
this,
|
||||
metrics,
|
||||
startPosition,
|
||||
currentLine.key,
|
||||
currentLine.value,
|
||||
);
|
||||
}
|
||||
|
||||
void _paintContents(PaintingContext context, Offset offset) {
|
||||
debugAssertLayoutUpToDate();
|
||||
final Offset effectiveOffset = offset + _paintOffset;
|
||||
|
@ -80,8 +80,31 @@ class TextSelection extends TextRange {
|
||||
|
||||
/// The position at which the selection originates.
|
||||
///
|
||||
/// {@template flutter.services.TextSelection.TextAffinity}
|
||||
/// The [TextAffinity] of the resulting [TextPosition] is based on the
|
||||
/// relative logical position in the text to the other selection endpoint:
|
||||
/// * if [baseOffset] < [extentOffset], [base] will have
|
||||
/// [TextAffinity.downstream] and [extent] will have
|
||||
/// [TextAffinity.upstream].
|
||||
/// * if [baseOffset] > [extentOffset], [base] will have
|
||||
/// [TextAffinity.upstream] and [extent] will have
|
||||
/// [TextAffinity.downstream].
|
||||
/// * if [baseOffset] == [extentOffset], [base] and [extent] will both have
|
||||
/// the collapsed selection's [affinity].
|
||||
/// {@endtemplate}
|
||||
///
|
||||
/// Might be larger than, smaller than, or equal to extent.
|
||||
TextPosition get base => TextPosition(offset: baseOffset, affinity: affinity);
|
||||
TextPosition get base {
|
||||
final TextAffinity affinity;
|
||||
if (!isValid || baseOffset == extentOffset) {
|
||||
affinity = this.affinity;
|
||||
} else if (baseOffset < extentOffset) {
|
||||
affinity = TextAffinity.downstream;
|
||||
} else {
|
||||
affinity = TextAffinity.upstream;
|
||||
}
|
||||
return TextPosition(offset: baseOffset, affinity: affinity);
|
||||
}
|
||||
|
||||
/// The position at which the selection terminates.
|
||||
///
|
||||
@ -89,8 +112,20 @@ class TextSelection extends TextRange {
|
||||
/// value that changes. Similarly, if the current theme paints a caret on one
|
||||
/// side of the selection, this is the location at which to paint the caret.
|
||||
///
|
||||
/// {@macro flutter.services.TextSelection.TextAffinity}
|
||||
///
|
||||
/// Might be larger than, smaller than, or equal to base.
|
||||
TextPosition get extent => TextPosition(offset: extentOffset, affinity: affinity);
|
||||
TextPosition get extent {
|
||||
final TextAffinity affinity;
|
||||
if (!isValid || baseOffset == extentOffset) {
|
||||
affinity = this.affinity;
|
||||
} else if (baseOffset < extentOffset) {
|
||||
affinity = TextAffinity.upstream;
|
||||
} else {
|
||||
affinity = TextAffinity.downstream;
|
||||
}
|
||||
return TextPosition(offset: extentOffset, affinity: affinity);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
|
@ -834,6 +834,55 @@ class TextEditingValue {
|
||||
/// programming error.
|
||||
bool get isComposingRangeValid => composing.isValid && composing.isNormalized && composing.end <= text.length;
|
||||
|
||||
/// Returns a new [TextEditingValue], which is this [TextEditingValue] with
|
||||
/// its [text] partially replaced by the `replacementString`.
|
||||
///
|
||||
/// The `replacementRange` parameter specifies the range of the
|
||||
/// [TextEditingValue.text] that needs to be replaced.
|
||||
///
|
||||
/// The `replacementString` parameter specifies the string to replace the
|
||||
/// given range of text with.
|
||||
///
|
||||
/// This method also adjusts the selection range and the composing range of the
|
||||
/// resulting [TextEditingValue], such that they point to the same substrings
|
||||
/// as the correspoinding ranges in the original [TextEditingValue]. For
|
||||
/// example, if the original [TextEditingValue] is "Hello world" with the word
|
||||
/// "world" selected, replacing "Hello" with a different string using this
|
||||
/// method will not change the selected word.
|
||||
///
|
||||
/// This method does nothing if the given `replacementRange` is not
|
||||
/// [TextRange.isValid].
|
||||
TextEditingValue replaced(TextRange replacementRange, String replacementString) {
|
||||
if (!replacementRange.isValid) {
|
||||
return this;
|
||||
}
|
||||
final String newText = text.replaceRange(replacementRange.start, replacementRange.end, replacementString);
|
||||
|
||||
if (replacementRange.end - replacementRange.start == replacementString.length) {
|
||||
return copyWith(text: newText);
|
||||
}
|
||||
|
||||
int adjustIndex(int originalIndex) {
|
||||
// The length added by adding the replacementString.
|
||||
final int replacedLength = originalIndex <= replacementRange.start && originalIndex < replacementRange.end ? 0 : replacementString.length;
|
||||
// The length removed by removing the replacementRange.
|
||||
final int removedLength = originalIndex.clamp(replacementRange.start, replacementRange.end) - replacementRange.start;
|
||||
return originalIndex + replacedLength - removedLength;
|
||||
}
|
||||
|
||||
return TextEditingValue(
|
||||
text: newText,
|
||||
selection: TextSelection(
|
||||
baseOffset: adjustIndex(selection.baseOffset),
|
||||
extentOffset: adjustIndex(selection.extentOffset),
|
||||
),
|
||||
composing: TextRange(
|
||||
start: adjustIndex(composing.start),
|
||||
end: adjustIndex(composing.end),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns a representation of this object as a JSON object.
|
||||
Map<String, dynamic> toJSON() {
|
||||
return <String, dynamic>{
|
||||
|
@ -142,8 +142,8 @@ abstract class Action<T extends Intent> with Diagnosticable {
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// This sample implements a custom text input field that handles the
|
||||
/// [DeleteTextIntent] intent, as well as a US telephone number input widget
|
||||
/// that consists of multiple text fields for area code, prefix and line
|
||||
/// [DeleteCharacterIntent] intent, as well as a US telephone number input
|
||||
/// widget that consists of multiple text fields for area code, prefix and line
|
||||
/// number. When the backspace key is pressed, the phone number input widget
|
||||
/// sends the focus to the preceding text field when the currently focused
|
||||
/// field becomes empty.
|
||||
|
@ -12,7 +12,6 @@ import 'actions.dart';
|
||||
import 'banner.dart';
|
||||
import 'basic.dart';
|
||||
import 'binding.dart';
|
||||
import 'default_text_editing_actions.dart';
|
||||
import 'default_text_editing_shortcuts.dart';
|
||||
import 'focus_traversal.dart';
|
||||
import 'framework.dart';
|
||||
@ -1053,9 +1052,6 @@ class WidgetsApp extends StatefulWidget {
|
||||
/// 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]
|
||||
/// widget.
|
||||
///
|
||||
/// Passing this will not replace [DefaultTextEditingActions]. These can be
|
||||
/// overridden by placing an [Actions] widget lower in the widget tree.
|
||||
/// {@endtemplate}
|
||||
///
|
||||
/// {@tool snippet}
|
||||
@ -1676,7 +1672,6 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
|
||||
child: DefaultTextEditingShortcuts(
|
||||
child: Actions(
|
||||
actions: widget.actions ?? WidgetsApp.defaultActions,
|
||||
child: DefaultTextEditingActions(
|
||||
child: FocusTraversalGroup(
|
||||
policy: ReadingOrderTraversalPolicy(),
|
||||
child: child,
|
||||
@ -1684,7 +1679,6 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -512,16 +512,12 @@ class _AutocompleteCallbackAction<T extends Intent> extends CallbackAction<T> {
|
||||
}
|
||||
|
||||
/// An [Intent] to highlight the previous option in the autocomplete list.
|
||||
///
|
||||
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
|
||||
class AutocompletePreviousOptionIntent extends Intent {
|
||||
/// Creates an instance of AutocompletePreviousOptionIntent.
|
||||
const AutocompletePreviousOptionIntent();
|
||||
}
|
||||
|
||||
/// An [Intent] to highlight the next option in the autocomplete list.
|
||||
///
|
||||
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
|
||||
class AutocompleteNextOptionIntent extends Intent {
|
||||
/// Creates an instance of AutocompleteNextOptionIntent.
|
||||
const AutocompleteNextOptionIntent();
|
||||
|
@ -1,326 +0,0 @@
|
||||
// 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';
|
||||
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(),
|
||||
DeleteTextIntent: _DeleteTextAction(),
|
||||
DeleteByWordTextIntent: _DeleteByWordTextAction(),
|
||||
DeleteByLineTextIntent: _DeleteByLineTextAction(),
|
||||
DeleteForwardTextIntent: _DeleteForwardTextAction(),
|
||||
DeleteForwardByWordTextIntent: _DeleteForwardByWordTextAction(),
|
||||
DeleteForwardByLineTextIntent: _DeleteForwardByLineTextAction(),
|
||||
ExtendSelectionDownTextIntent: _ExtendSelectionDownTextAction(),
|
||||
ExtendSelectionLeftByLineTextIntent: _ExtendSelectionLeftByLineTextAction(),
|
||||
ExtendSelectionLeftByWordTextIntent: _ExtendSelectionLeftByWordTextAction(),
|
||||
ExtendSelectionLeftByWordAndStopAtReversalTextIntent: _ExtendSelectionLeftByWordAndStopAtReversalTextAction(),
|
||||
ExtendSelectionLeftTextIntent: _ExtendSelectionLeftTextAction(),
|
||||
ExtendSelectionRightByWordAndStopAtReversalTextIntent: _ExtendSelectionRightByWordAndStopAtReversalTextAction(),
|
||||
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(),
|
||||
SelectAllTextIntent: _SelectAllTextAction(),
|
||||
CopySelectionTextIntent: _CopySelectionTextAction(),
|
||||
CutSelectionTextIntent: _CutSelectionTextAction(),
|
||||
PasteTextIntent: _PasteTextAction(),
|
||||
};
|
||||
}
|
||||
|
||||
// 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) => false;
|
||||
|
||||
@override
|
||||
void invoke(DoNothingAndStopPropagationTextIntent intent, [BuildContext? context]) {}
|
||||
}
|
||||
|
||||
class _DeleteTextAction extends TextEditingAction<DeleteTextIntent> {
|
||||
@override
|
||||
Object? invoke(DeleteTextIntent intent, [BuildContext? context]) {
|
||||
textEditingActionTarget!.delete(SelectionChangedCause.keyboard);
|
||||
}
|
||||
}
|
||||
|
||||
class _DeleteByWordTextAction extends TextEditingAction<DeleteByWordTextIntent> {
|
||||
@override
|
||||
Object? invoke(DeleteByWordTextIntent intent, [BuildContext? context]) {
|
||||
textEditingActionTarget!.deleteByWord(SelectionChangedCause.keyboard, false);
|
||||
}
|
||||
}
|
||||
|
||||
class _DeleteByLineTextAction extends TextEditingAction<DeleteByLineTextIntent> {
|
||||
@override
|
||||
Object? invoke(DeleteByLineTextIntent intent, [BuildContext? context]) {
|
||||
textEditingActionTarget!.deleteByLine(SelectionChangedCause.keyboard);
|
||||
}
|
||||
}
|
||||
|
||||
class _DeleteForwardTextAction extends TextEditingAction<DeleteForwardTextIntent> {
|
||||
@override
|
||||
Object? invoke(DeleteForwardTextIntent intent, [BuildContext? context]) {
|
||||
textEditingActionTarget!.deleteForward(SelectionChangedCause.keyboard);
|
||||
}
|
||||
}
|
||||
|
||||
class _DeleteForwardByWordTextAction extends TextEditingAction<DeleteForwardByWordTextIntent> {
|
||||
@override
|
||||
Object? invoke(DeleteForwardByWordTextIntent intent, [BuildContext? context]) {
|
||||
textEditingActionTarget!.deleteForwardByWord(SelectionChangedCause.keyboard, false);
|
||||
}
|
||||
}
|
||||
|
||||
class _DeleteForwardByLineTextAction extends TextEditingAction<DeleteForwardByLineTextIntent> {
|
||||
@override
|
||||
Object? invoke(DeleteForwardByLineTextIntent intent, [BuildContext? context]) {
|
||||
textEditingActionTarget!.deleteForwardByLine(SelectionChangedCause.keyboard);
|
||||
}
|
||||
}
|
||||
|
||||
class _ExpandSelectionLeftByLineTextAction extends TextEditingAction<ExpandSelectionLeftByLineTextIntent> {
|
||||
@override
|
||||
Object? invoke(ExpandSelectionLeftByLineTextIntent intent, [BuildContext? context]) {
|
||||
textEditingActionTarget!.expandSelectionLeftByLine(SelectionChangedCause.keyboard);
|
||||
}
|
||||
}
|
||||
|
||||
class _ExpandSelectionRightByLineTextAction extends TextEditingAction<ExpandSelectionRightByLineTextIntent> {
|
||||
@override
|
||||
Object? invoke(ExpandSelectionRightByLineTextIntent intent, [BuildContext? context]) {
|
||||
textEditingActionTarget!.expandSelectionRightByLine(SelectionChangedCause.keyboard);
|
||||
}
|
||||
}
|
||||
|
||||
class _ExpandSelectionToEndTextAction extends TextEditingAction<ExpandSelectionToEndTextIntent> {
|
||||
@override
|
||||
Object? invoke(ExpandSelectionToEndTextIntent intent, [BuildContext? context]) {
|
||||
textEditingActionTarget!.expandSelectionToEnd(SelectionChangedCause.keyboard);
|
||||
}
|
||||
}
|
||||
|
||||
class _ExpandSelectionToStartTextAction extends TextEditingAction<ExpandSelectionToStartTextIntent> {
|
||||
@override
|
||||
Object? invoke(ExpandSelectionToStartTextIntent intent, [BuildContext? context]) {
|
||||
textEditingActionTarget!.expandSelectionToStart(SelectionChangedCause.keyboard);
|
||||
}
|
||||
}
|
||||
|
||||
class _ExtendSelectionDownTextAction extends TextEditingAction<ExtendSelectionDownTextIntent> {
|
||||
@override
|
||||
Object? invoke(ExtendSelectionDownTextIntent intent, [BuildContext? context]) {
|
||||
textEditingActionTarget!.extendSelectionDown(SelectionChangedCause.keyboard);
|
||||
}
|
||||
}
|
||||
|
||||
class _ExtendSelectionLeftByLineTextAction extends TextEditingAction<ExtendSelectionLeftByLineTextIntent> {
|
||||
@override
|
||||
Object? invoke(ExtendSelectionLeftByLineTextIntent intent, [BuildContext? context]) {
|
||||
textEditingActionTarget!.extendSelectionLeftByLine(SelectionChangedCause.keyboard);
|
||||
}
|
||||
}
|
||||
|
||||
class _ExtendSelectionLeftByWordAndStopAtReversalTextAction extends TextEditingAction<ExtendSelectionLeftByWordAndStopAtReversalTextIntent> {
|
||||
@override
|
||||
Object? invoke(ExtendSelectionLeftByWordAndStopAtReversalTextIntent intent, [BuildContext? context]) {
|
||||
textEditingActionTarget!.extendSelectionLeftByWord(SelectionChangedCause.keyboard, false, true);
|
||||
}
|
||||
}
|
||||
|
||||
class _ExtendSelectionLeftByWordTextAction extends TextEditingAction<ExtendSelectionLeftByWordTextIntent> {
|
||||
@override
|
||||
Object? invoke(ExtendSelectionLeftByWordTextIntent intent, [BuildContext? context]) {
|
||||
textEditingActionTarget!.extendSelectionLeftByWord(SelectionChangedCause.keyboard, false);
|
||||
}
|
||||
}
|
||||
|
||||
class _ExtendSelectionLeftTextAction extends TextEditingAction<ExtendSelectionLeftTextIntent> {
|
||||
@override
|
||||
Object? invoke(ExtendSelectionLeftTextIntent intent, [BuildContext? context]) {
|
||||
textEditingActionTarget!.extendSelectionLeft(SelectionChangedCause.keyboard);
|
||||
}
|
||||
}
|
||||
|
||||
class _ExtendSelectionRightByLineTextAction extends TextEditingAction<ExtendSelectionRightByLineTextIntent> {
|
||||
@override
|
||||
Object? invoke(ExtendSelectionRightByLineTextIntent intent, [BuildContext? context]) {
|
||||
textEditingActionTarget!.extendSelectionRightByLine(SelectionChangedCause.keyboard);
|
||||
}
|
||||
}
|
||||
|
||||
class _ExtendSelectionRightByWordAndStopAtReversalTextAction extends TextEditingAction<ExtendSelectionRightByWordAndStopAtReversalTextIntent> {
|
||||
@override
|
||||
Object? invoke(ExtendSelectionRightByWordAndStopAtReversalTextIntent intent, [BuildContext? context]) {
|
||||
textEditingActionTarget!.extendSelectionRightByWord(SelectionChangedCause.keyboard, false, true);
|
||||
}
|
||||
}
|
||||
|
||||
class _ExtendSelectionRightByWordTextAction extends TextEditingAction<ExtendSelectionRightByWordTextIntent> {
|
||||
@override
|
||||
Object? invoke(ExtendSelectionRightByWordTextIntent intent, [BuildContext? context]) {
|
||||
textEditingActionTarget!.extendSelectionRightByWord(SelectionChangedCause.keyboard, false);
|
||||
}
|
||||
}
|
||||
|
||||
class _ExtendSelectionRightTextAction extends TextEditingAction<ExtendSelectionRightTextIntent> {
|
||||
@override
|
||||
Object? invoke(ExtendSelectionRightTextIntent intent, [BuildContext? context]) {
|
||||
textEditingActionTarget!.extendSelectionRight(SelectionChangedCause.keyboard);
|
||||
}
|
||||
}
|
||||
|
||||
class _ExtendSelectionUpTextAction extends TextEditingAction<ExtendSelectionUpTextIntent> {
|
||||
@override
|
||||
Object? invoke(ExtendSelectionUpTextIntent intent, [BuildContext? context]) {
|
||||
textEditingActionTarget!.extendSelectionUp(SelectionChangedCause.keyboard);
|
||||
}
|
||||
}
|
||||
|
||||
class _MoveSelectionDownTextAction extends TextEditingAction<MoveSelectionDownTextIntent> {
|
||||
@override
|
||||
Object? invoke(MoveSelectionDownTextIntent intent, [BuildContext? context]) {
|
||||
textEditingActionTarget!.moveSelectionDown(SelectionChangedCause.keyboard);
|
||||
}
|
||||
}
|
||||
|
||||
class _MoveSelectionLeftTextAction extends TextEditingAction<MoveSelectionLeftTextIntent> {
|
||||
@override
|
||||
Object? invoke(MoveSelectionLeftTextIntent intent, [BuildContext? context]) {
|
||||
textEditingActionTarget!.moveSelectionLeft(SelectionChangedCause.keyboard);
|
||||
}
|
||||
}
|
||||
|
||||
class _MoveSelectionRightTextAction extends TextEditingAction<MoveSelectionRightTextIntent> {
|
||||
@override
|
||||
Object? invoke(MoveSelectionRightTextIntent intent, [BuildContext? context]) {
|
||||
textEditingActionTarget!.moveSelectionRight(SelectionChangedCause.keyboard);
|
||||
}
|
||||
}
|
||||
|
||||
class _MoveSelectionUpTextAction extends TextEditingAction<MoveSelectionUpTextIntent> {
|
||||
@override
|
||||
Object? invoke(MoveSelectionUpTextIntent intent, [BuildContext? context]) {
|
||||
textEditingActionTarget!.moveSelectionUp(SelectionChangedCause.keyboard);
|
||||
}
|
||||
}
|
||||
|
||||
class _MoveSelectionLeftByLineTextAction extends TextEditingAction<MoveSelectionLeftByLineTextIntent> {
|
||||
@override
|
||||
Object? invoke(MoveSelectionLeftByLineTextIntent intent, [BuildContext? context]) {
|
||||
textEditingActionTarget!.moveSelectionLeftByLine(SelectionChangedCause.keyboard);
|
||||
}
|
||||
}
|
||||
|
||||
class _MoveSelectionLeftByWordTextAction extends TextEditingAction<MoveSelectionLeftByWordTextIntent> {
|
||||
@override
|
||||
Object? invoke(MoveSelectionLeftByWordTextIntent intent, [BuildContext? context]) {
|
||||
textEditingActionTarget!.moveSelectionLeftByWord(SelectionChangedCause.keyboard, false);
|
||||
}
|
||||
}
|
||||
|
||||
class _MoveSelectionRightByLineTextAction extends TextEditingAction<MoveSelectionRightByLineTextIntent> {
|
||||
@override
|
||||
Object? invoke(MoveSelectionRightByLineTextIntent intent, [BuildContext? context]) {
|
||||
textEditingActionTarget!.moveSelectionRightByLine(SelectionChangedCause.keyboard);
|
||||
}
|
||||
}
|
||||
|
||||
class _MoveSelectionRightByWordTextAction extends TextEditingAction<MoveSelectionRightByWordTextIntent> {
|
||||
@override
|
||||
Object? invoke(MoveSelectionRightByWordTextIntent intent, [BuildContext? context]) {
|
||||
textEditingActionTarget!.moveSelectionRightByWord(SelectionChangedCause.keyboard, false);
|
||||
}
|
||||
}
|
||||
|
||||
class _MoveSelectionToEndTextAction extends TextEditingAction<MoveSelectionToEndTextIntent> {
|
||||
@override
|
||||
Object? invoke(MoveSelectionToEndTextIntent intent, [BuildContext? context]) {
|
||||
textEditingActionTarget!.moveSelectionToEnd(SelectionChangedCause.keyboard);
|
||||
}
|
||||
}
|
||||
|
||||
class _MoveSelectionToStartTextAction extends TextEditingAction<MoveSelectionToStartTextIntent> {
|
||||
@override
|
||||
Object? invoke(MoveSelectionToStartTextIntent intent, [BuildContext? context]) {
|
||||
textEditingActionTarget!.moveSelectionToStart(SelectionChangedCause.keyboard);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class _SelectAllTextAction extends TextEditingAction<SelectAllTextIntent> {
|
||||
@override
|
||||
Object? invoke(SelectAllTextIntent intent, [BuildContext? context]) {
|
||||
textEditingActionTarget!.selectAll(SelectionChangedCause.keyboard);
|
||||
}
|
||||
}
|
||||
|
||||
class _CopySelectionTextAction extends TextEditingAction<CopySelectionTextIntent> {
|
||||
@override
|
||||
Object? invoke(CopySelectionTextIntent intent, [BuildContext? context]) {
|
||||
textEditingActionTarget!.copySelection(SelectionChangedCause.keyboard);
|
||||
}
|
||||
}
|
||||
|
||||
class _CutSelectionTextAction extends TextEditingAction<CutSelectionTextIntent> {
|
||||
@override
|
||||
Object? invoke(CutSelectionTextIntent intent, [BuildContext? context]) {
|
||||
textEditingActionTarget!.cutSelection(SelectionChangedCause.keyboard);
|
||||
}
|
||||
}
|
||||
|
||||
class _PasteTextAction extends TextEditingAction<PasteTextIntent> {
|
||||
@override
|
||||
Object? invoke(PasteTextIntent intent, [BuildContext? context]) {
|
||||
textEditingActionTarget!.pasteText(SelectionChangedCause.keyboard);
|
||||
}
|
||||
}
|
@ -14,8 +14,8 @@ import 'text_editing_intents.dart';
|
||||
/// 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].
|
||||
/// lower in the widget tree than this. See the [Action] class for an example
|
||||
/// of remapping an [Intent] to a custom [Action].
|
||||
///
|
||||
/// {@tool snippet}
|
||||
///
|
||||
@ -143,9 +143,6 @@ import 'text_editing_intents.dart';
|
||||
///
|
||||
/// 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
|
||||
@ -160,90 +157,51 @@ class DefaultTextEditingShortcuts extends Shortcuts {
|
||||
child: child,
|
||||
);
|
||||
|
||||
static const Map<ShortcutActivator, Intent> _androidShortcuts = <ShortcutActivator, Intent>{
|
||||
SingleActivator(LogicalKeyboardKey.backspace): DeleteTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.backspace, control: true): DeleteByWordTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.backspace, alt: true): DeleteByLineTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.delete): DeleteForwardTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.delete, control: true): DeleteForwardByWordTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.delete, alt: true): DeleteForwardByLineTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowDown, alt: true): MoveSelectionToEndTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowLeft, alt: true): MoveSelectionLeftByLineTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowRight, alt: true): MoveSelectionRightByLineTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowUp, alt: true): MoveSelectionToStartTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowDown, shift: true, alt: true): ExpandSelectionToEndTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true, alt: true): ExpandSelectionLeftByLineTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowRight, shift: true, alt: true): ExpandSelectionRightByLineTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowUp, shift: true, alt: true): ExpandSelectionToStartTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowDown): MoveSelectionDownTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowLeft): MoveSelectionLeftTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowRight): MoveSelectionRightTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowUp): MoveSelectionUpTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowLeft, control: true): MoveSelectionLeftByWordTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowRight, control: true): MoveSelectionRightByWordTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true, control: true): ExtendSelectionLeftByWordTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowRight, shift: true, control: true): ExtendSelectionRightByWordTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowDown, shift: true): ExtendSelectionDownTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true): ExtendSelectionLeftTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowRight, shift: true): ExtendSelectionRightTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowUp, shift: true): ExtendSelectionUpTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.keyX, control: true): CutSelectionTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.keyC, control: true): CopySelectionTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.keyV, control: true): PasteTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.keyA, control: true): SelectAllTextIntent(),
|
||||
// The following key combinations have no effect on text editing on this
|
||||
// platform:
|
||||
// * End
|
||||
// * Home
|
||||
// * Meta + X
|
||||
// * Meta + C
|
||||
// * Meta + V
|
||||
// * Meta + A
|
||||
// * 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
|
||||
// * Meta + delete
|
||||
// * Meta + backspace
|
||||
// These are shortcuts are shared between most platforms except macOS for it
|
||||
// uses different modifier keys as the line/word modifer.
|
||||
static const Map<ShortcutActivator, Intent> _commonShortcuts = <ShortcutActivator, Intent>{
|
||||
// Delete Shortcuts.
|
||||
SingleActivator(LogicalKeyboardKey.backspace): DeleteCharacterIntent(forward: false),
|
||||
SingleActivator(LogicalKeyboardKey.backspace, control: true): DeleteToNextWordBoundaryIntent(forward: false),
|
||||
SingleActivator(LogicalKeyboardKey.backspace, alt: true): DeleteToLineBreakIntent(forward: false),
|
||||
SingleActivator(LogicalKeyboardKey.delete): DeleteCharacterIntent(forward: true),
|
||||
SingleActivator(LogicalKeyboardKey.delete, control: true): DeleteToNextWordBoundaryIntent(forward: true),
|
||||
SingleActivator(LogicalKeyboardKey.delete, alt: true): DeleteToLineBreakIntent(forward: true),
|
||||
|
||||
// Arrow: Move Selection.
|
||||
SingleActivator(LogicalKeyboardKey.arrowLeft): ExtendSelectionByCharacterIntent(forward: false, collapseSelection: true),
|
||||
SingleActivator(LogicalKeyboardKey.arrowRight): ExtendSelectionByCharacterIntent(forward: true, collapseSelection: true),
|
||||
SingleActivator(LogicalKeyboardKey.arrowUp): ExtendSelectionVerticallyToAdjacentLineIntent(forward: false, collapseSelection: true),
|
||||
SingleActivator(LogicalKeyboardKey.arrowDown): ExtendSelectionVerticallyToAdjacentLineIntent(forward: true, collapseSelection: true),
|
||||
|
||||
// Shift + Arrow: Extend Selection.
|
||||
SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true): ExtendSelectionByCharacterIntent(forward: false, collapseSelection: false),
|
||||
SingleActivator(LogicalKeyboardKey.arrowRight, shift: true): ExtendSelectionByCharacterIntent(forward: true, collapseSelection: false),
|
||||
SingleActivator(LogicalKeyboardKey.arrowUp, shift: true): ExtendSelectionVerticallyToAdjacentLineIntent(forward: false, collapseSelection: false),
|
||||
SingleActivator(LogicalKeyboardKey.arrowDown, shift: true): ExtendSelectionVerticallyToAdjacentLineIntent(forward: true, collapseSelection: false),
|
||||
|
||||
SingleActivator(LogicalKeyboardKey.arrowLeft, alt: true): ExtendSelectionToLineBreakIntent(forward: false, collapseSelection: true),
|
||||
SingleActivator(LogicalKeyboardKey.arrowRight, alt: true): ExtendSelectionToLineBreakIntent(forward: true, collapseSelection: true),
|
||||
SingleActivator(LogicalKeyboardKey.arrowUp, alt: true): ExtendSelectionToDocumentBoundaryIntent(forward: false, collapseSelection: true),
|
||||
SingleActivator(LogicalKeyboardKey.arrowDown, alt: true): ExtendSelectionToDocumentBoundaryIntent(forward: true, collapseSelection: true),
|
||||
|
||||
SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true, alt: true): ExtendSelectionToLineBreakIntent(forward: false, collapseSelection: false),
|
||||
SingleActivator(LogicalKeyboardKey.arrowRight, shift: true, alt: true): ExtendSelectionToLineBreakIntent(forward: true, collapseSelection: false),
|
||||
SingleActivator(LogicalKeyboardKey.arrowUp, shift: true, alt: true): ExtendSelectionToDocumentBoundaryIntent(forward: false, collapseSelection: false),
|
||||
SingleActivator(LogicalKeyboardKey.arrowDown, shift: true, alt: true): ExtendSelectionToDocumentBoundaryIntent(forward: true, collapseSelection: false),
|
||||
|
||||
SingleActivator(LogicalKeyboardKey.arrowLeft, control: true): ExtendSelectionToNextWordBoundaryIntent(forward: false, collapseSelection: true),
|
||||
SingleActivator(LogicalKeyboardKey.arrowRight, control: true): ExtendSelectionToNextWordBoundaryIntent(forward: true, collapseSelection: true),
|
||||
|
||||
SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true, control: true): ExtendSelectionToNextWordBoundaryIntent(forward: false, collapseSelection: false),
|
||||
SingleActivator(LogicalKeyboardKey.arrowRight, shift: true, control: true): ExtendSelectionToNextWordBoundaryIntent(forward: true, collapseSelection: false),
|
||||
|
||||
SingleActivator(LogicalKeyboardKey.keyX, control: true): CopySelectionTextIntent.cut(SelectionChangedCause.keyboard),
|
||||
SingleActivator(LogicalKeyboardKey.keyC, control: true): CopySelectionTextIntent.copy,
|
||||
SingleActivator(LogicalKeyboardKey.keyV, control: true): PasteTextIntent(SelectionChangedCause.keyboard),
|
||||
SingleActivator(LogicalKeyboardKey.keyA, control: true): SelectAllTextIntent(SelectionChangedCause.keyboard),
|
||||
};
|
||||
|
||||
static const Map<ShortcutActivator, Intent> _fuchsiaShortcuts = <ShortcutActivator, Intent>{
|
||||
SingleActivator(LogicalKeyboardKey.backspace): DeleteTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.backspace, control: true): DeleteByWordTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.backspace, alt: true): DeleteByLineTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.delete): DeleteForwardTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.delete, control: true): DeleteForwardByWordTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.delete, alt: true): DeleteForwardByLineTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowDown, alt: true): MoveSelectionToEndTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowLeft, alt: true): MoveSelectionLeftByLineTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowRight, alt: true): MoveSelectionRightByLineTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowUp, alt: true): MoveSelectionToStartTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowDown, shift: true, alt: true): ExpandSelectionToEndTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true, alt: true): ExpandSelectionLeftByLineTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowRight, shift: true, alt: true): ExpandSelectionRightByLineTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowUp, shift: true, alt: true): ExpandSelectionToStartTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowDown): MoveSelectionDownTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowLeft): MoveSelectionLeftTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowRight): MoveSelectionRightTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowUp): MoveSelectionUpTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowLeft, control: true): MoveSelectionLeftByWordTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowRight, control: true): MoveSelectionRightByWordTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true, control: true): ExtendSelectionLeftByWordTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowRight, shift: true, control: true): ExtendSelectionRightByWordTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowDown, shift: true): ExtendSelectionDownTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true): ExtendSelectionLeftTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowRight, shift: true): ExtendSelectionRightTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowUp, shift: true): ExtendSelectionUpTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.keyX, control: true): CutSelectionTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.keyC, control: true): CopySelectionTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.keyV, control: true): PasteTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.keyA, control: true): SelectAllTextIntent(),
|
||||
// The following key combinations have no effect on text editing on this
|
||||
// platform:
|
||||
// * End
|
||||
@ -264,39 +222,10 @@ class DefaultTextEditingShortcuts extends Shortcuts {
|
||||
// * Shift + home
|
||||
// * Meta + delete
|
||||
// * Meta + backspace
|
||||
};
|
||||
static const Map<ShortcutActivator, Intent> _androidShortcuts = _commonShortcuts;
|
||||
|
||||
static const Map<ShortcutActivator, Intent> _fuchsiaShortcuts = _androidShortcuts;
|
||||
|
||||
static const Map<ShortcutActivator, Intent> _iOSShortcuts = <ShortcutActivator, Intent>{
|
||||
SingleActivator(LogicalKeyboardKey.backspace): DeleteTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.backspace, control: true): DeleteByWordTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.backspace, alt: true): DeleteByLineTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.delete): DeleteForwardTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.delete, control: true): DeleteForwardByWordTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.delete, alt: true): DeleteForwardByLineTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowDown, alt: true): MoveSelectionToEndTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowLeft, alt: true): MoveSelectionLeftByLineTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowRight, alt: true): MoveSelectionRightByLineTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowUp, alt: true): MoveSelectionToStartTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowDown, shift: true, alt: true): ExpandSelectionToEndTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true, alt: true): ExpandSelectionLeftByLineTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowRight, shift: true, alt: true): ExpandSelectionRightByLineTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowUp, shift: true, alt: true): ExpandSelectionToStartTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowDown): MoveSelectionDownTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowLeft): MoveSelectionLeftTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowRight): MoveSelectionRightTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowUp): MoveSelectionUpTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowRight, control: true): MoveSelectionRightByWordTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowLeft, control: true): MoveSelectionLeftByWordTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true, control: true): ExtendSelectionLeftByWordTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowRight, shift: true, control: true): ExtendSelectionRightByWordTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowDown, shift: true): ExtendSelectionDownTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true): ExtendSelectionLeftTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowRight, shift: true): ExtendSelectionRightTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowUp, shift: true): ExtendSelectionUpTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.keyX, control: true): CutSelectionTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.keyC, control: true): CopySelectionTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.keyV, control: true): PasteTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.keyA, control: true): SelectAllTextIntent(),
|
||||
// The following key combinations have no effect on text editing on this
|
||||
// platform:
|
||||
// * End
|
||||
@ -317,43 +246,14 @@ class DefaultTextEditingShortcuts extends Shortcuts {
|
||||
// * Shift + home
|
||||
// * Meta + delete
|
||||
// * Meta + backspace
|
||||
};
|
||||
static const Map<ShortcutActivator, Intent> _iOSShortcuts = _commonShortcuts;
|
||||
|
||||
static const Map<ShortcutActivator, Intent> _linuxShortcuts = <ShortcutActivator, Intent>{
|
||||
SingleActivator(LogicalKeyboardKey.backspace): DeleteTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.backspace, control: true): DeleteByWordTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.backspace, alt: true): DeleteByLineTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.delete): DeleteForwardTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.delete, control: true): DeleteForwardByWordTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.delete, alt: true): DeleteForwardByLineTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowDown, alt: true): MoveSelectionToEndTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowLeft, alt: true): MoveSelectionLeftByLineTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowRight, alt: true): MoveSelectionRightByLineTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowUp, alt: true): MoveSelectionToStartTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowDown, shift: true, alt: true): ExpandSelectionToEndTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true, alt: true): ExpandSelectionLeftByLineTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowRight, shift: true, alt: true): ExpandSelectionRightByLineTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowUp, shift: true, alt: true): ExpandSelectionToStartTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowDown): MoveSelectionDownTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowLeft): MoveSelectionLeftTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowRight): MoveSelectionRightTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowUp): MoveSelectionUpTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowLeft, control: true): MoveSelectionLeftByWordTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowRight, control: true): MoveSelectionRightByWordTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true, control: true): ExtendSelectionLeftByWordTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowRight, shift: true, control: true): ExtendSelectionRightByWordTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowDown, shift: true): ExtendSelectionDownTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true): ExtendSelectionLeftTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowRight, shift: true): ExtendSelectionRightTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowUp, shift: true): ExtendSelectionUpTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.end): MoveSelectionRightByLineTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.home): MoveSelectionLeftByLineTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.end, shift: true): ExtendSelectionRightByLineTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.home, shift: true): ExtendSelectionLeftByLineTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.keyX, control: true): CutSelectionTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.keyC, control: true): CopySelectionTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.keyV, control: true): PasteTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.keyA, control: true): SelectAllTextIntent(),
|
||||
..._commonShortcuts,
|
||||
SingleActivator(LogicalKeyboardKey.home): ExtendSelectionToLineBreakIntent(forward: false, collapseSelection: true),
|
||||
SingleActivator(LogicalKeyboardKey.end): ExtendSelectionToLineBreakIntent(forward: true, collapseSelection: true),
|
||||
SingleActivator(LogicalKeyboardKey.home, shift: true): ExtendSelectionToLineBreakIntent(forward: false, collapseSelection: false),
|
||||
SingleActivator(LogicalKeyboardKey.end, shift: true): ExtendSelectionToLineBreakIntent(forward: true, collapseSelection: false),
|
||||
// The following key combinations have no effect on text editing on this
|
||||
// platform:
|
||||
// * Meta + X
|
||||
@ -372,94 +272,57 @@ class DefaultTextEditingShortcuts extends Shortcuts {
|
||||
// * Meta + backspace
|
||||
};
|
||||
|
||||
// macOS document shortcuts: https://support.apple.com/en-us/HT201236.
|
||||
// The macOS shortcuts uses different word/line modifiers than most other
|
||||
// platforms.
|
||||
static const Map<ShortcutActivator, Intent> _macShortcuts = <ShortcutActivator, Intent>{
|
||||
SingleActivator(LogicalKeyboardKey.backspace): DeleteTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.backspace, alt: true): DeleteByWordTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.backspace, meta: true): DeleteByLineTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.delete): DeleteForwardTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.delete, alt: true): DeleteForwardByWordTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.delete, meta: true): DeleteForwardByLineTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowDown, alt: true): MoveSelectionRightByLineTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowLeft, alt: true): MoveSelectionLeftByWordTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowRight, alt: true): MoveSelectionRightByWordTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowUp, alt: true): MoveSelectionLeftByLineTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowDown, shift: true, alt: true): ExtendSelectionRightByLineTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true, alt: true): ExtendSelectionLeftByWordAndStopAtReversalTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowRight, shift: true, alt: true): ExtendSelectionRightByWordAndStopAtReversalTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowUp, shift: true, alt: true): ExtendSelectionLeftByLineTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowDown): MoveSelectionDownTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowLeft): MoveSelectionLeftTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowRight): MoveSelectionRightTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowUp): MoveSelectionUpTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowDown, meta: true): MoveSelectionToEndTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowLeft, meta: true): MoveSelectionLeftByLineTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowRight, meta: true): MoveSelectionRightByLineTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowUp, meta: true): MoveSelectionToStartTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowDown, shift: true, meta: true): ExpandSelectionToEndTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true, meta: true): ExpandSelectionLeftByLineTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowRight, shift: true, meta: true): ExpandSelectionRightByLineTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowUp, shift: true, meta: true): ExpandSelectionToStartTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowDown, shift: true): ExtendSelectionDownTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true): ExtendSelectionLeftTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowRight, shift: true): ExtendSelectionRightTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowUp, shift: true): ExtendSelectionUpTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.end, shift: true): ExpandSelectionToEndTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.home, shift: true): ExpandSelectionToStartTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.keyX, meta: true): CutSelectionTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.keyC, meta: true): CopySelectionTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.keyV, meta: true): PasteTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.keyA, meta: true): SelectAllTextIntent(),
|
||||
// The following key combinations have no effect on text editing on this
|
||||
// platform:
|
||||
// * Control + X
|
||||
// * Control + C
|
||||
// * Control + V
|
||||
// * Control + A
|
||||
// * Control + arrow left
|
||||
// * Control + arrow right
|
||||
// * Control + shift + arrow left
|
||||
// * Control + shift + arrow right
|
||||
// * End
|
||||
// * Home
|
||||
// * Control + delete
|
||||
// * Control + backspace
|
||||
SingleActivator(LogicalKeyboardKey.backspace): DeleteCharacterIntent(forward: false),
|
||||
SingleActivator(LogicalKeyboardKey.backspace, alt: true): DeleteToNextWordBoundaryIntent(forward: false),
|
||||
SingleActivator(LogicalKeyboardKey.backspace, meta: true): DeleteToLineBreakIntent(forward: false),
|
||||
SingleActivator(LogicalKeyboardKey.delete): DeleteCharacterIntent(forward: true),
|
||||
SingleActivator(LogicalKeyboardKey.delete, alt: true): DeleteToNextWordBoundaryIntent(forward: true),
|
||||
SingleActivator(LogicalKeyboardKey.delete, meta: true): DeleteToLineBreakIntent(forward: true),
|
||||
|
||||
SingleActivator(LogicalKeyboardKey.arrowLeft): ExtendSelectionByCharacterIntent(forward: false, collapseSelection: true),
|
||||
SingleActivator(LogicalKeyboardKey.arrowRight): ExtendSelectionByCharacterIntent(forward: true, collapseSelection: true),
|
||||
SingleActivator(LogicalKeyboardKey.arrowUp): ExtendSelectionVerticallyToAdjacentLineIntent(forward: false, collapseSelection: true),
|
||||
SingleActivator(LogicalKeyboardKey.arrowDown): ExtendSelectionVerticallyToAdjacentLineIntent(forward: true, collapseSelection: true),
|
||||
|
||||
// Shift + Arrow: Extend Selection.
|
||||
SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true): ExtendSelectionByCharacterIntent(forward: false, collapseSelection: false),
|
||||
SingleActivator(LogicalKeyboardKey.arrowRight, shift: true): ExtendSelectionByCharacterIntent(forward: true, collapseSelection: false),
|
||||
SingleActivator(LogicalKeyboardKey.arrowUp, shift: true): ExtendSelectionVerticallyToAdjacentLineIntent(forward: false, collapseSelection: false),
|
||||
SingleActivator(LogicalKeyboardKey.arrowDown, shift: true): ExtendSelectionVerticallyToAdjacentLineIntent(forward: true, collapseSelection: false),
|
||||
|
||||
SingleActivator(LogicalKeyboardKey.arrowLeft, alt: true): ExtendSelectionToNextWordBoundaryIntent(forward: false, collapseSelection: true),
|
||||
SingleActivator(LogicalKeyboardKey.arrowRight, alt: true): ExtendSelectionToNextWordBoundaryIntent(forward: true, collapseSelection: true),
|
||||
SingleActivator(LogicalKeyboardKey.arrowUp, alt: true): ExtendSelectionToLineBreakIntent(forward: false, collapseSelection: true),
|
||||
SingleActivator(LogicalKeyboardKey.arrowDown, alt: true): ExtendSelectionToLineBreakIntent(forward: true, collapseSelection: true),
|
||||
|
||||
SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true, alt: true): ExtendSelectionToNextWordBoundaryOrCaretLocationIntent(forward: false),
|
||||
SingleActivator(LogicalKeyboardKey.arrowRight, shift: true, alt: true): ExtendSelectionToNextWordBoundaryOrCaretLocationIntent(forward: true),
|
||||
SingleActivator(LogicalKeyboardKey.arrowUp, shift: true, alt: true): ExtendSelectionToLineBreakIntent(forward: false, collapseSelection: false),
|
||||
SingleActivator(LogicalKeyboardKey.arrowDown, shift: true, alt: true): ExtendSelectionToLineBreakIntent(forward: true, collapseSelection: false),
|
||||
|
||||
SingleActivator(LogicalKeyboardKey.arrowLeft, meta: true): ExtendSelectionToLineBreakIntent(forward: false, collapseSelection: true),
|
||||
SingleActivator(LogicalKeyboardKey.arrowRight, meta: true): ExtendSelectionToLineBreakIntent(forward: true, collapseSelection: true),
|
||||
SingleActivator(LogicalKeyboardKey.arrowUp, meta: true): ExtendSelectionToDocumentBoundaryIntent(forward: false, collapseSelection: true),
|
||||
SingleActivator(LogicalKeyboardKey.arrowDown, meta: true): ExtendSelectionToDocumentBoundaryIntent(forward: true, collapseSelection: true),
|
||||
|
||||
SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true, meta: true): ExtendSelectionToLineBreakIntent(forward: false, collapseSelection: false),
|
||||
SingleActivator(LogicalKeyboardKey.arrowRight, shift: true, meta: true): ExtendSelectionToLineBreakIntent(forward: true, collapseSelection: false),
|
||||
SingleActivator(LogicalKeyboardKey.arrowUp, shift: true, meta: true): ExtendSelectionToDocumentBoundaryIntent(forward: false, collapseSelection: false),
|
||||
SingleActivator(LogicalKeyboardKey.arrowDown, shift: true, meta: true): ExtendSelectionToDocumentBoundaryIntent(forward: true, collapseSelection: false),
|
||||
|
||||
SingleActivator(LogicalKeyboardKey.home, shift: true): ExtendSelectionToDocumentBoundaryIntent(forward: false, collapseSelection: false),
|
||||
SingleActivator(LogicalKeyboardKey.end, shift: true): ExtendSelectionToDocumentBoundaryIntent(forward: true, collapseSelection: false),
|
||||
|
||||
SingleActivator(LogicalKeyboardKey.keyX, meta: true): CopySelectionTextIntent.cut(SelectionChangedCause.keyboard),
|
||||
SingleActivator(LogicalKeyboardKey.keyC, meta: true): CopySelectionTextIntent.copy,
|
||||
SingleActivator(LogicalKeyboardKey.keyV, meta: true): PasteTextIntent(SelectionChangedCause.keyboard),
|
||||
SingleActivator(LogicalKeyboardKey.keyA, meta: true): SelectAllTextIntent(SelectionChangedCause.keyboard),
|
||||
};
|
||||
|
||||
static const Map<ShortcutActivator, Intent> _windowsShortcuts = <ShortcutActivator, Intent>{
|
||||
SingleActivator(LogicalKeyboardKey.backspace): DeleteTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.backspace, control: true): DeleteByWordTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.backspace, alt: true): DeleteByLineTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.delete): DeleteForwardTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.delete, control: true): DeleteForwardByWordTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.delete, alt: true): DeleteForwardByLineTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowDown, alt: true): MoveSelectionToEndTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowLeft, alt: true): MoveSelectionLeftByLineTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowRight, alt: true): MoveSelectionRightByLineTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowUp, alt: true): MoveSelectionToStartTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowDown, shift: true, alt: true): ExpandSelectionToEndTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true, alt: true): ExpandSelectionLeftByLineTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowRight, shift: true, alt: true): ExpandSelectionRightByLineTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowUp, shift: true, alt: true): ExpandSelectionToStartTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowDown): MoveSelectionDownTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowLeft): MoveSelectionLeftTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowRight): MoveSelectionRightTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowUp): MoveSelectionUpTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowLeft, control: true): MoveSelectionLeftByWordTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowRight, control: true): MoveSelectionRightByWordTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true, control: true): ExtendSelectionLeftByWordTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowRight, shift: true, control: true): ExtendSelectionRightByWordTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.end): MoveSelectionRightByLineTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.home): MoveSelectionLeftByLineTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowDown, shift: true): ExtendSelectionDownTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true): ExtendSelectionLeftTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowRight, shift: true): ExtendSelectionRightTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowUp, shift: true): ExtendSelectionUpTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.end, shift: true): ExtendSelectionRightByLineTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.home, shift: true): ExtendSelectionLeftByLineTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.keyX, control: true): CutSelectionTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.keyC, control: true): CopySelectionTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.keyV, control: true): PasteTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.keyA, control: true): SelectAllTextIntent(),
|
||||
// The following key combinations have no effect on text editing on this
|
||||
// platform:
|
||||
// * Meta + X
|
||||
@ -476,7 +339,7 @@ class DefaultTextEditingShortcuts extends Shortcuts {
|
||||
// * Meta + shift + arrow up
|
||||
// * Meta + delete
|
||||
// * Meta + backspace
|
||||
};
|
||||
static const Map<ShortcutActivator, Intent> _windowsShortcuts = _linuxShortcuts;
|
||||
|
||||
// Web handles its text selection natively and doesn't use any of these
|
||||
// shortcuts in Flutter.
|
||||
|
@ -6,12 +6,14 @@ import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui' as ui hide TextStyle;
|
||||
|
||||
import 'package:characters/characters.dart' show CharacterRange;
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart' show DragStartBehavior;
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'actions.dart';
|
||||
import 'autofill.dart';
|
||||
import 'automatic_keep_alive.dart';
|
||||
import 'basic.dart';
|
||||
@ -20,6 +22,7 @@ import 'constants.dart';
|
||||
import 'debug.dart';
|
||||
import 'focus_manager.dart';
|
||||
import 'focus_scope.dart';
|
||||
import 'focus_traversal.dart';
|
||||
import 'framework.dart';
|
||||
import 'localizations.dart';
|
||||
import 'media_query.dart';
|
||||
@ -28,7 +31,7 @@ import 'scroll_controller.dart';
|
||||
import 'scroll_physics.dart';
|
||||
import 'scrollable.dart';
|
||||
import 'text.dart';
|
||||
import 'text_editing_action_target.dart';
|
||||
import 'text_editing_intents.dart';
|
||||
import 'text_selection.dart';
|
||||
import 'ticker_provider.dart';
|
||||
import 'widget_span.dart';
|
||||
@ -345,6 +348,61 @@ class ToolbarOptions {
|
||||
/// is a full-featured, material-design text input field with placeholder text,
|
||||
/// labels, and [Form] integration.
|
||||
///
|
||||
/// ## Text Editing [Intent]s and Their Default [Action]s
|
||||
///
|
||||
/// This widget provides default [Action]s for handling common text editing
|
||||
/// [Intent]s such as deleting, copying and pasting in the text field. These
|
||||
/// [Action]s can be directly invoked using [Actions.invoke] or the
|
||||
/// [Actions.maybeInvoke] method. The default text editing keyboard [Shortcuts]
|
||||
/// also use these [Intent]s and [Action]s to perform the text editing
|
||||
/// operations they are bound to.
|
||||
///
|
||||
/// The default handling of a specific [Intent] can be overridden by placing an
|
||||
/// [Actions] widget above this widget. See the [Action] class and the
|
||||
/// [Action.overridable] constructor for more information on how a pre-defined
|
||||
/// overridable [Action] can be overridden.
|
||||
///
|
||||
/// ### Intents for Deleting Text and Their Default Behavior
|
||||
///
|
||||
/// | **Intent Class** | **Default Behavior when there's selected text** | **Default Behavior when there is a [caret](https://en.wikipedia.org/wiki/Caret_navigation) (The selection is [TextSelection.collapsed])** |
|
||||
/// | :------------------------------- | :--------------------------------------------------- | :----------------------------------------------------------------------- |
|
||||
/// | [DeleteCharacterIntent] | Deletes the selected text | Deletes the user-perceived character before or after the caret location. |
|
||||
/// | [DeleteToNextWordBoundaryIntent] | Deletes the selected text and the word before/after the selection's [TextSelection.extent] position | Deletes from the caret location to the previous or the next word boundary |
|
||||
/// | [DeleteToLineBreakIntent] | Deletes the selected text, and deletes to the start/end of the line from the selection's [TextSelection.extent] position | Deletes from the caret location to the logical start or end of the current line |
|
||||
///
|
||||
/// ### Intents for Moving the [Caret](https://en.wikipedia.org/wiki/Caret_navigation)
|
||||
///
|
||||
/// | **Intent Class** | **Default Behavior when there's selected text** | **Default Behavior when there is a caret ([TextSelection.collapsed])** |
|
||||
/// | :----------------------------------------------------------------------------------- | :--------------------------------------------------------------- | :---------------------------------------------------------------------- |
|
||||
/// | [ExtendSelectionByCharacterIntent](`collapseSelection: true`) | Collapses the selection to the logical start/end of the selection | Moves the caret past the user-perceived character before or after the current caret location. |
|
||||
/// | [ExtendSelectionToNextWordBoundaryIntent](`collapseSelection: true`) | Collapses the selection to the word boundary before/after the selection's [TextSelection.extent] position | Moves the caret to the previous/next word boundary. |
|
||||
/// | [ExtendSelectionToNextWordBoundaryOrCaretLocationIntent](`collapseSelection: true`) | Collapses the selection to the word boundary before/after the selection's [TextSelection.extent] position, or [TextSelection.base], whichever is closest in the given direction | Moves the caret to the previous/next word boundary. |
|
||||
/// | [ExtendSelectionToLineBreakIntent](`collapseSelection: true`) | Collapses the selection to the start/end of the line at the selection's [TextSelection.extent] position | Moves the caret to the start/end of the current line .|
|
||||
/// | [ExtendSelectionVerticallyToAdjacentLineIntent](`collapseSelection: true`) | Collapses the selection to the position closest to the selection's [TextSelection.extent], on the previous/next adjacent line | Moves the caret to the closest position on the previous/next adjacent line. |
|
||||
/// | [ExtendSelectionToDocumentBoundaryIntent](`collapseSelection: true`) | Collapses the selection to the start/end of the document | Moves the caret to the start/end of the document. |
|
||||
///
|
||||
/// #### Intents for Extending the Selection
|
||||
///
|
||||
/// | **Intent Class** | **Default Behavior when there's selected text** | **Default Behavior when there is a caret ([TextSelection.collapsed])** |
|
||||
/// | :----------------------------------------------------------------------------------- | :--------------------------------------------------------------- | :---------------------------------------------------------------------- |
|
||||
/// | [ExtendSelectionByCharacterIntent](`collapseSelection: false`) | Moves the selection's [TextSelection.extent] past the user-perceived character before/after it |
|
||||
/// | [ExtendSelectionToNextWordBoundaryIntent](`collapseSelection: false`) | Moves the selection's [TextSelection.extent] to the previous/next word boundary |
|
||||
/// | [ExtendSelectionToNextWordBoundaryOrCaretLocationIntent](`collapseSelection: false`) | Moves the selection's [TextSelection.extent] to the previous/next word boundary, or [TextSelection.base] whichever is closest in the given direction | Moves the selection's [TextSelection.extent] to the previous/next word boundary. |
|
||||
/// | [ExtendSelectionToLineBreakIntent](`collapseSelection: false`) | Moves the selection's [TextSelection.extent] to the start/end of the line |
|
||||
/// | [ExtendSelectionVerticallyToAdjacentLineIntent](`collapseSelection: false`) | Moves the selection's [TextSelection.extent] to the closest position on the previous/next adjacent line |
|
||||
/// | [ExtendSelectionToDocumentBoundaryIntent](`collapseSelection: false`) | Moves the selection's [TextSelection.extent] to the start/end of the document |
|
||||
/// | [SelectAllTextIntent] | Selects the entire document |
|
||||
///
|
||||
/// ### Other Intents
|
||||
///
|
||||
/// | **Intent Class** | **Default Behavior** |
|
||||
/// | :-------------------------------------- | :--------------------------------------------------- |
|
||||
/// | [DoNothingAndStopPropagationTextIntent] | Does nothing in the input field, and prevents the key event from further propagating in the widget tree. |
|
||||
/// | [ReplaceTextIntent] | Replaces the current [TextEditingValue] in the input field's [TextEditingController], and triggers all related user callbacks and [TextInputFormatter]s. |
|
||||
/// | [UpdateSelectionIntent] | Updates the current selection in the input field's [TextEditingController], and triggers the [onSelectionChanged] callback. |
|
||||
/// | [CopySelectionTextIntent] | Copies or cuts the selected text into the clipboard |
|
||||
/// | [PasteTextIntent] | Inserts the current text in the clipboard after the caret location, or replaces the selected text if the selection is not collapsed. |
|
||||
///
|
||||
/// ## Gesture Events Handling
|
||||
///
|
||||
/// This widget provides rudimentary, platform-agnostic gesture handling for
|
||||
@ -1453,7 +1511,7 @@ class EditableText extends StatefulWidget {
|
||||
}
|
||||
|
||||
/// State for a [EditableText].
|
||||
class EditableTextState extends State<EditableText> with AutomaticKeepAliveClientMixin<EditableText>, WidgetsBindingObserver, TickerProviderStateMixin<EditableText>, TextSelectionDelegate, TextEditingActionTarget implements TextInputClient, AutofillClient {
|
||||
class EditableTextState extends State<EditableText> with AutomaticKeepAliveClientMixin<EditableText>, WidgetsBindingObserver, TickerProviderStateMixin<EditableText>, TextSelectionDelegate implements TextInputClient, AutofillClient {
|
||||
Timer? _cursorTimer;
|
||||
bool _targetCursorVisibility = false;
|
||||
final ValueNotifier<bool> _cursorVisibilityNotifier = ValueNotifier<bool>(true);
|
||||
@ -1476,7 +1534,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
final LayerLink _endHandleLayerLink = LayerLink();
|
||||
|
||||
bool _didAutoFocus = false;
|
||||
FocusAttachment? _focusAttachment;
|
||||
|
||||
AutofillGroupState? _currentAutofillScope;
|
||||
@override
|
||||
@ -1534,62 +1591,24 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
});
|
||||
}
|
||||
|
||||
// Start TextEditingActionTarget.
|
||||
|
||||
@override
|
||||
TextLayoutMetrics get textLayoutMetrics => renderEditable;
|
||||
|
||||
@override
|
||||
bool get readOnly => widget.readOnly;
|
||||
|
||||
@override
|
||||
bool get obscureText => widget.obscureText;
|
||||
|
||||
@override
|
||||
bool get selectionEnabled => widget.selectionEnabled;
|
||||
|
||||
@override
|
||||
void debugAssertLayoutUpToDate() => renderEditable.debugAssertLayoutUpToDate();
|
||||
|
||||
/// {@macro flutter.widgets.TextEditingActionTarget.setSelection}
|
||||
@override
|
||||
void setSelection(TextSelection nextSelection, SelectionChangedCause cause) {
|
||||
if (nextSelection == textEditingValue.selection) {
|
||||
return;
|
||||
TextEditingValue get _textEditingValueforTextLayoutMetrics {
|
||||
final Widget? editableWidget =_editableKey.currentContext?.widget;
|
||||
if (editableWidget is! _Editable) {
|
||||
throw StateError('_Editable must be mounted.');
|
||||
}
|
||||
if (nextSelection.isValid) {
|
||||
// The nextSelection is calculated based on _plainText, which can be out
|
||||
// of sync with the textSelectionDelegate.textEditingValue by one frame.
|
||||
// This is due to the render editable and editable text handle pointer
|
||||
// event separately. If the editable text changes the text during the
|
||||
// event handler, the render editable will use the outdated text stored in
|
||||
// the _plainText when handling the pointer event.
|
||||
//
|
||||
// If this happens, we need to make sure the new selection is still valid.
|
||||
final int textLength = textEditingValue.text.length;
|
||||
nextSelection = nextSelection.copyWith(
|
||||
baseOffset: math.min(nextSelection.baseOffset, textLength),
|
||||
extentOffset: math.min(nextSelection.extentOffset, textLength),
|
||||
);
|
||||
}
|
||||
_handleSelectionChange(nextSelection, cause);
|
||||
return super.setSelection(nextSelection, cause);
|
||||
return editableWidget.value;
|
||||
}
|
||||
|
||||
/// {@macro flutter.widgets.TextEditingActionTarget.setTextEditingValue}
|
||||
@override
|
||||
void setTextEditingValue(TextEditingValue newValue, SelectionChangedCause cause) {
|
||||
if (newValue == textEditingValue) {
|
||||
return;
|
||||
}
|
||||
textEditingValue = newValue;
|
||||
userUpdateTextEditingValue(newValue, cause);
|
||||
}
|
||||
|
||||
/// {@macro flutter.widgets.TextEditingActionTarget.copySelection}
|
||||
/// Copy current selection to [Clipboard].
|
||||
@override
|
||||
void copySelection(SelectionChangedCause cause) {
|
||||
super.copySelection(cause);
|
||||
final TextSelection selection = textEditingValue.selection;
|
||||
final String text = textEditingValue.text;
|
||||
assert(selection != null);
|
||||
if (selection.isCollapsed) {
|
||||
return;
|
||||
}
|
||||
Clipboard.setData(ClipboardData(text: selection.textInside(text)));
|
||||
if (cause == SelectionChangedCause.toolbar) {
|
||||
bringIntoView(textEditingValue.selection.extent);
|
||||
hideToolbar(false);
|
||||
@ -1615,20 +1634,45 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
}
|
||||
}
|
||||
|
||||
/// {@macro flutter.widgets.TextEditingActionTarget.cutSelection}
|
||||
/// Cut current selection to [Clipboard].
|
||||
@override
|
||||
void cutSelection(SelectionChangedCause cause) {
|
||||
super.cutSelection(cause);
|
||||
if (widget.readOnly) {
|
||||
return;
|
||||
}
|
||||
final TextSelection selection = textEditingValue.selection;
|
||||
final String text = textEditingValue.text;
|
||||
assert(selection != null);
|
||||
if (selection.isCollapsed) {
|
||||
return;
|
||||
}
|
||||
Clipboard.setData(ClipboardData(text: selection.textInside(text)));
|
||||
_replaceText(ReplaceTextIntent(textEditingValue, '', selection, cause));
|
||||
if (cause == SelectionChangedCause.toolbar) {
|
||||
bringIntoView(textEditingValue.selection.extent);
|
||||
hideToolbar();
|
||||
}
|
||||
}
|
||||
|
||||
/// {@macro flutter.widgets.TextEditingActionTarget.pasteText}
|
||||
/// Paste text from [Clipboard].
|
||||
@override
|
||||
Future<void> pasteText(SelectionChangedCause cause) async {
|
||||
super.pasteText(cause);
|
||||
if (widget.readOnly) {
|
||||
return;
|
||||
}
|
||||
final TextSelection selection = textEditingValue.selection;
|
||||
assert(selection != null);
|
||||
if (!selection.isValid) {
|
||||
return;
|
||||
}
|
||||
// Snapshot the input before using `await`.
|
||||
// See https://github.com/flutter/flutter/issues/11427
|
||||
final ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain);
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
_replaceText(ReplaceTextIntent(textEditingValue, data.text!, selection, cause));
|
||||
if (cause == SelectionChangedCause.toolbar) {
|
||||
bringIntoView(textEditingValue.selection.extent);
|
||||
hideToolbar();
|
||||
@ -1638,29 +1682,17 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
/// Select the entire text value.
|
||||
@override
|
||||
void selectAll(SelectionChangedCause cause) {
|
||||
super.selectAll(cause);
|
||||
userUpdateTextEditingValue(
|
||||
textEditingValue.copyWith(
|
||||
selection: TextSelection(baseOffset: 0, extentOffset: textEditingValue.text.length),
|
||||
),
|
||||
cause,
|
||||
);
|
||||
if (cause == SelectionChangedCause.toolbar) {
|
||||
bringIntoView(textEditingValue.selection.extent);
|
||||
}
|
||||
}
|
||||
|
||||
// End TextEditingActionTarget.
|
||||
|
||||
void _handleSelectionChange(
|
||||
TextSelection nextSelection,
|
||||
SelectionChangedCause cause,
|
||||
) {
|
||||
// Changes made by the keyboard can sometimes be "out of band" for listening
|
||||
// components, so always send those events, even if we didn't think it
|
||||
// changed. Also, focusing an empty field is sent as a selection change even
|
||||
// if the selection offset didn't change.
|
||||
final bool focusingEmpty = nextSelection.baseOffset == 0 && nextSelection.extentOffset == 0 && !_hasFocus;
|
||||
if (nextSelection == textEditingValue.selection && cause != SelectionChangedCause.keyboard && !focusingEmpty) {
|
||||
return;
|
||||
}
|
||||
widget.onSelectionChanged?.call(nextSelection, cause);
|
||||
}
|
||||
|
||||
// State lifecycle:
|
||||
|
||||
@override
|
||||
@ -1668,7 +1700,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
super.initState();
|
||||
_clipboardStatus?.addListener(_onChangedClipboardStatus);
|
||||
widget.controller.addListener(_didChangeTextEditingValue);
|
||||
_focusAttachment = widget.focusNode.attach(context);
|
||||
widget.focusNode.addListener(_handleFocusChanged);
|
||||
_scrollController.addListener(_updateSelectionOverlayForScroll);
|
||||
_cursorVisibilityNotifier.value = widget.showCursor;
|
||||
@ -1732,8 +1763,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
|
||||
if (widget.focusNode != oldWidget.focusNode) {
|
||||
oldWidget.focusNode.removeListener(_handleFocusChanged);
|
||||
_focusAttachment?.detach();
|
||||
_focusAttachment = widget.focusNode.attach(context);
|
||||
widget.focusNode.addListener(_handleFocusChanged);
|
||||
updateKeepAlive();
|
||||
}
|
||||
@ -1787,7 +1816,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
_cursorBlinkOpacityController.dispose();
|
||||
_selectionOverlay?.dispose();
|
||||
_selectionOverlay = null;
|
||||
_focusAttachment!.detach();
|
||||
widget.focusNode.removeListener(_handleFocusChanged);
|
||||
WidgetsBinding.instance!.removeObserver(this);
|
||||
_clipboardStatus?.removeListener(_onChangedClipboardStatus);
|
||||
@ -2603,6 +2631,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
// TODO(abarth): Teach RenderEditable about ValueNotifier<TextEditingValue>
|
||||
// to avoid this setState().
|
||||
setState(() { /* We use widget.controller.value in build(). */ });
|
||||
_adjacentLineAction.stopCurrentVerticalRunIfSelectionChanges();
|
||||
}
|
||||
|
||||
void _handleFocusChanged() {
|
||||
@ -2818,15 +2847,122 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
: null;
|
||||
}
|
||||
|
||||
|
||||
// --------------------------- Text Editing Actions ---------------------------
|
||||
|
||||
_TextBoundary _characterBoundary(DirectionalTextEditingIntent intent) {
|
||||
final _TextBoundary atomicTextBoundary = widget.obscureText ? _CodeUnitBoundary(_value) : _CharacterBoundary(_value);
|
||||
return _CollapsedSelectionBoundary(atomicTextBoundary, intent.forward);
|
||||
}
|
||||
|
||||
_TextBoundary _nextWordBoundary(DirectionalTextEditingIntent intent) {
|
||||
final _TextBoundary atomicTextBoundary;
|
||||
final _TextBoundary boundary;
|
||||
|
||||
if (widget.obscureText) {
|
||||
atomicTextBoundary = _CodeUnitBoundary(_value);
|
||||
boundary = _DocumentBoundary(_value);
|
||||
} else {
|
||||
final TextEditingValue textEditingValue = _textEditingValueforTextLayoutMetrics;
|
||||
atomicTextBoundary = _CharacterBoundary(textEditingValue);
|
||||
// This isn't enough. Newline characters.
|
||||
boundary = _ExpandedTextBoundary(_WhitespaceBoundary(textEditingValue), _WordBoundary(renderEditable, textEditingValue));
|
||||
}
|
||||
|
||||
final _MixedBoundary mixedBoundary = intent.forward
|
||||
? _MixedBoundary(atomicTextBoundary, boundary)
|
||||
: _MixedBoundary(boundary, atomicTextBoundary);
|
||||
// Use a _MixedBoundary to make sure we don't leave invalid codepoints in
|
||||
// the field after deletion.
|
||||
return _CollapsedSelectionBoundary(mixedBoundary, intent.forward);
|
||||
}
|
||||
|
||||
_TextBoundary _linebreak(DirectionalTextEditingIntent intent) {
|
||||
final _TextBoundary atomicTextBoundary;
|
||||
final _TextBoundary boundary;
|
||||
|
||||
if (widget.obscureText) {
|
||||
atomicTextBoundary = _CodeUnitBoundary(_value);
|
||||
boundary = _DocumentBoundary(_value);
|
||||
} else {
|
||||
final TextEditingValue textEditingValue = _textEditingValueforTextLayoutMetrics;
|
||||
atomicTextBoundary = _CharacterBoundary(textEditingValue);
|
||||
boundary = _LineBreak(renderEditable, textEditingValue);
|
||||
}
|
||||
|
||||
// The _MixedBoundary is to make sure we don't leave invalid code units in
|
||||
// the field after deletion.
|
||||
// `boundary` doesn't need to be wrapped in a _CollapsedSelectionBoundary,
|
||||
// since the document boundary is unique and the linebreak boundary is
|
||||
// already caret-location based.
|
||||
return intent.forward
|
||||
? _MixedBoundary(_CollapsedSelectionBoundary(atomicTextBoundary, true), boundary)
|
||||
: _MixedBoundary(boundary, _CollapsedSelectionBoundary(atomicTextBoundary, false));
|
||||
}
|
||||
|
||||
_TextBoundary _documentBoundary(DirectionalTextEditingIntent intent) => _DocumentBoundary(_value);
|
||||
|
||||
Action<T> _makeOverridable<T extends Intent>(Action<T> defaultAction) {
|
||||
return Action<T>.overridable(context: context, defaultAction: defaultAction);
|
||||
}
|
||||
|
||||
void _replaceText(ReplaceTextIntent intent) {
|
||||
userUpdateTextEditingValue(
|
||||
intent.currentTextEditingValue.replaced(intent.replacementRange, intent.replacementText),
|
||||
intent.cause,
|
||||
);
|
||||
}
|
||||
late final Action<ReplaceTextIntent> _replaceTextAction = CallbackAction<ReplaceTextIntent>(onInvoke: _replaceText);
|
||||
|
||||
void _updateSelection(UpdateSelectionIntent intent) {
|
||||
userUpdateTextEditingValue(
|
||||
intent.currentTextEditingValue.copyWith(selection: intent.newSelection),
|
||||
intent.cause,
|
||||
);
|
||||
}
|
||||
late final Action<UpdateSelectionIntent> _updateSelectionAction = CallbackAction<UpdateSelectionIntent>(onInvoke: _updateSelection);
|
||||
|
||||
late final _UpdateTextSelectionToAdjacentLineAction<ExtendSelectionVerticallyToAdjacentLineIntent> _adjacentLineAction = _UpdateTextSelectionToAdjacentLineAction<ExtendSelectionVerticallyToAdjacentLineIntent>(this);
|
||||
|
||||
late final Map<Type, Action<Intent>> _actions = <Type, Action<Intent>>{
|
||||
DoNothingAndStopPropagationTextIntent: DoNothingAction(consumesKey: false),
|
||||
ReplaceTextIntent: _replaceTextAction,
|
||||
UpdateSelectionIntent: _updateSelectionAction,
|
||||
DirectionalFocusIntent: DirectionalFocusAction.forTextField(),
|
||||
|
||||
// Delete
|
||||
DeleteCharacterIntent: _makeOverridable(_DeleteTextAction<DeleteCharacterIntent>(this, _characterBoundary)),
|
||||
DeleteToNextWordBoundaryIntent: _makeOverridable(_DeleteTextAction<DeleteToNextWordBoundaryIntent>(this, _nextWordBoundary)),
|
||||
DeleteToLineBreakIntent: _makeOverridable(_DeleteTextAction<DeleteToLineBreakIntent>(this, _linebreak)),
|
||||
|
||||
// Extend/Move Selection
|
||||
ExtendSelectionByCharacterIntent: _makeOverridable(_UpdateTextSelectionAction<ExtendSelectionByCharacterIntent>(this, false, _characterBoundary,)),
|
||||
ExtendSelectionToNextWordBoundaryIntent: _makeOverridable(_UpdateTextSelectionAction<ExtendSelectionToNextWordBoundaryIntent>(this, true, _nextWordBoundary)),
|
||||
ExtendSelectionToLineBreakIntent: _makeOverridable(_UpdateTextSelectionAction<ExtendSelectionToLineBreakIntent>(this, true, _linebreak)),
|
||||
ExtendSelectionVerticallyToAdjacentLineIntent: _makeOverridable(_adjacentLineAction),
|
||||
ExtendSelectionToDocumentBoundaryIntent: _makeOverridable(_UpdateTextSelectionAction<ExtendSelectionToDocumentBoundaryIntent>(this, true, _documentBoundary)),
|
||||
ExtendSelectionToNextWordBoundaryOrCaretLocationIntent: _makeOverridable(_ExtendSelectionOrCaretPositionAction(this, _nextWordBoundary)),
|
||||
|
||||
// Copy Paste
|
||||
SelectAllTextIntent: _makeOverridable(_SelectAllAction(this)),
|
||||
CopySelectionTextIntent: _makeOverridable(_CopySelectionAction(this)),
|
||||
PasteTextIntent: _makeOverridable(CallbackAction<PasteTextIntent>(onInvoke: (PasteTextIntent intent) => pasteText(intent.cause))),
|
||||
};
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasMediaQuery(context));
|
||||
_focusAttachment!.reparent();
|
||||
super.build(context); // See AutomaticKeepAliveClientMixin.
|
||||
|
||||
final TextSelectionControls? controls = widget.selectionControls;
|
||||
return MouseRegion(
|
||||
cursor: widget.mouseCursor ?? SystemMouseCursors.text,
|
||||
child: Actions(
|
||||
actions: _actions,
|
||||
child: Focus(
|
||||
focusNode: widget.focusNode,
|
||||
includeSemantics: false,
|
||||
debugLabel: 'EditableText',
|
||||
child: Scrollable(
|
||||
excludeFromSemantics: true,
|
||||
axisDirection: _isMultiline ? AxisDirection.down : AxisDirection.right,
|
||||
@ -2900,6 +3036,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -3125,3 +3263,536 @@ class _Editable extends MultiChildRenderObjectWidget {
|
||||
..setPromptRectRange(promptRectRange);
|
||||
}
|
||||
}
|
||||
|
||||
/// An interface for retriving the logical text boundary (left-closed-right-open)
|
||||
/// at a given location in a document.
|
||||
///
|
||||
/// Depending on the implementation of the [_TextBoundary], the input
|
||||
/// [TextPosition] can either point to a code unit, or a position between 2 code
|
||||
/// units (which can be visually represented by the caret if the selection were
|
||||
/// to collapse to that position).
|
||||
///
|
||||
/// For example, [_LineBreak] interprets the input [TextPosition] as a caret
|
||||
/// location, since in Flutter the caret is generally painted between the
|
||||
/// character the [TextPosition] points to and its previous character, and
|
||||
/// [_LineBreak] cares about the affinity of the input [TextPosition]. Most
|
||||
/// other text boundaries however, interpret the input [TextPosition] as the
|
||||
/// location of a code unit in the document, since it's easier to reason about
|
||||
/// the text boundary given a code unit in the text.
|
||||
///
|
||||
/// To convert a "code-unit-based" [_TextBoundary] to "caret-location-based",
|
||||
/// use the [_CollapsedSelectionBoundary] combinator.
|
||||
abstract class _TextBoundary {
|
||||
const _TextBoundary();
|
||||
|
||||
TextEditingValue get textEditingValue;
|
||||
|
||||
/// Returns the leading text boundary at the given location, inclusive.
|
||||
TextPosition getLeadingTextBoundaryAt(TextPosition position);
|
||||
|
||||
/// Returns the trailing text boundary at the given location, exclusive.
|
||||
TextPosition getTrailingTextBoundaryAt(TextPosition position);
|
||||
|
||||
TextRange getTextBoundaryAt(TextPosition position) {
|
||||
return TextRange(
|
||||
start: getLeadingTextBoundaryAt(position).offset,
|
||||
end: getTrailingTextBoundaryAt(position).offset,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------- Text Boundaries -----------------------------
|
||||
|
||||
class _CodeUnitBoundary extends _TextBoundary {
|
||||
const _CodeUnitBoundary(this.textEditingValue);
|
||||
|
||||
@override
|
||||
final TextEditingValue textEditingValue;
|
||||
|
||||
@override
|
||||
TextPosition getLeadingTextBoundaryAt(TextPosition position) => TextPosition(offset: position.offset);
|
||||
@override
|
||||
TextPosition getTrailingTextBoundaryAt(TextPosition position) => TextPosition(offset: math.min(position.offset + 1, textEditingValue.text.length));
|
||||
}
|
||||
|
||||
// The word modifier generally removes the word boundaries around white spaces
|
||||
// (and newlines), IOW white spaces and some other punctuations are considered
|
||||
// a part of the next word in the search direction.
|
||||
class _WhitespaceBoundary extends _TextBoundary {
|
||||
const _WhitespaceBoundary(this.textEditingValue);
|
||||
|
||||
@override
|
||||
final TextEditingValue textEditingValue;
|
||||
|
||||
@override
|
||||
TextPosition getLeadingTextBoundaryAt(TextPosition position) {
|
||||
for (int index = position.offset; index >= 0; index -= 1) {
|
||||
if (!TextLayoutMetrics.isWhitespace(textEditingValue.text.codeUnitAt(index))) {
|
||||
return TextPosition(offset: index);
|
||||
}
|
||||
}
|
||||
return const TextPosition(offset: 0);
|
||||
}
|
||||
|
||||
@override
|
||||
TextPosition getTrailingTextBoundaryAt(TextPosition position) {
|
||||
for (int index = position.offset; index < textEditingValue.text.length; index += 1) {
|
||||
if (!TextLayoutMetrics.isWhitespace(textEditingValue.text.codeUnitAt(index))) {
|
||||
return TextPosition(offset: index + 1);
|
||||
}
|
||||
}
|
||||
return TextPosition(offset: textEditingValue.text.length);
|
||||
}
|
||||
}
|
||||
|
||||
// Most apps delete the entire grapheme when the backspace key is pressed.
|
||||
// Also always put the new caret location to character boundaries to avoid
|
||||
// sending malformed UTF-16 code units to the paragraph builder.
|
||||
class _CharacterBoundary extends _TextBoundary {
|
||||
const _CharacterBoundary(this.textEditingValue);
|
||||
|
||||
@override
|
||||
final TextEditingValue textEditingValue;
|
||||
|
||||
@override
|
||||
TextPosition getLeadingTextBoundaryAt(TextPosition position) {
|
||||
final int endOffset = math.min(position.offset + 1, textEditingValue.text.length);
|
||||
return TextPosition(
|
||||
offset: CharacterRange.at(textEditingValue.text, position.offset, endOffset).stringBeforeLength,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
TextPosition getTrailingTextBoundaryAt(TextPosition position) {
|
||||
final int endOffset = math.min(position.offset + 1, textEditingValue.text.length);
|
||||
final CharacterRange range = CharacterRange.at(textEditingValue.text, position.offset, endOffset);
|
||||
return TextPosition(
|
||||
offset: textEditingValue.text.length - range.stringAfterLength,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
TextRange getTextBoundaryAt(TextPosition position) {
|
||||
final int endOffset = math.min(position.offset + 1, textEditingValue.text.length);
|
||||
final CharacterRange range = CharacterRange.at(textEditingValue.text, position.offset, endOffset);
|
||||
return TextRange(
|
||||
start: range.stringBeforeLength,
|
||||
end: textEditingValue.text.length - range.stringAfterLength,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// [UAX #29](https://unicode.org/reports/tr29/) defined word boundaries.
|
||||
class _WordBoundary extends _TextBoundary {
|
||||
const _WordBoundary(this.textLayout, this.textEditingValue);
|
||||
|
||||
final TextLayoutMetrics textLayout;
|
||||
|
||||
@override
|
||||
final TextEditingValue textEditingValue;
|
||||
|
||||
@override
|
||||
TextPosition getLeadingTextBoundaryAt(TextPosition position) {
|
||||
return TextPosition(
|
||||
offset: textLayout.getWordBoundary(position).start,
|
||||
// Word boundary seems to always report downstream on many platforms.
|
||||
affinity: TextAffinity.downstream, // ignore: avoid_redundant_argument_values
|
||||
);
|
||||
}
|
||||
@override
|
||||
TextPosition getTrailingTextBoundaryAt(TextPosition position) {
|
||||
return TextPosition(
|
||||
offset: textLayout.getWordBoundary(position).end,
|
||||
// Word boundary seems to always report downstream on many platforms.
|
||||
affinity: TextAffinity.downstream, // ignore: avoid_redundant_argument_values
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// The linebreaks of the current text layout. The input [TextPosition]s are
|
||||
// interpreted as caret locations because [TextPainter.getLineAtOffset] is
|
||||
// text-affinity-aware.
|
||||
class _LineBreak extends _TextBoundary {
|
||||
const _LineBreak(this.textLayout, this.textEditingValue);
|
||||
|
||||
final TextLayoutMetrics textLayout;
|
||||
|
||||
@override
|
||||
final TextEditingValue textEditingValue;
|
||||
|
||||
@override
|
||||
TextPosition getLeadingTextBoundaryAt(TextPosition position) {
|
||||
return TextPosition(
|
||||
offset: textLayout.getLineAtOffset(position).start,
|
||||
);
|
||||
}
|
||||
@override
|
||||
TextPosition getTrailingTextBoundaryAt(TextPosition position) {
|
||||
return TextPosition(
|
||||
offset: textLayout.getLineAtOffset(position).end,
|
||||
affinity: TextAffinity.upstream,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// The document boundary is unique and is a constant function of the input
|
||||
// position.
|
||||
class _DocumentBoundary extends _TextBoundary {
|
||||
const _DocumentBoundary(this.textEditingValue);
|
||||
|
||||
@override
|
||||
final TextEditingValue textEditingValue;
|
||||
|
||||
@override
|
||||
TextPosition getLeadingTextBoundaryAt(TextPosition position) => const TextPosition(offset: 0);
|
||||
@override
|
||||
TextPosition getTrailingTextBoundaryAt(TextPosition position) {
|
||||
return TextPosition(
|
||||
offset: textEditingValue.text.length,
|
||||
affinity: TextAffinity.upstream,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------ Text Boundary Combinators ------------------------
|
||||
|
||||
// Expands the innerTextBoundary with outerTextBoundary.
|
||||
class _ExpandedTextBoundary extends _TextBoundary {
|
||||
_ExpandedTextBoundary(this.innerTextBoundary, this.outerTextBoundary);
|
||||
|
||||
final _TextBoundary innerTextBoundary;
|
||||
final _TextBoundary outerTextBoundary;
|
||||
|
||||
@override
|
||||
TextEditingValue get textEditingValue {
|
||||
assert(innerTextBoundary.textEditingValue == outerTextBoundary.textEditingValue);
|
||||
return innerTextBoundary.textEditingValue;
|
||||
}
|
||||
|
||||
@override
|
||||
TextPosition getLeadingTextBoundaryAt(TextPosition position) {
|
||||
return outerTextBoundary.getLeadingTextBoundaryAt(
|
||||
innerTextBoundary.getLeadingTextBoundaryAt(position),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
TextPosition getTrailingTextBoundaryAt(TextPosition position) {
|
||||
return outerTextBoundary.getTrailingTextBoundaryAt(
|
||||
innerTextBoundary.getTrailingTextBoundaryAt(position),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Force the innerTextBoundary to interpret the input [TextPosition]s as caret
|
||||
// locations instead of code unit positions.
|
||||
//
|
||||
// The innerTextBoundary must be a [_TextBoundary] that interprets the input
|
||||
// [TextPosition]s as code unit positions.
|
||||
class _CollapsedSelectionBoundary extends _TextBoundary {
|
||||
_CollapsedSelectionBoundary(this.innerTextBoundary, this.isForward);
|
||||
|
||||
final _TextBoundary innerTextBoundary;
|
||||
final bool isForward;
|
||||
|
||||
@override
|
||||
TextEditingValue get textEditingValue => innerTextBoundary.textEditingValue;
|
||||
|
||||
@override
|
||||
TextPosition getLeadingTextBoundaryAt(TextPosition position) {
|
||||
return isForward
|
||||
? innerTextBoundary.getLeadingTextBoundaryAt(position)
|
||||
: position.offset <= 0 ? const TextPosition(offset: 0) : innerTextBoundary.getLeadingTextBoundaryAt(TextPosition(offset: position.offset - 1));
|
||||
}
|
||||
|
||||
@override
|
||||
TextPosition getTrailingTextBoundaryAt(TextPosition position) {
|
||||
return isForward
|
||||
? innerTextBoundary.getTrailingTextBoundaryAt(position)
|
||||
: position.offset <= 0 ? const TextPosition(offset: 0) : innerTextBoundary.getTrailingTextBoundaryAt(TextPosition(offset: position.offset - 1));
|
||||
}
|
||||
}
|
||||
|
||||
// A _TextBoundary that creates a [TextRange] where its start is from the
|
||||
// specified leading text boundary and its end is from the specified trailing
|
||||
// text boundary.
|
||||
class _MixedBoundary extends _TextBoundary {
|
||||
_MixedBoundary(this.leadingTextBoundary, this.trailingTextBoundary);
|
||||
|
||||
final _TextBoundary leadingTextBoundary;
|
||||
final _TextBoundary trailingTextBoundary;
|
||||
|
||||
@override
|
||||
TextEditingValue get textEditingValue {
|
||||
assert(leadingTextBoundary.textEditingValue == trailingTextBoundary.textEditingValue);
|
||||
return leadingTextBoundary.textEditingValue;
|
||||
}
|
||||
|
||||
@override
|
||||
TextPosition getLeadingTextBoundaryAt(TextPosition position) => leadingTextBoundary.getLeadingTextBoundaryAt(position);
|
||||
|
||||
@override
|
||||
TextPosition getTrailingTextBoundaryAt(TextPosition position) => trailingTextBoundary.getTrailingTextBoundaryAt(position);
|
||||
}
|
||||
|
||||
// ------------------------------- Text Actions -------------------------------
|
||||
class _DeleteTextAction<T extends DirectionalTextEditingIntent> extends ContextAction<T> {
|
||||
_DeleteTextAction(this.state, this.getTextBoundariesForIntent);
|
||||
|
||||
final EditableTextState state;
|
||||
final _TextBoundary Function(T intent) getTextBoundariesForIntent;
|
||||
|
||||
TextRange _expandNonCollapsedRange(TextEditingValue value) {
|
||||
final TextRange selection = value.selection;
|
||||
assert(selection.isValid);
|
||||
assert(!selection.isCollapsed);
|
||||
final _TextBoundary atomicBoundary = state.widget.obscureText
|
||||
? _CodeUnitBoundary(value)
|
||||
: _CharacterBoundary(value);
|
||||
|
||||
return TextRange(
|
||||
start: atomicBoundary.getLeadingTextBoundaryAt(TextPosition(offset: selection.start)).offset,
|
||||
end: atomicBoundary.getTrailingTextBoundaryAt(TextPosition(offset: selection.end - 1)).offset,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Object? invoke(T intent, [BuildContext? context]) {
|
||||
final TextSelection selection = state._value.selection;
|
||||
assert(selection.isValid);
|
||||
|
||||
if (!selection.isCollapsed) {
|
||||
return Actions.invoke(
|
||||
context!,
|
||||
ReplaceTextIntent(state._value, '', _expandNonCollapsedRange(state._value), SelectionChangedCause.keyboard),
|
||||
);
|
||||
}
|
||||
|
||||
final _TextBoundary textBoundary = getTextBoundariesForIntent(intent);
|
||||
if (!textBoundary.textEditingValue.selection.isValid) {
|
||||
return null;
|
||||
}
|
||||
if (!textBoundary.textEditingValue.selection.isCollapsed) {
|
||||
return Actions.invoke(
|
||||
context!,
|
||||
ReplaceTextIntent(state._value, '', _expandNonCollapsedRange(textBoundary.textEditingValue), SelectionChangedCause.keyboard),
|
||||
);
|
||||
}
|
||||
|
||||
return Actions.invoke(
|
||||
context!,
|
||||
ReplaceTextIntent(
|
||||
textBoundary.textEditingValue,
|
||||
'',
|
||||
textBoundary.getTextBoundaryAt(textBoundary.textEditingValue.selection.base),
|
||||
SelectionChangedCause.keyboard,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get isActionEnabled => !state.widget.readOnly && state._value.selection.isValid;
|
||||
}
|
||||
|
||||
class _UpdateTextSelectionAction<T extends DirectionalCaretMovementIntent> extends ContextAction<T> {
|
||||
_UpdateTextSelectionAction(this.state, this.ignoreNonCollapsedSelection, this.getTextBoundariesForIntent);
|
||||
|
||||
final EditableTextState state;
|
||||
final bool ignoreNonCollapsedSelection;
|
||||
final _TextBoundary Function(T intent) getTextBoundariesForIntent;
|
||||
|
||||
@override
|
||||
Object? invoke(T intent, [BuildContext? context]) {
|
||||
final TextSelection selection = state._value.selection;
|
||||
assert(selection.isValid);
|
||||
|
||||
final bool collapseSelection = intent.collapseSelection || !state.widget.selectionEnabled;
|
||||
// Collapse to the logical start/end.
|
||||
TextSelection _collapse(TextSelection selection) {
|
||||
assert(selection.isValid);
|
||||
assert(!selection.isCollapsed);
|
||||
return selection.copyWith(
|
||||
baseOffset: intent.forward ? selection.end : selection.start,
|
||||
extentOffset: intent.forward ? selection.end : selection.start,
|
||||
);
|
||||
}
|
||||
|
||||
if (!selection.isCollapsed && !ignoreNonCollapsedSelection && collapseSelection) {
|
||||
return Actions.invoke(
|
||||
context!,
|
||||
UpdateSelectionIntent(state._value, _collapse(selection), SelectionChangedCause.keyboard),
|
||||
);
|
||||
}
|
||||
|
||||
final _TextBoundary textBoundary = getTextBoundariesForIntent(intent);
|
||||
final TextSelection textBoundarySelection = textBoundary.textEditingValue.selection;
|
||||
if (!textBoundarySelection.isValid) {
|
||||
return null;
|
||||
}
|
||||
if (!textBoundarySelection.isCollapsed && !ignoreNonCollapsedSelection && collapseSelection) {
|
||||
return Actions.invoke(
|
||||
context!,
|
||||
UpdateSelectionIntent(state._value, _collapse(textBoundarySelection), SelectionChangedCause.keyboard),
|
||||
);
|
||||
}
|
||||
final TextPosition extent = textBoundarySelection.extent;
|
||||
final TextPosition newExtent = intent.forward
|
||||
? textBoundary.getTrailingTextBoundaryAt(extent)
|
||||
: textBoundary.getLeadingTextBoundaryAt(extent);
|
||||
|
||||
final TextSelection newSelection = collapseSelection
|
||||
? TextSelection.fromPosition(newExtent)
|
||||
: textBoundarySelection.extendTo(newExtent);
|
||||
|
||||
return Actions.invoke(
|
||||
context!,
|
||||
UpdateSelectionIntent(textBoundary.textEditingValue, newSelection, SelectionChangedCause.keyboard),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get isActionEnabled => state._value.selection.isValid;
|
||||
}
|
||||
|
||||
class _ExtendSelectionOrCaretPositionAction extends ContextAction<ExtendSelectionToNextWordBoundaryOrCaretLocationIntent> {
|
||||
_ExtendSelectionOrCaretPositionAction(this.state, this.getTextBoundariesForIntent);
|
||||
|
||||
final EditableTextState state;
|
||||
final _TextBoundary Function(ExtendSelectionToNextWordBoundaryOrCaretLocationIntent intent) getTextBoundariesForIntent;
|
||||
|
||||
@override
|
||||
Object? invoke(ExtendSelectionToNextWordBoundaryOrCaretLocationIntent intent, [BuildContext? context]) {
|
||||
final TextSelection selection = state._value.selection;
|
||||
assert(selection.isValid);
|
||||
|
||||
final _TextBoundary textBoundary = getTextBoundariesForIntent(intent);
|
||||
final TextSelection textBoundarySelection = textBoundary.textEditingValue.selection;
|
||||
if (!textBoundarySelection.isValid) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final TextPosition extent = textBoundarySelection.extent;
|
||||
final TextPosition newExtent = intent.forward
|
||||
? textBoundary.getTrailingTextBoundaryAt(extent)
|
||||
: textBoundary.getLeadingTextBoundaryAt(extent);
|
||||
|
||||
final TextSelection newSelection = (newExtent.offset - textBoundarySelection.baseOffset) * (textBoundarySelection.extentOffset - textBoundarySelection.baseOffset) < 0
|
||||
? textBoundarySelection.copyWith(
|
||||
extentOffset: textBoundarySelection.baseOffset,
|
||||
affinity: textBoundarySelection.extentOffset > textBoundarySelection.baseOffset ? TextAffinity.downstream : TextAffinity.upstream,
|
||||
)
|
||||
: textBoundarySelection.extendTo(newExtent);
|
||||
|
||||
return Actions.invoke(
|
||||
context!,
|
||||
UpdateSelectionIntent(textBoundary.textEditingValue, newSelection, SelectionChangedCause.keyboard),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get isActionEnabled => state.widget.selectionEnabled && state._value.selection.isValid;
|
||||
}
|
||||
|
||||
class _UpdateTextSelectionToAdjacentLineAction<T extends DirectionalCaretMovementIntent> extends ContextAction<T> {
|
||||
_UpdateTextSelectionToAdjacentLineAction(this.state);
|
||||
|
||||
final EditableTextState state;
|
||||
|
||||
VerticalCaretMovementRun? _verticalMovementRun;
|
||||
TextSelection? _runSelection;
|
||||
|
||||
void stopCurrentVerticalRunIfSelectionChanges() {
|
||||
final TextSelection? runSelection = _runSelection;
|
||||
if (runSelection == null) {
|
||||
assert(_verticalMovementRun == null);
|
||||
return;
|
||||
}
|
||||
_runSelection = state._value.selection;
|
||||
final TextSelection currentSelection = state.widget.controller.selection;
|
||||
final bool continueCurrentRun = currentSelection.isValid && currentSelection.isCollapsed
|
||||
&& currentSelection.baseOffset == runSelection.baseOffset
|
||||
&& currentSelection.extentOffset == runSelection.extentOffset;
|
||||
if (!continueCurrentRun) {
|
||||
_verticalMovementRun = null;
|
||||
_runSelection = null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Object? invoke(T intent, [BuildContext? context]) {
|
||||
assert(state._value.selection.isValid);
|
||||
|
||||
final bool collapseSelection = intent.collapseSelection || !state.widget.selectionEnabled;
|
||||
final TextEditingValue value = state._textEditingValueforTextLayoutMetrics;
|
||||
if (!value.selection.isValid) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (_verticalMovementRun?.isValid == false) {
|
||||
_verticalMovementRun = null;
|
||||
_runSelection = null;
|
||||
}
|
||||
|
||||
final VerticalCaretMovementRun currentRun = _verticalMovementRun
|
||||
?? state.renderEditable.startVerticalCaretMovement(state.renderEditable.selection!.extent);
|
||||
|
||||
final bool shouldMove = intent.forward ? currentRun.moveNext() : currentRun.movePrevious();
|
||||
final TextPosition newExtent = shouldMove
|
||||
? currentRun.current
|
||||
: (intent.forward ? TextPosition(offset: state._value.text.length) : const TextPosition(offset: 0));
|
||||
final TextSelection newSelection = collapseSelection
|
||||
? TextSelection.fromPosition(newExtent)
|
||||
: value.selection.extendTo(newExtent);
|
||||
|
||||
Actions.invoke(
|
||||
context!,
|
||||
UpdateSelectionIntent(value, newSelection, SelectionChangedCause.keyboard),
|
||||
);
|
||||
if (state._value.selection == newSelection) {
|
||||
_verticalMovementRun = currentRun;
|
||||
_runSelection = newSelection;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool get isActionEnabled => state._value.selection.isValid;
|
||||
}
|
||||
|
||||
class _SelectAllAction extends ContextAction<SelectAllTextIntent> {
|
||||
_SelectAllAction(this.state);
|
||||
|
||||
final EditableTextState state;
|
||||
|
||||
@override
|
||||
Object? invoke(SelectAllTextIntent intent, [BuildContext? context]) {
|
||||
return Actions.invoke(
|
||||
context!,
|
||||
UpdateSelectionIntent(
|
||||
state._value,
|
||||
TextSelection(baseOffset: 0, extentOffset: state._value.text.length),
|
||||
intent.cause,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get isActionEnabled => state.widget.selectionEnabled;
|
||||
}
|
||||
|
||||
class _CopySelectionAction extends ContextAction<CopySelectionTextIntent> {
|
||||
_CopySelectionAction(this.state);
|
||||
|
||||
final EditableTextState state;
|
||||
|
||||
@override
|
||||
Object? invoke(CopySelectionTextIntent intent, [BuildContext? context]) {
|
||||
if (intent.collapseSelection) {
|
||||
state.cutSelection(intent.cause);
|
||||
} else {
|
||||
state.copySelection(intent.cause);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool get isActionEnabled => state._value.selection.isValid && !state._value.selection.isCollapsed;
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'actions.dart';
|
||||
import 'basic.dart';
|
||||
import 'editable_text.dart';
|
||||
import 'focus_manager.dart';
|
||||
import 'focus_scope.dart';
|
||||
import 'framework.dart';
|
||||
@ -1722,9 +1721,18 @@ class DirectionalFocusIntent extends Intent {
|
||||
/// [LogicalKeyboardKey.arrowLeft], and [LogicalKeyboardKey.arrowRight] keys in
|
||||
/// the [WidgetsApp], with the appropriate associated directions.
|
||||
class DirectionalFocusAction extends Action<DirectionalFocusIntent> {
|
||||
/// Creates a [DirectionalFocusAction].
|
||||
DirectionalFocusAction() : _isForTextField = false;
|
||||
|
||||
/// Creates a [DirectionalFocusAction] that ignores [DirectionalFocusIntent]s
|
||||
/// whose `ignoreTextFields` field is true.
|
||||
DirectionalFocusAction.forTextField() : _isForTextField = true;
|
||||
|
||||
// Whether this action is defined in a text field.
|
||||
final bool _isForTextField;
|
||||
@override
|
||||
void invoke(DirectionalFocusIntent intent) {
|
||||
if (!intent.ignoreTextFields || primaryFocus!.context!.widget is! EditableText) {
|
||||
if (!intent.ignoreTextFields || !_isForTextField) {
|
||||
primaryFocus!.focusInDirection(intent.direction);
|
||||
}
|
||||
}
|
||||
|
@ -1,48 +0,0 @@
|
||||
// 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';
|
||||
import 'focus_manager.dart';
|
||||
import 'framework.dart';
|
||||
import 'text_editing_action_target.dart';
|
||||
|
||||
/// 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.
|
||||
return textEditingActionTarget != null;
|
||||
}
|
||||
}
|
@ -2,299 +2,226 @@
|
||||
// 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 'actions.dart';
|
||||
|
||||
/// An [Intent] to delete a character in the backwards direction.
|
||||
/// An [Intent] to send the event straight to the engine.
|
||||
///
|
||||
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
|
||||
class DeleteTextIntent extends Intent {
|
||||
/// Creates an instance of DeleteTextIntent.
|
||||
const DeleteTextIntent();
|
||||
}
|
||||
|
||||
/// An [Intent] to delete a word in the backwards direction.
|
||||
///
|
||||
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
|
||||
class DeleteByWordTextIntent extends Intent {
|
||||
/// Creates an instance of DeleteByWordTextIntent.
|
||||
const DeleteByWordTextIntent();
|
||||
}
|
||||
|
||||
/// An [Intent] to delete a line in the backwards direction.
|
||||
///
|
||||
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
|
||||
class DeleteByLineTextIntent extends Intent {
|
||||
/// Creates an instance of DeleteByLineTextIntent.
|
||||
const DeleteByLineTextIntent();
|
||||
}
|
||||
|
||||
/// An [Intent] to delete in the forward direction.
|
||||
///
|
||||
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
|
||||
class DeleteForwardTextIntent extends Intent {
|
||||
/// Creates an instance of DeleteForwardTextIntent.
|
||||
const DeleteForwardTextIntent();
|
||||
}
|
||||
|
||||
/// An [Intent] to delete a word in the forward direction.
|
||||
///
|
||||
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
|
||||
class DeleteForwardByWordTextIntent extends Intent {
|
||||
/// Creates an instance of DeleteByWordTextIntent.
|
||||
const DeleteForwardByWordTextIntent();
|
||||
}
|
||||
|
||||
/// An [Intent] to delete a line in the forward direction.
|
||||
///
|
||||
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
|
||||
class DeleteForwardByLineTextIntent extends Intent {
|
||||
/// Creates an instance of DeleteByLineTextIntent.
|
||||
const DeleteForwardByLineTextIntent();
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// Creates an instance of [DoNothingAndStopPropagationTextIntent].
|
||||
const DoNothingAndStopPropagationTextIntent();
|
||||
}
|
||||
|
||||
/// An [Intent] to expand the selection left to the start/end of the current
|
||||
/// A text editing related [Intent] that performs an operation towards a given
|
||||
/// direction of the current caret location.
|
||||
abstract class DirectionalTextEditingIntent extends Intent {
|
||||
/// Creates a [DirectionalTextEditingIntent].
|
||||
const DirectionalTextEditingIntent(this.forward);
|
||||
|
||||
/// Whether the input field, if applicable, should perform the text editing
|
||||
/// operation from the current caret location towards the end of the document.
|
||||
///
|
||||
/// Unless otherwise specified by the recipient of this intent, this parameter
|
||||
/// uses the logical order of characters in the string to determind the
|
||||
/// direction, and is not affected by the writing direction of the text.
|
||||
final bool forward;
|
||||
}
|
||||
|
||||
/// Deletes the character before or after the caret location, based on whether
|
||||
/// `forward` is true.
|
||||
///
|
||||
/// {@template flutter.widgets.TextEditingIntents.logicalOrder}
|
||||
/// {@endtemplate}
|
||||
///
|
||||
/// Typically a text field will not respond to this intent if it has no active
|
||||
/// caret ([TextSelection.isValid] is false for the current selection).
|
||||
class DeleteCharacterIntent extends DirectionalTextEditingIntent {
|
||||
/// Creates a [DeleteCharacterIntent].
|
||||
const DeleteCharacterIntent({ required bool forward }) : super(forward);
|
||||
}
|
||||
|
||||
/// Deletes from the current caret location to the previous or next word
|
||||
/// boundary, based on whether `forward` is true.
|
||||
class DeleteToNextWordBoundaryIntent extends DirectionalTextEditingIntent {
|
||||
/// Creates a [DeleteToNextWordBoundaryIntent].
|
||||
const DeleteToNextWordBoundaryIntent({ required bool forward }) : super(forward);
|
||||
}
|
||||
|
||||
/// Deletes from the current caret location to the previous or next soft or hard
|
||||
/// line break, based on whether `forward` is true.
|
||||
class DeleteToLineBreakIntent extends DirectionalTextEditingIntent {
|
||||
/// Creates a [DeleteToLineBreakIntent].
|
||||
const DeleteToLineBreakIntent({ required bool forward }) : super(forward);
|
||||
}
|
||||
|
||||
/// A [DirectionalTextEditingIntent] that moves the caret or the selection to a
|
||||
/// new location.
|
||||
abstract class DirectionalCaretMovementIntent extends DirectionalTextEditingIntent {
|
||||
/// Creates a [DirectionalCaretMovementIntent].
|
||||
const DirectionalCaretMovementIntent(bool forward, this.collapseSelection)
|
||||
: super(forward);
|
||||
|
||||
/// Whether this [Intent] should make the selection collapsed (so it becomes a
|
||||
/// caret), after the movement.
|
||||
///
|
||||
/// When [collapseSelection] is false, the input field typically only moves
|
||||
/// the current [TextSelection.extent] to the new location, while maintains
|
||||
/// the current [TextSelection.base] location.
|
||||
///
|
||||
/// When [collapseSelection] is true, the input field typically should move
|
||||
/// both the [TextSelection.base] and the [TextSelection.extent] to the new
|
||||
/// location.
|
||||
final bool collapseSelection;
|
||||
}
|
||||
|
||||
/// Expands, or moves the current selection from the current
|
||||
/// [TextSelection.extent] position to the previous or the next character
|
||||
/// boundary.
|
||||
class ExtendSelectionByCharacterIntent extends DirectionalCaretMovementIntent {
|
||||
/// Creates an [ExtendSelectionByCharacterIntent].
|
||||
const ExtendSelectionByCharacterIntent({
|
||||
required bool forward,
|
||||
required bool collapseSelection,
|
||||
}) : super(forward, collapseSelection);
|
||||
}
|
||||
|
||||
/// Expands, or moves the current selection from the current
|
||||
/// [TextSelection.extent] position to the previous or the next word
|
||||
/// boundary.
|
||||
class ExtendSelectionToNextWordBoundaryIntent extends DirectionalCaretMovementIntent {
|
||||
/// Creates an [ExtendSelectionToNextWordBoundaryIntent].
|
||||
const ExtendSelectionToNextWordBoundaryIntent({
|
||||
required bool forward,
|
||||
required bool collapseSelection,
|
||||
}) : super(forward, collapseSelection);
|
||||
}
|
||||
|
||||
/// Expands, or moves the current selection from the current
|
||||
/// [TextSelection.extent] position to the previous or the next word
|
||||
/// boundary, or the [TextSelection.base] position if it's closer in the move
|
||||
/// direction.
|
||||
///
|
||||
/// This [Intent] typically has the same effect as an
|
||||
/// [ExtendSelectionToNextWordBoundaryIntent], except it collapses the selection
|
||||
/// when the order of [TextSelection.base] and [TextSelection.extent] would
|
||||
/// reverse.
|
||||
///
|
||||
/// This is typically only used on macOS.
|
||||
class ExtendSelectionToNextWordBoundaryOrCaretLocationIntent extends DirectionalTextEditingIntent {
|
||||
/// Creates an [ExtendSelectionToNextWordBoundaryOrCaretLocationIntent].
|
||||
const ExtendSelectionToNextWordBoundaryOrCaretLocationIntent({
|
||||
required bool forward,
|
||||
}) : super(forward);
|
||||
}
|
||||
|
||||
/// Expands, or moves the current selection from the current
|
||||
/// [TextSelection.extent] position to the closest line break in the direction
|
||||
/// given by [forward].
|
||||
class ExtendSelectionToLineBreakIntent extends DirectionalCaretMovementIntent {
|
||||
/// Creates an [ExtendSelectionToLineBreakIntent].
|
||||
const ExtendSelectionToLineBreakIntent({
|
||||
required bool forward,
|
||||
required bool collapseSelection,
|
||||
}) : super(forward, collapseSelection);
|
||||
}
|
||||
|
||||
/// Expands, or moves the current selection from the current
|
||||
/// [TextSelection.extent] position to the closest position on the adjacent
|
||||
/// line.
|
||||
///
|
||||
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
|
||||
class ExpandSelectionLeftByLineTextIntent extends Intent {
|
||||
/// Creates an instance of ExpandSelectionLeftByLineTextIntent.
|
||||
const ExpandSelectionLeftByLineTextIntent();
|
||||
class ExtendSelectionVerticallyToAdjacentLineIntent extends DirectionalCaretMovementIntent {
|
||||
/// Creates an [ExtendSelectionVerticallyToAdjacentLineIntent].
|
||||
const ExtendSelectionVerticallyToAdjacentLineIntent({
|
||||
required bool forward,
|
||||
required bool collapseSelection,
|
||||
}) : super(forward, collapseSelection);
|
||||
}
|
||||
|
||||
/// 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, collapsing
|
||||
/// the selection if the order of [TextSelection.extentOffset] and
|
||||
/// [TextSelection.baseOffset] would reverse.
|
||||
///
|
||||
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
|
||||
class ExtendSelectionLeftByWordAndStopAtReversalTextIntent extends Intent {
|
||||
/// Creates an instance of ExtendSelectionLeftByWordAndStopAtReversalTextIntent.
|
||||
const ExtendSelectionLeftByWordAndStopAtReversalTextIntent();
|
||||
}
|
||||
|
||||
/// 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, collapsing
|
||||
/// the selection if the order of [TextSelection.extentOffset] and
|
||||
/// [TextSelection.baseOffset] would reverse.
|
||||
///
|
||||
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
|
||||
class ExtendSelectionRightByWordAndStopAtReversalTextIntent extends Intent {
|
||||
/// Creates an instance of ExtendSelectionRightByWordAndStopAtReversalTextIntent.
|
||||
const ExtendSelectionRightByWordAndStopAtReversalTextIntent();
|
||||
}
|
||||
|
||||
/// 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();
|
||||
/// Expands, or moves the current selection from the current
|
||||
/// [TextSelection.extent] position to the start or the end of the document.
|
||||
class ExtendSelectionToDocumentBoundaryIntent extends DirectionalCaretMovementIntent {
|
||||
/// Creates an [ExtendSelectionToDocumentBoundaryIntent].
|
||||
const ExtendSelectionToDocumentBoundaryIntent({
|
||||
required bool forward,
|
||||
required bool collapseSelection,
|
||||
}) : super(forward, collapseSelection);
|
||||
}
|
||||
|
||||
/// An [Intent] to select everything in the field.
|
||||
///
|
||||
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
|
||||
class SelectAllTextIntent extends Intent {
|
||||
/// Creates an instance of SelectAllTextIntent.
|
||||
const SelectAllTextIntent();
|
||||
/// Creates an instance of [SelectAllTextIntent].
|
||||
const SelectAllTextIntent(this.cause);
|
||||
|
||||
/// {@template flutter.widgets.TextEditingIntents.cause}
|
||||
/// The [SelectionChangedCause] that triggered the intent.
|
||||
/// {@endtemplate}
|
||||
final SelectionChangedCause cause;
|
||||
}
|
||||
|
||||
/// An [Intent] to copy selection in the field.
|
||||
///
|
||||
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
|
||||
/// An [Intent] that represents a user interaction that attempts to copy or cut
|
||||
/// the current selection in the field.
|
||||
class CopySelectionTextIntent extends Intent {
|
||||
/// Creates an instance of CopyTextIntent.
|
||||
const CopySelectionTextIntent();
|
||||
}
|
||||
const CopySelectionTextIntent._(this.cause, this.collapseSelection);
|
||||
|
||||
/// An [Intent] to cut selection in the field.
|
||||
///
|
||||
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
|
||||
class CutSelectionTextIntent extends Intent {
|
||||
/// Creates an instance of CutTextIntent.
|
||||
const CutSelectionTextIntent();
|
||||
/// Creates an [Intent] that represents a user interaction that attempts to
|
||||
/// cut the current selection in the field.
|
||||
const CopySelectionTextIntent.cut(SelectionChangedCause cause) : this._(cause, true);
|
||||
|
||||
/// An [Intent] that represents a user interaction that attempts to copy the
|
||||
/// current selection in the field.
|
||||
static const CopySelectionTextIntent copy = CopySelectionTextIntent._(SelectionChangedCause.keyboard, false);
|
||||
|
||||
/// {@macro flutter.widgets.TextEditingIntents.cause}
|
||||
final SelectionChangedCause cause;
|
||||
|
||||
/// Whether the original text needs to be removed from the input field if the
|
||||
/// copy action was successful.
|
||||
final bool collapseSelection;
|
||||
}
|
||||
|
||||
/// An [Intent] to paste text from [Clipboard] to the field.
|
||||
///
|
||||
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
|
||||
class PasteTextIntent extends Intent {
|
||||
/// Creates an instance of PasteTextIntent.
|
||||
const PasteTextIntent();
|
||||
/// Creates an instance of [PasteTextIntent].
|
||||
const PasteTextIntent(this.cause);
|
||||
|
||||
/// {@macro flutter.widgets.TextEditingIntents.cause}
|
||||
final SelectionChangedCause cause;
|
||||
}
|
||||
|
||||
/// An [Intent] that represents a user interaction that attempts to modify the
|
||||
/// current [TextEditingValue] in an input field.
|
||||
class ReplaceTextIntent extends Intent {
|
||||
/// Creates a [ReplaceTextIntent].
|
||||
const ReplaceTextIntent(this.currentTextEditingValue, this.replacementText, this.replacementRange, this.cause);
|
||||
|
||||
/// The [TextEditingValue] that this [Intent]'s action should perform on.
|
||||
final TextEditingValue currentTextEditingValue;
|
||||
|
||||
/// The text to replace the original text within the [replacementRange] with.
|
||||
final String replacementText;
|
||||
|
||||
/// The range of text in [currentTextEditingValue] that needs to be replaced.
|
||||
final TextRange replacementRange;
|
||||
|
||||
/// {@macro flutter.widgets.TextEditingIntents.cause}
|
||||
final SelectionChangedCause cause;
|
||||
}
|
||||
|
||||
/// An [Intent] that represents a user interaction that attempts to change the
|
||||
/// selection in an input field.
|
||||
class UpdateSelectionIntent extends Intent {
|
||||
/// Creates a [UpdateSelectionIntent].
|
||||
const UpdateSelectionIntent(this.currentTextEditingValue, this.newSelection, this.cause);
|
||||
|
||||
/// The [TextEditingValue] that this [Intent]'s action should perform on.
|
||||
final TextEditingValue currentTextEditingValue;
|
||||
|
||||
/// The new [TextSelection] the input field should adopt.
|
||||
final TextSelection newSelection;
|
||||
|
||||
/// {@macro flutter.widgets.TextEditingIntents.cause}
|
||||
final SelectionChangedCause cause;
|
||||
}
|
||||
|
@ -33,7 +33,6 @@ export 'src/widgets/bottom_navigation_bar_item.dart';
|
||||
export 'src/widgets/color_filter.dart';
|
||||
export 'src/widgets/container.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/dismissible.dart';
|
||||
@ -120,8 +119,6 @@ export 'src/widgets/spacer.dart';
|
||||
export 'src/widgets/status_transitions.dart';
|
||||
export 'src/widgets/table.dart';
|
||||
export 'src/widgets/text.dart';
|
||||
export 'src/widgets/text_editing_action.dart';
|
||||
export 'src/widgets/text_editing_action_target.dart';
|
||||
export 'src/widgets/text_editing_intents.dart';
|
||||
export 'src/widgets/text_selection.dart';
|
||||
export 'src/widgets/text_selection_toolbar_layout_delegate.dart';
|
||||
|
@ -263,7 +263,7 @@ void main() {
|
||||
await tester.tap(find.text('Paste'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(controller.text, 'blah1 blah2blah1');
|
||||
expect(controller.selection, const TextSelection(baseOffset: 16, extentOffset: 16, affinity: TextAffinity.upstream));
|
||||
expect(controller.selection, const TextSelection.collapsed(offset: 16));
|
||||
|
||||
// Cut the first word.
|
||||
await gesture.down(midBlah1);
|
||||
|
@ -189,8 +189,6 @@ void main() {
|
||||
' _FocusTraversalGroupMarker\n'
|
||||
' FocusTraversalGroup\n'
|
||||
' _ActionsMarker\n'
|
||||
' DefaultTextEditingActions\n'
|
||||
' _ActionsMarker\n'
|
||||
' Actions\n'
|
||||
' _ShortcutsMarker\n'
|
||||
' Semantics\n'
|
||||
|
@ -79,7 +79,6 @@ Widget overlayWithEntry(OverlayEntry entry) {
|
||||
MaterialLocalizationsDelegate(),
|
||||
],
|
||||
child: DefaultTextEditingShortcuts(
|
||||
child: DefaultTextEditingActions(
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: MediaQuery(
|
||||
@ -92,7 +91,6 @@ Widget overlayWithEntry(OverlayEntry entry) {
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -260,14 +258,14 @@ void main() {
|
||||
await tester.pump();
|
||||
await gesture.up();
|
||||
await tester.pumpAndSettle();
|
||||
expect(controller.selection, const TextSelection(baseOffset: 11, extentOffset: 11, affinity: TextAffinity.upstream));
|
||||
expect(controller.selection, const TextSelection.collapsed(offset: 11, affinity: TextAffinity.upstream));
|
||||
expect(find.text('Cut'), findsNothing);
|
||||
expect(find.text('Copy'), findsNothing);
|
||||
expect(find.text('Paste'), findsOneWidget);
|
||||
await tester.tap(find.text('Paste'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(controller.text, 'blah1 blah2blah1');
|
||||
expect(controller.selection, const TextSelection(baseOffset: 16, extentOffset: 16, affinity: TextAffinity.upstream));
|
||||
expect(controller.selection, const TextSelection.collapsed(offset: 16));
|
||||
|
||||
// Cut the first word.
|
||||
await gesture.down(midBlah1);
|
||||
|
@ -74,14 +74,14 @@ void main() {
|
||||
await tester.pump();
|
||||
await gesture.up();
|
||||
await tester.pumpAndSettle();
|
||||
expect(controller.selection, const TextSelection(baseOffset: 11, extentOffset: 11, affinity: TextAffinity.upstream));
|
||||
expect(controller.selection, const TextSelection.collapsed(offset: 11, affinity: TextAffinity.upstream));
|
||||
expect(find.text('Cut'), findsNothing);
|
||||
expect(find.text('Copy'), findsNothing);
|
||||
expect(find.text('Paste'), findsOneWidget);
|
||||
await tester.tap(find.text('Paste'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(controller.text, 'blah1 blah2blah1');
|
||||
expect(controller.selection, const TextSelection(baseOffset: 16, extentOffset: 16, affinity: TextAffinity.upstream));
|
||||
expect(controller.selection, const TextSelection.collapsed(offset: 16));
|
||||
|
||||
// Cut the first word.
|
||||
await gesture.down(midBlah1);
|
||||
|
@ -43,6 +43,110 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
group('TextEditingValue', () {
|
||||
group('replaced', () {
|
||||
const String testText = 'From a false proposition, anything follows.';
|
||||
|
||||
test('selection deletion', () {
|
||||
const TextSelection selection = TextSelection(baseOffset: 5, extentOffset: 13);
|
||||
expect(
|
||||
const TextEditingValue(text: testText, selection: selection).replaced(selection, ''),
|
||||
const TextEditingValue(text: 'From proposition, anything follows.', selection: TextSelection.collapsed(offset: 5)),
|
||||
);
|
||||
});
|
||||
|
||||
test('reversed selection deletion', () {
|
||||
const TextSelection selection = TextSelection(baseOffset: 13, extentOffset: 5);
|
||||
expect(
|
||||
const TextEditingValue(text: testText, selection: selection).replaced(selection, ''),
|
||||
const TextEditingValue(text: 'From proposition, anything follows.', selection: TextSelection.collapsed(offset: 5)),
|
||||
);
|
||||
});
|
||||
|
||||
test('insert', () {
|
||||
const TextSelection selection = TextSelection.collapsed(offset: 5);
|
||||
expect(
|
||||
const TextEditingValue(text: testText, selection: selection).replaced(selection, 'AA'),
|
||||
const TextEditingValue(
|
||||
text: 'From AAa false proposition, anything follows.',
|
||||
// The caret moves to the end of the text inserted.
|
||||
selection: TextSelection.collapsed(offset: 7),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('replace before selection', () {
|
||||
const TextSelection selection = TextSelection(baseOffset: 13, extentOffset: 5);
|
||||
expect(
|
||||
// From |a false |proposition, anything follows.
|
||||
// Replace the first whitespace with "AA".
|
||||
const TextEditingValue(text: testText, selection: selection).replaced(const TextRange(start: 4, end: 5), 'AA'),
|
||||
const TextEditingValue(text: 'FromAAa false proposition, anything follows.', selection: TextSelection(baseOffset: 14, extentOffset: 6)),
|
||||
);
|
||||
});
|
||||
|
||||
test('replace after selection', () {
|
||||
const TextSelection selection = TextSelection(baseOffset: 13, extentOffset: 5);
|
||||
expect(
|
||||
// From |a false |proposition, anything follows.
|
||||
// replace the first "p" with "AA".
|
||||
const TextEditingValue(text: testText, selection: selection).replaced(const TextRange(start: 13, end: 14), 'AA'),
|
||||
const TextEditingValue(text: 'From a false AAroposition, anything follows.', selection: selection),
|
||||
);
|
||||
});
|
||||
|
||||
test('replace inside selection - start boundary', () {
|
||||
const TextSelection selection = TextSelection(baseOffset: 13, extentOffset: 5);
|
||||
expect(
|
||||
// From |a false |proposition, anything follows.
|
||||
// replace the first "a" with "AA".
|
||||
const TextEditingValue(text: testText, selection: selection).replaced(const TextRange(start: 5, end: 6), 'AA'),
|
||||
const TextEditingValue(text: 'From AA false proposition, anything follows.', selection: TextSelection(baseOffset: 14, extentOffset: 5)),
|
||||
);
|
||||
});
|
||||
|
||||
test('replace inside selection - end boundary', () {
|
||||
const TextSelection selection = TextSelection(baseOffset: 13, extentOffset: 5);
|
||||
expect(
|
||||
// From |a false |proposition, anything follows.
|
||||
// replace the second whitespace with "AA".
|
||||
const TextEditingValue(text: testText, selection: selection).replaced(const TextRange(start: 12, end: 13), 'AA'),
|
||||
const TextEditingValue(text: 'From a falseAAproposition, anything follows.', selection: TextSelection(baseOffset: 14, extentOffset: 5)),
|
||||
);
|
||||
});
|
||||
|
||||
test('delete after selection', () {
|
||||
const TextSelection selection = TextSelection(baseOffset: 13, extentOffset: 5);
|
||||
expect(
|
||||
// From |a false |proposition, anything follows.
|
||||
// Delete the first "p".
|
||||
const TextEditingValue(text: testText, selection: selection).replaced(const TextRange(start: 13, end: 14), ''),
|
||||
const TextEditingValue(text: 'From a false roposition, anything follows.', selection: selection),
|
||||
);
|
||||
});
|
||||
|
||||
test('delete inside selection - start boundary', () {
|
||||
const TextSelection selection = TextSelection(baseOffset: 13, extentOffset: 5);
|
||||
expect(
|
||||
// From |a false |proposition, anything follows.
|
||||
// Delete the first "a".
|
||||
const TextEditingValue(text: testText, selection: selection).replaced(const TextRange(start: 5, end: 6), ''),
|
||||
const TextEditingValue(text: 'From false proposition, anything follows.', selection: TextSelection(baseOffset: 12, extentOffset: 5)),
|
||||
);
|
||||
});
|
||||
|
||||
test('delete inside selection - end boundary', () {
|
||||
const TextSelection selection = TextSelection(baseOffset: 13, extentOffset: 5);
|
||||
expect(
|
||||
// From |a false |proposition, anything follows.
|
||||
// Delete the second whitespace.
|
||||
const TextEditingValue(text: testText, selection: selection).replaced(const TextRange(start: 12, end: 13), ''),
|
||||
const TextEditingValue(text: 'From a falseproposition, anything follows.', selection: TextSelection(baseOffset: 12, extentOffset: 5)),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
group('TextInput message channels', () {
|
||||
late FakeTextChannel fakeTextChannel;
|
||||
|
||||
|
1769
packages/flutter/test/widgets/editable_text_shortcuts_tests.dart
Normal file
1769
packages/flutter/test/widgets/editable_text_shortcuts_tests.dart
Normal file
File diff suppressed because it is too large
Load Diff
@ -4312,10 +4312,10 @@ void main() {
|
||||
// toolbar. Until we change that, this test should remain skipped.
|
||||
}, skip: kIsWeb); // [intended]
|
||||
|
||||
const String testText = 'Now is the time for\n'
|
||||
'all good people\n'
|
||||
'to come to the aid\n'
|
||||
'of their country.';
|
||||
const String testText = 'Now is the time for\n' // 20
|
||||
'all good people\n' // 20 + 16 => 36
|
||||
'to come to the aid\n' // 36 + 19 => 55
|
||||
'of their country.'; // 55 + 17 => 72
|
||||
|
||||
Future<void> sendKeys(
|
||||
WidgetTester tester,
|
||||
@ -4461,7 +4461,6 @@ void main() {
|
||||
const TextSelection(
|
||||
baseOffset: 0,
|
||||
extentOffset: 0,
|
||||
affinity: TextAffinity.upstream,
|
||||
),
|
||||
),
|
||||
reason: 'on $platform',
|
||||
@ -4483,7 +4482,6 @@ void main() {
|
||||
const TextSelection(
|
||||
baseOffset: 0,
|
||||
extentOffset: 0,
|
||||
affinity: TextAffinity.upstream,
|
||||
),
|
||||
),
|
||||
reason: 'on $platform',
|
||||
@ -4637,7 +4635,7 @@ void main() {
|
||||
equals(
|
||||
const TextSelection(
|
||||
baseOffset: 20,
|
||||
extentOffset: 57,
|
||||
extentOffset: 39,
|
||||
),
|
||||
),
|
||||
reason: 'on $platform',
|
||||
@ -4659,14 +4657,13 @@ void main() {
|
||||
equals(
|
||||
const TextSelection(
|
||||
baseOffset: 20,
|
||||
extentOffset: 72,
|
||||
extentOffset: 54,
|
||||
affinity: TextAffinity.upstream,
|
||||
),
|
||||
),
|
||||
reason: 'on $platform',
|
||||
);
|
||||
|
||||
// Can't move left by line because we're already at the beginning of a line.
|
||||
await sendKeys(
|
||||
tester,
|
||||
<LogicalKeyboardKey>[
|
||||
@ -4682,7 +4679,7 @@ void main() {
|
||||
equals(
|
||||
const TextSelection(
|
||||
baseOffset: 20,
|
||||
extentOffset: 72,
|
||||
extentOffset: 36,
|
||||
affinity: TextAffinity.upstream,
|
||||
),
|
||||
),
|
||||
@ -4746,6 +4743,7 @@ void main() {
|
||||
equals(
|
||||
const TextSelection.collapsed(
|
||||
offset: testText.length,
|
||||
affinity: TextAffinity.upstream,
|
||||
),
|
||||
),
|
||||
reason: 'on $platform',
|
||||
@ -4767,7 +4765,6 @@ void main() {
|
||||
equals(
|
||||
const TextSelection.collapsed(
|
||||
offset: 0,
|
||||
affinity: TextAffinity.upstream,
|
||||
),
|
||||
),
|
||||
reason: 'on $platform',
|
||||
@ -4832,7 +4829,7 @@ void main() {
|
||||
selection,
|
||||
equals(
|
||||
const TextSelection(
|
||||
baseOffset: testText.length,
|
||||
baseOffset: 3,
|
||||
extentOffset: 0,
|
||||
affinity: TextAffinity.upstream,
|
||||
),
|
||||
@ -4854,7 +4851,6 @@ void main() {
|
||||
equals(
|
||||
const TextSelection.collapsed(
|
||||
offset: 0,
|
||||
affinity: TextAffinity.upstream,
|
||||
),
|
||||
),
|
||||
reason: 'on $platform',
|
||||
@ -4878,7 +4874,6 @@ void main() {
|
||||
const TextSelection(
|
||||
baseOffset: 10,
|
||||
extentOffset: 10,
|
||||
affinity: TextAffinity.upstream,
|
||||
),
|
||||
),
|
||||
reason: 'on $platform',
|
||||
@ -4948,7 +4943,6 @@ void main() {
|
||||
const TextSelection(
|
||||
baseOffset: 4,
|
||||
extentOffset: 4,
|
||||
affinity: TextAffinity.upstream,
|
||||
),
|
||||
),
|
||||
reason: 'on $platform',
|
||||
@ -4985,7 +4979,6 @@ void main() {
|
||||
const TextSelection(
|
||||
baseOffset: 10,
|
||||
extentOffset: 10,
|
||||
affinity: TextAffinity.upstream,
|
||||
),
|
||||
),
|
||||
reason: 'on $platform',
|
||||
@ -5031,7 +5024,6 @@ void main() {
|
||||
const TextSelection(
|
||||
baseOffset: 0,
|
||||
extentOffset: 0,
|
||||
affinity: TextAffinity.upstream,
|
||||
),
|
||||
),
|
||||
reason: 'on $platform',
|
||||
@ -5308,9 +5300,9 @@ void main() {
|
||||
targetPlatform: defaultTargetPlatform,
|
||||
);
|
||||
|
||||
late final int afterHomeOffset;
|
||||
late final int afterEndOffset;
|
||||
late final TextAffinity afterEndAffinity;
|
||||
final int afterHomeOffset;
|
||||
final int afterEndOffset;
|
||||
final TextAffinity afterEndAffinity;
|
||||
switch (defaultTargetPlatform) {
|
||||
// These platforms don't handle home/end at all.
|
||||
case TargetPlatform.android:
|
||||
@ -7869,11 +7861,11 @@ void main() {
|
||||
|
||||
testWidgets('can change behavior by overriding text editing shortcuts', (WidgetTester tester) async {
|
||||
const Map<SingleActivator, Intent> testShortcuts = <SingleActivator, Intent>{
|
||||
SingleActivator(LogicalKeyboardKey.arrowLeft): MoveSelectionRightTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.keyX, control: true): MoveSelectionRightTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.keyC, control: true): MoveSelectionRightTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.keyV, control: true): MoveSelectionRightTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.keyA, control: true): MoveSelectionRightTextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowLeft): ExtendSelectionByCharacterIntent(forward: true, collapseSelection: true),
|
||||
SingleActivator(LogicalKeyboardKey.keyX, control: true): ExtendSelectionByCharacterIntent(forward: true, collapseSelection: true),
|
||||
SingleActivator(LogicalKeyboardKey.keyC, control: true): ExtendSelectionByCharacterIntent(forward: true, collapseSelection: true),
|
||||
SingleActivator(LogicalKeyboardKey.keyV, control: true): ExtendSelectionByCharacterIntent(forward: true, collapseSelection: true),
|
||||
SingleActivator(LogicalKeyboardKey.keyA, control: true): ExtendSelectionByCharacterIntent(forward: true, collapseSelection: true),
|
||||
};
|
||||
final TextEditingController controller = TextEditingController(text: testText);
|
||||
controller.selection = const TextSelection(
|
||||
@ -8071,7 +8063,7 @@ void main() {
|
||||
}, variant: TargetPlatformVariant.all(), skip: kIsWeb); // [intended]
|
||||
|
||||
testWidgets('navigating multiline text', (WidgetTester tester) async {
|
||||
const String multilineText = 'word word word\nword word\nword';
|
||||
const String multilineText = 'word word word\nword word\nword'; // 15 + 10 + 4;
|
||||
final TextEditingController controller = TextEditingController(text: multilineText);
|
||||
// wo|rd wo|rd
|
||||
controller.selection = const TextSelection(
|
||||
@ -8134,8 +8126,8 @@ void main() {
|
||||
targetPlatform: defaultTargetPlatform,
|
||||
);
|
||||
expect(controller.selection.isCollapsed, false);
|
||||
expect(controller.selection.baseOffset, 15);
|
||||
expect(controller.selection.extentOffset, 24);
|
||||
expect(controller.selection.baseOffset, 17);
|
||||
expect(controller.selection.extentOffset, 15);
|
||||
|
||||
// Set the caret to the end of a line.
|
||||
controller.selection = const TextSelection(
|
||||
@ -8173,8 +8165,8 @@ void main() {
|
||||
targetPlatform: defaultTargetPlatform,
|
||||
);
|
||||
expect(controller.selection.isCollapsed, false);
|
||||
expect(controller.selection.baseOffset, 15);
|
||||
expect(controller.selection.extentOffset, 24);
|
||||
expect(controller.selection.baseOffset, 24);
|
||||
expect(controller.selection.extentOffset, 15);
|
||||
|
||||
// Set the caret to the start of a line.
|
||||
controller.selection = const TextSelection(
|
||||
@ -8267,7 +8259,7 @@ void main() {
|
||||
controller.selection,
|
||||
equals(
|
||||
const TextSelection(
|
||||
baseOffset: 9,
|
||||
baseOffset: 7,
|
||||
extentOffset: 0,
|
||||
affinity: TextAffinity.upstream,
|
||||
),
|
||||
@ -8290,7 +8282,7 @@ void main() {
|
||||
controller.selection,
|
||||
equals(
|
||||
const TextSelection(
|
||||
baseOffset: 9,
|
||||
baseOffset: 7,
|
||||
extentOffset: 0,
|
||||
affinity: TextAffinity.upstream,
|
||||
),
|
||||
@ -8304,7 +8296,7 @@ void main() {
|
||||
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.macOS })
|
||||
);
|
||||
|
||||
testWidgets('can change behavior by overriding text editing actions', (WidgetTester tester) async {
|
||||
testWidgets('can change text editing behavior by overriding actions', (WidgetTester tester) async {
|
||||
final TextEditingController controller = TextEditingController(text: testText);
|
||||
controller.selection = const TextSelection(
|
||||
baseOffset: 0,
|
||||
@ -8312,34 +8304,16 @@ void main() {
|
||||
affinity: TextAffinity.upstream,
|
||||
);
|
||||
bool myIntentWasCalled = false;
|
||||
final _MyMoveSelectionRightTextAction myMoveSelectionRightTextAction =
|
||||
_MyMoveSelectionRightTextAction(
|
||||
onInvoke: () {
|
||||
myIntentWasCalled = true;
|
||||
},
|
||||
final CallbackAction<ExtendSelectionByCharacterIntent> overrideAction = CallbackAction<ExtendSelectionByCharacterIntent>(
|
||||
onInvoke: (ExtendSelectionByCharacterIntent intent) { myIntentWasCalled = true; },
|
||||
);
|
||||
const Iterable<SingleActivator> testSingleActivators = <SingleActivator>{
|
||||
SingleActivator(LogicalKeyboardKey.arrowLeft),
|
||||
SingleActivator(LogicalKeyboardKey.keyX, control: true),
|
||||
SingleActivator(LogicalKeyboardKey.keyC, control: true),
|
||||
SingleActivator(LogicalKeyboardKey.keyV, control: true),
|
||||
SingleActivator(LogicalKeyboardKey.keyA, control: true),
|
||||
};
|
||||
|
||||
final Map<Type, Action<Intent>> testActions = <Type, Action<Intent>>{
|
||||
MoveSelectionLeftTextIntent: myMoveSelectionRightTextAction,
|
||||
CutSelectionTextIntent: myMoveSelectionRightTextAction,
|
||||
CopySelectionTextIntent: myMoveSelectionRightTextAction,
|
||||
PasteTextIntent: myMoveSelectionRightTextAction,
|
||||
SelectAllTextIntent: myMoveSelectionRightTextAction,
|
||||
};
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: SizedBox(
|
||||
width: 400,
|
||||
child: Actions(
|
||||
actions: testActions,
|
||||
actions: <Type, Action<Intent>>{ ExtendSelectionByCharacterIntent: overrideAction, },
|
||||
child: EditableText(
|
||||
maxLines: 10,
|
||||
controller: controller,
|
||||
@ -8357,32 +8331,13 @@ void main() {
|
||||
),
|
||||
),
|
||||
));
|
||||
|
||||
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);
|
||||
expect(myIntentWasCalled, isFalse);
|
||||
|
||||
// And the testSingleActivators also moves to the right due to the Shortcuts override.
|
||||
for (final SingleActivator singleActivator in testSingleActivators) {
|
||||
myIntentWasCalled = false;
|
||||
controller.selection = const TextSelection.collapsed(offset: 0);
|
||||
await tester.pump();
|
||||
|
||||
await sendKeys(
|
||||
tester,
|
||||
<LogicalKeyboardKey>[singleActivator.trigger],
|
||||
shortcutModifier: singleActivator.control,
|
||||
targetPlatform: defaultTargetPlatform,
|
||||
);
|
||||
expect(controller.selection.isCollapsed, isTrue);
|
||||
expect(controller.selection.baseOffset, 1);
|
||||
expect(controller.selection.baseOffset, 0);
|
||||
expect(myIntentWasCalled, isTrue);
|
||||
}
|
||||
|
||||
// On web, using keyboard for selection is handled by the browser.
|
||||
}, skip: kIsWeb); // [intended]
|
||||
@ -8404,10 +8359,8 @@ void main() {
|
||||
width: 400,
|
||||
child: Actions(
|
||||
actions: <Type, Action<Intent>>{
|
||||
MoveSelectionRightTextIntent: _MyMoveSelectionRightTextAction(
|
||||
onInvoke: () {
|
||||
myIntentWasCalled = true;
|
||||
},
|
||||
ExtendSelectionByCharacterIntent: CallbackAction<ExtendSelectionByCharacterIntent>(
|
||||
onInvoke: (ExtendSelectionByCharacterIntent intent) { myIntentWasCalled = true; },
|
||||
),
|
||||
},
|
||||
child: EditableText(
|
||||
@ -8441,7 +8394,7 @@ void main() {
|
||||
await tester.pump();
|
||||
expect(myIntentWasCalled, isTrue);
|
||||
expect(controller.selection.isCollapsed, true);
|
||||
expect(controller.selection.baseOffset, 1);
|
||||
expect(controller.selection.baseOffset, 0);
|
||||
}
|
||||
}, variant: KeySimulatorTransitModeVariant.all());
|
||||
|
||||
@ -8860,20 +8813,6 @@ class _AccentColorTextEditingController extends TextEditingController {
|
||||
}
|
||||
}
|
||||
|
||||
class _MyMoveSelectionRightTextAction extends TextEditingAction<Intent> {
|
||||
_MyMoveSelectionRightTextAction({
|
||||
required this.onInvoke,
|
||||
}) : super();
|
||||
|
||||
final VoidCallback onInvoke;
|
||||
|
||||
@override
|
||||
Object? invoke(Intent intent, [BuildContext? context]) {
|
||||
textEditingActionTarget!.moveSelectionRight(SelectionChangedCause.keyboard);
|
||||
onInvoke();
|
||||
}
|
||||
}
|
||||
|
||||
class _TestScrollController extends ScrollController {
|
||||
bool get attached => hasListeners;
|
||||
}
|
||||
|
@ -184,80 +184,6 @@ void main() {
|
||||
);
|
||||
}
|
||||
|
||||
testWidgets('can use the desktop cut/copy/paste buttons on desktop', (WidgetTester tester) async {
|
||||
final TextEditingController controller = TextEditingController(
|
||||
text: 'blah1 blah2',
|
||||
);
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Material(
|
||||
child: Center(
|
||||
child: TextFormField(
|
||||
controller: controller,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Initially, the menu is not shown and there is no selection.
|
||||
expect(find.byType(CupertinoButton), findsNothing);
|
||||
expect(controller.selection, const TextSelection(baseOffset: -1, extentOffset: -1));
|
||||
|
||||
final Offset midBlah1 = textOffsetToPosition(tester, 2);
|
||||
|
||||
// Right clicking shows the menu.
|
||||
final TestGesture gesture = await tester.startGesture(
|
||||
midBlah1,
|
||||
kind: PointerDeviceKind.mouse,
|
||||
buttons: kSecondaryMouseButton,
|
||||
);
|
||||
addTearDown(gesture.removePointer);
|
||||
await tester.pump();
|
||||
await gesture.up();
|
||||
await tester.pumpAndSettle();
|
||||
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 5));
|
||||
expect(find.text('Copy'), findsOneWidget);
|
||||
expect(find.text('Cut'), findsOneWidget);
|
||||
expect(find.text('Paste'), findsOneWidget);
|
||||
|
||||
// Copy the first word.
|
||||
await tester.tap(find.text('Copy'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(controller.text, 'blah1 blah2');
|
||||
expect(controller.selection, const TextSelection(baseOffset: 5, extentOffset: 5));
|
||||
expect(find.byType(CupertinoButton), findsNothing);
|
||||
|
||||
// Paste it at the end.
|
||||
await gesture.down(textOffsetToPosition(tester, controller.text.length));
|
||||
await tester.pump();
|
||||
await gesture.up();
|
||||
await tester.pumpAndSettle();
|
||||
expect(controller.selection, const TextSelection(baseOffset: 11, extentOffset: 11, affinity: TextAffinity.upstream));
|
||||
expect(find.text('Cut'), findsNothing);
|
||||
expect(find.text('Copy'), findsNothing);
|
||||
expect(find.text('Paste'), findsOneWidget);
|
||||
await tester.tap(find.text('Paste'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(controller.text, 'blah1 blah2blah1');
|
||||
expect(controller.selection, const TextSelection(baseOffset: 16, extentOffset: 16, affinity: TextAffinity.upstream));
|
||||
|
||||
// Cut the first word.
|
||||
await gesture.down(midBlah1);
|
||||
await tester.pump();
|
||||
await gesture.up();
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Cut'), findsOneWidget);
|
||||
expect(find.text('Copy'), findsOneWidget);
|
||||
expect(find.text('Paste'), findsOneWidget);
|
||||
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 5));
|
||||
await tester.tap(find.text('Cut'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(controller.text, ' blah2blah1');
|
||||
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 0));
|
||||
expect(find.byType(CupertinoButton), findsNothing);
|
||||
}, variant: TargetPlatformVariant.desktop(), skip: kIsWeb); // [intended] toolbar is handled by the browser.
|
||||
|
||||
testWidgets('has expected defaults', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
boilerplate(
|
||||
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user