Unverified Commit a46139a2 authored by chunhtai's avatar chunhtai Committed by GitHub

fixes TextInputFormatter gets wrong old value of a selection (#75541)

parent b7d48062
...@@ -102,6 +102,7 @@ class _CupertinoTextFieldSelectionGestureDetectorBuilder extends TextSelectionGe ...@@ -102,6 +102,7 @@ class _CupertinoTextFieldSelectionGestureDetectorBuilder extends TextSelectionGe
@override @override
void onSingleTapUp(TapUpDetails details) { void onSingleTapUp(TapUpDetails details) {
editableText.hideToolbar();
// Because TextSelectionGestureDetector listens to taps that happen on // Because TextSelectionGestureDetector listens to taps that happen on
// widgets in front of it, tapping the clear button will also trigger // widgets in front of it, tapping the clear button will also trigger
// this handler. If the clear button widget recognizes the up event, // this handler. If the clear button widget recognizes the up event,
......
...@@ -494,6 +494,8 @@ class _SelectableTextState extends State<SelectableText> with AutomaticKeepAlive ...@@ -494,6 +494,8 @@ class _SelectableTextState extends State<SelectableText> with AutomaticKeepAlive
}); });
} }
TextSelection? _lastSeenTextSelection;
void _handleSelectionChanged(TextSelection selection, SelectionChangedCause? cause) { void _handleSelectionChanged(TextSelection selection, SelectionChangedCause? cause) {
final bool willShowSelectionHandles = _shouldShowSelectionHandles(cause); final bool willShowSelectionHandles = _shouldShowSelectionHandles(cause);
if (willShowSelectionHandles != _showSelectionHandles) { if (willShowSelectionHandles != _showSelectionHandles) {
...@@ -501,10 +503,12 @@ class _SelectableTextState extends State<SelectableText> with AutomaticKeepAlive ...@@ -501,10 +503,12 @@ class _SelectableTextState extends State<SelectableText> with AutomaticKeepAlive
_showSelectionHandles = willShowSelectionHandles; _showSelectionHandles = willShowSelectionHandles;
}); });
} }
// TODO(chunhtai): The selection may be the same. We should remove this
if (widget.onSelectionChanged != null) { // check once this is fixed https://github.com/flutter/flutter/issues/76349.
if (widget.onSelectionChanged != null && _lastSeenTextSelection != selection) {
widget.onSelectionChanged!(selection, cause); widget.onSelectionChanged!(selection, cause);
} }
_lastSeenTextSelection = selection;
switch (Theme.of(context).platform) { switch (Theme.of(context).platform) {
case TargetPlatform.iOS: case TargetPlatform.iOS:
......
...@@ -31,39 +31,13 @@ const Radius _kFloatingCaretRadius = Radius.circular(1.0); ...@@ -31,39 +31,13 @@ const Radius _kFloatingCaretRadius = Radius.circular(1.0);
/// (including the cursor location). /// (including the cursor location).
/// ///
/// Used by [RenderEditable.onSelectionChanged]. /// 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); 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. /// Signature for the callback that reports when the caret location changes.
/// ///
/// Used by [RenderEditable.onCaretChanged]. /// Used by [RenderEditable.onCaretChanged].
...@@ -158,10 +132,6 @@ bool _isWhitespace(int codeUnit) { ...@@ -158,10 +132,6 @@ bool _isWhitespace(int codeUnit) {
/// If, when the render object paints, the caret is found to have changed /// If, when the render object paints, the caret is found to have changed
/// location, [onCaretChanged] is called. /// 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 /// Keyboard handling, IME handling, scrolling, toggling the [showCursor] value
/// to actually blink the cursor, and other features not mentioned above are the /// to actually blink the cursor, and other features not mentioned above are the
/// responsibility of higher layers and not handled by this object. /// responsibility of higher layers and not handled by this object.
...@@ -198,6 +168,10 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -198,6 +168,10 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
double textScaleFactor = 1.0, double textScaleFactor = 1.0,
TextSelection? selection, TextSelection? selection,
required ViewportOffset offset, required ViewportOffset offset,
@Deprecated(
'Uses the textSelectionDelegate.userUpdateTextEditingValue instead. '
'This feature was deprecated after v1.26.0-17.2.pre.'
)
this.onSelectionChanged, this.onSelectionChanged,
this.onCaretChanged, this.onCaretChanged,
this.ignorePointer = false, this.ignorePointer = false,
...@@ -401,6 +375,10 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -401,6 +375,10 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
/// Called when the selection changes. /// Called when the selection changes.
/// ///
/// If this is null, then selection changes will be ignored. /// 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; SelectionChangedHandler? onSelectionChanged;
double? _textLayoutLastMaxWidth; double? _textLayoutLastMaxWidth;
...@@ -579,7 +557,19 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -579,7 +557,19 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
// down in a multiline text field when selecting using the keyboard. // down in a multiline text field when selecting using the keyboard.
bool _wasSelectingVerticallyWithKeyboard = false; 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( void _handleSelectionChange(
TextSelection nextSelection, TextSelection nextSelection,
SelectionChangedCause cause, SelectionChangedCause cause,
...@@ -642,7 +632,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -642,7 +632,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
return; return;
} }
if (keyEvent is! RawKeyDownEvent || onSelectionChanged == null) if (keyEvent is! RawKeyDownEvent)
return; return;
final Set<LogicalKeyboardKey> keysPressed = LogicalKeyboardKey.collapseSynonyms(RawKeyboard.instance.keysPressed); final Set<LogicalKeyboardKey> keysPressed = LogicalKeyboardKey.collapseSynonyms(RawKeyboard.instance.keysPressed);
final LogicalKeyboardKey key = keyEvent.logicalKey; final LogicalKeyboardKey key = keyEvent.logicalKey;
...@@ -908,12 +898,10 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -908,12 +898,10 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
newSelection = TextSelection.fromPosition(TextPosition(offset: newOffset)); newSelection = TextSelection.fromPosition(TextPosition(offset: newOffset));
} }
_handleSelectionChange( _setSelection(
newSelection, newSelection,
SelectionChangedCause.keyboard, 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 // Handles shortcut functionality including cut, copy, paste and select all
...@@ -961,14 +949,11 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -961,14 +949,11 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
); );
} }
if (value != null) { if (value != null) {
if (textSelectionDelegate.textEditingValue.selection != value.selection) { _setTextEditingValue(
_handleSelectionChange( value,
value.selection,
SelectionChangedCause.keyboard, SelectionChangedCause.keyboard,
); );
} }
textSelectionDelegate.textEditingValue = value;
}
} }
void _handleDelete({ required bool forward }) { void _handleDelete({ required bool forward }) {
...@@ -994,15 +979,12 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -994,15 +979,12 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
} }
} }
final TextSelection newSelection = TextSelection.collapsed(offset: cursorPosition); final TextSelection newSelection = TextSelection.collapsed(offset: cursorPosition);
if (selection != newSelection) { _setTextEditingValue(
_handleSelectionChange( TextEditingValue(
newSelection,
SelectionChangedCause.keyboard,
);
}
textSelectionDelegate.textEditingValue = TextEditingValue(
text: textBefore + textAfter, text: textBefore + textAfter,
selection: newSelection, selection: newSelection,
),
SelectionChangedCause.keyboard,
); );
} }
...@@ -1530,7 +1512,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -1530,7 +1512,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
// callbacks are invoked, in which case the callbacks will crash... // callbacks are invoked, in which case the callbacks will crash...
void _handleSetSelection(TextSelection selection) { void _handleSetSelection(TextSelection selection) {
_handleSelectionChange(selection, SelectionChangedCause.keyboard); _setSelection(selection, SelectionChangedCause.keyboard);
} }
void _handleMoveCursorForwardByCharacter(bool extentSelection) { void _handleMoveCursorForwardByCharacter(bool extentSelection) {
...@@ -1539,8 +1521,9 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -1539,8 +1521,9 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
if (extentOffset == null) if (extentOffset == null)
return; return;
final int baseOffset = !extentSelection ? extentOffset : selection!.baseOffset; final int baseOffset = !extentSelection ? extentOffset : selection!.baseOffset;
_handleSelectionChange( _setSelection(
TextSelection(baseOffset: baseOffset, extentOffset: extentOffset), SelectionChangedCause.keyboard, TextSelection(baseOffset: baseOffset, extentOffset: extentOffset),
SelectionChangedCause.keyboard,
); );
} }
...@@ -1550,8 +1533,9 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -1550,8 +1533,9 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
if (extentOffset == null) if (extentOffset == null)
return; return;
final int baseOffset = !extentSelection ? extentOffset : selection!.baseOffset; final int baseOffset = !extentSelection ? extentOffset : selection!.baseOffset;
_handleSelectionChange( _setSelection(
TextSelection(baseOffset: baseOffset, extentOffset: extentOffset), SelectionChangedCause.keyboard, TextSelection(baseOffset: baseOffset, extentOffset: extentOffset),
SelectionChangedCause.keyboard
); );
} }
...@@ -1562,7 +1546,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -1562,7 +1546,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
if (nextWord == null) if (nextWord == null)
return; return;
final int baseOffset = extentSelection ? selection!.baseOffset : nextWord.start; final int baseOffset = extentSelection ? selection!.baseOffset : nextWord.start;
_handleSelectionChange( _setSelection(
TextSelection( TextSelection(
baseOffset: baseOffset, baseOffset: baseOffset,
extentOffset: nextWord.start, extentOffset: nextWord.start,
...@@ -1578,12 +1562,12 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -1578,12 +1562,12 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
if (previousWord == null) if (previousWord == null)
return; return;
final int baseOffset = extentSelection ? selection!.baseOffset : previousWord.start; final int baseOffset = extentSelection ? selection!.baseOffset : previousWord.start;
_handleSelectionChange( _setSelection(
TextSelection( TextSelection(
baseOffset: baseOffset, baseOffset: baseOffset,
extentOffset: previousWord.start, extentOffset: previousWord.start,
), ),
SelectionChangedCause.keyboard, SelectionChangedCause.keyboard
); );
} }
...@@ -1894,7 +1878,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -1894,7 +1878,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
textSpan.recognizer?.addPointer(event); textSpan.recognizer?.addPointer(event);
} }
if (!ignorePointer && onSelectionChanged != null) { if (!ignorePointer) {
// Propagates the pointer event to selection handlers. // Propagates the pointer event to selection handlers.
_tap.addPointer(event); _tap.addPointer(event);
_longPress.addPointer(event); _longPress.addPointer(event);
...@@ -1992,9 +1976,6 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -1992,9 +1976,6 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
assert(cause != null); assert(cause != null);
assert(from != null); assert(from != null);
_layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth); _layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth);
if (onSelectionChanged == null) {
return;
}
final TextPosition fromPosition = _textPainter.getPositionForOffset(globalToLocal(from - _paintOffset)); final TextPosition fromPosition = _textPainter.getPositionForOffset(globalToLocal(from - _paintOffset));
final TextPosition? toPosition = to == null final TextPosition? toPosition = to == null
? null ? null
...@@ -2008,8 +1989,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -2008,8 +1989,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
extentOffset: extentOffset, extentOffset: extentOffset,
affinity: fromPosition.affinity, affinity: fromPosition.affinity,
); );
// Call [onSelectionChanged] only when the selection actually changed. _setSelection(newSelection, cause);
_handleSelectionChange(newSelection, cause);
} }
/// Select a word around the location of the last tap down. /// Select a word around the location of the last tap down.
...@@ -2029,22 +2009,16 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -2029,22 +2009,16 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
assert(cause != null); assert(cause != null);
assert(from != null); assert(from != null);
_layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth); _layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth);
if (onSelectionChanged == null) {
return;
}
final TextPosition firstPosition = _textPainter.getPositionForOffset(globalToLocal(from - _paintOffset)); final TextPosition firstPosition = _textPainter.getPositionForOffset(globalToLocal(from - _paintOffset));
final TextSelection firstWord = _selectWordAtOffset(firstPosition); final TextSelection firstWord = _selectWordAtOffset(firstPosition);
final TextSelection lastWord = to == null ? final TextSelection lastWord = to == null ?
firstWord : _selectWordAtOffset(_textPainter.getPositionForOffset(globalToLocal(to - _paintOffset))); firstWord : _selectWordAtOffset(_textPainter.getPositionForOffset(globalToLocal(to - _paintOffset)));
final TextSelection newSelection = TextSelection(
_handleSelectionChange(
TextSelection(
baseOffset: firstWord.base.offset, baseOffset: firstWord.base.offset,
extentOffset: lastWord.extent.offset, extentOffset: lastWord.extent.offset,
affinity: firstWord.affinity, affinity: firstWord.affinity,
),
cause,
); );
_setSelection(newSelection, cause);
} }
/// Move the selection to the beginning or end of a word. /// Move the selection to the beginning or end of a word.
...@@ -2054,22 +2028,15 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -2054,22 +2028,15 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
assert(cause != null); assert(cause != null);
_layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth); _layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth);
assert(_lastTapDownPosition != null); assert(_lastTapDownPosition != null);
if (onSelectionChanged == null) {
return;
}
final TextPosition position = _textPainter.getPositionForOffset(globalToLocal(_lastTapDownPosition! - _paintOffset)); final TextPosition position = _textPainter.getPositionForOffset(globalToLocal(_lastTapDownPosition! - _paintOffset));
final TextRange word = _textPainter.getWordBoundary(position); final TextRange word = _textPainter.getWordBoundary(position);
late TextSelection newSelection;
if (position.offset - word.start <= 1) { if (position.offset - word.start <= 1) {
_handleSelectionChange( newSelection = TextSelection.collapsed(offset: word.start, affinity: TextAffinity.downstream);
TextSelection.collapsed(offset: word.start, affinity: TextAffinity.downstream),
cause,
);
} else { } else {
_handleSelectionChange( newSelection = TextSelection.collapsed(offset: word.end, affinity: TextAffinity.upstream);
TextSelection.collapsed(offset: word.end, affinity: TextAffinity.upstream),
cause,
);
} }
_setSelection(newSelection, cause);
} }
TextSelection _selectWordAtOffset(TextPosition position) { TextSelection _selectWordAtOffset(TextPosition position) {
......
...@@ -754,12 +754,60 @@ class TextEditingValue { ...@@ -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 /// An interface for manipulating the selection, to be used by the implementor
/// of the toolbar widget. /// of the toolbar widget.
abstract class TextSelectionDelegate { abstract class TextSelectionDelegate {
/// Gets the current text input. /// Gets the current text input.
TextEditingValue get textEditingValue; 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 /// Indicates that the user has requested the delegate to replace its current
/// text editing state with [value]. /// text editing state with [value].
/// ///
...@@ -768,10 +816,10 @@ abstract class TextSelectionDelegate { ...@@ -768,10 +816,10 @@ abstract class TextSelectionDelegate {
/// ///
/// See also: /// See also:
/// ///
/// * [EditableTextState.textEditingValue]: an implementation that applies /// * [EditableTextState.userUpdateTextEditingValue]: an implementation that
/// additional pre-processing to the specified [value], before updating the /// applies additional pre-processing to the specified [value], before
/// text editing state. /// updating the text editing state.
set textEditingValue(TextEditingValue value); void userUpdateTextEditingValue(TextEditingValue value, SelectionChangedCause cause);
/// Hides the text selection toolbar. /// Hides the text selection toolbar.
void hideToolbar(); void hideToolbar();
......
...@@ -32,8 +32,7 @@ import 'text.dart'; ...@@ -32,8 +32,7 @@ import 'text.dart';
import 'text_selection.dart'; import 'text_selection.dart';
import 'ticker_provider.dart'; import 'ticker_provider.dart';
export 'package:flutter/rendering.dart' show SelectionChangedCause; export 'package:flutter/services.dart' show SelectionChangedCause, TextEditingValue, TextSelection, TextInputType, SmartQuotesType, SmartDashesType;
export 'package:flutter/services.dart' show TextEditingValue, TextSelection, TextInputType, SmartQuotesType, SmartDashesType;
/// Signature for the callback that reports when the user changes the selection /// Signature for the callback that reports when the user changes the selection
/// (including the cursor location). /// (including the cursor location).
...@@ -1480,7 +1479,7 @@ class EditableText extends StatefulWidget { ...@@ -1480,7 +1479,7 @@ class EditableText extends StatefulWidget {
} }
/// State for a [EditableText]. /// State for a [EditableText].
class EditableTextState extends State<EditableText> with AutomaticKeepAliveClientMixin<EditableText>, WidgetsBindingObserver, TickerProviderStateMixin<EditableText> implements TextSelectionDelegate, TextInputClient, AutofillClient { class EditableTextState extends State<EditableText> with AutomaticKeepAliveClientMixin<EditableText>, WidgetsBindingObserver, TickerProviderStateMixin<EditableText>, TextSelectionDelegate implements TextInputClient, AutofillClient {
Timer? _cursorTimer; Timer? _cursorTimer;
bool _targetCursorVisibility = false; bool _targetCursorVisibility = false;
final ValueNotifier<bool> _cursorVisibilityNotifier = ValueNotifier<bool>(true); final ValueNotifier<bool> _cursorVisibilityNotifier = ValueNotifier<bool>(true);
...@@ -1715,7 +1714,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -1715,7 +1714,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
if (value.text == _value.text && value.composing == _value.composing) { if (value.text == _value.text && value.composing == _value.composing) {
// `selection` is the only change. // `selection` is the only change.
_handleSelectionChanged(value.selection, renderEditable, SelectionChangedCause.keyboard); _handleSelectionChanged(value.selection, SelectionChangedCause.keyboard);
} else { } else {
hideToolbar(); hideToolbar();
_currentPromptRectRange = null; _currentPromptRectRange = null;
...@@ -1728,7 +1727,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -1728,7 +1727,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
} }
} }
_formatAndSetValue(value); _formatAndSetValue(value, SelectionChangedCause.keyboard);
} }
if (_hasInputConnection) { if (_hasInputConnection) {
...@@ -1836,7 +1835,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -1836,7 +1835,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
renderEditable.setFloatingCursor(FloatingCursorDragState.End, finalPosition, _lastTextPosition!); renderEditable.setFloatingCursor(FloatingCursorDragState.End, finalPosition, _lastTextPosition!);
if (_lastTextPosition!.offset != renderEditable.selection!.baseOffset) 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. // 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; _startCaretRect = null;
_lastTextPosition = null; _lastTextPosition = null;
_pointOffsetOrigin = null; _pointOffsetOrigin = null;
...@@ -2102,7 +2101,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -2102,7 +2101,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 // 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 // text of [EditableText] is updated at the same time as the selection is
// changed by a gesture event. // changed by a gesture event.
...@@ -2114,11 +2113,11 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -2114,11 +2113,11 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
// This will show the keyboard for all selection changes on the // This will show the keyboard for all selection changes on the
// EditableWidget, not just changes triggered by user gestures. // EditableWidget, not just changes triggered by user gestures.
requestKeyboard(); requestKeyboard();
if (widget.selectionControls == null) {
_selectionOverlay?.hide(); _selectionOverlay?.hide();
_selectionOverlay = null; _selectionOverlay = null;
} else {
if (widget.selectionControls != null) { if (_selectionOverlay == null) {
_selectionOverlay = TextSelectionOverlay( _selectionOverlay = TextSelectionOverlay(
clipboardStatus: _clipboardStatus, clipboardStatus: _clipboardStatus,
context: context, context: context,
...@@ -2127,14 +2126,21 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -2127,14 +2126,21 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
toolbarLayerLink: _toolbarLayerLink, toolbarLayerLink: _toolbarLayerLink,
startHandleLayerLink: _startHandleLayerLink, startHandleLayerLink: _startHandleLayerLink,
endHandleLayerLink: _endHandleLayerLink, endHandleLayerLink: _endHandleLayerLink,
renderObject: renderObject, renderObject: renderEditable,
selectionControls: widget.selectionControls, selectionControls: widget.selectionControls,
selectionDelegate: this, selectionDelegate: this,
dragStartBehavior: widget.dragStartBehavior, dragStartBehavior: widget.dragStartBehavior,
onSelectionHandleTapped: widget.onSelectionHandleTapped, onSelectionHandleTapped: widget.onSelectionHandleTapped,
); );
} else {
_selectionOverlay!.update(_value);
}
_selectionOverlay!.handlesVisible = widget.showSelectionHandles; _selectionOverlay!.handlesVisible = widget.showSelectionHandles;
_selectionOverlay!.showHandles(); _selectionOverlay!.showHandles();
}
// TODO(chunhtai): we should make sure selection actually changed before
// we call the onSelectionChanged.
// https://github.com/flutter/flutter/issues/76349.
try { try {
widget.onSelectionChanged?.call(selection, cause); widget.onSelectionChanged?.call(selection, cause);
} catch (exception, stack) { } catch (exception, stack) {
...@@ -2145,7 +2151,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -2145,7 +2151,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
context: ErrorDescription('while calling onSelectionChanged for $cause'), context: ErrorDescription('while calling onSelectionChanged for $cause'),
)); ));
} }
}
// To keep the cursor from blinking while it moves, restart the timer here. // To keep the cursor from blinking while it moves, restart the timer here.
if (_cursorTimer != null) { if (_cursorTimer != null) {
...@@ -2239,7 +2244,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -2239,7 +2244,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
late final _WhitespaceDirectionalityFormatter _whitespaceFormatter = _WhitespaceDirectionalityFormatter(textDirection: _textDirection); 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 // Only apply input formatters if the text has changed (including uncommited
// text in the composing region), or when the user committed the composing // text in the composing region), or when the user committed the composing
// text. // text.
...@@ -2271,6 +2276,16 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -2271,6 +2276,16 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
// sending multiple `TextInput.updateEditingValue` messages. // sending multiple `TextInput.updateEditingValue` messages.
beginBatchEdit(); beginBatchEdit();
_value = value; _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) { if (textChanged) {
try { try {
widget.onChanged?.call(value.text); widget.onChanged?.call(value.text);
...@@ -2284,19 +2299,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -2284,19 +2299,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(); endBatchEdit();
} }
...@@ -2407,7 +2409,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -2407,7 +2409,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
_showCaretOnScreen(); _showCaretOnScreen();
if (!_value.selection.isValid) { if (!_value.selection.isValid) {
// Place cursor at the end if the selection is invalid when we receive focus. // 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 { } else {
WidgetsBinding.instance!.removeObserver(this); WidgetsBinding.instance!.removeObserver(this);
...@@ -2469,9 +2471,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -2469,9 +2471,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
double get _devicePixelRatio => MediaQuery.of(context).devicePixelRatio; double get _devicePixelRatio => MediaQuery.of(context).devicePixelRatio;
@override @override
set textEditingValue(TextEditingValue value) { void userUpdateTextEditingValue(TextEditingValue value, SelectionChangedCause? cause) {
_selectionOverlay?.update(value); _formatAndSetValue(value, cause, userInteraction: true);
_formatAndSetValue(value);
} }
@override @override
...@@ -2634,7 +2635,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -2634,7 +2635,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
smartQuotesType: widget.smartQuotesType, smartQuotesType: widget.smartQuotesType,
enableSuggestions: widget.enableSuggestions, enableSuggestions: widget.enableSuggestions,
offset: offset, offset: offset,
onSelectionChanged: _handleSelectionChanged,
onCaretChanged: _handleCaretChanged, onCaretChanged: _handleCaretChanged,
rendererIgnoresPointer: widget.rendererIgnoresPointer, rendererIgnoresPointer: widget.rendererIgnoresPointer,
cursorWidth: widget.cursorWidth, cursorWidth: widget.cursorWidth,
...@@ -2718,7 +2718,6 @@ class _Editable extends LeafRenderObjectWidget { ...@@ -2718,7 +2718,6 @@ class _Editable extends LeafRenderObjectWidget {
required this.smartQuotesType, required this.smartQuotesType,
required this.enableSuggestions, required this.enableSuggestions,
required this.offset, required this.offset,
this.onSelectionChanged,
this.onCaretChanged, this.onCaretChanged,
this.rendererIgnoresPointer = false, this.rendererIgnoresPointer = false,
required this.cursorWidth, required this.cursorWidth,
...@@ -2766,7 +2765,6 @@ class _Editable extends LeafRenderObjectWidget { ...@@ -2766,7 +2765,6 @@ class _Editable extends LeafRenderObjectWidget {
final SmartQuotesType smartQuotesType; final SmartQuotesType smartQuotesType;
final bool enableSuggestions; final bool enableSuggestions;
final ViewportOffset offset; final ViewportOffset offset;
final SelectionChangedHandler? onSelectionChanged;
final CaretChangedHandler? onCaretChanged; final CaretChangedHandler? onCaretChanged;
final bool rendererIgnoresPointer; final bool rendererIgnoresPointer;
final double cursorWidth; final double cursorWidth;
...@@ -2806,7 +2804,6 @@ class _Editable extends LeafRenderObjectWidget { ...@@ -2806,7 +2804,6 @@ class _Editable extends LeafRenderObjectWidget {
locale: locale ?? Localizations.maybeLocaleOf(context), locale: locale ?? Localizations.maybeLocaleOf(context),
selection: value.selection, selection: value.selection,
offset: offset, offset: offset,
onSelectionChanged: onSelectionChanged,
onCaretChanged: onCaretChanged, onCaretChanged: onCaretChanged,
ignorePointer: rendererIgnoresPointer, ignorePointer: rendererIgnoresPointer,
obscuringCharacter: obscuringCharacter, obscuringCharacter: obscuringCharacter,
...@@ -2851,7 +2848,6 @@ class _Editable extends LeafRenderObjectWidget { ...@@ -2851,7 +2848,6 @@ class _Editable extends LeafRenderObjectWidget {
..locale = locale ?? Localizations.maybeLocaleOf(context) ..locale = locale ?? Localizations.maybeLocaleOf(context)
..selection = value.selection ..selection = value.selection
..offset = offset ..offset = offset
..onSelectionChanged = onSelectionChanged
..onCaretChanged = onCaretChanged ..onCaretChanged = onCaretChanged
..ignorePointer = rendererIgnoresPointer ..ignorePointer = rendererIgnoresPointer
..textHeightBehavior = textHeightBehavior ..textHeightBehavior = textHeightBehavior
......
...@@ -205,12 +205,15 @@ abstract class TextSelectionControls { ...@@ -205,12 +205,15 @@ abstract class TextSelectionControls {
Clipboard.setData(ClipboardData( Clipboard.setData(ClipboardData(
text: value.selection.textInside(value.text), text: value.selection.textInside(value.text),
)); ));
delegate.textEditingValue = TextEditingValue( delegate.userUpdateTextEditingValue(
TextEditingValue(
text: value.selection.textBefore(value.text) text: value.selection.textBefore(value.text)
+ value.selection.textAfter(value.text), + value.selection.textAfter(value.text),
selection: TextSelection.collapsed( selection: TextSelection.collapsed(
offset: value.selection.start offset: value.selection.start
)
), ),
SelectionChangedCause.toolBar,
); );
delegate.bringIntoView(delegate.textEditingValue.selection.extent); delegate.bringIntoView(delegate.textEditingValue.selection.extent);
delegate.hideToolbar(); delegate.hideToolbar();
...@@ -228,9 +231,12 @@ abstract class TextSelectionControls { ...@@ -228,9 +231,12 @@ abstract class TextSelectionControls {
text: value.selection.textInside(value.text), text: value.selection.textInside(value.text),
)); ));
clipboardStatus?.update(); clipboardStatus?.update();
delegate.textEditingValue = TextEditingValue( delegate.userUpdateTextEditingValue(
TextEditingValue(
text: value.text, text: value.text,
selection: TextSelection.collapsed(offset: value.selection.end), selection: TextSelection.collapsed(offset: value.selection.end),
),
SelectionChangedCause.toolBar,
); );
delegate.bringIntoView(delegate.textEditingValue.selection.extent); delegate.bringIntoView(delegate.textEditingValue.selection.extent);
delegate.hideToolbar(); delegate.hideToolbar();
...@@ -251,13 +257,16 @@ abstract class TextSelectionControls { ...@@ -251,13 +257,16 @@ abstract class TextSelectionControls {
final TextEditingValue value = delegate.textEditingValue; // Snapshot the input before using `await`. final TextEditingValue value = delegate.textEditingValue; // Snapshot the input before using `await`.
final ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain); final ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain);
if (data != null) { if (data != null) {
delegate.textEditingValue = TextEditingValue( delegate.userUpdateTextEditingValue(
TextEditingValue(
text: value.selection.textBefore(value.text) text: value.selection.textBefore(value.text)
+ data.text! + data.text!
+ value.selection.textAfter(value.text), + value.selection.textAfter(value.text),
selection: TextSelection.collapsed( selection: TextSelection.collapsed(
offset: value.selection.start + data.text!.length offset: value.selection.start + data.text!.length
), ),
),
SelectionChangedCause.toolBar,
); );
} }
delegate.bringIntoView(delegate.textEditingValue.selection.extent); delegate.bringIntoView(delegate.textEditingValue.selection.extent);
...@@ -272,12 +281,15 @@ abstract class TextSelectionControls { ...@@ -272,12 +281,15 @@ abstract class TextSelectionControls {
/// This is called by subclasses when their select-all affordance is activated /// This is called by subclasses when their select-all affordance is activated
/// by the user. /// by the user.
void handleSelectAll(TextSelectionDelegate delegate) { void handleSelectAll(TextSelectionDelegate delegate) {
delegate.textEditingValue = TextEditingValue( delegate.userUpdateTextEditingValue(
TextEditingValue(
text: delegate.textEditingValue.text, text: delegate.textEditingValue.text,
selection: TextSelection( selection: TextSelection(
baseOffset: 0, baseOffset: 0,
extentOffset: delegate.textEditingValue.text.length, extentOffset: delegate.textEditingValue.text.length,
), ),
),
SelectionChangedCause.toolBar,
); );
delegate.bringIntoView(delegate.textEditingValue.selection.extent); delegate.bringIntoView(delegate.textEditingValue.selection.extent);
} }
...@@ -436,13 +448,16 @@ class TextSelectionOverlay { ...@@ -436,13 +448,16 @@ class TextSelectionOverlay {
/// Builds the handles by inserting them into the [context]'s overlay. /// Builds the handles by inserting them into the [context]'s overlay.
void showHandles() { void showHandles() {
assert(_handles == null); if (_handles != null)
return;
_handles = <OverlayEntry>[ _handles = <OverlayEntry>[
OverlayEntry(builder: (BuildContext context) => _buildHandle(context, _TextSelectionHandlePosition.start)), OverlayEntry(builder: (BuildContext context) => _buildHandle(context, _TextSelectionHandlePosition.start)),
OverlayEntry(builder: (BuildContext context) => _buildHandle(context, _TextSelectionHandlePosition.end)), 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. /// Destroys the handles by removing them from overlay.
...@@ -613,10 +628,13 @@ class TextSelectionOverlay { ...@@ -613,10 +628,13 @@ class TextSelectionOverlay {
textPosition = newSelection.base; textPosition = newSelection.base;
break; break;
case _TextSelectionHandlePosition.end: case _TextSelectionHandlePosition.end:
textPosition =newSelection.extent; textPosition = newSelection.extent;
break; break;
} }
selectionDelegate!.textEditingValue = _value.copyWith(selection: newSelection, composing: TextRange.empty); selectionDelegate!.userUpdateTextEditingValue(
_value.copyWith(selection: newSelection, composing: TextRange.empty),
SelectionChangedCause.drag,
);
selectionDelegate!.bringIntoView(textPosition); selectionDelegate!.bringIntoView(textPosition);
} }
} }
......
...@@ -3475,7 +3475,6 @@ void main() { ...@@ -3475,7 +3475,6 @@ void main() {
from: tester.getTopRight(find.byType(CupertinoApp)), from: tester.getTopRight(find.byType(CupertinoApp)),
cause: SelectionChangedCause.tap, cause: SelectionChangedCause.tap,
); );
expect(state.showToolbar(), true);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
// -1 because we want to reach the end of the line, not the start of a new line. // -1 because we want to reach the end of the line, not the start of a new line.
...@@ -3536,7 +3535,6 @@ void main() { ...@@ -3536,7 +3535,6 @@ void main() {
from: tester.getCenter(find.byType(EditableText)), from: tester.getCenter(find.byType(EditableText)),
cause: SelectionChangedCause.tap, cause: SelectionChangedCause.tap,
); );
expect(state.showToolbar(), true);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
bottomLeftSelectionPosition = textOffsetToBottomLeftPosition(tester, state.renderEditable.selection!.baseOffset); bottomLeftSelectionPosition = textOffsetToBottomLeftPosition(tester, state.renderEditable.selection!.baseOffset);
......
...@@ -119,6 +119,8 @@ void main() { ...@@ -119,6 +119,8 @@ void main() {
expect(tester.testTextInput.isVisible, isTrue); expect(tester.testTextInput.isVisible, isTrue);
tester.testTextInput.hide(); tester.testTextInput.hide();
final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
state.connectionClosed();
expect(tester.testTextInput.isVisible, isFalse); expect(tester.testTextInput.isVisible, isFalse);
......
...@@ -18,6 +18,8 @@ import '../widgets/editable_text_utils.dart' show findRenderEditable, globalize, ...@@ -18,6 +18,8 @@ import '../widgets/editable_text_utils.dart' show findRenderEditable, globalize,
import '../widgets/semantics_tester.dart'; import '../widgets/semantics_tester.dart';
import 'feedback_tester.dart'; import 'feedback_tester.dart';
typedef FormatEditUpdateCallback = void Function(TextEditingValue, TextEditingValue);
class MockClipboard { class MockClipboard {
Object _clipboardData = <String, dynamic>{ Object _clipboardData = <String, dynamic>{
'text': null, 'text': null,
...@@ -127,6 +129,16 @@ double getOpacity(WidgetTester tester, Finder finder) { ...@@ -127,6 +129,16 @@ double getOpacity(WidgetTester tester, Finder finder) {
).opacity.value; ).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() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final MockClipboard mockClipboard = MockClipboard(); final MockClipboard mockClipboard = MockClipboard();
...@@ -474,6 +486,47 @@ void main() { ...@@ -474,6 +486,47 @@ void main() {
); );
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); }, 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 { testWidgets('text field selection toolbar renders correctly inside opacity', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
...@@ -1071,11 +1124,9 @@ void main() { ...@@ -1071,11 +1124,9 @@ void main() {
)); ));
expect(find.text('Paste'), findsNothing); expect(find.text('Paste'), findsNothing);
final Offset emptyPos = textOffsetToPosition(tester, 0); final Offset emptyPos = textOffsetToPosition(tester, 0);
await tester.longPressAt(emptyPos, pointer: 7); await tester.longPressAt(emptyPos, pointer: 7);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.text('Paste'), findsOneWidget); expect(find.text('Paste'), findsOneWidget);
}); });
......
...@@ -18,6 +18,9 @@ class FakeEditableTextState with TextSelectionDelegate { ...@@ -18,6 +18,9 @@ class FakeEditableTextState with TextSelectionDelegate {
@override @override
TextEditingValue textEditingValue = TextEditingValue.empty; TextEditingValue textEditingValue = TextEditingValue.empty;
@override
void userUpdateTextEditingValue(TextEditingValue value, SelectionChangedCause cause) { }
@override @override
void hideToolbar() { } void hideToolbar() { }
......
...@@ -48,6 +48,7 @@ void main() { ...@@ -48,6 +48,7 @@ void main() {
}); });
testWidgets('cursor layout has correct width', (WidgetTester tester) async { testWidgets('cursor layout has correct width', (WidgetTester tester) async {
EditableText.debugDeterministicCursor = true;
final GlobalKey<EditableTextState> editableTextKey = GlobalKey<EditableTextState>(); final GlobalKey<EditableTextState> editableTextKey = GlobalKey<EditableTextState>();
late String changedValue; late String changedValue;
...@@ -87,8 +88,7 @@ void main() { ...@@ -87,8 +88,7 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
await tester.tap(find.text('Paste')); await tester.tap(find.text('Paste'));
// Wait for cursor to appear. await tester.pump();
await tester.pump(const Duration(milliseconds: 600));
expect(changedValue, clipboardContent); expect(changedValue, clipboardContent);
...@@ -96,6 +96,7 @@ void main() { ...@@ -96,6 +96,7 @@ void main() {
find.byKey(const ValueKey<int>(1)), find.byKey(const ValueKey<int>(1)),
matchesGoldenFile('editable_text_test.0.png'), matchesGoldenFile('editable_text_test.0.png'),
); );
EditableText.debugDeterministicCursor = false;
}); });
testWidgets('cursor layout has correct radius', (WidgetTester tester) async { testWidgets('cursor layout has correct radius', (WidgetTester tester) async {
...@@ -787,6 +788,7 @@ void main() { ...@@ -787,6 +788,7 @@ void main() {
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
testWidgets('cursor layout', (WidgetTester tester) async { testWidgets('cursor layout', (WidgetTester tester) async {
EditableText.debugDeterministicCursor = true;
final GlobalKey<EditableTextState> editableTextKey = GlobalKey<EditableTextState>(); final GlobalKey<EditableTextState> editableTextKey = GlobalKey<EditableTextState>();
late String changedValue; late String changedValue;
...@@ -831,8 +833,7 @@ void main() { ...@@ -831,8 +833,7 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
await tester.tap(find.text('Paste')); await tester.tap(find.text('Paste'));
// Wait for cursor to appear. await tester.pump();
await tester.pump(const Duration(milliseconds: 600));
expect(changedValue, clipboardContent); expect(changedValue, clipboardContent);
...@@ -840,9 +841,11 @@ void main() { ...@@ -840,9 +841,11 @@ void main() {
find.byKey(const ValueKey<int>(1)), find.byKey(const ValueKey<int>(1)),
matchesGoldenFile('editable_text_test.2.png'), matchesGoldenFile('editable_text_test.2.png'),
); );
EditableText.debugDeterministicCursor = false;
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
testWidgets('cursor layout has correct height', (WidgetTester tester) async { testWidgets('cursor layout has correct height', (WidgetTester tester) async {
EditableText.debugDeterministicCursor = true;
final GlobalKey<EditableTextState> editableTextKey = GlobalKey<EditableTextState>(); final GlobalKey<EditableTextState> editableTextKey = GlobalKey<EditableTextState>();
late String changedValue; late String changedValue;
...@@ -888,8 +891,7 @@ void main() { ...@@ -888,8 +891,7 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
await tester.tap(find.text('Paste')); await tester.tap(find.text('Paste'));
// Wait for cursor to appear. await tester.pump();
await tester.pump(const Duration(milliseconds: 600));
expect(changedValue, clipboardContent); expect(changedValue, clipboardContent);
...@@ -897,5 +899,6 @@ void main() { ...@@ -897,5 +899,6 @@ void main() {
find.byKey(const ValueKey<int>(1)), find.byKey(const ValueKey<int>(1)),
matchesGoldenFile('editable_text_test.3.png'), matchesGoldenFile('editable_text_test.3.png'),
); );
EditableText.debugDeterministicCursor = false;
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
} }
...@@ -5199,7 +5199,7 @@ void main() { ...@@ -5199,7 +5199,7 @@ void main() {
tester.testTextInput.log.clear(); tester.testTextInput.log.clear();
final EditableTextState state = tester.state<EditableTextState>(find.byWidget(editableText)); 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. // Apply in order: length formatter -> listener -> onChanged -> listener.
expect(controller.text, 'remote listener onChanged listener'); expect(controller.text, 'remote listener onChanged listener');
...@@ -5355,6 +5355,7 @@ void main() { ...@@ -5355,6 +5355,7 @@ void main() {
'TextInput.setEditingState', 'TextInput.setEditingState',
'TextInput.setEditingState', 'TextInput.setEditingState',
'TextInput.show', 'TextInput.show',
'TextInput.show',
]; ];
expect(tester.testTextInput.log.length, logOrder.length); expect(tester.testTextInput.log.length, logOrder.length);
int index = 0; int index = 0;
...@@ -5469,16 +5470,18 @@ void main() { ...@@ -5469,16 +5470,18 @@ void main() {
log.clear(); log.clear();
final EditableTextState state = tester.firstState(find.byType(EditableText)); final EditableTextState state = tester.firstState(find.byType(EditableText));
// setEditingState is not called when only the remote changes // setEditingState is not called when only the remote changes
state.updateEditingValue(const TextEditingValue( state.updateEditingValue(TextEditingValue(
text: 'a', text: 'a',
selection: controller.selection,
)); ));
expect(log.length, 0); expect(log.length, 0);
// setEditingState is called when remote value modified by the formatter. // 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.', text: 'I will be modified by the formatter.',
selection: controller.selection,
)); ));
expect(log.length, 1); expect(log.length, 1);
MethodCall methodCall = log[0]; MethodCall methodCall = log[0];
...@@ -5592,8 +5595,9 @@ void main() { ...@@ -5592,8 +5595,9 @@ void main() {
final EditableTextState state = tester.firstState(find.byType(EditableText)); final EditableTextState state = tester.firstState(find.byType(EditableText));
// setEditingState is called when remote value modified by the formatter. // 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.', text: 'I will be modified by the formatter.',
selection: controller.selection,
)); ));
expect(log.length, 1); expect(log.length, 1);
expect(log, contains(matchesMethodCall( expect(log, contains(matchesMethodCall(
...@@ -5665,8 +5669,9 @@ void main() { ...@@ -5665,8 +5669,9 @@ void main() {
final EditableTextState state = tester.firstState(find.byType(EditableText)); final EditableTextState state = tester.firstState(find.byType(EditableText));
state.updateEditingValue(const TextEditingValue( state.updateEditingValue(TextEditingValue(
text: 'a', text: 'a',
selection: controller.selection,
)); ));
await tester.pump(); await tester.pump();
...@@ -5689,8 +5694,9 @@ void main() { ...@@ -5689,8 +5694,9 @@ void main() {
log.clear(); log.clear();
// Send repeat value from the engine. // Send repeat value from the engine.
state.updateEditingValue(const TextEditingValue( state.updateEditingValue(TextEditingValue(
text: 'a', text: 'a',
selection: controller.selection,
)); ));
await tester.pump(); await tester.pump();
...@@ -5784,8 +5790,9 @@ void main() { ...@@ -5784,8 +5790,9 @@ void main() {
final EditableTextState state = tester.firstState(find.byType(EditableText)); final EditableTextState state = tester.firstState(find.byType(EditableText));
state.updateEditingValue(const TextEditingValue( state.updateEditingValue(TextEditingValue(
text: 'a', text: 'a',
selection: controller.selection,
)); ));
await tester.pump(); await tester.pump();
...@@ -6579,6 +6586,7 @@ void main() { ...@@ -6579,6 +6586,7 @@ void main() {
final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText)); final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
state.updateEditingValue(const TextEditingValue( state.updateEditingValue(const TextEditingValue(
text: 'foo composing bar', text: 'foo composing bar',
selection: TextSelection.collapsed(offset: 4),
composing: TextRange(start: 4, end: 12), composing: TextRange(start: 4, end: 12),
)); ));
controller.selection = const TextSelection.collapsed(offset: 2); controller.selection = const TextSelection.collapsed(offset: 2);
...@@ -6587,6 +6595,7 @@ void main() { ...@@ -6587,6 +6595,7 @@ void main() {
// Reset the composing range. // Reset the composing range.
state.updateEditingValue(const TextEditingValue( state.updateEditingValue(const TextEditingValue(
text: 'foo composing bar', text: 'foo composing bar',
selection: TextSelection.collapsed(offset: 4),
composing: TextRange(start: 4, end: 12), composing: TextRange(start: 4, end: 12),
)); ));
expect(state.currentTextEditingValue.composing, const TextRange(start: 4, end: 12)); expect(state.currentTextEditingValue.composing, const TextRange(start: 4, end: 12));
...@@ -6594,13 +6603,14 @@ void main() { ...@@ -6594,13 +6603,14 @@ void main() {
// Positioning cursor after the composing range should clear the composing range. // Positioning cursor after the composing range should clear the composing range.
state.updateEditingValue(const TextEditingValue( state.updateEditingValue(const TextEditingValue(
text: 'foo composing bar', text: 'foo composing bar',
selection: TextSelection.collapsed(offset: 4),
composing: TextRange(start: 4, end: 12), composing: TextRange(start: 4, end: 12),
)); ));
controller.selection = const TextSelection.collapsed(offset: 14); controller.selection = const TextSelection.collapsed(offset: 14);
expect(state.currentTextEditingValue.composing, TextRange.empty); 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( final Widget widget = MaterialApp(
home: EditableText( home: EditableText(
backgroundCursorColor: Colors.grey, backgroundCursorColor: Colors.grey,
...@@ -6617,6 +6627,7 @@ void main() { ...@@ -6617,6 +6627,7 @@ void main() {
final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText)); final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
state.updateEditingValue(const TextEditingValue( state.updateEditingValue(const TextEditingValue(
text: 'foo composing bar', text: 'foo composing bar',
selection: TextSelection.collapsed(offset: 4),
composing: TextRange(start: 4, end: 12), composing: TextRange(start: 4, end: 12),
)); ));
controller.selection = const TextSelection(baseOffset: 1, extentOffset: 2); controller.selection = const TextSelection(baseOffset: 1, extentOffset: 2);
...@@ -6625,6 +6636,7 @@ void main() { ...@@ -6625,6 +6636,7 @@ void main() {
// Reset the composing range. // Reset the composing range.
state.updateEditingValue(const TextEditingValue( state.updateEditingValue(const TextEditingValue(
text: 'foo composing bar', text: 'foo composing bar',
selection: TextSelection.collapsed(offset: 4),
composing: TextRange(start: 4, end: 12), composing: TextRange(start: 4, end: 12),
)); ));
expect(state.currentTextEditingValue.composing, const TextRange(start: 4, end: 12)); expect(state.currentTextEditingValue.composing, const TextRange(start: 4, end: 12));
...@@ -6632,6 +6644,7 @@ void main() { ...@@ -6632,6 +6644,7 @@ void main() {
// Setting a selection within the composing range clears the composing range. // Setting a selection within the composing range clears the composing range.
state.updateEditingValue(const TextEditingValue( state.updateEditingValue(const TextEditingValue(
text: 'foo composing bar', text: 'foo composing bar',
selection: TextSelection.collapsed(offset: 4),
composing: TextRange(start: 4, end: 12), composing: TextRange(start: 4, end: 12),
)); ));
controller.selection = const TextSelection(baseOffset: 5, extentOffset: 7); controller.selection = const TextSelection(baseOffset: 5, extentOffset: 7);
...@@ -6640,6 +6653,7 @@ void main() { ...@@ -6640,6 +6653,7 @@ void main() {
// Reset the composing range. // Reset the composing range.
state.updateEditingValue(const TextEditingValue( state.updateEditingValue(const TextEditingValue(
text: 'foo composing bar', text: 'foo composing bar',
selection: TextSelection.collapsed(offset: 4),
composing: TextRange(start: 4, end: 12), composing: TextRange(start: 4, end: 12),
)); ));
expect(state.currentTextEditingValue.composing, const TextRange(start: 4, end: 12)); expect(state.currentTextEditingValue.composing, const TextRange(start: 4, end: 12));
...@@ -6647,6 +6661,7 @@ void main() { ...@@ -6647,6 +6661,7 @@ void main() {
// Setting a selection after the composing range clears the composing range. // Setting a selection after the composing range clears the composing range.
state.updateEditingValue(const TextEditingValue( state.updateEditingValue(const TextEditingValue(
text: 'foo composing bar', text: 'foo composing bar',
selection: TextSelection.collapsed(offset: 4),
composing: TextRange(start: 4, end: 12), composing: TextRange(start: 4, end: 12),
)); ));
controller.selection = const TextSelection(baseOffset: 13, extentOffset: 15); controller.selection = const TextSelection(baseOffset: 13, extentOffset: 15);
......
...@@ -797,6 +797,7 @@ class FakeRenderEditable extends RenderEditable { ...@@ -797,6 +797,7 @@ class FakeRenderEditable extends RenderEditable {
), ),
startHandleLayerLink: LayerLink(), startHandleLayerLink: LayerLink(),
endHandleLayerLink: LayerLink(), endHandleLayerLink: LayerLink(),
ignorePointer: true,
textAlign: TextAlign.start, textAlign: TextAlign.start,
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
locale: const Locale('en', 'US'), locale: const Locale('en', 'US'),
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment