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
a57f45e0
Unverified
Commit
a57f45e0
authored
Oct 13, 2020
by
Ren You
Committed by
GitHub
Oct 13, 2020
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Revert "More EditableText docs (#66864)" (#68025)
This reverts commit
daa6b2cc
.
parent
62cf4dbf
Changes
3
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
107 additions
and
431 deletions
+107
-431
text_input.dart
packages/flutter/lib/src/services/text_input.dart
+3
-69
editable_text.dart
packages/flutter/lib/src/widgets/editable_text.dart
+98
-114
editable_text_test.dart
packages/flutter/test/widgets/editable_text_test.dart
+6
-248
No files found.
packages/flutter/lib/src/services/text_input.dart
View file @
a57f45e0
...
@@ -756,17 +756,7 @@ abstract class TextSelectionDelegate {
...
@@ -756,17 +756,7 @@ 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
/// Sets the current text input (replaces the whole line).
/// text editing state with [value].
///
/// The new [value] is treated as user input and thus may subject to input
/// formatting.
///
/// See also:
///
/// * [EditableTextState.textEditingValue]: an implementation that applies
/// additional pre-processing to the specified [value], before updating the
/// text editing state.
set
textEditingValue
(
TextEditingValue
value
);
set
textEditingValue
(
TextEditingValue
value
);
/// Hides the text selection toolbar.
/// Hides the text selection toolbar.
...
@@ -794,7 +784,6 @@ abstract class TextSelectionDelegate {
...
@@ -794,7 +784,6 @@ abstract class TextSelectionDelegate {
/// See also:
/// See also:
///
///
/// * [TextInput.attach]
/// * [TextInput.attach]
/// * [EditableText], a [TextInputClient] implementation.
abstract
class
TextInputClient
{
abstract
class
TextInputClient
{
/// Abstract const constructor. This constructor enables subclasses to provide
/// Abstract const constructor. This constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions.
/// const constructors so that they can be used in const expressions.
...
@@ -816,9 +805,6 @@ abstract class TextInputClient {
...
@@ -816,9 +805,6 @@ abstract class TextInputClient {
AutofillScope
?
get
currentAutofillScope
;
AutofillScope
?
get
currentAutofillScope
;
/// Requests that this client update its editing state to the given value.
/// Requests that this client update its editing state to the given value.
///
/// The new [value] is treated as user input and thus may subject to input
/// formatting.
void
updateEditingValue
(
TextEditingValue
value
);
void
updateEditingValue
(
TextEditingValue
value
);
/// Requests that this client perform the given action.
/// Requests that this client perform the given action.
...
@@ -846,10 +832,7 @@ abstract class TextInputClient {
...
@@ -846,10 +832,7 @@ abstract class TextInputClient {
///
///
/// See also:
/// See also:
///
///
/// * [TextInput.attach], a method used to establish a [TextInputConnection]
/// * [TextInput.attach]
/// between the system's text input and a [TextInputClient].
/// * [EditableText], a [TextInputClient] that connects to and interacts with
/// the system's text input using a [TextInputConnection].
class
TextInputConnection
{
class
TextInputConnection
{
TextInputConnection
.
_
(
this
.
_client
)
TextInputConnection
.
_
(
this
.
_client
)
:
assert
(
_client
!=
null
),
:
assert
(
_client
!=
null
),
...
@@ -906,8 +889,7 @@ class TextInputConnection {
...
@@ -906,8 +889,7 @@ class TextInputConnection {
TextInput
.
_instance
.
_updateConfig
(
configuration
);
TextInput
.
_instance
.
_updateConfig
(
configuration
);
}
}
/// Requests that the text input control change its internal state to match
/// Requests that the text input control change its internal state to match the given state.
/// the given state.
void
setEditingState
(
TextEditingValue
value
)
{
void
setEditingState
(
TextEditingValue
value
)
{
assert
(
attached
);
assert
(
attached
);
TextInput
.
_instance
.
_setEditingState
(
value
);
TextInput
.
_instance
.
_setEditingState
(
value
);
...
@@ -1060,57 +1042,9 @@ RawFloatingCursorPoint _toTextPoint(FloatingCursorDragState state, Map<String, d
...
@@ -1060,57 +1042,9 @@ RawFloatingCursorPoint _toTextPoint(FloatingCursorDragState state, Map<String, d
/// An low-level interface to the system's text input control.
/// An low-level interface to the system's text input control.
///
///
/// To start interacting with the system's text input control, call [attach] to
/// establish a [TextInputConnection] between the system's text input control
/// and a [TextInputClient]. The majority of commands available for
/// interacting with the text input control reside in the returned
/// [TextInputConnection]. The communication between the system text input and
/// the [TextInputClient] is asynchronous.
///
/// The platform text input plugin (which represents the system's text input)
/// and the [TextInputClient] usually maintain their own text editing states
/// ([TextEditingValue]) separately. They must be kept in sync as long as the
/// [TextInputClient] is connected. The following methods can be used to send
/// [TextEditingValue] to update the other party, when either party's text
/// editing states change:
///
/// * The [TextInput.attach] method allows a [TextInputClient] to establish a
/// connection to the text input. An optional field in its `configuration`
/// parameter can be used to specify an initial value for the platform text
/// input plugin's [TextEditingValue].
///
/// * The [TextInputClient] sends its [TextEditingValue] to the platform text
/// input plugin using [TextInputConnection.setEditingState].
///
/// * The platform text input plugin sends its [TextEditingValue] to the
/// connected [TextInputClient] via a "TextInput.setEditingState" message.
///
/// * When autofill happens on a disconnected [TextInputClient], the platform
/// text input plugin sends the [TextEditingValue] to the connected
/// [TextInputClient]'s [AutofillScope], and the [AutofillScope] will further
/// relay the value to the correct [TextInputClient].
///
/// When synchronizing the [TextEditingValue]s, the communication may get stuck
/// in an infinite when both parties are trying to send their own update. To
/// mitigate the problem, only [TextInputClient]s are allowed to alter the
/// received [TextEditingValue]s while platform text input plugins are to accept
/// the received [TextEditingValue]s unmodified. More specifically:
///
/// * When a [TextInputClient] receives a new [TextEditingValue] from the
/// platform text input plugin, it's allowed to modify the value (for example,
/// apply [TextInputFormatter]s). If it decides to do so, it must send the
/// updated [TextEditingValue] back to the platform text input plugin to keep
/// the [TextEditingValue]s in sync.
///
/// * When the platform text input plugin receives a new value from the
/// connected [TextInputClient], it must accept the new value as-is, to avoid
/// sending back an updated value.
///
/// See also:
/// See also:
///
///
/// * [TextField], a widget in which the user may enter text.
/// * [TextField], a widget in which the user may enter text.
/// * [EditableText], a [TextInputClient] that connects to [TextInput] when it
/// wants to take user input from the keyboard.
class
TextInput
{
class
TextInput
{
TextInput
.
_
()
{
TextInput
.
_
()
{
_channel
=
SystemChannels
.
textInput
;
_channel
=
SystemChannels
.
textInput
;
...
...
packages/flutter/lib/src/widgets/editable_text.dart
View file @
a57f45e0
...
@@ -320,19 +320,6 @@ class ToolbarOptions {
...
@@ -320,19 +320,6 @@ class ToolbarOptions {
/// movement. This widget does not provide any focus management (e.g.,
/// movement. This widget does not provide any focus management (e.g.,
/// tap-to-focus).
/// tap-to-focus).
///
///
/// ## Handling User Input
///
/// Currently the user may change the text this widget contains via keyboard or
/// the text selection menu. When the user inserted or deleted text, you will be
/// notified of the change and get a chance to modify the new text value:
///
/// * The [inputFormatters] will be first applied to the user input.
///
/// * The [controller]'s [TextEditingController.value] will be updated with the
/// formatted result, and the [controller]'s listeners will be notified.
///
/// * The [onChanged] callback, if specified, will be called last.
///
/// ## Input Actions
/// ## Input Actions
///
///
/// A [TextInputAction] can be provided to customize the appearance of the
/// A [TextInputAction] can be provided to customize the appearance of the
...
@@ -1095,9 +1082,7 @@ class EditableText extends StatefulWidget {
...
@@ -1095,9 +1082,7 @@ class EditableText extends StatefulWidget {
/// {@template flutter.widgets.editableText.inputFormatters}
/// {@template flutter.widgets.editableText.inputFormatters}
/// Optional input validation and formatting overrides.
/// Optional input validation and formatting overrides.
///
///
/// Formatters are run in the provided order when the text input changes. When
/// Formatters are run in the provided order when the text input changes.
/// this parameter changes, the new formatters will not be applied until the
/// next time the user inserts or deletes text.
/// {@endtemplate}
/// {@endtemplate}
final
List
<
TextInputFormatter
>?
inputFormatters
;
final
List
<
TextInputFormatter
>?
inputFormatters
;
...
@@ -1652,66 +1637,61 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
...
@@ -1652,66 +1637,61 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
_clipboardStatus
?.
removeListener
(
_onChangedClipboardStatus
);
_clipboardStatus
?.
removeListener
(
_onChangedClipboardStatus
);
_clipboardStatus
?.
dispose
();
_clipboardStatus
?.
dispose
();
super
.
dispose
();
super
.
dispose
();
assert
(
_batchEditDepth
<=
0
,
'unfinished batch edits:
$_batchEditDepth
'
);
}
}
// TextInputClient implementation:
// TextInputClient implementation:
//
/ The last known [TextEditingValue] of the platform text input plugin.
//
_lastFormattedUnmodifiedTextEditingValue tracks the last value
//
/
//
that the formatter ran on and is used to prevent double-formatting.
/// This value is updated when the platform text input plugin sends a new
TextEditingValue
?
_lastFormattedUnmodifiedTextEditingValue
;
//
/ update via [updateEditingValue], or when [EditableText] calls
//
_lastFormattedValue tracks the last post-format value, so that it can be
//
/ [TextInputConnection.setEditingState] to overwrite the platform text input
//
reused without rerunning the formatter when the input value is repeated.
/// plugin's [TextEditingValue].
TextEditingValue
?
_lastFormattedValue
;
//
/
//
_receivedRemoteTextEditingValue is the direct value last passed in
//
/ Used in [_updateRemoteEditingValueIfNeeded] to determine whether the
//
updateEditingValue. This value does not get updated with the formatted
//
/ remote value is outdated and needs updating
.
//
version
.
TextEditingValue
?
_
lastKnown
RemoteTextEditingValue
;
TextEditingValue
?
_
received
RemoteTextEditingValue
;
@override
@override
TextEditingValue
get
currentTextEditingValue
=>
_value
;
TextEditingValue
get
currentTextEditingValue
=>
_value
;
bool
_updateEditingValueInProgress
=
false
;
@override
@override
void
updateEditingValue
(
TextEditingValue
value
)
{
void
updateEditingValue
(
TextEditingValue
value
)
{
// This method handles text editing state updates from the platform text
_updateEditingValueInProgress
=
true
;
// input plugin. The [EditableText] may not have the focus or an open input
// connection, as autofill can update a disconnected [EditableText].
// Since we still have to support keyboard select, this is the best place
// Since we still have to support keyboard select, this is the best place
// to disable text updating.
// to disable text updating.
if
(!
_shouldCreateInputConnection
)
{
if
(!
_shouldCreateInputConnection
)
{
_updateEditingValueInProgress
=
false
;
return
;
return
;
}
}
if
(
widget
.
readOnly
)
{
if
(
widget
.
readOnly
)
{
// In the read-only case, we only care about selection changes, and reject
// In the read-only case, we only care about selection changes, and reject
// everything else.
// everything else.
value
=
_value
.
copyWith
(
selection:
value
.
selection
);
value
=
_value
.
copyWith
(
selection:
value
.
selection
);
}
}
_lastKnownRemoteTextEditingValue
=
value
;
_receivedRemoteTextEditingValue
=
value
;
if
(
value
.
text
!=
_value
.
text
)
{
hideToolbar
();
_showCaretOnScreen
();
_currentPromptRectRange
=
null
;
if
(
widget
.
obscureText
&&
value
.
text
.
length
==
_value
.
text
.
length
+
1
)
{
_obscureShowCharTicksPending
=
_kObscureShowLatestCharCursorTicks
;
_obscureLatestCharIndex
=
_value
.
selection
.
baseOffset
;
}
}
if
(
value
==
_value
)
{
if
(
value
==
_value
)
{
// This is possible, for example, when the numeric keyboard is input,
// This is possible, for example, when the numeric keyboard is input,
// the engine will notify twice for the same value.
// the engine will notify twice for the same value.
// Track at https://github.com/flutter/flutter/issues/65811
// Track at https://github.com/flutter/flutter/issues/65811
_updateEditingValueInProgress
=
false
;
return
;
return
;
}
}
else
if
(
value
.
text
==
_value
.
text
&&
value
.
composing
==
_value
.
composing
&&
value
.
selection
!=
_value
.
selection
)
{
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
,
renderEditable
,
SelectionChangedCause
.
keyboard
);
}
else
{
}
else
{
hideToolbar
();
_currentPromptRectRange
=
null
;
if
(
_hasInputConnection
)
{
_showCaretOnScreen
();
if
(
widget
.
obscureText
&&
value
.
text
.
length
==
_value
.
text
.
length
+
1
)
{
_obscureShowCharTicksPending
=
_kObscureShowLatestCharCursorTicks
;
_obscureLatestCharIndex
=
_value
.
selection
.
baseOffset
;
}
}
_formatAndSetValue
(
value
);
_formatAndSetValue
(
value
);
}
}
...
@@ -1721,6 +1701,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
...
@@ -1721,6 +1701,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
_stopCursorTimer
(
resetCharTicks:
false
);
_stopCursorTimer
(
resetCharTicks:
false
);
_startCursorTimer
();
_startCursorTimer
();
}
}
_updateEditingValueInProgress
=
false
;
}
}
@override
@override
...
@@ -1878,52 +1859,33 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
...
@@ -1878,52 +1859,33 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
}
}
// Invoke optional callback with the user's submitted content.
// Invoke optional callback with the user's submitted content.
try
{
if
(
widget
.
onSubmitted
!=
null
)
{
widget
.
onSubmitted
?.
call
(
_value
.
text
);
try
{
}
catch
(
exception
,
stack
)
{
widget
.
onSubmitted
!(
_value
.
text
);
FlutterError
.
reportError
(
FlutterErrorDetails
(
}
catch
(
exception
,
stack
)
{
exception:
exception
,
FlutterError
.
reportError
(
FlutterErrorDetails
(
stack:
stack
,
exception:
exception
,
library
:
'widgets'
,
stack:
stack
,
context:
ErrorDescription
(
'while calling onSubmitted for
$action
'
),
library
:
'widgets'
,
));
context:
ErrorDescription
(
'while calling onSubmitted for
$action
'
),
));
}
}
}
}
}
int
_batchEditDepth
=
0
;
/// Begins a new batch edit, within which new updates made to the text editing
/// value will not be sent to the platform text input plugin.
///
/// Batch edits nest. When the outmost batch edit finishes, [endBatchEdit]
/// will attempt to send [currentTextEditingValue] to the text input plugin if
/// it detected a change.
void
beginBatchEdit
()
{
_batchEditDepth
+=
1
;
}
/// Ends the current batch edit started by the last call to [beginBatchEdit],
/// and send [currentTextEditingValue] to the text input plugin if needed.
///
/// Throws an error in debug mode if this [EditableText] is not in a batch
/// edit.
void
endBatchEdit
()
{
_batchEditDepth
-=
1
;
assert
(
_batchEditDepth
>=
0
,
'Unbalanced call to endBatchEdit: beginBatchEdit must be called first.'
,
);
_updateRemoteEditingValueIfNeeded
();
}
void
_updateRemoteEditingValueIfNeeded
()
{
void
_updateRemoteEditingValueIfNeeded
()
{
if
(
_batchEditDepth
>
0
||
!
_hasInputConnection
)
if
(!
_hasInputConnection
)
return
;
return
;
final
TextEditingValue
localValue
=
_value
;
final
TextEditingValue
localValue
=
_value
;
if
(
localValue
==
_lastKnownRemoteTextEditingValue
)
// We should not update back the value notified by the remote(engine) in reverse, this is redundant.
// Unless we modify this value for some reason during processing, such as `TextInputFormatter`.
if
(
_updateEditingValueInProgress
&&
localValue
==
_receivedRemoteTextEditingValue
)
return
;
return
;
// In other cases, as long as the value of the [widget.controller.value] is modified,
// `setEditingState` should be called as we do not want to skip sending real changes
// to the engine.
// Also see https://github.com/flutter/flutter/issues/65059#issuecomment-690254379
_textInputConnection
!.
setEditingState
(
localValue
);
_textInputConnection
!.
setEditingState
(
localValue
);
_lastKnownRemoteTextEditingValue
=
localValue
;
}
}
TextEditingValue
get
_value
=>
widget
.
controller
.
value
;
TextEditingValue
get
_value
=>
widget
.
controller
.
value
;
...
@@ -1987,7 +1949,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
...
@@ -1987,7 +1949,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
return
RevealedOffset
(
rect:
rect
.
shift
(
unitOffset
*
offsetDelta
),
offset:
targetOffset
);
return
RevealedOffset
(
rect:
rect
.
shift
(
unitOffset
*
offsetDelta
),
offset:
targetOffset
);
}
}
bool
get
_hasInputConnection
=>
_textInputConnection
?.
attached
??
false
;
bool
get
_hasInputConnection
=>
_textInputConnection
!=
null
&&
_textInputConnection
!.
attached
;
bool
get
_needsAutofill
=>
widget
.
autofillHints
?.
isNotEmpty
??
false
;
bool
get
_needsAutofill
=>
widget
.
autofillHints
?.
isNotEmpty
??
false
;
bool
get
_shouldBeInAutofillContext
=>
_needsAutofill
&&
currentAutofillScope
!=
null
;
bool
get
_shouldBeInAutofillContext
=>
_needsAutofill
&&
currentAutofillScope
!=
null
;
...
@@ -1997,6 +1959,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
...
@@ -1997,6 +1959,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
}
}
if
(!
_hasInputConnection
)
{
if
(!
_hasInputConnection
)
{
final
TextEditingValue
localValue
=
_value
;
final
TextEditingValue
localValue
=
_value
;
_lastFormattedUnmodifiedTextEditingValue
=
localValue
;
// When _needsAutofill == true && currentAutofillScope == null, autofill
// When _needsAutofill == true && currentAutofillScope == null, autofill
// is allowed but saving the user input from the text field is
// is allowed but saving the user input from the text field is
...
@@ -2037,7 +2000,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
...
@@ -2037,7 +2000,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
if
(
_hasInputConnection
)
{
if
(
_hasInputConnection
)
{
_textInputConnection
!.
close
();
_textInputConnection
!.
close
();
_textInputConnection
=
null
;
_textInputConnection
=
null
;
_lastKnownRemoteTextEditingValue
=
null
;
_lastFormattedUnmodifiedTextEditingValue
=
null
;
_receivedRemoteTextEditingValue
=
null
;
}
}
}
}
...
@@ -2055,7 +2019,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
...
@@ -2055,7 +2019,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
if
(
_hasInputConnection
)
{
if
(
_hasInputConnection
)
{
_textInputConnection
!.
connectionClosedReceived
();
_textInputConnection
!.
connectionClosedReceived
();
_textInputConnection
=
null
;
_textInputConnection
=
null
;
_lastKnownRemoteTextEditingValue
=
null
;
_lastFormattedUnmodifiedTextEditingValue
=
null
;
_receivedRemoteTextEditingValue
=
null
;
_finalizeEditing
(
TextInputAction
.
done
,
shouldUnfocus:
true
);
_finalizeEditing
(
TextInputAction
.
done
,
shouldUnfocus:
true
);
}
}
}
}
...
@@ -2119,15 +2084,17 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
...
@@ -2119,15 +2084,17 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
);
);
_selectionOverlay
!.
handlesVisible
=
widget
.
showSelectionHandles
;
_selectionOverlay
!.
handlesVisible
=
widget
.
showSelectionHandles
;
_selectionOverlay
!.
showHandles
();
_selectionOverlay
!.
showHandles
();
try
{
if
(
widget
.
onSelectionChanged
!=
null
)
{
widget
.
onSelectionChanged
?.
call
(
selection
,
cause
);
try
{
}
catch
(
exception
,
stack
)
{
widget
.
onSelectionChanged
!(
selection
,
cause
);
FlutterError
.
reportError
(
FlutterErrorDetails
(
}
catch
(
exception
,
stack
)
{
exception:
exception
,
FlutterError
.
reportError
(
FlutterErrorDetails
(
stack:
stack
,
exception:
exception
,
library
:
'widgets'
,
stack:
stack
,
context:
ErrorDescription
(
'while calling onSelectionChanged for
$cause
'
),
library
:
'widgets'
,
));
context:
ErrorDescription
(
'while calling onSelectionChanged for
$cause
'
),
));
}
}
}
}
}
}
}
...
@@ -2215,35 +2182,53 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
...
@@ -2215,35 +2182,53 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
_lastBottomViewInset
=
WidgetsBinding
.
instance
!.
window
.
viewInsets
.
bottom
;
_lastBottomViewInset
=
WidgetsBinding
.
instance
!.
window
.
viewInsets
.
bottom
;
}
}
late
final
_WhitespaceDirectionalityFormatter
_whitespaceFormatter
=
_WhitespaceDirectionalityFormatter
(
textDirection:
_textDirection
)
;
_WhitespaceDirectionalityFormatter
?
_whitespaceFormatter
;
void
_formatAndSetValue
(
TextEditingValue
value
)
{
void
_formatAndSetValue
(
TextEditingValue
value
)
{
_whitespaceFormatter
??=
_WhitespaceDirectionalityFormatter
(
textDirection:
_textDirection
);
// Check if the new value is the same as the current local value, or is the same
// Check if the new value is the same as the current local value, or is the same
// as the pre-formatting value of the previous pass (repeat call).
// as the pre-formatting value of the previous pass (repeat call).
final
bool
textChanged
=
_value
.
text
!=
value
.
text
||
_value
.
composing
!=
value
.
composing
;
final
bool
textChanged
=
_value
.
text
!=
value
.
text
;
final
bool
isRepeat
=
value
==
_lastFormattedUnmodifiedTextEditingValue
;
// There's no need to format when starting to compose or when continuing
// an existing composition.
final
bool
isComposing
=
value
.
composing
.
isValid
;
final
bool
isPreviouslyComposing
=
_lastFormattedUnmodifiedTextEditingValue
?.
composing
.
isValid
??
false
;
if
(
textChanged
)
{
if
((
textChanged
||
(!
isComposing
&&
isPreviouslyComposing
))
&&
widget
.
inputFormatters
!=
null
&&
widget
.
inputFormatters
!.
isNotEmpty
)
{
// Only format when the text has changed and there are available formatters.
// Only format when the text has changed and there are available formatters.
// Pass through the formatter regardless of repeat status if the input value is
// Pass through the formatter regardless of repeat status if the input value is
// different than the stored value.
// different than the stored value.
value
=
widget
.
inputFormatters
?.
fold
<
TextEditingValue
>(
for
(
final
TextInputFormatter
formatter
in
widget
.
inputFormatters
!)
{
value
,
value
=
formatter
.
formatEditUpdate
(
_value
,
value
);
(
TextEditingValue
newValue
,
TextInputFormatter
formatter
)
=>
formatter
.
formatEditUpdate
(
_value
,
newValue
),
}
)
??
value
;
// Always pass the text through the whitespace directionality formatter to
// Always pass the text through the whitespace directionality formatter to
// maintain expected behavior with carets on trailing whitespace.
// maintain expected behavior with carets on trailing whitespace.
value
=
_whitespaceFormatter
.
formatEditUpdate
(
_value
,
value
);
value
=
_whitespaceFormatter
!.
formatEditUpdate
(
_value
,
value
);
_lastFormattedValue
=
value
;
}
}
// Put all optional user callback invocations in a batch edit to prevent
if
(
value
==
_value
)
{
// sending multiple `TextInput.updateEditingValue` messages.
// If the value was modified by the formatter, the remote should be notified to keep in sync,
beginBatchEdit
();
// if not modified, it will short-circuit.
_updateRemoteEditingValueIfNeeded
();
}
else
{
// Setting _value here ensures the selection and composing region info is passed.
_value
=
value
;
}
_value
=
value
;
// Use the last formatted value when an identical repeat pass is detected.
if
(
textChanged
)
{
if
(
isRepeat
&&
textChanged
&&
_lastFormattedValue
!=
null
)
{
_value
=
_lastFormattedValue
!;
}
if
(
textChanged
&&
widget
.
onChanged
!=
null
)
{
try
{
try
{
widget
.
onChanged
?.
call
(
value
.
text
);
widget
.
onChanged
!
(
value
.
text
);
}
catch
(
exception
,
stack
)
{
}
catch
(
exception
,
stack
)
{
FlutterError
.
reportError
(
FlutterErrorDetails
(
FlutterError
.
reportError
(
FlutterErrorDetails
(
exception:
exception
,
exception:
exception
,
...
@@ -2253,8 +2238,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
...
@@ -2253,8 +2238,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
));
));
}
}
}
}
_lastFormattedUnmodifiedTextEditingValue
=
_receivedRemoteTextEditingValue
;
endBatchEdit
();
}
}
void
_onCursorColorTick
()
{
void
_onCursorColorTick
()
{
...
...
packages/flutter/test/widgets/editable_text_test.dart
View file @
a57f45e0
...
@@ -4736,246 +4736,6 @@ void main() {
...
@@ -4736,246 +4736,6 @@ void main() {
);
);
});
});
group
(
'batch editing'
,
()
{
final
TextEditingController
controller
=
TextEditingController
(
text:
testText
);
final
EditableText
editableText
=
EditableText
(
showSelectionHandles:
true
,
maxLines:
2
,
controller:
controller
,
focusNode:
FocusNode
(),
cursorColor:
Colors
.
red
,
backgroundCursorColor:
Colors
.
blue
,
style:
Typography
.
material2018
(
platform:
TargetPlatform
.
android
).
black
.
subtitle1
.
copyWith
(
fontFamily:
'Roboto'
),
keyboardType:
TextInputType
.
text
,
);
final
Widget
widget
=
MediaQuery
(
data:
const
MediaQueryData
(),
child:
Directionality
(
textDirection:
TextDirection
.
ltr
,
child:
editableText
,
),
);
testWidgets
(
'batch editing works'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
widget
);
// Connect.
await
tester
.
showKeyboard
(
find
.
byType
(
EditableText
));
final
EditableTextState
state
=
tester
.
state
<
EditableTextState
>(
find
.
byWidget
(
editableText
));
state
.
updateEditingValue
(
const
TextEditingValue
(
text:
'remote value'
));
tester
.
testTextInput
.
log
.
clear
();
state
.
beginBatchEdit
();
controller
.
text
=
'new change 1'
;
expect
(
state
.
currentTextEditingValue
.
text
,
'new change 1'
);
expect
(
tester
.
testTextInput
.
log
,
isEmpty
);
// Nesting.
state
.
beginBatchEdit
();
controller
.
text
=
'new change 2'
;
expect
(
state
.
currentTextEditingValue
.
text
,
'new change 2'
);
expect
(
tester
.
testTextInput
.
log
,
isEmpty
);
// End the innermost batch edit. Not yet.
state
.
endBatchEdit
();
expect
(
tester
.
testTextInput
.
log
,
isEmpty
);
controller
.
text
=
'new change 3'
;
expect
(
state
.
currentTextEditingValue
.
text
,
'new change 3'
);
expect
(
tester
.
testTextInput
.
log
,
isEmpty
);
// Finish the outermost batch edit.
state
.
endBatchEdit
();
expect
(
tester
.
testTextInput
.
log
,
hasLength
(
1
));
expect
(
tester
.
testTextInput
.
log
,
contains
(
matchesMethodCall
(
'TextInput.setEditingState'
,
args:
containsPair
(
'text'
,
'new change 3'
))),
);
});
testWidgets
(
'batch edits need to be nested properly'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
widget
);
// Connect.
await
tester
.
showKeyboard
(
find
.
byType
(
EditableText
));
final
EditableTextState
state
=
tester
.
state
<
EditableTextState
>(
find
.
byWidget
(
editableText
));
state
.
updateEditingValue
(
const
TextEditingValue
(
text:
'remote value'
));
tester
.
testTextInput
.
log
.
clear
();
String
errorString
;
try
{
state
.
endBatchEdit
();
}
catch
(
e
)
{
errorString
=
e
.
toString
();
}
expect
(
errorString
,
contains
(
'Unbalanced call to endBatchEdit'
));
});
testWidgets
(
'catch unfinished batch edits on disposal'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
widget
);
// Connect.
await
tester
.
showKeyboard
(
find
.
byType
(
EditableText
));
final
EditableTextState
state
=
tester
.
state
<
EditableTextState
>(
find
.
byWidget
(
editableText
));
state
.
updateEditingValue
(
const
TextEditingValue
(
text:
'remote value'
));
tester
.
testTextInput
.
log
.
clear
();
state
.
beginBatchEdit
();
expect
(
tester
.
takeException
(),
isNull
);
await
tester
.
pumpWidget
(
Container
());
expect
(
tester
.
takeException
(),
isNotNull
);
});
});
group
(
'EditableText does not send editing values more than once'
,
()
{
final
TextEditingController
controller
=
TextEditingController
(
text:
testText
);
final
EditableText
editableText
=
EditableText
(
showSelectionHandles:
true
,
maxLines:
2
,
controller:
controller
,
focusNode:
FocusNode
(),
cursorColor:
Colors
.
red
,
backgroundCursorColor:
Colors
.
blue
,
style:
Typography
.
material2018
(
platform:
TargetPlatform
.
android
).
black
.
subtitle1
.
copyWith
(
fontFamily:
'Roboto'
),
keyboardType:
TextInputType
.
text
,
inputFormatters:
<
TextInputFormatter
>[
LengthLimitingTextInputFormatter
(
6
)],
onChanged:
(
String
s
)
=>
controller
.
text
+=
' onChanged'
,
);
final
Widget
widget
=
MediaQuery
(
data:
const
MediaQueryData
(),
child:
Directionality
(
textDirection:
TextDirection
.
ltr
,
child:
editableText
,
),
);
controller
.
addListener
(()
{
if
(!
controller
.
text
.
endsWith
(
'listener'
))
controller
.
text
+=
' listener'
;
});
testWidgets
(
'input from text input plugin'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
widget
);
// Connect.
await
tester
.
showKeyboard
(
find
.
byType
(
EditableText
));
tester
.
testTextInput
.
log
.
clear
();
final
EditableTextState
state
=
tester
.
state
<
EditableTextState
>(
find
.
byWidget
(
editableText
));
state
.
updateEditingValue
(
const
TextEditingValue
(
text:
'remoteremoteremote'
));
// Apply in order: length formatter -> listener -> onChanged -> listener.
expect
(
controller
.
text
,
'remote listener onChanged listener'
);
final
List
<
TextEditingValue
>
updates
=
tester
.
testTextInput
.
log
.
where
((
MethodCall
call
)
=>
call
.
method
==
'TextInput.setEditingState'
)
.
map
((
MethodCall
call
)
=>
TextEditingValue
.
fromJSON
(
call
.
arguments
as
Map
<
String
,
dynamic
>))
.
toList
(
growable:
false
);
expect
(
updates
,
const
<
TextEditingValue
>[
TextEditingValue
(
text:
'remote listener onChanged listener'
)]);
tester
.
testTextInput
.
log
.
clear
();
// If by coincidence the text input plugin sends the same value back,
// do nothing.
state
.
updateEditingValue
(
const
TextEditingValue
(
text:
'remote listener onChanged listener'
));
expect
(
controller
.
text
,
'remote listener onChanged listener'
);
expect
(
tester
.
testTextInput
.
log
,
isEmpty
);
});
testWidgets
(
'input from text selection menu'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
widget
);
// Connect.
await
tester
.
showKeyboard
(
find
.
byType
(
EditableText
));
tester
.
testTextInput
.
log
.
clear
();
final
EditableTextState
state
=
tester
.
state
<
EditableTextState
>(
find
.
byWidget
(
editableText
));
state
.
textEditingValue
=
const
TextEditingValue
(
text:
'remoteremoteremote'
);
// Apply in order: length formatter -> listener -> onChanged -> listener.
expect
(
controller
.
text
,
'remote listener onChanged listener'
);
final
List
<
TextEditingValue
>
updates
=
tester
.
testTextInput
.
log
.
where
((
MethodCall
call
)
=>
call
.
method
==
'TextInput.setEditingState'
)
.
map
((
MethodCall
call
)
=>
TextEditingValue
.
fromJSON
(
call
.
arguments
as
Map
<
String
,
dynamic
>))
.
toList
(
growable:
false
);
expect
(
updates
,
const
<
TextEditingValue
>[
TextEditingValue
(
text:
'remote listener onChanged listener'
)]);
tester
.
testTextInput
.
log
.
clear
();
});
testWidgets
(
'input from controller'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
widget
);
// Connect.
await
tester
.
showKeyboard
(
find
.
byType
(
EditableText
));
tester
.
testTextInput
.
log
.
clear
();
controller
.
text
=
'remoteremoteremote'
;
final
List
<
TextEditingValue
>
updates
=
tester
.
testTextInput
.
log
.
where
((
MethodCall
call
)
=>
call
.
method
==
'TextInput.setEditingState'
)
.
map
((
MethodCall
call
)
=>
TextEditingValue
.
fromJSON
(
call
.
arguments
as
Map
<
String
,
dynamic
>))
.
toList
(
growable:
false
);
expect
(
updates
,
const
<
TextEditingValue
>[
TextEditingValue
(
text:
'remoteremoteremote listener'
)]);
});
testWidgets
(
'input from changing controller'
,
(
WidgetTester
tester
)
async
{
final
TextEditingController
controller
=
TextEditingController
(
text:
testText
);
Widget
build
({
TextEditingController
textEditingController
})
{
return
MediaQuery
(
data:
const
MediaQueryData
(),
child:
Directionality
(
textDirection:
TextDirection
.
ltr
,
child:
EditableText
(
showSelectionHandles:
true
,
maxLines:
2
,
controller:
textEditingController
??
controller
,
focusNode:
FocusNode
(),
cursorColor:
Colors
.
red
,
backgroundCursorColor:
Colors
.
blue
,
style:
Typography
.
material2018
(
platform:
TargetPlatform
.
android
).
black
.
subtitle1
.
copyWith
(
fontFamily:
'Roboto'
),
keyboardType:
TextInputType
.
text
,
inputFormatters:
<
TextInputFormatter
>[
LengthLimitingTextInputFormatter
(
6
)],
),
),
);
}
await
tester
.
pumpWidget
(
build
());
// Connect.
await
tester
.
showKeyboard
(
find
.
byType
(
EditableText
));
tester
.
testTextInput
.
log
.
clear
();
await
tester
.
pumpWidget
(
build
(
textEditingController:
TextEditingController
(
text:
'new text'
)));
List
<
TextEditingValue
>
updates
=
tester
.
testTextInput
.
log
.
where
((
MethodCall
call
)
=>
call
.
method
==
'TextInput.setEditingState'
)
.
map
((
MethodCall
call
)
=>
TextEditingValue
.
fromJSON
(
call
.
arguments
as
Map
<
String
,
dynamic
>))
.
toList
(
growable:
false
);
expect
(
updates
,
const
<
TextEditingValue
>[
TextEditingValue
(
text:
'new text'
)]);
tester
.
testTextInput
.
log
.
clear
();
await
tester
.
pumpWidget
(
build
(
textEditingController:
TextEditingController
(
text:
'new new text'
)));
updates
=
tester
.
testTextInput
.
log
.
where
((
MethodCall
call
)
=>
call
.
method
==
'TextInput.setEditingState'
)
.
map
((
MethodCall
call
)
=>
TextEditingValue
.
fromJSON
(
call
.
arguments
as
Map
<
String
,
dynamic
>))
.
toList
(
growable:
false
);
expect
(
updates
,
const
<
TextEditingValue
>[
TextEditingValue
(
text:
'new new text'
)]);
});
});
testWidgets
(
'input imm channel calls are ordered correctly'
,
(
WidgetTester
tester
)
async
{
testWidgets
(
'input imm channel calls are ordered correctly'
,
(
WidgetTester
tester
)
async
{
const
String
testText
=
'flutter is the best!'
;
const
String
testText
=
'flutter is the best!'
;
final
TextEditingController
controller
=
TextEditingController
(
text:
testText
);
final
TextEditingController
controller
=
TextEditingController
(
text:
testText
);
...
@@ -5475,12 +5235,12 @@ void main() {
...
@@ -5475,12 +5235,12 @@ void main() {
expect
(
formatter
.
formatCallCount
,
3
);
expect
(
formatter
.
formatCallCount
,
3
);
state
.
updateEditingValue
(
const
TextEditingValue
(
text:
'0123'
,
selection:
TextSelection
.
collapsed
(
offset:
2
)));
// No text change, does not format
state
.
updateEditingValue
(
const
TextEditingValue
(
text:
'0123'
,
selection:
TextSelection
.
collapsed
(
offset:
2
)));
// No text change, does not format
expect
(
formatter
.
formatCallCount
,
3
);
expect
(
formatter
.
formatCallCount
,
3
);
state
.
updateEditingValue
(
const
TextEditingValue
(
text:
'0123'
,
selection:
TextSelection
.
collapsed
(
offset:
2
),
composing:
TextRange
(
start:
1
,
end:
2
)));
// Composing change
triggers
reformat
state
.
updateEditingValue
(
const
TextEditingValue
(
text:
'0123'
,
selection:
TextSelection
.
collapsed
(
offset:
2
),
composing:
TextRange
(
start:
1
,
end:
2
)));
// Composing change
does not
reformat
expect
(
formatter
.
formatCallCount
,
4
);
expect
(
formatter
.
formatCallCount
,
3
);
expect
(
formatter
.
lastOldValue
.
composing
,
const
TextRange
(
start:
-
1
,
end:
-
1
));
expect
(
formatter
.
lastOldValue
.
composing
,
const
TextRange
(
start:
-
1
,
end:
-
1
));
expect
(
formatter
.
lastNewValue
.
composing
,
const
TextRange
(
start:
1
,
end:
2
));
// The new composing was
registered in formatter.
expect
(
formatter
.
lastNewValue
.
composing
,
const
TextRange
(
start:
-
1
,
end:
-
1
));
// Since did not format, the new composing was not
registered in formatter.
state
.
updateEditingValue
(
const
TextEditingValue
(
text:
'01234'
,
selection:
TextSelection
.
collapsed
(
offset:
2
)));
// Formats, with oldValue containing composing region.
state
.
updateEditingValue
(
const
TextEditingValue
(
text:
'01234'
,
selection:
TextSelection
.
collapsed
(
offset:
2
)));
// Formats, with oldValue containing composing region.
expect
(
formatter
.
formatCallCount
,
5
);
expect
(
formatter
.
formatCallCount
,
4
);
expect
(
formatter
.
lastOldValue
.
composing
,
const
TextRange
(
start:
1
,
end:
2
));
expect
(
formatter
.
lastOldValue
.
composing
,
const
TextRange
(
start:
1
,
end:
2
));
expect
(
formatter
.
lastNewValue
.
composing
,
const
TextRange
(
start:
-
1
,
end:
-
1
));
expect
(
formatter
.
lastNewValue
.
composing
,
const
TextRange
(
start:
-
1
,
end:
-
1
));
...
@@ -5491,10 +5251,8 @@ void main() {
...
@@ -5491,10 +5251,8 @@ void main() {
'[2]: normal aaaa'
,
'[2]: normal aaaa'
,
'[3]: 012, 0123'
,
'[3]: 012, 0123'
,
'[3]: normal aaaaaa'
,
'[3]: normal aaaaaa'
,
'[4]: 0123, 0123'
,
'[4]: 0123, 01234'
,
'[4]: normal aaaaaaaa'
,
'[4]: normal aaaaaaaa'
'[5]: 0123, 01234'
,
'[5]: normal aaaaaaaaaa'
,
];
];
expect
(
formatter
.
log
,
referenceLog
);
expect
(
formatter
.
log
,
referenceLog
);
...
...
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