Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in
Toggle navigation
F
Front-End
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
abdullh.alsoleman
Front-End
Commits
6e224ee0
Unverified
Commit
6e224ee0
authored
Aug 13, 2021
by
LongCatIsLooong
Committed by
GitHub
Aug 13, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
[EditableText] call `onSelectionChanged` only when there're actual selection/cause changes (#87971)
parent
fbc4e9bc
Changes
8
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
267 additions
and
128 deletions
+267
-128
editable.dart
packages/flutter/lib/src/rendering/editable.dart
+1
-2
editable_text.dart
packages/flutter/lib/src/widgets/editable_text.dart
+134
-105
text_selection.dart
packages/flutter/lib/src/widgets/text_selection.dart
+1
-1
text_field_test.dart
packages/flutter/test/cupertino/text_field_test.dart
+3
-0
text_field_test.dart
packages/flutter/test/material/text_field_test.dart
+5
-2
editable_test.dart
packages/flutter/test/rendering/editable_test.dart
+3
-1
editable_text_show_on_screen_test.dart
...utter/test/widgets/editable_text_show_on_screen_test.dart
+2
-2
editable_text_test.dart
packages/flutter/test/widgets/editable_text_test.dart
+118
-15
No files found.
packages/flutter/lib/src/rendering/editable.dart
View file @
6e224ee0
...
@@ -593,7 +593,6 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
...
@@ -593,7 +593,6 @@ 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
);
}
}
...
@@ -613,11 +612,11 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
...
@@ -613,11 +612,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
(
...
...
packages/flutter/lib/src/widgets/editable_text.dart
View file @
6e224ee0
...
@@ -1079,6 +1079,9 @@ class EditableText extends StatefulWidget {
...
@@ -1079,6 +1079,9 @@ 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
;
...
@@ -1535,9 +1538,18 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
...
@@ -1535,9 +1538,18 @@ 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
AnimationController
_cursorBlinkOpacityController
;
late
final
AnimationController
_cursorBlinkOpacityController
=
AnimationController
(
vsync:
this
,
duration:
_fadeDuration
,
)..
addListener
(
_onCursorColorTick
);
final
LayerLink
_toolbarLayerLink
=
LayerLink
();
final
LayerLink
_toolbarLayerLink
=
LayerLink
();
final
LayerLink
_startHandleLayerLink
=
LayerLink
();
final
LayerLink
_startHandleLayerLink
=
LayerLink
();
...
@@ -1612,8 +1624,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
...
@@ -1612,8 +1624,6 @@ 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
;
...
@@ -1750,17 +1760,22 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
...
@@ -1750,17 +1760,22 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
}
}
_lastKnownRemoteTextEditingValue
=
value
;
_lastKnownRemoteTextEditingValue
=
value
;
if
(
value
==
_value
)
{
final
bool
shouldShowCaret
=
widget
.
readOnly
// This is possible, for example, when the numeric keyboard is input,
?
_value
.
selection
!=
value
.
selection
// the engine will notify twice for the same value.
:
_value
!=
value
;
// Track at https://github.com/flutter/flutter/issues/65811
if
(
shouldShowCaret
)
{
return
;
_scheduleShowCaretOnScreen
()
;
}
}
if
(
value
.
text
==
_value
.
text
&&
value
.
composing
==
_value
.
composing
)
{
// Wherever the value is changed by the user, schedule a showCaretOnScreen
// `selection` is the only change.
// to make sure the user can see the changes they just made. Programmatical
_handleSelectionChanged
(
value
.
selection
,
SelectionChangedCause
.
keyboard
);
// changes to `textEditingValue` do not trigger the behavior even if the
}
else
{
// text field is focused.
_scheduleShowCaretOnScreen
();
// Apply the input formatters.
value
=
_formatUserInput
(
value
);
if
(
value
.
text
!=
_value
.
text
||
value
.
composing
!=
_value
.
composing
)
{
hideToolbar
();
hideToolbar
();
_currentPromptRectRange
=
null
;
_currentPromptRectRange
=
null
;
...
@@ -1770,21 +1785,9 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
...
@@ -1770,21 +1785,9 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
_obscureLatestCharIndex
=
_value
.
selection
.
baseOffset
;
_obscureLatestCharIndex
=
_value
.
selection
.
baseOffset
;
}
}
}
}
_formatAndSetValue
(
value
,
SelectionChangedCause
.
keyboard
);
}
}
// Wherever the value is changed by the user, schedule a showCaretOnScreen
_updateEditingValueForUserInteraction
(
value
,
SelectionChangedCause
.
keyboard
);
// 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
...
@@ -1882,9 +1885,13 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
...
@@ -1882,9 +1885,13 @@ 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.
_handleSelectionChanged
(
TextSelection
.
collapsed
(
offset:
_lastTextPosition
!.
offset
),
SelectionChangedCause
.
forcePress
);
final
TextEditingValue
newValue
=
_value
.
copyWith
(
selection:
TextSelection
.
fromPosition
(
_lastTextPosition
!),
);
_updateEditingValueForUserInteraction
(
newValue
,
SelectionChangedCause
.
forcePress
);
}
_startCaretRect
=
null
;
_startCaretRect
=
null
;
_lastTextPosition
=
null
;
_lastTextPosition
=
null
;
_pointOffsetOrigin
=
null
;
_pointOffsetOrigin
=
null
;
...
@@ -2152,63 +2159,30 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
...
@@ -2152,63 +2159,30 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
}
}
}
}
@pragma
(
'vm:notify-debugger-on-exception'
)
void
_updateSelectionOverlayForNewEditingValue
(
TextEditingValue
newEditingValue
)
{
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
;
}
else
{
return
;
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
'
),
));
}
}
// To keep the cursor from blinking while it moves, restart the timer here.
_selectionOverlay
?.
update
(
newEditingValue
);
if
(
_cursorTimer
!=
null
)
{
_selectionOverlay
??=
TextSelectionOverlay
(
_stopCursorTimer
(
resetCharTicks:
false
);
clipboardStatus:
_clipboardStatus
,
_startCursorTimer
();
context:
context
,
}
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
;
...
@@ -2292,7 +2266,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
...
@@ -2292,7 +2266,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
}
}
@pragma
(
'vm:notify-debugger-on-exception'
)
@pragma
(
'vm:notify-debugger-on-exception'
)
void
_formatAndSetValue
(
TextEditingValue
value
,
SelectionChangedCause
?
cause
,
{
bool
userInteraction
=
false
}
)
{
TextEditingValue
_formatUserInput
(
TextEditingValue
newValue
)
{
// 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.
...
@@ -2301,32 +2275,41 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
...
@@ -2301,32 +2275,41 @@ 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
textChanged
=
_value
.
text
!=
value
.
text
final
bool
needsFormatting
=
_value
.
text
!=
newValue
.
text
||
(!
_value
.
composing
.
isCollapsed
&&
value
.
composing
.
isCollapsed
);
||
(!
_value
.
composing
.
isCollapsed
&&
newValue
.
composing
.
isCollapsed
);
final
bool
selectionChanged
=
_value
.
selection
!=
value
.
selection
;
return
needsFormatting
if
(
textChanged
)
{
?
widget
.
inputFormatters
?.
fold
<
TextEditingValue
>(
value
=
widget
.
inputFormatters
?.
fold
<
TextEditingValue
>(
newValue
,
value
,
(
TextEditingValue
newValue
,
TextInputFormatter
formatter
)
=>
formatter
.
formatEditUpdate
(
_value
,
newValue
),
(
TextEditingValue
newValue
,
TextInputFormatter
formatter
)
=>
formatter
.
formatEditUpdate
(
_value
,
newValue
),
)
??
newValue
)
??
value
;
:
newValue
;
}
}
// 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
// components, so always send those events, even if we didn't think it
// Call the onChanged callback first in case it changes the selection.
// changed. Also, the user long pressing should always send a selection change
if
(
_value
.
text
!=
previousValue
.
text
)
{
// 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
)
{
...
@@ -2339,6 +2322,30 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
...
@@ -2339,6 +2322,30 @@ 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
();
}
}
...
@@ -2452,7 +2459,10 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
...
@@ -2452,7 +2459,10 @@ 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.
_handleSelectionChanged
(
TextSelection
.
collapsed
(
offset:
_value
.
text
.
length
),
null
);
final
TextEditingValue
valueWithValidSelection
=
_value
.
copyWith
(
selection:
TextSelection
.
collapsed
(
offset:
_value
.
text
.
length
),
);
_updateEditingValueForUserInteraction
(
valueWithValidSelection
,
null
);
}
}
}
else
{
}
else
{
WidgetsBinding
.
instance
!.
removeObserver
(
this
);
WidgetsBinding
.
instance
!.
removeObserver
(
this
);
...
@@ -2483,7 +2493,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
...
@@ -2483,7 +2493,6 @@ 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
));
}
}
...
@@ -2525,8 +2534,11 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
...
@@ -2525,8 +2534,11 @@ 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
...
@@ -2535,7 +2547,24 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
...
@@ -2535,7 +2547,24 @@ 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
...
...
packages/flutter/lib/src/widgets/text_selection.dart
View file @
6e224ee0
...
@@ -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
.
t
ap
);
renderEditable
.
selectWord
(
cause:
SelectionChangedCause
.
doubleT
ap
);
if
(
shouldShowSelectionToolbar
)
if
(
shouldShowSelectionToolbar
)
editableText
.
showToolbar
();
editableText
.
showToolbar
();
}
}
...
...
packages/flutter/test/cupertino/text_field_test.dart
View file @
6e224ee0
...
@@ -2381,6 +2381,7 @@ void main() {
...
@@ -2381,6 +2381,7 @@ 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
),
...
@@ -2388,12 +2389,14 @@ void main() {
...
@@ -2388,12 +2389,14 @@ 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
),
...
...
packages/flutter/test/material/text_field_test.dart
View file @
6e224ee0
...
@@ -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
),
),
),
null
,
SelectionChangedCause
.
tap
,
);
);
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
),
),
),
null
,
SelectionChangedCause
.
tap
,
);
);
await
tester
.
pump
();
await
tester
.
pump
();
await
skipPastScrollingAnimation
(
tester
);
await
skipPastScrollingAnimation
(
tester
);
...
@@ -7709,6 +7709,7 @@ void main() {
...
@@ -7709,6 +7709,7 @@ 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
),
...
@@ -7716,12 +7717,14 @@ void main() {
...
@@ -7716,12 +7717,14 @@ 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
),
...
...
packages/flutter/test/rendering/editable_test.dart
View file @
6e224ee0
...
@@ -34,7 +34,9 @@ class FakeEditableTextState with TextSelectionDelegate {
...
@@ -34,7 +34,9 @@ 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
)
{
}
...
...
packages/flutter/test/widgets/editable_text_show_on_screen_test.dart
View file @
6e224ee0
...
@@ -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
)),
null
,
SelectionChangedCause
.
tap
,
);
);
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
)),
null
,
SelectionChangedCause
.
tap
,
);
);
await
tester
.
pumpAndSettle
();
await
tester
.
pumpAndSettle
();
expect
(
isCaretOnScreen
(
tester
),
isTrue
);
expect
(
isCaretOnScreen
(
tester
),
isTrue
);
...
...
packages/flutter/test/widgets/editable_text_test.dart
View file @
6e224ee0
...
@@ -4938,21 +4938,19 @@ void main() {
...
@@ -4938,21 +4938,19 @@ 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]
...
@@ -6047,7 +6045,6 @@ void main() {
...
@@ -6047,7 +6045,6 @@ void main() {
'TextInput.setStyle'
,
'TextInput.setStyle'
,
'TextInput.setEditingState'
,
'TextInput.setEditingState'
,
'TextInput.setEditingState'
,
'TextInput.setEditingState'
,
'TextInput.show'
,
'TextInput.setCaretRect'
,
'TextInput.setCaretRect'
,
];
];
expect
(
expect
(
...
@@ -6091,16 +6088,14 @@ void main() {
...
@@ -6091,16 +6088,14 @@ 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
);
int
index
=
0
;
expect
(
for
(
final
MethodCall
m
in
tester
.
testTextInput
.
log
)
{
tester
.
testTextInput
.
log
.
map
((
MethodCall
methodCall
)
=>
methodCall
.
method
),
expect
(
m
.
method
,
logOrder
[
index
]);
logOrder
,
index
++
;
)
;
}
expect
(
tester
.
testTextInput
.
editingState
![
'text'
],
'flutter is the best!'
);
expect
(
tester
.
testTextInput
.
editingState
![
'text'
],
'flutter is the best!'
);
});
});
...
@@ -6141,7 +6136,6 @@ void main() {
...
@@ -6141,7 +6136,6 @@ void main() {
'TextInput.setStyle'
,
'TextInput.setStyle'
,
'TextInput.setEditingState'
,
'TextInput.setEditingState'
,
'TextInput.setEditingState'
,
'TextInput.setEditingState'
,
'TextInput.show'
,
'TextInput.setCaretRect'
,
'TextInput.setCaretRect'
,
'TextInput.setEditingState'
,
'TextInput.setEditingState'
,
];
];
...
@@ -6215,7 +6209,7 @@ void main() {
...
@@ -6215,7 +6209,7 @@ void main() {
selection:
controller
.
selection
,
selection:
controller
.
selection
,
));
));
expect
(
log
.
length
,
0
);
expect
(
log
,
isEmpty
);
// 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
(
...
@@ -7460,6 +7454,115 @@ void main() {
...
@@ -7460,6 +7454,115 @@ 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'
;
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment