Reland "fixes TextInputFormatter gets wrong old value of a selection" (#76653)
This commit is contained in:
@@ -102,6 +102,7 @@ class _CupertinoTextFieldSelectionGestureDetectorBuilder extends TextSelectionGe
|
||||
|
||||
@override
|
||||
void onSingleTapUp(TapUpDetails details) {
|
||||
editableText.hideToolbar();
|
||||
// Because TextSelectionGestureDetector listens to taps that happen on
|
||||
// widgets in front of it, tapping the clear button will also trigger
|
||||
// this handler. If the clear button widget recognizes the up event,
|
||||
|
||||
@@ -494,6 +494,8 @@ class _SelectableTextState extends State<SelectableText> with AutomaticKeepAlive
|
||||
});
|
||||
}
|
||||
|
||||
TextSelection? _lastSeenTextSelection;
|
||||
|
||||
void _handleSelectionChanged(TextSelection selection, SelectionChangedCause? cause) {
|
||||
final bool willShowSelectionHandles = _shouldShowSelectionHandles(cause);
|
||||
if (willShowSelectionHandles != _showSelectionHandles) {
|
||||
@@ -501,10 +503,12 @@ class _SelectableTextState extends State<SelectableText> with AutomaticKeepAlive
|
||||
_showSelectionHandles = willShowSelectionHandles;
|
||||
});
|
||||
}
|
||||
|
||||
if (widget.onSelectionChanged != null) {
|
||||
// TODO(chunhtai): The selection may be the same. We should remove this
|
||||
// check once this is fixed https://github.com/flutter/flutter/issues/76349.
|
||||
if (widget.onSelectionChanged != null && _lastSeenTextSelection != selection) {
|
||||
widget.onSelectionChanged!(selection, cause);
|
||||
}
|
||||
_lastSeenTextSelection = selection;
|
||||
|
||||
switch (Theme.of(context).platform) {
|
||||
case TargetPlatform.iOS:
|
||||
|
||||
@@ -31,39 +31,13 @@ const Radius _kFloatingCaretRadius = Radius.circular(1.0);
|
||||
/// (including the cursor location).
|
||||
///
|
||||
/// Used by [RenderEditable.onSelectionChanged].
|
||||
@Deprecated(
|
||||
'Signature of a deprecated class method, '
|
||||
'textSelectionDelegate.userUpdateTextEditingValue. '
|
||||
'This feature was deprecated after v1.26.0-17.2.pre.'
|
||||
)
|
||||
typedef SelectionChangedHandler = void Function(TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause);
|
||||
|
||||
/// Indicates what triggered the change in selected text (including changes to
|
||||
/// the cursor location).
|
||||
enum SelectionChangedCause {
|
||||
/// The user tapped on the text and that caused the selection (or the location
|
||||
/// of the cursor) to change.
|
||||
tap,
|
||||
|
||||
/// The user tapped twice in quick succession on the text and that caused
|
||||
/// the selection (or the location of the cursor) to change.
|
||||
doubleTap,
|
||||
|
||||
/// The user long-pressed the text and that caused the selection (or the
|
||||
/// location of the cursor) to change.
|
||||
longPress,
|
||||
|
||||
/// The user force-pressed the text and that caused the selection (or the
|
||||
/// location of the cursor) to change.
|
||||
forcePress,
|
||||
|
||||
/// The user used the keyboard to change the selection or the location of the
|
||||
/// cursor.
|
||||
///
|
||||
/// Keyboard-triggered selection changes may be caused by the IME as well as
|
||||
/// by accessibility tools (e.g. TalkBack on Android).
|
||||
keyboard,
|
||||
|
||||
/// The user used the mouse to change the selection by dragging over a piece
|
||||
/// of text.
|
||||
drag,
|
||||
}
|
||||
|
||||
/// Signature for the callback that reports when the caret location changes.
|
||||
///
|
||||
/// Used by [RenderEditable.onCaretChanged].
|
||||
@@ -158,10 +132,6 @@ bool _isWhitespace(int codeUnit) {
|
||||
/// If, when the render object paints, the caret is found to have changed
|
||||
/// location, [onCaretChanged] is called.
|
||||
///
|
||||
/// The user may interact with the render object by tapping or long-pressing.
|
||||
/// When the user does so, the selection is updated, and [onSelectionChanged] is
|
||||
/// called.
|
||||
///
|
||||
/// Keyboard handling, IME handling, scrolling, toggling the [showCursor] value
|
||||
/// to actually blink the cursor, and other features not mentioned above are the
|
||||
/// responsibility of higher layers and not handled by this object.
|
||||
@@ -198,6 +168,10 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
|
||||
double textScaleFactor = 1.0,
|
||||
TextSelection? selection,
|
||||
required ViewportOffset offset,
|
||||
@Deprecated(
|
||||
'Uses the textSelectionDelegate.userUpdateTextEditingValue instead. '
|
||||
'This feature was deprecated after v1.26.0-17.2.pre.'
|
||||
)
|
||||
this.onSelectionChanged,
|
||||
this.onCaretChanged,
|
||||
this.ignorePointer = false,
|
||||
@@ -401,6 +375,10 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
|
||||
/// Called when the selection changes.
|
||||
///
|
||||
/// If this is null, then selection changes will be ignored.
|
||||
@Deprecated(
|
||||
'Uses the textSelectionDelegate.userUpdateTextEditingValue instead. '
|
||||
'This feature was deprecated after v1.26.0-17.2.pre.'
|
||||
)
|
||||
SelectionChangedHandler? onSelectionChanged;
|
||||
|
||||
double? _textLayoutLastMaxWidth;
|
||||
@@ -579,7 +557,19 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
|
||||
// down in a multiline text field when selecting using the keyboard.
|
||||
bool _wasSelectingVerticallyWithKeyboard = false;
|
||||
|
||||
// Call through to onSelectionChanged.
|
||||
void _setTextEditingValue(TextEditingValue newValue, SelectionChangedCause cause) {
|
||||
textSelectionDelegate.textEditingValue = newValue;
|
||||
textSelectionDelegate.userUpdateTextEditingValue(newValue, cause);
|
||||
}
|
||||
|
||||
void _setSelection(TextSelection nextSelection, SelectionChangedCause cause) {
|
||||
_handleSelectionChange(nextSelection, cause);
|
||||
_setTextEditingValue(
|
||||
textSelectionDelegate.textEditingValue.copyWith(selection: nextSelection),
|
||||
cause,
|
||||
);
|
||||
}
|
||||
|
||||
void _handleSelectionChange(
|
||||
TextSelection nextSelection,
|
||||
SelectionChangedCause cause,
|
||||
@@ -642,7 +632,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
|
||||
return;
|
||||
}
|
||||
|
||||
if (keyEvent is! RawKeyDownEvent || onSelectionChanged == null)
|
||||
if (keyEvent is! RawKeyDownEvent)
|
||||
return;
|
||||
final Set<LogicalKeyboardKey> keysPressed = LogicalKeyboardKey.collapseSynonyms(RawKeyboard.instance.keysPressed);
|
||||
final LogicalKeyboardKey key = keyEvent.logicalKey;
|
||||
@@ -908,12 +898,10 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
|
||||
newSelection = TextSelection.fromPosition(TextPosition(offset: newOffset));
|
||||
}
|
||||
|
||||
_handleSelectionChange(
|
||||
_setSelection(
|
||||
newSelection,
|
||||
SelectionChangedCause.keyboard,
|
||||
);
|
||||
// Update the text selection delegate so that the engine knows what we did.
|
||||
textSelectionDelegate.textEditingValue = textSelectionDelegate.textEditingValue.copyWith(selection: newSelection);
|
||||
}
|
||||
|
||||
// Handles shortcut functionality including cut, copy, paste and select all
|
||||
@@ -961,13 +949,10 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
|
||||
);
|
||||
}
|
||||
if (value != null) {
|
||||
if (textSelectionDelegate.textEditingValue.selection != value.selection) {
|
||||
_handleSelectionChange(
|
||||
value.selection,
|
||||
SelectionChangedCause.keyboard,
|
||||
);
|
||||
}
|
||||
textSelectionDelegate.textEditingValue = value;
|
||||
_setTextEditingValue(
|
||||
value,
|
||||
SelectionChangedCause.keyboard,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -994,15 +979,12 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
|
||||
}
|
||||
}
|
||||
final TextSelection newSelection = TextSelection.collapsed(offset: cursorPosition);
|
||||
if (selection != newSelection) {
|
||||
_handleSelectionChange(
|
||||
newSelection,
|
||||
SelectionChangedCause.keyboard,
|
||||
);
|
||||
}
|
||||
textSelectionDelegate.textEditingValue = TextEditingValue(
|
||||
text: textBefore + textAfter,
|
||||
selection: newSelection,
|
||||
_setTextEditingValue(
|
||||
TextEditingValue(
|
||||
text: textBefore + textAfter,
|
||||
selection: newSelection,
|
||||
),
|
||||
SelectionChangedCause.keyboard,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1530,7 +1512,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
|
||||
// callbacks are invoked, in which case the callbacks will crash...
|
||||
|
||||
void _handleSetSelection(TextSelection selection) {
|
||||
_handleSelectionChange(selection, SelectionChangedCause.keyboard);
|
||||
_setSelection(selection, SelectionChangedCause.keyboard);
|
||||
}
|
||||
|
||||
void _handleMoveCursorForwardByCharacter(bool extentSelection) {
|
||||
@@ -1539,8 +1521,9 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
|
||||
if (extentOffset == null)
|
||||
return;
|
||||
final int baseOffset = !extentSelection ? extentOffset : selection!.baseOffset;
|
||||
_handleSelectionChange(
|
||||
TextSelection(baseOffset: baseOffset, extentOffset: extentOffset), SelectionChangedCause.keyboard,
|
||||
_setSelection(
|
||||
TextSelection(baseOffset: baseOffset, extentOffset: extentOffset),
|
||||
SelectionChangedCause.keyboard,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1550,8 +1533,9 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
|
||||
if (extentOffset == null)
|
||||
return;
|
||||
final int baseOffset = !extentSelection ? extentOffset : selection!.baseOffset;
|
||||
_handleSelectionChange(
|
||||
TextSelection(baseOffset: baseOffset, extentOffset: extentOffset), SelectionChangedCause.keyboard,
|
||||
_setSelection(
|
||||
TextSelection(baseOffset: baseOffset, extentOffset: extentOffset),
|
||||
SelectionChangedCause.keyboard
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1562,7 +1546,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
|
||||
if (nextWord == null)
|
||||
return;
|
||||
final int baseOffset = extentSelection ? selection!.baseOffset : nextWord.start;
|
||||
_handleSelectionChange(
|
||||
_setSelection(
|
||||
TextSelection(
|
||||
baseOffset: baseOffset,
|
||||
extentOffset: nextWord.start,
|
||||
@@ -1578,12 +1562,12 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
|
||||
if (previousWord == null)
|
||||
return;
|
||||
final int baseOffset = extentSelection ? selection!.baseOffset : previousWord.start;
|
||||
_handleSelectionChange(
|
||||
_setSelection(
|
||||
TextSelection(
|
||||
baseOffset: baseOffset,
|
||||
extentOffset: previousWord.start,
|
||||
),
|
||||
SelectionChangedCause.keyboard,
|
||||
SelectionChangedCause.keyboard
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1894,7 +1878,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
|
||||
textSpan.recognizer?.addPointer(event);
|
||||
}
|
||||
|
||||
if (!ignorePointer && onSelectionChanged != null) {
|
||||
if (!ignorePointer) {
|
||||
// Propagates the pointer event to selection handlers.
|
||||
_tap.addPointer(event);
|
||||
_longPress.addPointer(event);
|
||||
@@ -1992,9 +1976,6 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
|
||||
assert(cause != null);
|
||||
assert(from != null);
|
||||
_layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth);
|
||||
if (onSelectionChanged == null) {
|
||||
return;
|
||||
}
|
||||
final TextPosition fromPosition = _textPainter.getPositionForOffset(globalToLocal(from - _paintOffset));
|
||||
final TextPosition? toPosition = to == null
|
||||
? null
|
||||
@@ -2008,8 +1989,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
|
||||
extentOffset: extentOffset,
|
||||
affinity: fromPosition.affinity,
|
||||
);
|
||||
// Call [onSelectionChanged] only when the selection actually changed.
|
||||
_handleSelectionChange(newSelection, cause);
|
||||
_setSelection(newSelection, cause);
|
||||
}
|
||||
|
||||
/// Select a word around the location of the last tap down.
|
||||
@@ -2029,22 +2009,16 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
|
||||
assert(cause != null);
|
||||
assert(from != null);
|
||||
_layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth);
|
||||
if (onSelectionChanged == null) {
|
||||
return;
|
||||
}
|
||||
final TextPosition firstPosition = _textPainter.getPositionForOffset(globalToLocal(from - _paintOffset));
|
||||
final TextSelection firstWord = _selectWordAtOffset(firstPosition);
|
||||
final TextSelection lastWord = to == null ?
|
||||
firstWord : _selectWordAtOffset(_textPainter.getPositionForOffset(globalToLocal(to - _paintOffset)));
|
||||
|
||||
_handleSelectionChange(
|
||||
TextSelection(
|
||||
baseOffset: firstWord.base.offset,
|
||||
extentOffset: lastWord.extent.offset,
|
||||
affinity: firstWord.affinity,
|
||||
),
|
||||
cause,
|
||||
final TextSelection newSelection = TextSelection(
|
||||
baseOffset: firstWord.base.offset,
|
||||
extentOffset: lastWord.extent.offset,
|
||||
affinity: firstWord.affinity,
|
||||
);
|
||||
_setSelection(newSelection, cause);
|
||||
}
|
||||
|
||||
/// Move the selection to the beginning or end of a word.
|
||||
@@ -2054,22 +2028,15 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
|
||||
assert(cause != null);
|
||||
_layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth);
|
||||
assert(_lastTapDownPosition != null);
|
||||
if (onSelectionChanged == null) {
|
||||
return;
|
||||
}
|
||||
final TextPosition position = _textPainter.getPositionForOffset(globalToLocal(_lastTapDownPosition! - _paintOffset));
|
||||
final TextRange word = _textPainter.getWordBoundary(position);
|
||||
late TextSelection newSelection;
|
||||
if (position.offset - word.start <= 1) {
|
||||
_handleSelectionChange(
|
||||
TextSelection.collapsed(offset: word.start, affinity: TextAffinity.downstream),
|
||||
cause,
|
||||
);
|
||||
newSelection = TextSelection.collapsed(offset: word.start, affinity: TextAffinity.downstream);
|
||||
} else {
|
||||
_handleSelectionChange(
|
||||
TextSelection.collapsed(offset: word.end, affinity: TextAffinity.upstream),
|
||||
cause,
|
||||
);
|
||||
newSelection = TextSelection.collapsed(offset: word.end, affinity: TextAffinity.upstream);
|
||||
}
|
||||
_setSelection(newSelection, cause);
|
||||
}
|
||||
|
||||
TextSelection _selectWordAtOffset(TextPosition position) {
|
||||
|
||||
@@ -754,12 +754,60 @@ class TextEditingValue {
|
||||
);
|
||||
}
|
||||
|
||||
/// Indicates what triggered the change in selected text (including changes to
|
||||
/// the cursor location).
|
||||
enum SelectionChangedCause {
|
||||
/// The user tapped on the text and that caused the selection (or the location
|
||||
/// of the cursor) to change.
|
||||
tap,
|
||||
|
||||
/// The user tapped twice in quick succession on the text and that caused
|
||||
/// the selection (or the location of the cursor) to change.
|
||||
doubleTap,
|
||||
|
||||
/// The user long-pressed the text and that caused the selection (or the
|
||||
/// location of the cursor) to change.
|
||||
longPress,
|
||||
|
||||
/// The user force-pressed the text and that caused the selection (or the
|
||||
/// location of the cursor) to change.
|
||||
forcePress,
|
||||
|
||||
/// The user used the keyboard to change the selection or the location of the
|
||||
/// cursor.
|
||||
///
|
||||
/// Keyboard-triggered selection changes may be caused by the IME as well as
|
||||
/// by accessibility tools (e.g. TalkBack on Android).
|
||||
keyboard,
|
||||
|
||||
/// The user used the selection toolbar to change the selection or the
|
||||
/// location of the cursor.
|
||||
///
|
||||
/// An example is when the user taps on select all in the tool bar.
|
||||
toolBar,
|
||||
|
||||
/// The user used the mouse to change the selection by dragging over a piece
|
||||
/// of text.
|
||||
drag,
|
||||
}
|
||||
|
||||
/// An interface for manipulating the selection, to be used by the implementor
|
||||
/// of the toolbar widget.
|
||||
abstract class TextSelectionDelegate {
|
||||
/// Gets the current text input.
|
||||
TextEditingValue get textEditingValue;
|
||||
|
||||
/// Indicates that the user has requested the delegate to replace its current
|
||||
/// text editing state with [value].
|
||||
///
|
||||
/// The new [value] is treated as user input and thus may subject to input
|
||||
/// formatting.
|
||||
@Deprecated(
|
||||
'Use the userUpdateTextEditingValue instead. '
|
||||
'This feature was deprecated after v1.26.0-17.2.pre.'
|
||||
)
|
||||
set textEditingValue(TextEditingValue value) {}
|
||||
|
||||
/// Indicates that the user has requested the delegate to replace its current
|
||||
/// text editing state with [value].
|
||||
///
|
||||
@@ -768,10 +816,10 @@ abstract class TextSelectionDelegate {
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [EditableTextState.textEditingValue]: an implementation that applies
|
||||
/// additional pre-processing to the specified [value], before updating the
|
||||
/// text editing state.
|
||||
set textEditingValue(TextEditingValue value);
|
||||
/// * [EditableTextState.userUpdateTextEditingValue]: an implementation that
|
||||
/// applies additional pre-processing to the specified [value], before
|
||||
/// updating the text editing state.
|
||||
void userUpdateTextEditingValue(TextEditingValue value, SelectionChangedCause cause);
|
||||
|
||||
/// Hides the text selection toolbar.
|
||||
///
|
||||
|
||||
@@ -32,8 +32,7 @@ import 'text.dart';
|
||||
import 'text_selection.dart';
|
||||
import 'ticker_provider.dart';
|
||||
|
||||
export 'package:flutter/rendering.dart' show SelectionChangedCause;
|
||||
export 'package:flutter/services.dart' show TextEditingValue, TextSelection, TextInputType, SmartQuotesType, SmartDashesType;
|
||||
export 'package:flutter/services.dart' show SelectionChangedCause, TextEditingValue, TextSelection, TextInputType, SmartQuotesType, SmartDashesType;
|
||||
|
||||
/// Signature for the callback that reports when the user changes the selection
|
||||
/// (including the cursor location).
|
||||
@@ -1492,7 +1491,7 @@ class EditableText extends StatefulWidget {
|
||||
}
|
||||
|
||||
/// State for a [EditableText].
|
||||
class EditableTextState extends State<EditableText> with AutomaticKeepAliveClientMixin<EditableText>, WidgetsBindingObserver, TickerProviderStateMixin<EditableText> implements TextSelectionDelegate, 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);
|
||||
@@ -1727,7 +1726,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
|
||||
if (value.text == _value.text && value.composing == _value.composing) {
|
||||
// `selection` is the only change.
|
||||
_handleSelectionChanged(value.selection, renderEditable, SelectionChangedCause.keyboard);
|
||||
_handleSelectionChanged(value.selection, SelectionChangedCause.keyboard);
|
||||
} else {
|
||||
hideToolbar();
|
||||
_currentPromptRectRange = null;
|
||||
@@ -1739,7 +1738,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
}
|
||||
}
|
||||
|
||||
_formatAndSetValue(value);
|
||||
_formatAndSetValue(value, SelectionChangedCause.keyboard);
|
||||
}
|
||||
|
||||
// Wherever the value is changed by the user, schedule a showCaretOnScreen
|
||||
@@ -1852,7 +1851,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
renderEditable.setFloatingCursor(FloatingCursorDragState.End, finalPosition, _lastTextPosition!);
|
||||
if (_lastTextPosition!.offset != renderEditable.selection!.baseOffset)
|
||||
// The cause is technically the force cursor, but the cause is listed as tap as the desired functionality is the same.
|
||||
_handleSelectionChanged(TextSelection.collapsed(offset: _lastTextPosition!.offset), renderEditable, SelectionChangedCause.forcePress);
|
||||
_handleSelectionChanged(TextSelection.collapsed(offset: _lastTextPosition!.offset), SelectionChangedCause.forcePress);
|
||||
_startCaretRect = null;
|
||||
_lastTextPosition = null;
|
||||
_pointOffsetOrigin = null;
|
||||
@@ -2118,7 +2117,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
}
|
||||
}
|
||||
|
||||
void _handleSelectionChanged(TextSelection selection, RenderEditable renderObject, SelectionChangedCause? cause) {
|
||||
void _handleSelectionChanged(TextSelection selection, SelectionChangedCause? cause) {
|
||||
// We return early if the selection is not valid. This can happen when the
|
||||
// text of [EditableText] is updated at the same time as the selection is
|
||||
// changed by a gesture event.
|
||||
@@ -2130,37 +2129,43 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
// This will show the keyboard for all selection changes on the
|
||||
// EditableWidget, not just changes triggered by user gestures.
|
||||
requestKeyboard();
|
||||
|
||||
_selectionOverlay?.hide();
|
||||
_selectionOverlay = null;
|
||||
|
||||
if (widget.selectionControls != null) {
|
||||
_selectionOverlay = TextSelectionOverlay(
|
||||
clipboardStatus: _clipboardStatus,
|
||||
context: context,
|
||||
value: _value,
|
||||
debugRequiredFor: widget,
|
||||
toolbarLayerLink: _toolbarLayerLink,
|
||||
startHandleLayerLink: _startHandleLayerLink,
|
||||
endHandleLayerLink: _endHandleLayerLink,
|
||||
renderObject: renderObject,
|
||||
selectionControls: widget.selectionControls,
|
||||
selectionDelegate: this,
|
||||
dragStartBehavior: widget.dragStartBehavior,
|
||||
onSelectionHandleTapped: widget.onSelectionHandleTapped,
|
||||
);
|
||||
if (widget.selectionControls == null) {
|
||||
_selectionOverlay?.hide();
|
||||
_selectionOverlay = null;
|
||||
} else {
|
||||
if (_selectionOverlay == null) {
|
||||
_selectionOverlay = TextSelectionOverlay(
|
||||
clipboardStatus: _clipboardStatus,
|
||||
context: context,
|
||||
value: _value,
|
||||
debugRequiredFor: widget,
|
||||
toolbarLayerLink: _toolbarLayerLink,
|
||||
startHandleLayerLink: _startHandleLayerLink,
|
||||
endHandleLayerLink: _endHandleLayerLink,
|
||||
renderObject: renderEditable,
|
||||
selectionControls: widget.selectionControls,
|
||||
selectionDelegate: this,
|
||||
dragStartBehavior: widget.dragStartBehavior,
|
||||
onSelectionHandleTapped: widget.onSelectionHandleTapped,
|
||||
);
|
||||
} else {
|
||||
_selectionOverlay!.update(_value);
|
||||
}
|
||||
_selectionOverlay!.handlesVisible = widget.showSelectionHandles;
|
||||
_selectionOverlay!.showHandles();
|
||||
try {
|
||||
widget.onSelectionChanged?.call(selection, cause);
|
||||
} catch (exception, stack) {
|
||||
FlutterError.reportError(FlutterErrorDetails(
|
||||
exception: exception,
|
||||
stack: stack,
|
||||
library: 'widgets',
|
||||
context: ErrorDescription('while calling onSelectionChanged for $cause'),
|
||||
));
|
||||
}
|
||||
}
|
||||
// TODO(chunhtai): we should make sure selection actually changed before
|
||||
// we call the onSelectionChanged.
|
||||
// https://github.com/flutter/flutter/issues/76349.
|
||||
try {
|
||||
widget.onSelectionChanged?.call(selection, cause);
|
||||
} catch (exception, stack) {
|
||||
FlutterError.reportError(FlutterErrorDetails(
|
||||
exception: exception,
|
||||
stack: stack,
|
||||
library: 'widgets',
|
||||
context: ErrorDescription('while calling onSelectionChanged for $cause'),
|
||||
));
|
||||
}
|
||||
|
||||
// To keep the cursor from blinking while it moves, restart the timer here.
|
||||
@@ -2247,7 +2252,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
|
||||
late final _WhitespaceDirectionalityFormatter _whitespaceFormatter = _WhitespaceDirectionalityFormatter(textDirection: _textDirection);
|
||||
|
||||
void _formatAndSetValue(TextEditingValue value) {
|
||||
void _formatAndSetValue(TextEditingValue value, SelectionChangedCause? cause, {bool userInteraction = false}) {
|
||||
// Only apply input formatters if the text has changed (including uncommited
|
||||
// text in the composing region), or when the user committed the composing
|
||||
// text.
|
||||
@@ -2279,6 +2284,16 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
// sending multiple `TextInput.updateEditingValue` messages.
|
||||
beginBatchEdit();
|
||||
_value = value;
|
||||
// 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, the user long pressing should always send a selection change
|
||||
// as well.
|
||||
if (selectionChanged ||
|
||||
(userInteraction &&
|
||||
(cause == SelectionChangedCause.longPress ||
|
||||
cause == SelectionChangedCause.keyboard))) {
|
||||
_handleSelectionChanged(value.selection, cause);
|
||||
}
|
||||
if (textChanged) {
|
||||
try {
|
||||
widget.onChanged?.call(value.text);
|
||||
@@ -2292,19 +2307,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
}
|
||||
}
|
||||
|
||||
if (selectionChanged) {
|
||||
try {
|
||||
widget.onSelectionChanged?.call(value.selection, null);
|
||||
} catch (exception, stack) {
|
||||
FlutterError.reportError(FlutterErrorDetails(
|
||||
exception: exception,
|
||||
stack: stack,
|
||||
library: 'widgets',
|
||||
context: ErrorDescription('while calling onSelectionChanged'),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
endBatchEdit();
|
||||
}
|
||||
|
||||
@@ -2416,7 +2418,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
}
|
||||
if (!_value.selection.isValid) {
|
||||
// Place cursor at the end if the selection is invalid when we receive focus.
|
||||
_handleSelectionChanged(TextSelection.collapsed(offset: _value.text.length), renderEditable, null);
|
||||
_handleSelectionChanged(TextSelection.collapsed(offset: _value.text.length), null);
|
||||
}
|
||||
} else {
|
||||
WidgetsBinding.instance!.removeObserver(this);
|
||||
@@ -2478,8 +2480,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
double get _devicePixelRatio => MediaQuery.of(context).devicePixelRatio;
|
||||
|
||||
@override
|
||||
set textEditingValue(TextEditingValue value) {
|
||||
_selectionOverlay?.update(value);
|
||||
void userUpdateTextEditingValue(TextEditingValue value, SelectionChangedCause? cause) {
|
||||
// Compare the current TextEditingValue with the pre-format new
|
||||
// TextEditingValue value, in case the formatter would reject the change.
|
||||
final bool shouldShowCaret = widget.readOnly
|
||||
@@ -2488,7 +2489,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
if (shouldShowCaret) {
|
||||
_scheduleShowCaretOnScreen();
|
||||
}
|
||||
_formatAndSetValue(value);
|
||||
_formatAndSetValue(value, cause, userInteraction: true);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -2657,7 +2658,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
smartQuotesType: widget.smartQuotesType,
|
||||
enableSuggestions: widget.enableSuggestions,
|
||||
offset: offset,
|
||||
onSelectionChanged: _handleSelectionChanged,
|
||||
onCaretChanged: _handleCaretChanged,
|
||||
rendererIgnoresPointer: widget.rendererIgnoresPointer,
|
||||
cursorWidth: widget.cursorWidth,
|
||||
@@ -2741,7 +2741,6 @@ class _Editable extends LeafRenderObjectWidget {
|
||||
required this.smartQuotesType,
|
||||
required this.enableSuggestions,
|
||||
required this.offset,
|
||||
this.onSelectionChanged,
|
||||
this.onCaretChanged,
|
||||
this.rendererIgnoresPointer = false,
|
||||
required this.cursorWidth,
|
||||
@@ -2789,7 +2788,6 @@ class _Editable extends LeafRenderObjectWidget {
|
||||
final SmartQuotesType smartQuotesType;
|
||||
final bool enableSuggestions;
|
||||
final ViewportOffset offset;
|
||||
final SelectionChangedHandler? onSelectionChanged;
|
||||
final CaretChangedHandler? onCaretChanged;
|
||||
final bool rendererIgnoresPointer;
|
||||
final double cursorWidth;
|
||||
@@ -2829,7 +2827,6 @@ class _Editable extends LeafRenderObjectWidget {
|
||||
locale: locale ?? Localizations.maybeLocaleOf(context),
|
||||
selection: value.selection,
|
||||
offset: offset,
|
||||
onSelectionChanged: onSelectionChanged,
|
||||
onCaretChanged: onCaretChanged,
|
||||
ignorePointer: rendererIgnoresPointer,
|
||||
obscuringCharacter: obscuringCharacter,
|
||||
@@ -2874,7 +2871,6 @@ class _Editable extends LeafRenderObjectWidget {
|
||||
..locale = locale ?? Localizations.maybeLocaleOf(context)
|
||||
..selection = value.selection
|
||||
..offset = offset
|
||||
..onSelectionChanged = onSelectionChanged
|
||||
..onCaretChanged = onCaretChanged
|
||||
..ignorePointer = rendererIgnoresPointer
|
||||
..textHeightBehavior = textHeightBehavior
|
||||
|
||||
@@ -205,12 +205,15 @@ abstract class TextSelectionControls {
|
||||
Clipboard.setData(ClipboardData(
|
||||
text: value.selection.textInside(value.text),
|
||||
));
|
||||
delegate.textEditingValue = TextEditingValue(
|
||||
text: value.selection.textBefore(value.text)
|
||||
+ value.selection.textAfter(value.text),
|
||||
selection: TextSelection.collapsed(
|
||||
offset: value.selection.start
|
||||
delegate.userUpdateTextEditingValue(
|
||||
TextEditingValue(
|
||||
text: value.selection.textBefore(value.text)
|
||||
+ value.selection.textAfter(value.text),
|
||||
selection: TextSelection.collapsed(
|
||||
offset: value.selection.start
|
||||
)
|
||||
),
|
||||
SelectionChangedCause.toolBar,
|
||||
);
|
||||
delegate.bringIntoView(delegate.textEditingValue.selection.extent);
|
||||
delegate.hideToolbar();
|
||||
@@ -241,9 +244,12 @@ abstract class TextSelectionControls {
|
||||
case TargetPlatform.linux:
|
||||
case TargetPlatform.windows:
|
||||
// Collapse the selection and hide the toolbar and handles.
|
||||
delegate.textEditingValue = TextEditingValue(
|
||||
text: value.text,
|
||||
selection: TextSelection.collapsed(offset: value.selection.end),
|
||||
delegate.userUpdateTextEditingValue(
|
||||
TextEditingValue(
|
||||
text: value.text,
|
||||
selection: TextSelection.collapsed(offset: value.selection.end),
|
||||
),
|
||||
SelectionChangedCause.toolBar,
|
||||
);
|
||||
delegate.hideToolbar();
|
||||
return;
|
||||
@@ -265,13 +271,16 @@ abstract class TextSelectionControls {
|
||||
final TextEditingValue value = delegate.textEditingValue; // Snapshot the input before using `await`.
|
||||
final ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain);
|
||||
if (data != null) {
|
||||
delegate.textEditingValue = TextEditingValue(
|
||||
text: value.selection.textBefore(value.text)
|
||||
+ data.text!
|
||||
+ value.selection.textAfter(value.text),
|
||||
selection: TextSelection.collapsed(
|
||||
offset: value.selection.start + data.text!.length
|
||||
delegate.userUpdateTextEditingValue(
|
||||
TextEditingValue(
|
||||
text: value.selection.textBefore(value.text)
|
||||
+ data.text!
|
||||
+ value.selection.textAfter(value.text),
|
||||
selection: TextSelection.collapsed(
|
||||
offset: value.selection.start + data.text!.length
|
||||
),
|
||||
),
|
||||
SelectionChangedCause.toolBar,
|
||||
);
|
||||
}
|
||||
delegate.bringIntoView(delegate.textEditingValue.selection.extent);
|
||||
@@ -286,12 +295,15 @@ abstract class TextSelectionControls {
|
||||
/// This is called by subclasses when their select-all affordance is activated
|
||||
/// by the user.
|
||||
void handleSelectAll(TextSelectionDelegate delegate) {
|
||||
delegate.textEditingValue = TextEditingValue(
|
||||
text: delegate.textEditingValue.text,
|
||||
selection: TextSelection(
|
||||
baseOffset: 0,
|
||||
extentOffset: delegate.textEditingValue.text.length,
|
||||
delegate.userUpdateTextEditingValue(
|
||||
TextEditingValue(
|
||||
text: delegate.textEditingValue.text,
|
||||
selection: TextSelection(
|
||||
baseOffset: 0,
|
||||
extentOffset: delegate.textEditingValue.text.length,
|
||||
),
|
||||
),
|
||||
SelectionChangedCause.toolBar,
|
||||
);
|
||||
delegate.bringIntoView(delegate.textEditingValue.selection.extent);
|
||||
}
|
||||
@@ -450,13 +462,16 @@ class TextSelectionOverlay {
|
||||
|
||||
/// Builds the handles by inserting them into the [context]'s overlay.
|
||||
void showHandles() {
|
||||
assert(_handles == null);
|
||||
if (_handles != null)
|
||||
return;
|
||||
|
||||
_handles = <OverlayEntry>[
|
||||
OverlayEntry(builder: (BuildContext context) => _buildHandle(context, _TextSelectionHandlePosition.start)),
|
||||
OverlayEntry(builder: (BuildContext context) => _buildHandle(context, _TextSelectionHandlePosition.end)),
|
||||
];
|
||||
|
||||
Overlay.of(context, rootOverlay: true, debugRequiredFor: debugRequiredFor)!.insertAll(_handles!);
|
||||
Overlay.of(context, rootOverlay: true, debugRequiredFor: debugRequiredFor)!
|
||||
.insertAll(_handles!);
|
||||
}
|
||||
|
||||
/// Destroys the handles by removing them from overlay.
|
||||
@@ -636,10 +651,13 @@ class TextSelectionOverlay {
|
||||
textPosition = newSelection.base;
|
||||
break;
|
||||
case _TextSelectionHandlePosition.end:
|
||||
textPosition =newSelection.extent;
|
||||
textPosition = newSelection.extent;
|
||||
break;
|
||||
}
|
||||
selectionDelegate!.textEditingValue = _value.copyWith(selection: newSelection, composing: TextRange.empty);
|
||||
selectionDelegate!.userUpdateTextEditingValue(
|
||||
_value.copyWith(selection: newSelection, composing: TextRange.empty),
|
||||
SelectionChangedCause.drag,
|
||||
);
|
||||
selectionDelegate!.bringIntoView(textPosition);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3475,7 +3475,6 @@ void main() {
|
||||
from: tester.getTopRight(find.byType(CupertinoApp)),
|
||||
cause: SelectionChangedCause.tap,
|
||||
);
|
||||
expect(state.showToolbar(), true);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// -1 because we want to reach the end of the line, not the start of a new line.
|
||||
@@ -3536,7 +3535,6 @@ void main() {
|
||||
from: tester.getCenter(find.byType(EditableText)),
|
||||
cause: SelectionChangedCause.tap,
|
||||
);
|
||||
expect(state.showToolbar(), true);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
bottomLeftSelectionPosition = textOffsetToBottomLeftPosition(tester, state.renderEditable.selection!.baseOffset);
|
||||
|
||||
@@ -119,6 +119,8 @@ void main() {
|
||||
expect(tester.testTextInput.isVisible, isTrue);
|
||||
|
||||
tester.testTextInput.hide();
|
||||
final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
|
||||
state.connectionClosed();
|
||||
|
||||
expect(tester.testTextInput.isVisible, isFalse);
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@ import '../widgets/editable_text_utils.dart' show findRenderEditable, globalize,
|
||||
import '../widgets/semantics_tester.dart';
|
||||
import 'feedback_tester.dart';
|
||||
|
||||
typedef FormatEditUpdateCallback = void Function(TextEditingValue, TextEditingValue);
|
||||
|
||||
class MockClipboard {
|
||||
Object _clipboardData = <String, dynamic>{
|
||||
'text': null,
|
||||
@@ -127,6 +129,16 @@ double getOpacity(WidgetTester tester, Finder finder) {
|
||||
).opacity.value;
|
||||
}
|
||||
|
||||
class TestFormatter extends TextInputFormatter {
|
||||
TestFormatter(this.onFormatEditUpdate);
|
||||
FormatEditUpdateCallback onFormatEditUpdate;
|
||||
@override
|
||||
TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) {
|
||||
onFormatEditUpdate(oldValue, newValue);
|
||||
return newValue;
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
final MockClipboard mockClipboard = MockClipboard();
|
||||
@@ -474,6 +486,47 @@ void main() {
|
||||
);
|
||||
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
|
||||
|
||||
testWidgets('TextInputFormatter gets correct selection value', (WidgetTester tester) async {
|
||||
late TextEditingValue actualOldValue;
|
||||
late TextEditingValue actualNewValue;
|
||||
final FormatEditUpdateCallback callBack = (TextEditingValue oldValue, TextEditingValue newValue) {
|
||||
actualOldValue = oldValue;
|
||||
actualNewValue = newValue;
|
||||
};
|
||||
final FocusNode focusNode = FocusNode();
|
||||
final TextEditingController controller = TextEditingController(text: '123');
|
||||
await tester.pumpWidget(
|
||||
boilerplate(
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
focusNode: focusNode,
|
||||
inputFormatters: <TextInputFormatter>[TestFormatter(callBack)],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.byType(TextField));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.backspace);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
actualOldValue,
|
||||
const TextEditingValue(
|
||||
text: '123',
|
||||
selection: TextSelection.collapsed(offset: 3, affinity: TextAffinity.upstream),
|
||||
),
|
||||
);
|
||||
expect(
|
||||
actualNewValue,
|
||||
const TextEditingValue(
|
||||
text: '12',
|
||||
selection: TextSelection.collapsed(offset: 2),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('text field selection toolbar renders correctly inside opacity', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
@@ -1071,11 +1124,9 @@ void main() {
|
||||
));
|
||||
|
||||
expect(find.text('Paste'), findsNothing);
|
||||
|
||||
final Offset emptyPos = textOffsetToPosition(tester, 0);
|
||||
await tester.longPressAt(emptyPos, pointer: 7);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Paste'), findsOneWidget);
|
||||
});
|
||||
|
||||
@@ -3497,8 +3548,11 @@ void main() {
|
||||
// scrolls to make the caret visible.
|
||||
scrollableState = tester.firstState(find.byType(Scrollable));
|
||||
final EditableTextState editableTextState = tester.firstState(find.byType(EditableText));
|
||||
editableTextState.textEditingValue = editableTextState.textEditingValue.copyWith(
|
||||
selection: TextSelection.collapsed(offset: longText.length),
|
||||
editableTextState.userUpdateTextEditingValue(
|
||||
editableTextState.textEditingValue.copyWith(
|
||||
selection: TextSelection.collapsed(offset: longText.length),
|
||||
),
|
||||
null,
|
||||
);
|
||||
|
||||
await tester.pump(); // TODO(ianh): Figure out why this extra pump is needed.
|
||||
@@ -3533,8 +3587,11 @@ void main() {
|
||||
// Move the caret to the end of the text and check that the text field
|
||||
// scrolls to make the caret visible.
|
||||
final EditableTextState editableTextState = tester.firstState(find.byType(EditableText));
|
||||
editableTextState.textEditingValue = editableTextState.textEditingValue.copyWith(
|
||||
selection: const TextSelection.collapsed(offset: tallText.length),
|
||||
editableTextState.userUpdateTextEditingValue(
|
||||
editableTextState.textEditingValue.copyWith(
|
||||
selection: const TextSelection.collapsed(offset: tallText.length),
|
||||
),
|
||||
null,
|
||||
);
|
||||
await tester.pump();
|
||||
await skipPastScrollingAnimation(tester);
|
||||
|
||||
@@ -21,6 +21,9 @@ class FakeEditableTextState with TextSelectionDelegate {
|
||||
@override
|
||||
void hideToolbar([bool hideHandles = true]) { }
|
||||
|
||||
@override
|
||||
void userUpdateTextEditingValue(TextEditingValue value, SelectionChangedCause cause) { }
|
||||
|
||||
@override
|
||||
void bringIntoView(TextPosition position) { }
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('cursor layout has correct width', (WidgetTester tester) async {
|
||||
EditableText.debugDeterministicCursor = true;
|
||||
final GlobalKey<EditableTextState> editableTextKey = GlobalKey<EditableTextState>();
|
||||
|
||||
late String changedValue;
|
||||
@@ -87,8 +88,7 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text('Paste'));
|
||||
// Wait for cursor to appear.
|
||||
await tester.pump(const Duration(milliseconds: 600));
|
||||
await tester.pump();
|
||||
|
||||
expect(changedValue, clipboardContent);
|
||||
|
||||
@@ -96,6 +96,7 @@ void main() {
|
||||
find.byKey(const ValueKey<int>(1)),
|
||||
matchesGoldenFile('editable_text_test.0.png'),
|
||||
);
|
||||
EditableText.debugDeterministicCursor = false;
|
||||
});
|
||||
|
||||
testWidgets('cursor layout has correct radius', (WidgetTester tester) async {
|
||||
@@ -787,6 +788,7 @@ void main() {
|
||||
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
|
||||
|
||||
testWidgets('cursor layout', (WidgetTester tester) async {
|
||||
EditableText.debugDeterministicCursor = true;
|
||||
final GlobalKey<EditableTextState> editableTextKey = GlobalKey<EditableTextState>();
|
||||
|
||||
late String changedValue;
|
||||
@@ -831,8 +833,7 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text('Paste'));
|
||||
// Wait for cursor to appear.
|
||||
await tester.pump(const Duration(milliseconds: 600));
|
||||
await tester.pump();
|
||||
|
||||
expect(changedValue, clipboardContent);
|
||||
|
||||
@@ -840,9 +841,11 @@ void main() {
|
||||
find.byKey(const ValueKey<int>(1)),
|
||||
matchesGoldenFile('editable_text_test.2.png'),
|
||||
);
|
||||
EditableText.debugDeterministicCursor = false;
|
||||
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
|
||||
|
||||
testWidgets('cursor layout has correct height', (WidgetTester tester) async {
|
||||
EditableText.debugDeterministicCursor = true;
|
||||
final GlobalKey<EditableTextState> editableTextKey = GlobalKey<EditableTextState>();
|
||||
|
||||
late String changedValue;
|
||||
@@ -888,8 +891,7 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text('Paste'));
|
||||
// Wait for cursor to appear.
|
||||
await tester.pump(const Duration(milliseconds: 600));
|
||||
await tester.pump();
|
||||
|
||||
expect(changedValue, clipboardContent);
|
||||
|
||||
@@ -897,5 +899,6 @@ void main() {
|
||||
find.byKey(const ValueKey<int>(1)),
|
||||
matchesGoldenFile('editable_text_test.3.png'),
|
||||
);
|
||||
EditableText.debugDeterministicCursor = false;
|
||||
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
|
||||
}
|
||||
|
||||
@@ -646,7 +646,10 @@ void main() {
|
||||
|
||||
// Change the selection. Show caret on screen even when readyOnly is
|
||||
// false.
|
||||
state.textEditingValue = state.textEditingValue.copyWith(selection: const TextSelection.collapsed(offset: 90));
|
||||
state.userUpdateTextEditingValue(
|
||||
state.textEditingValue.copyWith(selection: const TextSelection.collapsed(offset: 90)),
|
||||
null,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(isCaretOnScreen(tester), isTrue);
|
||||
expect(scrollController.offset, greaterThan(0.0));
|
||||
@@ -667,7 +670,10 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
expect(isCaretOnScreen(tester), isFalse);
|
||||
|
||||
state.textEditingValue = state.textEditingValue.copyWith(selection: const TextSelection.collapsed(offset: 100));
|
||||
state.userUpdateTextEditingValue(
|
||||
state.textEditingValue.copyWith(selection: const TextSelection.collapsed(offset: 100)),
|
||||
null,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(isCaretOnScreen(tester), isTrue);
|
||||
expect(scrollController.offset, greaterThan(0.0));
|
||||
|
||||
@@ -5198,7 +5198,7 @@ void main() {
|
||||
tester.testTextInput.log.clear();
|
||||
|
||||
final EditableTextState state = tester.state<EditableTextState>(find.byWidget(editableText));
|
||||
state.textEditingValue = const TextEditingValue(text: 'remoteremoteremote');
|
||||
state.userUpdateTextEditingValue(const TextEditingValue(text: 'remoteremoteremote'), SelectionChangedCause.keyboard);
|
||||
|
||||
// Apply in order: length formatter -> listener -> onChanged -> listener.
|
||||
expect(controller.text, 'remote listener onChanged listener');
|
||||
@@ -5354,6 +5354,7 @@ void main() {
|
||||
'TextInput.setEditingState',
|
||||
'TextInput.setEditingState',
|
||||
'TextInput.show',
|
||||
'TextInput.show',
|
||||
];
|
||||
expect(tester.testTextInput.log.length, logOrder.length);
|
||||
int index = 0;
|
||||
@@ -5468,16 +5469,18 @@ void main() {
|
||||
log.clear();
|
||||
|
||||
final EditableTextState state = tester.firstState(find.byType(EditableText));
|
||||
|
||||
// setEditingState is not called when only the remote changes
|
||||
state.updateEditingValue(const TextEditingValue(
|
||||
state.updateEditingValue(TextEditingValue(
|
||||
text: 'a',
|
||||
selection: controller.selection,
|
||||
));
|
||||
|
||||
expect(log.length, 0);
|
||||
|
||||
// setEditingState is called when remote value modified by the formatter.
|
||||
state.updateEditingValue(const TextEditingValue(
|
||||
state.updateEditingValue(TextEditingValue(
|
||||
text: 'I will be modified by the formatter.',
|
||||
selection: controller.selection,
|
||||
));
|
||||
expect(log.length, 1);
|
||||
MethodCall methodCall = log[0];
|
||||
@@ -5591,8 +5594,9 @@ void main() {
|
||||
final EditableTextState state = tester.firstState(find.byType(EditableText));
|
||||
|
||||
// setEditingState is called when remote value modified by the formatter.
|
||||
state.updateEditingValue(const TextEditingValue(
|
||||
state.updateEditingValue(TextEditingValue(
|
||||
text: 'I will be modified by the formatter.',
|
||||
selection: controller.selection,
|
||||
));
|
||||
expect(log.length, 1);
|
||||
expect(log, contains(matchesMethodCall(
|
||||
@@ -5664,8 +5668,9 @@ void main() {
|
||||
|
||||
final EditableTextState state = tester.firstState(find.byType(EditableText));
|
||||
|
||||
state.updateEditingValue(const TextEditingValue(
|
||||
state.updateEditingValue(TextEditingValue(
|
||||
text: 'a',
|
||||
selection: controller.selection,
|
||||
));
|
||||
await tester.pump();
|
||||
|
||||
@@ -5688,8 +5693,9 @@ void main() {
|
||||
log.clear();
|
||||
|
||||
// Send repeat value from the engine.
|
||||
state.updateEditingValue(const TextEditingValue(
|
||||
state.updateEditingValue(TextEditingValue(
|
||||
text: 'a',
|
||||
selection: controller.selection,
|
||||
));
|
||||
await tester.pump();
|
||||
|
||||
@@ -5783,8 +5789,9 @@ void main() {
|
||||
|
||||
final EditableTextState state = tester.firstState(find.byType(EditableText));
|
||||
|
||||
state.updateEditingValue(const TextEditingValue(
|
||||
state.updateEditingValue(TextEditingValue(
|
||||
text: 'a',
|
||||
selection: controller.selection,
|
||||
));
|
||||
await tester.pump();
|
||||
|
||||
@@ -6578,6 +6585,7 @@ void main() {
|
||||
final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
|
||||
state.updateEditingValue(const TextEditingValue(
|
||||
text: 'foo composing bar',
|
||||
selection: TextSelection.collapsed(offset: 4),
|
||||
composing: TextRange(start: 4, end: 12),
|
||||
));
|
||||
controller.selection = const TextSelection.collapsed(offset: 2);
|
||||
@@ -6586,6 +6594,7 @@ void main() {
|
||||
// Reset the composing range.
|
||||
state.updateEditingValue(const TextEditingValue(
|
||||
text: 'foo composing bar',
|
||||
selection: TextSelection.collapsed(offset: 4),
|
||||
composing: TextRange(start: 4, end: 12),
|
||||
));
|
||||
expect(state.currentTextEditingValue.composing, const TextRange(start: 4, end: 12));
|
||||
@@ -6593,13 +6602,14 @@ void main() {
|
||||
// Positioning cursor after the composing range should clear the composing range.
|
||||
state.updateEditingValue(const TextEditingValue(
|
||||
text: 'foo composing bar',
|
||||
selection: TextSelection.collapsed(offset: 4),
|
||||
composing: TextRange(start: 4, end: 12),
|
||||
));
|
||||
controller.selection = const TextSelection.collapsed(offset: 14);
|
||||
expect(state.currentTextEditingValue.composing, TextRange.empty);
|
||||
});
|
||||
|
||||
testWidgets('Clears composing range if cursor moves outside that range', (WidgetTester tester) async {
|
||||
testWidgets('Clears composing range if cursor moves outside that range - case two', (WidgetTester tester) async {
|
||||
final Widget widget = MaterialApp(
|
||||
home: EditableText(
|
||||
backgroundCursorColor: Colors.grey,
|
||||
@@ -6616,6 +6626,7 @@ void main() {
|
||||
final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
|
||||
state.updateEditingValue(const TextEditingValue(
|
||||
text: 'foo composing bar',
|
||||
selection: TextSelection.collapsed(offset: 4),
|
||||
composing: TextRange(start: 4, end: 12),
|
||||
));
|
||||
controller.selection = const TextSelection(baseOffset: 1, extentOffset: 2);
|
||||
@@ -6624,6 +6635,7 @@ void main() {
|
||||
// Reset the composing range.
|
||||
state.updateEditingValue(const TextEditingValue(
|
||||
text: 'foo composing bar',
|
||||
selection: TextSelection.collapsed(offset: 4),
|
||||
composing: TextRange(start: 4, end: 12),
|
||||
));
|
||||
expect(state.currentTextEditingValue.composing, const TextRange(start: 4, end: 12));
|
||||
@@ -6631,6 +6643,7 @@ void main() {
|
||||
// Setting a selection within the composing range clears the composing range.
|
||||
state.updateEditingValue(const TextEditingValue(
|
||||
text: 'foo composing bar',
|
||||
selection: TextSelection.collapsed(offset: 4),
|
||||
composing: TextRange(start: 4, end: 12),
|
||||
));
|
||||
controller.selection = const TextSelection(baseOffset: 5, extentOffset: 7);
|
||||
@@ -6639,6 +6652,7 @@ void main() {
|
||||
// Reset the composing range.
|
||||
state.updateEditingValue(const TextEditingValue(
|
||||
text: 'foo composing bar',
|
||||
selection: TextSelection.collapsed(offset: 4),
|
||||
composing: TextRange(start: 4, end: 12),
|
||||
));
|
||||
expect(state.currentTextEditingValue.composing, const TextRange(start: 4, end: 12));
|
||||
@@ -6646,6 +6660,7 @@ void main() {
|
||||
// Setting a selection after the composing range clears the composing range.
|
||||
state.updateEditingValue(const TextEditingValue(
|
||||
text: 'foo composing bar',
|
||||
selection: TextSelection.collapsed(offset: 4),
|
||||
composing: TextRange(start: 4, end: 12),
|
||||
));
|
||||
controller.selection = const TextSelection(baseOffset: 13, extentOffset: 15);
|
||||
|
||||
@@ -797,6 +797,7 @@ class FakeRenderEditable extends RenderEditable {
|
||||
),
|
||||
startHandleLayerLink: LayerLink(),
|
||||
endHandleLayerLink: LayerLink(),
|
||||
ignorePointer: true,
|
||||
textAlign: TextAlign.start,
|
||||
textDirection: TextDirection.ltr,
|
||||
locale: const Locale('en', 'US'),
|
||||
|
||||
Reference in New Issue
Block a user