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 {
/// Gets the current text input.
TextEditingValue
get
textEditingValue
;
/// Indicates that the user has requested the delegate to replace its current
/// text editing state with [value].
///
/// The new [value] is treated as user input and thus may subject to input
/// formatting.
///
/// See also:
///
/// * [EditableTextState.textEditingValue]: an implementation that applies
/// additional pre-processing to the specified [value], before updating the
/// text editing state.
/// Sets the current text input (replaces the whole line).
set
textEditingValue
(
TextEditingValue
value
);
/// Hides the text selection toolbar.
...
...
@@ -794,7 +784,6 @@ abstract class TextSelectionDelegate {
/// See also:
///
/// * [TextInput.attach]
/// * [EditableText], a [TextInputClient] implementation.
abstract
class
TextInputClient
{
/// Abstract const constructor. This constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions.
...
...
@@ -816,9 +805,6 @@ abstract class TextInputClient {
AutofillScope
?
get
currentAutofillScope
;
/// 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
);
/// Requests that this client perform the given action.
...
...
@@ -846,10 +832,7 @@ abstract class TextInputClient {
///
/// See also:
///
/// * [TextInput.attach], a method used to establish a [TextInputConnection]
/// 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].
/// * [TextInput.attach]
class
TextInputConnection
{
TextInputConnection
.
_
(
this
.
_client
)
:
assert
(
_client
!=
null
),
...
...
@@ -906,8 +889,7 @@ class TextInputConnection {
TextInput
.
_instance
.
_updateConfig
(
configuration
);
}
/// Requests that the text input control change its internal state to match
/// the given state.
/// Requests that the text input control change its internal state to match the given state.
void
setEditingState
(
TextEditingValue
value
)
{
assert
(
attached
);
TextInput
.
_instance
.
_setEditingState
(
value
);
...
...
@@ -1060,57 +1042,9 @@ RawFloatingCursorPoint _toTextPoint(FloatingCursorDragState state, Map<String, d
/// 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:
///
/// * [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
{
TextInput
.
_
()
{
_channel
=
SystemChannels
.
textInput
;
...
...
packages/flutter/lib/src/widgets/editable_text.dart
View file @
a57f45e0
...
...
@@ -320,19 +320,6 @@ class ToolbarOptions {
/// movement. This widget does not provide any focus management (e.g.,
/// 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
///
/// A [TextInputAction] can be provided to customize the appearance of the
...
...
@@ -1095,9 +1082,7 @@ class EditableText extends StatefulWidget {
/// {@template flutter.widgets.editableText.inputFormatters}
/// Optional input validation and formatting overrides.
///
/// Formatters are run in the provided order when the text input changes. When
/// this parameter changes, the new formatters will not be applied until the
/// next time the user inserts or deletes text.
/// Formatters are run in the provided order when the text input changes.
/// {@endtemplate}
final
List
<
TextInputFormatter
>?
inputFormatters
;
...
...
@@ -1652,66 +1637,61 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
_clipboardStatus
?.
removeListener
(
_onChangedClipboardStatus
);
_clipboardStatus
?.
dispose
();
super
.
dispose
();
assert
(
_batchEditDepth
<=
0
,
'unfinished batch edits:
$_batchEditDepth
'
);
}
// TextInputClient implementation:
//
/ The last known [TextEditingValue] of the platform text input plugin.
//
/
/// This value is updated when the platform text input plugin sends a new
//
/ update via [updateEditingValue], or when [EditableText] calls
//
/ [TextInputConnection.setEditingState] to overwrite the platform text input
/// plugin's [TextEditingValue].
//
/
//
/ Used in [_updateRemoteEditingValueIfNeeded] to determine whether the
//
/ remote value is outdated and needs updating
.
TextEditingValue
?
_
lastKnown
RemoteTextEditingValue
;
//
_lastFormattedUnmodifiedTextEditingValue tracks the last value
//
that the formatter ran on and is used to prevent double-formatting.
TextEditingValue
?
_lastFormattedUnmodifiedTextEditingValue
;
//
_lastFormattedValue tracks the last post-format value, so that it can be
//
reused without rerunning the formatter when the input value is repeated.
TextEditingValue
?
_lastFormattedValue
;
//
_receivedRemoteTextEditingValue is the direct value last passed in
//
updateEditingValue. This value does not get updated with the formatted
//
version
.
TextEditingValue
?
_
received
RemoteTextEditingValue
;
@override
TextEditingValue
get
currentTextEditingValue
=>
_value
;
bool
_updateEditingValueInProgress
=
false
;
@override
void
updateEditingValue
(
TextEditingValue
value
)
{
// This method handles text editing state updates from the platform text
// input plugin. The [EditableText] may not have the focus or an open input
// connection, as autofill can update a disconnected [EditableText].
_updateEditingValueInProgress
=
true
;
// Since we still have to support keyboard select, this is the best place
// to disable text updating.
if
(!
_shouldCreateInputConnection
)
{
_updateEditingValueInProgress
=
false
;
return
;
}
if
(
widget
.
readOnly
)
{
// In the read-only case, we only care about selection changes, and reject
// everything else.
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
)
{
// This is possible, for example, when the numeric keyboard is input,
// the engine will notify twice for the same value.
// Track at https://github.com/flutter/flutter/issues/65811
_updateEditingValueInProgress
=
false
;
return
;
}
if
(
value
.
text
==
_value
.
text
&&
value
.
composing
==
_value
.
composing
)
{
}
else
if
(
value
.
text
==
_value
.
text
&&
value
.
composing
==
_value
.
composing
&&
value
.
selection
!=
_value
.
selection
)
{
// `selection` is the only change.
_handleSelectionChanged
(
value
.
selection
,
renderEditable
,
SelectionChangedCause
.
keyboard
);
}
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
);
}
...
...
@@ -1721,6 +1701,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
_stopCursorTimer
(
resetCharTicks:
false
);
_startCursorTimer
();
}
_updateEditingValueInProgress
=
false
;
}
@override
...
...
@@ -1878,52 +1859,33 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
}
// Invoke optional callback with the user's submitted content.
try
{
widget
.
onSubmitted
?.
call
(
_value
.
text
);
}
catch
(
exception
,
stack
)
{
FlutterError
.
reportError
(
FlutterErrorDetails
(
exception:
exception
,
stack:
stack
,
library
:
'widgets'
,
context:
ErrorDescription
(
'while calling onSubmitted for
$action
'
),
));
if
(
widget
.
onSubmitted
!=
null
)
{
try
{
widget
.
onSubmitted
!(
_value
.
text
);
}
catch
(
exception
,
stack
)
{
FlutterError
.
reportError
(
FlutterErrorDetails
(
exception:
exception
,
stack:
stack
,
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
()
{
if
(
_batchEditDepth
>
0
||
!
_hasInputConnection
)
if
(!
_hasInputConnection
)
return
;
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
;
// 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
);
_lastKnownRemoteTextEditingValue
=
localValue
;
}
TextEditingValue
get
_value
=>
widget
.
controller
.
value
;
...
...
@@ -1987,7 +1949,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
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
_shouldBeInAutofillContext
=>
_needsAutofill
&&
currentAutofillScope
!=
null
;
...
...
@@ -1997,6 +1959,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
}
if
(!
_hasInputConnection
)
{
final
TextEditingValue
localValue
=
_value
;
_lastFormattedUnmodifiedTextEditingValue
=
localValue
;
// When _needsAutofill == true && currentAutofillScope == null, autofill
// is allowed but saving the user input from the text field is
...
...
@@ -2037,7 +2000,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
if
(
_hasInputConnection
)
{
_textInputConnection
!.
close
();
_textInputConnection
=
null
;
_lastKnownRemoteTextEditingValue
=
null
;
_lastFormattedUnmodifiedTextEditingValue
=
null
;
_receivedRemoteTextEditingValue
=
null
;
}
}
...
...
@@ -2055,7 +2019,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
if
(
_hasInputConnection
)
{
_textInputConnection
!.
connectionClosedReceived
();
_textInputConnection
=
null
;
_lastKnownRemoteTextEditingValue
=
null
;
_lastFormattedUnmodifiedTextEditingValue
=
null
;
_receivedRemoteTextEditingValue
=
null
;
_finalizeEditing
(
TextInputAction
.
done
,
shouldUnfocus:
true
);
}
}
...
...
@@ -2119,15 +2084,17 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
);
_selectionOverlay
!.
handlesVisible
=
widget
.
showSelectionHandles
;
_selectionOverlay
!.
showHandles
();
try
{
widget
.
onSelectionChanged
?.
call
(
selection
,
cause
);
}
catch
(
exception
,
stack
)
{
FlutterError
.
reportError
(
FlutterErrorDetails
(
exception:
exception
,
stack:
stack
,
library
:
'widgets'
,
context:
ErrorDescription
(
'while calling onSelectionChanged for
$cause
'
),
));
if
(
widget
.
onSelectionChanged
!=
null
)
{
try
{
widget
.
onSelectionChanged
!(
selection
,
cause
);
}
catch
(
exception
,
stack
)
{
FlutterError
.
reportError
(
FlutterErrorDetails
(
exception:
exception
,
stack:
stack
,
library
:
'widgets'
,
context:
ErrorDescription
(
'while calling onSelectionChanged for
$cause
'
),
));
}
}
}
}
...
...
@@ -2215,35 +2182,53 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
_lastBottomViewInset
=
WidgetsBinding
.
instance
!.
window
.
viewInsets
.
bottom
;
}
late
final
_WhitespaceDirectionalityFormatter
_whitespaceFormatter
=
_WhitespaceDirectionalityFormatter
(
textDirection:
_textDirection
)
;
_WhitespaceDirectionalityFormatter
?
_whitespaceFormatter
;
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
// 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.
// Pass through the formatter regardless of repeat status if the input value is
// different than the stored value.
value
=
widget
.
inputFormatters
?.
fold
<
TextEditingValue
>(
value
,
(
TextEditingValue
newValue
,
TextInputFormatter
formatter
)
=>
formatter
.
formatEditUpdate
(
_value
,
newValue
),
)
??
value
;
for
(
final
TextInputFormatter
formatter
in
widget
.
inputFormatters
!)
{
value
=
formatter
.
formatEditUpdate
(
_value
,
value
);
}
// Always pass the text through the whitespace directionality formatter to
// 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
// sending multiple `TextInput.updateEditingValue` messages.
beginBatchEdit
();
if
(
value
==
_value
)
{
// If the value was modified by the formatter, the remote should be notified to keep in sync,
// 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
;
if
(
textChanged
)
{
// Use the last formatted value when an identical repeat pass is detected.
if
(
isRepeat
&&
textChanged
&&
_lastFormattedValue
!=
null
)
{
_value
=
_lastFormattedValue
!;
}
if
(
textChanged
&&
widget
.
onChanged
!=
null
)
{
try
{
widget
.
onChanged
?.
call
(
value
.
text
);
widget
.
onChanged
!
(
value
.
text
);
}
catch
(
exception
,
stack
)
{
FlutterError
.
reportError
(
FlutterErrorDetails
(
exception:
exception
,
...
...
@@ -2253,8 +2238,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
));
}
}
endBatchEdit
();
_lastFormattedUnmodifiedTextEditingValue
=
_receivedRemoteTextEditingValue
;
}
void
_onCursorColorTick
()
{
...
...
packages/flutter/test/widgets/editable_text_test.dart
View file @
a57f45e0
...
...
@@ -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
{
const
String
testText
=
'flutter is the best!'
;
final
TextEditingController
controller
=
TextEditingController
(
text:
testText
);
...
...
@@ -5475,12 +5235,12 @@ void main() {
expect
(
formatter
.
formatCallCount
,
3
);
state
.
updateEditingValue
(
const
TextEditingValue
(
text:
'0123'
,
selection:
TextSelection
.
collapsed
(
offset:
2
)));
// No text change, does not format
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
expect
(
formatter
.
formatCallCount
,
4
);
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
,
3
);
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.
expect
(
formatter
.
formatCallCount
,
5
);
expect
(
formatter
.
formatCallCount
,
4
);
expect
(
formatter
.
lastOldValue
.
composing
,
const
TextRange
(
start:
1
,
end:
2
));
expect
(
formatter
.
lastNewValue
.
composing
,
const
TextRange
(
start:
-
1
,
end:
-
1
));
...
...
@@ -5491,10 +5251,8 @@ void main() {
'[2]: normal aaaa'
,
'[3]: 012, 0123'
,
'[3]: normal aaaaaa'
,
'[4]: 0123, 0123'
,
'[4]: normal aaaaaaaa'
,
'[5]: 0123, 01234'
,
'[5]: normal aaaaaaaaaa'
,
'[4]: 0123, 01234'
,
'[4]: normal aaaaaaaa'
];
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