Unverified Commit b3d12ebd authored by LongCatIsLooong's avatar LongCatIsLooong Committed by GitHub

Revert "[EditableText] call `onSelectionChanged` only when there're actual...

Revert "[EditableText] call `onSelectionChanged` only when there're actual selection/cause changes (#87971)" (#88183)
parent 72a1e65a
...@@ -593,6 +593,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin, ...@@ -593,6 +593,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
bool _wasSelectingVerticallyWithKeyboard = false; bool _wasSelectingVerticallyWithKeyboard = false;
void _setTextEditingValue(TextEditingValue newValue, SelectionChangedCause cause) { void _setTextEditingValue(TextEditingValue newValue, SelectionChangedCause cause) {
textSelectionDelegate.textEditingValue = newValue;
textSelectionDelegate.userUpdateTextEditingValue(newValue, cause); textSelectionDelegate.userUpdateTextEditingValue(newValue, cause);
} }
...@@ -612,11 +613,11 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin, ...@@ -612,11 +613,11 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
extentOffset: math.min(nextSelection.extentOffset, textLength), extentOffset: math.min(nextSelection.extentOffset, textLength),
); );
} }
_handleSelectionChange(nextSelection, cause);
_setTextEditingValue( _setTextEditingValue(
textSelectionDelegate.textEditingValue.copyWith(selection: nextSelection), textSelectionDelegate.textEditingValue.copyWith(selection: nextSelection),
cause, cause,
); );
_handleSelectionChange(nextSelection, cause);
} }
void _handleSelectionChange( void _handleSelectionChange(
......
...@@ -1079,9 +1079,6 @@ class EditableText extends StatefulWidget { ...@@ -1079,9 +1079,6 @@ class EditableText extends StatefulWidget {
/// {@template flutter.widgets.editableText.onSelectionChanged} /// {@template flutter.widgets.editableText.onSelectionChanged}
/// Called when the user changes the selection of text (including the cursor /// Called when the user changes the selection of text (including the cursor
/// location). /// location).
///
/// This callback is only called when the selected text is changed, or the
/// same range of text is selected via a different [SelectionChangedCause].
/// {@endtemplate} /// {@endtemplate}
final SelectionChangedCallback? onSelectionChanged; final SelectionChangedCallback? onSelectionChanged;
...@@ -1538,18 +1535,9 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -1538,18 +1535,9 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
TextInputConnection? _textInputConnection; TextInputConnection? _textInputConnection;
TextSelectionOverlay? _selectionOverlay; TextSelectionOverlay? _selectionOverlay;
// The source of the most recent selection change.
//
// Changing the selection programmatically does not update
// _selectionChangedCause.
SelectionChangedCause? _selectionChangedCause;
ScrollController? _scrollController; ScrollController? _scrollController;
late final AnimationController _cursorBlinkOpacityController = AnimationController( late AnimationController _cursorBlinkOpacityController;
vsync: this,
duration: _fadeDuration,
)..addListener(_onCursorColorTick);
final LayerLink _toolbarLayerLink = LayerLink(); final LayerLink _toolbarLayerLink = LayerLink();
final LayerLink _startHandleLayerLink = LayerLink(); final LayerLink _startHandleLayerLink = LayerLink();
...@@ -1624,6 +1612,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -1624,6 +1612,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
widget.focusNode.addListener(_handleFocusChanged); widget.focusNode.addListener(_handleFocusChanged);
_scrollController = widget.scrollController ?? ScrollController(); _scrollController = widget.scrollController ?? ScrollController();
_scrollController!.addListener(() { _selectionOverlay?.updateForScroll(); }); _scrollController!.addListener(() { _selectionOverlay?.updateForScroll(); });
_cursorBlinkOpacityController = AnimationController(vsync: this, duration: _fadeDuration);
_cursorBlinkOpacityController.addListener(_onCursorColorTick);
_floatingCursorResetController = AnimationController(vsync: this); _floatingCursorResetController = AnimationController(vsync: this);
_floatingCursorResetController.addListener(_onFloatingCursorResetTick); _floatingCursorResetController.addListener(_onFloatingCursorResetTick);
_cursorVisibilityNotifier.value = widget.showCursor; _cursorVisibilityNotifier.value = widget.showCursor;
...@@ -1760,22 +1750,17 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -1760,22 +1750,17 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
} }
_lastKnownRemoteTextEditingValue = value; _lastKnownRemoteTextEditingValue = value;
final bool shouldShowCaret = widget.readOnly if (value == _value) {
? _value.selection != value.selection // This is possible, for example, when the numeric keyboard is input,
: _value != value; // the engine will notify twice for the same value.
if (shouldShowCaret) { // Track at https://github.com/flutter/flutter/issues/65811
_scheduleShowCaretOnScreen(); return;
} }
// Wherever the value is changed by the user, schedule a showCaretOnScreen if (value.text == _value.text && value.composing == _value.composing) {
// to make sure the user can see the changes they just made. Programmatical // `selection` is the only change.
// changes to `textEditingValue` do not trigger the behavior even if the _handleSelectionChanged(value.selection, SelectionChangedCause.keyboard);
// text field is focused. } else {
_scheduleShowCaretOnScreen();
// Apply the input formatters.
value = _formatUserInput(value);
if (value.text != _value.text || value.composing != _value.composing) {
hideToolbar(); hideToolbar();
_currentPromptRectRange = null; _currentPromptRectRange = null;
...@@ -1785,9 +1770,21 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -1785,9 +1770,21 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
_obscureLatestCharIndex = _value.selection.baseOffset; _obscureLatestCharIndex = _value.selection.baseOffset;
} }
} }
_formatAndSetValue(value, SelectionChangedCause.keyboard);
} }
_updateEditingValueForUserInteraction(value, SelectionChangedCause.keyboard); // Wherever the value is changed by the user, schedule a showCaretOnScreen
// to make sure the user can see the changes they just made. Programmatical
// changes to `textEditingValue` do not trigger the behavior even if the
// text field is focused.
_scheduleShowCaretOnScreen();
if (_hasInputConnection) {
// To keep the cursor from blinking while typing, we want to restart the
// cursor timer every time a new character is typed.
_stopCursorTimer(resetCharTicks: false);
_startCursorTimer();
}
} }
@override @override
...@@ -1885,13 +1882,9 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -1885,13 +1882,9 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
final Offset finalPosition = renderEditable.getLocalRectForCaret(_lastTextPosition!).centerLeft - _floatingCursorOffset; final Offset finalPosition = renderEditable.getLocalRectForCaret(_lastTextPosition!).centerLeft - _floatingCursorOffset;
if (_floatingCursorResetController.isCompleted) { if (_floatingCursorResetController.isCompleted) {
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.
final TextEditingValue newValue = _value.copyWith( _handleSelectionChanged(TextSelection.collapsed(offset: _lastTextPosition!.offset), SelectionChangedCause.forcePress);
selection: TextSelection.fromPosition(_lastTextPosition!),
);
_updateEditingValueForUserInteraction(newValue, SelectionChangedCause.forcePress);
}
_startCaretRect = null; _startCaretRect = null;
_lastTextPosition = null; _lastTextPosition = null;
_pointOffsetOrigin = null; _pointOffsetOrigin = null;
...@@ -2159,30 +2152,63 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -2159,30 +2152,63 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
} }
} }
void _updateSelectionOverlayForNewEditingValue(TextEditingValue newEditingValue) { @pragma('vm:notify-debugger-on-exception')
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.
if (!widget.controller.isSelectionWithinTextBounds(selection))
return;
widget.controller.selection = selection;
// This will show the keyboard for all selection changes on the
// EditableWidget, not just changes triggered by user gestures.
requestKeyboard();
if (widget.selectionControls == null) { if (widget.selectionControls == null) {
_selectionOverlay?.dispose(); _selectionOverlay?.dispose();
_selectionOverlay = null; _selectionOverlay = null;
return; } 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();
}
// 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'),
));
} }
_selectionOverlay?.update(newEditingValue); // To keep the cursor from blinking while it moves, restart the timer here.
_selectionOverlay ??= TextSelectionOverlay( if (_cursorTimer != null) {
clipboardStatus: _clipboardStatus, _stopCursorTimer(resetCharTicks: false);
context: context, _startCursorTimer();
value: newEditingValue, }
debugRequiredFor: widget,
toolbarLayerLink: _toolbarLayerLink,
startHandleLayerLink: _startHandleLayerLink,
endHandleLayerLink: _endHandleLayerLink,
renderObject: renderEditable,
selectionControls: widget.selectionControls,
selectionDelegate: this,
dragStartBehavior: widget.dragStartBehavior,
onSelectionHandleTapped: widget.onSelectionHandleTapped,
);
_selectionOverlay?.handlesVisible = widget.showSelectionHandles;
_selectionOverlay?.showHandles();
} }
Rect? _currentCaretRect; Rect? _currentCaretRect;
...@@ -2266,7 +2292,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -2266,7 +2292,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
} }
@pragma('vm:notify-debugger-on-exception') @pragma('vm:notify-debugger-on-exception')
TextEditingValue _formatUserInput(TextEditingValue newValue) { void _formatAndSetValue(TextEditingValue value, SelectionChangedCause? cause, {bool userInteraction = false}) {
// Only apply input formatters if the text has changed (including uncommitted // Only apply input formatters if the text has changed (including uncommitted
// 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.
...@@ -2275,41 +2301,32 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -2275,41 +2301,32 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
// current composing region) is very infinite-loop-prone: the formatters // current composing region) is very infinite-loop-prone: the formatters
// will keep trying to modify the composing region while Gboard will keep // will keep trying to modify the composing region while Gboard will keep
// trying to restore the original composing region. // trying to restore the original composing region.
final bool needsFormatting = _value.text != newValue.text final bool textChanged = _value.text != value.text
|| (!_value.composing.isCollapsed && newValue.composing.isCollapsed); || (!_value.composing.isCollapsed && value.composing.isCollapsed);
final bool selectionChanged = _value.selection != value.selection;
return needsFormatting
? widget.inputFormatters?.fold<TextEditingValue>( if (textChanged) {
newValue, value = widget.inputFormatters?.fold<TextEditingValue>(
(TextEditingValue newValue, TextInputFormatter formatter) => formatter.formatEditUpdate(_value, newValue), value,
) ?? newValue (TextEditingValue newValue, TextInputFormatter formatter) => formatter.formatEditUpdate(_value, newValue),
: newValue; ) ?? value;
} }
// Update the TextEditingValue in the controller in response to user
// interactions (via hardware/software keyboards and gesture events).
//
// This method should not be called for programmatical changes made by
// directly modifying the TextEditingValue in the controller.
//
// Do not call request keyboard in this method: this method can be called
// when the text field does not have focus and should not request focus (for
// instance, during autofill).
@pragma('vm:notify-debugger-on-exception')
void _updateEditingValueForUserInteraction(TextEditingValue value, SelectionChangedCause? cause) {
final TextEditingValue previousValue = _value;
// Put all optional user callback invocations in a batch edit to prevent // Put all optional user callback invocations in a batch edit to prevent
// sending multiple `TextInput.updateEditingValue` messages. // sending multiple `TextInput.updateEditingValue` messages.
beginBatchEdit(); beginBatchEdit();
// Set the value before we invoke the onChanged callbacks.
// This is going to notify the listeners, which may potentially further
// modify the text editing value.
_value = value; _value = value;
// Changes made by the keyboard can sometimes be "out of band" for listening
// Call the onChanged callback first in case it changes the selection. // components, so always send those events, even if we didn't think it
if (_value.text != previousValue.text) { // 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 { try {
widget.onChanged?.call(_value.text); widget.onChanged?.call(_value.text);
} catch (exception, stack) { } catch (exception, stack) {
...@@ -2322,30 +2339,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -2322,30 +2339,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
} }
} }
final bool selectionChanged = _value.selection != previousValue.selection;
if (selectionChanged || cause != _selectionChangedCause) {
try {
widget.onSelectionChanged?.call(_value.selection, cause);
_selectionChangedCause = cause;
} catch (exception, stack) {
FlutterError.reportError(FlutterErrorDetails(
exception: exception,
stack: stack,
library: 'widgets',
context: ErrorDescription('while calling onSelectionChanged for $cause'),
));
}
// TODO(LongCatIsLoong): find a better place to populate the selection
// overlay. See: https://github.com/flutter/flutter/issues/87963.
_updateSelectionOverlayForNewEditingValue(_value);
// To keep the cursor from blinking while it moves, restart the timer here.
if (_cursorTimer != null) {
_stopCursorTimer(resetCharTicks: false);
_startCursorTimer();
}
}
endBatchEdit(); endBatchEdit();
} }
...@@ -2459,10 +2452,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -2459,10 +2452,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
} }
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.
final TextEditingValue valueWithValidSelection = _value.copyWith( _handleSelectionChanged(TextSelection.collapsed(offset: _value.text.length), null);
selection: TextSelection.collapsed(offset: _value.text.length),
);
_updateEditingValueForUserInteraction(valueWithValidSelection, null);
} }
} else { } else {
WidgetsBinding.instance!.removeObserver(this); WidgetsBinding.instance!.removeObserver(this);
...@@ -2493,6 +2483,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -2493,6 +2483,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
Rect? composingRect = renderEditable.getRectForComposingRange(composingRange); Rect? composingRect = renderEditable.getRectForComposingRange(composingRange);
// Send the caret location instead if there's no marked text yet. // Send the caret location instead if there's no marked text yet.
if (composingRect == null) { if (composingRect == null) {
assert(!composingRange.isValid || composingRange.isCollapsed);
final int offset = composingRange.isValid ? composingRange.start : 0; final int offset = composingRange.isValid ? composingRange.start : 0;
composingRect = renderEditable.getLocalRectForCaret(TextPosition(offset: offset)); composingRect = renderEditable.getLocalRectForCaret(TextPosition(offset: offset));
} }
...@@ -2534,11 +2525,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -2534,11 +2525,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
double get _devicePixelRatio => MediaQuery.of(context).devicePixelRatio; double get _devicePixelRatio => MediaQuery.of(context).devicePixelRatio;
// This method is similar to updateEditingValue, but is used to handle user
// input caused by hardware keyboard events and gesture events, while
// updateEditingValue handles IME/software keyboard input.
@override @override
void userUpdateTextEditingValue(TextEditingValue value, SelectionChangedCause cause) { void userUpdateTextEditingValue(TextEditingValue value, SelectionChangedCause? cause) {
// Compare the current TextEditingValue with the pre-format new // Compare the current TextEditingValue with the pre-format new
// TextEditingValue value, in case the formatter would reject the change. // TextEditingValue value, in case the formatter would reject the change.
final bool shouldShowCaret = widget.readOnly final bool shouldShowCaret = widget.readOnly
...@@ -2547,24 +2535,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -2547,24 +2535,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
if (shouldShowCaret) { if (shouldShowCaret) {
_scheduleShowCaretOnScreen(); _scheduleShowCaretOnScreen();
} }
_formatAndSetValue(value, cause, userInteraction: true);
final TextEditingValue formattedValue = _formatUserInput(value);
if (value.selection != _value.selection) {
requestKeyboard();
}
if (value.text != _value.text || value.composing != _value.composing) {
hideToolbar();
_currentPromptRectRange = null;
if (_hasInputConnection) {
if (widget.obscureText && value.text.length == _value.text.length + 1) {
_obscureShowCharTicksPending = _kObscureShowLatestCharCursorTicks;
_obscureLatestCharIndex = _value.selection.baseOffset;
}
}
}
_updateEditingValueForUserInteraction(formattedValue, cause);
} }
@override @override
......
...@@ -1226,7 +1226,7 @@ class TextSelectionGestureDetectorBuilder { ...@@ -1226,7 +1226,7 @@ class TextSelectionGestureDetectorBuilder {
@protected @protected
void onDoubleTapDown(TapDownDetails details) { void onDoubleTapDown(TapDownDetails details) {
if (delegate.selectionEnabled) { if (delegate.selectionEnabled) {
renderEditable.selectWord(cause: SelectionChangedCause.doubleTap); renderEditable.selectWord(cause: SelectionChangedCause.tap);
if (shouldShowSelectionToolbar) if (shouldShowSelectionToolbar)
editableText.showToolbar(); editableText.showToolbar();
} }
......
...@@ -2381,7 +2381,6 @@ void main() { ...@@ -2381,7 +2381,6 @@ void main() {
await gesture.moveBy(const Offset(600, 0)); await gesture.moveBy(const Offset(600, 0));
// To the edge of the screen basically. // To the edge of the screen basically.
await tester.pump(); await tester.pump();
await tester.pumpAndSettle();
expect( expect(
controller.selection, controller.selection,
const TextSelection.collapsed(offset: 54, affinity: TextAffinity.upstream), const TextSelection.collapsed(offset: 54, affinity: TextAffinity.upstream),
...@@ -2389,14 +2388,12 @@ void main() { ...@@ -2389,14 +2388,12 @@ void main() {
// Keep moving out. // Keep moving out.
await gesture.moveBy(const Offset(1, 0)); await gesture.moveBy(const Offset(1, 0));
await tester.pump(); await tester.pump();
await tester.pumpAndSettle();
expect( expect(
controller.selection, controller.selection,
const TextSelection.collapsed(offset: 61, affinity: TextAffinity.upstream), const TextSelection.collapsed(offset: 61, affinity: TextAffinity.upstream),
); );
await gesture.moveBy(const Offset(1, 0)); await gesture.moveBy(const Offset(1, 0));
await tester.pump(); await tester.pump();
await tester.pumpAndSettle();
expect( expect(
controller.selection, controller.selection,
const TextSelection.collapsed(offset: 66, affinity: TextAffinity.upstream), const TextSelection.collapsed(offset: 66, affinity: TextAffinity.upstream),
......
...@@ -3871,7 +3871,7 @@ void main() { ...@@ -3871,7 +3871,7 @@ void main() {
editableTextState.textEditingValue.copyWith( editableTextState.textEditingValue.copyWith(
selection: TextSelection.collapsed(offset: longText.length), selection: TextSelection.collapsed(offset: longText.length),
), ),
SelectionChangedCause.tap, null,
); );
await tester.pump(); // TODO(ianh): Figure out why this extra pump is needed. await tester.pump(); // TODO(ianh): Figure out why this extra pump is needed.
...@@ -3908,7 +3908,7 @@ void main() { ...@@ -3908,7 +3908,7 @@ void main() {
editableTextState.textEditingValue.copyWith( editableTextState.textEditingValue.copyWith(
selection: const TextSelection.collapsed(offset: tallText.length), selection: const TextSelection.collapsed(offset: tallText.length),
), ),
SelectionChangedCause.tap, null,
); );
await tester.pump(); await tester.pump();
await skipPastScrollingAnimation(tester); await skipPastScrollingAnimation(tester);
...@@ -7709,7 +7709,6 @@ void main() { ...@@ -7709,7 +7709,6 @@ void main() {
await gesture.moveBy(const Offset(600, 0)); await gesture.moveBy(const Offset(600, 0));
// To the edge of the screen basically. // To the edge of the screen basically.
await tester.pump(); await tester.pump();
await tester.pumpAndSettle();
expect( expect(
controller.selection, controller.selection,
const TextSelection.collapsed(offset: 56, affinity: TextAffinity.downstream), const TextSelection.collapsed(offset: 56, affinity: TextAffinity.downstream),
...@@ -7717,14 +7716,12 @@ void main() { ...@@ -7717,14 +7716,12 @@ void main() {
// Keep moving out. // Keep moving out.
await gesture.moveBy(const Offset(1, 0)); await gesture.moveBy(const Offset(1, 0));
await tester.pump(); await tester.pump();
await tester.pumpAndSettle();
expect( expect(
controller.selection, controller.selection,
const TextSelection.collapsed(offset: 62, affinity: TextAffinity.downstream), const TextSelection.collapsed(offset: 62, affinity: TextAffinity.downstream),
); );
await gesture.moveBy(const Offset(1, 0)); await gesture.moveBy(const Offset(1, 0));
await tester.pump(); await tester.pump();
await tester.pumpAndSettle();
expect( expect(
controller.selection, controller.selection,
const TextSelection.collapsed(offset: 66, affinity: TextAffinity.upstream), const TextSelection.collapsed(offset: 66, affinity: TextAffinity.upstream),
......
...@@ -34,9 +34,7 @@ class FakeEditableTextState with TextSelectionDelegate { ...@@ -34,9 +34,7 @@ class FakeEditableTextState with TextSelectionDelegate {
void hideToolbar([bool hideHandles = true]) { } void hideToolbar([bool hideHandles = true]) { }
@override @override
void userUpdateTextEditingValue(TextEditingValue value, SelectionChangedCause cause) { void userUpdateTextEditingValue(TextEditingValue value, SelectionChangedCause cause) { }
textEditingValue = value;
}
@override @override
void bringIntoView(TextPosition position) { } void bringIntoView(TextPosition position) { }
......
...@@ -650,7 +650,7 @@ void main() { ...@@ -650,7 +650,7 @@ void main() {
// false. // false.
state.userUpdateTextEditingValue( state.userUpdateTextEditingValue(
state.textEditingValue.copyWith(selection: const TextSelection.collapsed(offset: 90)), state.textEditingValue.copyWith(selection: const TextSelection.collapsed(offset: 90)),
SelectionChangedCause.tap, null,
); );
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(isCaretOnScreen(tester), isTrue); expect(isCaretOnScreen(tester), isTrue);
...@@ -674,7 +674,7 @@ void main() { ...@@ -674,7 +674,7 @@ void main() {
state.userUpdateTextEditingValue( state.userUpdateTextEditingValue(
state.textEditingValue.copyWith(selection: const TextSelection.collapsed(offset: 100)), state.textEditingValue.copyWith(selection: const TextSelection.collapsed(offset: 100)),
SelectionChangedCause.tap, null,
); );
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(isCaretOnScreen(tester), isTrue); expect(isCaretOnScreen(tester), isTrue);
......
...@@ -4938,19 +4938,21 @@ void main() { ...@@ -4938,19 +4938,21 @@ void main() {
testWidgets('keyboard text selection works (RawKeyEvent)', (WidgetTester tester) async { testWidgets('keyboard text selection works (RawKeyEvent)', (WidgetTester tester) async {
debugKeyEventSimulatorTransitModeOverride = KeyDataTransitMode.rawKeyData; debugKeyEventSimulatorTransitModeOverride = KeyDataTransitMode.rawKeyData;
addTearDown(() { debugKeyEventSimulatorTransitModeOverride = null; });
await testTextEditing(tester, targetPlatform: defaultTargetPlatform); await testTextEditing(tester, targetPlatform: defaultTargetPlatform);
debugKeyEventSimulatorTransitModeOverride = null; debugKeyEventSimulatorTransitModeOverride = null;
// On web, using keyboard for selection is handled by the browser. // On web, using keyboard for selection is handled by the browser.
}, variant: TargetPlatformVariant.all(), skip: kIsWeb); // [intended] }, variant: TargetPlatformVariant.all(), skip: kIsWeb); // [intended]
testWidgets('keyboard text selection works (ui.KeyData then RawKeyEvent)', (WidgetTester tester) async { testWidgets('keyboard text selection works (ui.KeyData then RawKeyEvent)', (WidgetTester tester) async {
debugKeyEventSimulatorTransitModeOverride = KeyDataTransitMode.keyDataThenRawKeyData; debugKeyEventSimulatorTransitModeOverride = KeyDataTransitMode.keyDataThenRawKeyData;
addTearDown(() { debugKeyEventSimulatorTransitModeOverride = null; });
await testTextEditing(tester, targetPlatform: defaultTargetPlatform); await testTextEditing(tester, targetPlatform: defaultTargetPlatform);
debugKeyEventSimulatorTransitModeOverride = null; debugKeyEventSimulatorTransitModeOverride = null;
// On web, using keyboard for selection is handled by the browser. // On web, using keyboard for selection is handled by the browser.
}, variant: TargetPlatformVariant.all(), skip: kIsWeb); // [intended] }, variant: TargetPlatformVariant.all(), skip: kIsWeb); // [intended]
...@@ -6045,6 +6047,7 @@ void main() { ...@@ -6045,6 +6047,7 @@ void main() {
'TextInput.setStyle', 'TextInput.setStyle',
'TextInput.setEditingState', 'TextInput.setEditingState',
'TextInput.setEditingState', 'TextInput.setEditingState',
'TextInput.show',
'TextInput.setCaretRect', 'TextInput.setCaretRect',
]; ];
expect( expect(
...@@ -6088,14 +6091,16 @@ void main() { ...@@ -6088,14 +6091,16 @@ void main() {
'TextInput.setStyle', 'TextInput.setStyle',
'TextInput.setEditingState', 'TextInput.setEditingState',
'TextInput.setEditingState', 'TextInput.setEditingState',
'TextInput.show',
'TextInput.setCaretRect', 'TextInput.setCaretRect',
'TextInput.show',
]; ];
expect(tester.testTextInput.log.length, logOrder.length);
expect( int index = 0;
tester.testTextInput.log.map((MethodCall methodCall) => methodCall.method), for (final MethodCall m in tester.testTextInput.log) {
logOrder, expect(m.method, logOrder[index]);
); index++;
}
expect(tester.testTextInput.editingState!['text'], 'flutter is the best!'); expect(tester.testTextInput.editingState!['text'], 'flutter is the best!');
}); });
...@@ -6136,6 +6141,7 @@ void main() { ...@@ -6136,6 +6141,7 @@ void main() {
'TextInput.setStyle', 'TextInput.setStyle',
'TextInput.setEditingState', 'TextInput.setEditingState',
'TextInput.setEditingState', 'TextInput.setEditingState',
'TextInput.show',
'TextInput.setCaretRect', 'TextInput.setCaretRect',
'TextInput.setEditingState', 'TextInput.setEditingState',
]; ];
...@@ -6209,7 +6215,7 @@ void main() { ...@@ -6209,7 +6215,7 @@ void main() {
selection: controller.selection, selection: controller.selection,
)); ));
expect(log, isEmpty); 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(TextEditingValue( state.updateEditingValue(TextEditingValue(
...@@ -7454,115 +7460,6 @@ void main() { ...@@ -7454,115 +7460,6 @@ void main() {
}); });
}); });
group('onChanged callbacks are edge-triggered', () {
SelectionChangedCallback? onSelectionChanged;
ValueChanged<String>? onChanged;
final TextEditingController controller = TextEditingController();
final Widget editableText = EditableText(
showSelectionHandles: false,
controller: controller,
focusNode: FocusNode(),
cursorColor: Colors.red,
backgroundCursorColor: Colors.blue,
style: Typography.material2018(platform: TargetPlatform.android).black.subtitle1!.copyWith(fontFamily: 'Roboto'),
keyboardType: TextInputType.text,
selectionControls: materialTextSelectionControls,
onSelectionChanged: (TextSelection selection, SelectionChangedCause? cause) => onSelectionChanged?.call(selection, cause),
onChanged: (String value) => onChanged?.call(value),
);
tearDown(() {
onSelectionChanged = null;
onChanged = null;
});
testWidgets('onSelectionChanged', (WidgetTester tester) async {
TextSelection? selection;
SelectionChangedCause? cause;
onSelectionChanged = (TextSelection newSelection, SelectionChangedCause? newCause) {
selection = newSelection;
cause = newCause;
};
controller.value = const TextEditingValue(text: 'text', selection: TextSelection(baseOffset: 1, extentOffset: 2));
await tester.pumpWidget(MaterialApp(
home: editableText,
));
final EditableTextState state = tester.state(find.byWidget(editableText));
await tester.showKeyboard(find.byWidget(editableText));
await tester.pump();
// No user input.
expect(selection, isNull);
expect(cause, isNull);
// Selection didn't change but the cause did (keyboard).
state.updateEditingValue(
const TextEditingValue(text: 'test text', selection: TextSelection(baseOffset: 1, extentOffset: 2)),
);
expect(selection, const TextSelection(baseOffset: 1, extentOffset: 2));
expect(cause, SelectionChangedCause.keyboard);
// Selection and cause both changed
await tester.enterText(find.byWidget(editableText), 'test text');
expect(selection, const TextSelection.collapsed(offset: 9));
expect(cause, SelectionChangedCause.keyboard);
selection = null;
cause = null;
// Nothing changes.
state.userUpdateTextEditingValue(
const TextEditingValue(text: 'test text', selection: TextSelection.collapsed(offset: 9)),
SelectionChangedCause.keyboard
);
expect(selection, isNull);
expect(cause, isNull);
// The cause changes.
state.userUpdateTextEditingValue(
const TextEditingValue(text: 'test text', selection: TextSelection.collapsed(offset: 9)),
SelectionChangedCause.toolBar,
);
expect(selection, const TextSelection.collapsed(offset: 9));
expect(cause, SelectionChangedCause.toolBar);
});
testWidgets('onChanged', (WidgetTester tester) async {
String? newText;
onChanged = (String text) => newText = text;
controller.value = const TextEditingValue(text: 'text', selection: TextSelection(baseOffset: 1, extentOffset: 2));
await tester.pumpWidget(MaterialApp(
home: editableText,
));
final EditableTextState state = tester.state(find.byWidget(editableText));
await tester.showKeyboard(find.byWidget(editableText));
await tester.pump();
// No user input.
expect(newText, isNull);
state.updateEditingValue(
const TextEditingValue(text: 'text', selection: TextSelection(baseOffset: 1, extentOffset: 3)),
);
// Selection & cause changed but the text didn't;
expect(newText, isNull);
state.updateEditingValue(
const TextEditingValue(text: 'test text', selection: TextSelection(baseOffset: 1, extentOffset: 3)),
);
// Now the text is changed.
expect(newText, 'test text');
});
});
group('callback errors', () { group('callback errors', () {
const String errorText = 'Test EditableText callback error'; const String errorText = 'Test EditableText callback error';
......
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