fix issue 14014 read only text field (#32059)
This commit is contained in:
@@ -139,6 +139,9 @@ class CupertinoTextField extends StatefulWidget {
|
||||
/// this is a single-line text field and will scroll horizontally when
|
||||
/// overflown. [maxLines] must not be zero.
|
||||
///
|
||||
/// The text cursor is not shown if [showCursor] is false or if [showCursor]
|
||||
/// is null (the default) and [readOnly] is true.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [minLines]
|
||||
@@ -167,6 +170,8 @@ class CupertinoTextField extends StatefulWidget {
|
||||
this.style,
|
||||
this.strutStyle,
|
||||
this.textAlign = TextAlign.start,
|
||||
this.readOnly = false,
|
||||
this.showCursor,
|
||||
this.autofocus = false,
|
||||
this.obscureText = false,
|
||||
this.autocorrect = true,
|
||||
@@ -190,6 +195,7 @@ class CupertinoTextField extends StatefulWidget {
|
||||
this.scrollController,
|
||||
this.scrollPhysics,
|
||||
}) : assert(textAlign != null),
|
||||
assert(readOnly != null),
|
||||
assert(autofocus != null),
|
||||
assert(obscureText != null),
|
||||
assert(autocorrect != null),
|
||||
@@ -313,6 +319,12 @@ class CupertinoTextField extends StatefulWidget {
|
||||
/// {@macro flutter.widgets.editableText.textAlign}
|
||||
final TextAlign textAlign;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.readOnly}
|
||||
final bool readOnly;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.showCursor}
|
||||
final bool showCursor;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.autofocus}
|
||||
final bool autofocus;
|
||||
|
||||
@@ -489,6 +501,8 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
|
||||
// For backwards-compatibility, we treat a null kind the same as touch.
|
||||
bool _shouldShowSelectionToolbar = true;
|
||||
|
||||
bool _showSelectionHandles = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -644,8 +658,11 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
|
||||
if (cause == SelectionChangedCause.longPress) {
|
||||
_editableText?.bringIntoView(selection.base);
|
||||
}
|
||||
if (_shouldShowSelectionHandles(cause)) {
|
||||
_editableText?.showHandles();
|
||||
final bool willShowSelectionHandles = _shouldShowSelectionHandles(cause);
|
||||
if (willShowSelectionHandles != _showSelectionHandles) {
|
||||
setState(() {
|
||||
_showSelectionHandles = willShowSelectionHandles;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -812,6 +829,9 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
|
||||
child: EditableText(
|
||||
key: _editableTextKey,
|
||||
controller: controller,
|
||||
readOnly: widget.readOnly,
|
||||
showCursor: widget.showCursor,
|
||||
showSelectionHandles: _showSelectionHandles,
|
||||
focusNode: _effectiveFocusNode,
|
||||
keyboardType: widget.keyboardType,
|
||||
textInputAction: widget.textInputAction,
|
||||
|
||||
@@ -123,7 +123,10 @@ class TextField extends StatefulWidget {
|
||||
/// characters may be entered, and the error counter and divider will
|
||||
/// switch to the [decoration.errorStyle] when the limit is exceeded.
|
||||
///
|
||||
/// The [textAlign], [autofocus], [obscureText], [autocorrect],
|
||||
/// The text cursor is not shown if [showCursor] is false or if [showCursor]
|
||||
/// is null (the default) and [readOnly] is true.
|
||||
///
|
||||
/// The [textAlign], [autofocus], [obscureText], [readOnly], [autocorrect],
|
||||
/// [maxLengthEnforced], [scrollPadding], [maxLines], and [maxLength]
|
||||
/// arguments must not be null.
|
||||
///
|
||||
@@ -143,6 +146,8 @@ class TextField extends StatefulWidget {
|
||||
this.strutStyle,
|
||||
this.textAlign = TextAlign.start,
|
||||
this.textDirection,
|
||||
this.readOnly = false,
|
||||
this.showCursor,
|
||||
this.autofocus = false,
|
||||
this.obscureText = false,
|
||||
this.autocorrect = true,
|
||||
@@ -168,6 +173,7 @@ class TextField extends StatefulWidget {
|
||||
this.scrollController,
|
||||
this.scrollPhysics,
|
||||
}) : assert(textAlign != null),
|
||||
assert(readOnly != null),
|
||||
assert(autofocus != null),
|
||||
assert(obscureText != null),
|
||||
assert(autocorrect != null),
|
||||
@@ -289,6 +295,12 @@ class TextField extends StatefulWidget {
|
||||
/// {@macro flutter.widgets.editableText.expands}
|
||||
final bool expands;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.readOnly}
|
||||
final bool readOnly;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.showCursor}
|
||||
final bool showCursor;
|
||||
|
||||
/// If [maxLength] is set to this value, only the "current input length"
|
||||
/// part of the character counter is shown.
|
||||
static const int noMaxLength = -1;
|
||||
@@ -522,6 +534,8 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
|
||||
|
||||
bool _shouldShowSelectionToolbar = true;
|
||||
|
||||
bool _showSelectionHandles = false;
|
||||
|
||||
InputDecoration _getEffectiveDecoration() {
|
||||
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
|
||||
final ThemeData themeData = Theme.of(context);
|
||||
@@ -606,6 +620,11 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
|
||||
if (wasEnabled && !isEnabled) {
|
||||
_effectiveFocusNode.unfocus();
|
||||
}
|
||||
if (_effectiveFocusNode.hasFocus && widget.readOnly != oldWidget.readOnly) {
|
||||
if(_effectiveController.selection.isCollapsed) {
|
||||
_showSelectionHandles = !widget.readOnly;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -629,6 +648,9 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
|
||||
if (cause == SelectionChangedCause.keyboard)
|
||||
return false;
|
||||
|
||||
if (widget.readOnly && _effectiveController.selection.isCollapsed)
|
||||
return false;
|
||||
|
||||
if (cause == SelectionChangedCause.longPress)
|
||||
return true;
|
||||
|
||||
@@ -639,10 +661,11 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
|
||||
}
|
||||
|
||||
void _handleSelectionChanged(TextSelection selection, SelectionChangedCause cause) {
|
||||
// iOS cursor doesn't move via a selection handle. The scroll happens
|
||||
// directly from new text selection changes.
|
||||
if (_shouldShowSelectionHandles(cause)) {
|
||||
_editableText?.showHandles();
|
||||
final bool willShowSelectionHandles = _shouldShowSelectionHandles(cause);
|
||||
if (willShowSelectionHandles != _showSelectionHandles) {
|
||||
setState(() {
|
||||
_showSelectionHandles = willShowSelectionHandles;
|
||||
});
|
||||
}
|
||||
|
||||
switch (Theme.of(context).platform) {
|
||||
@@ -927,6 +950,9 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
|
||||
Widget child = RepaintBoundary(
|
||||
child: EditableText(
|
||||
key: _editableTextKey,
|
||||
readOnly: widget.readOnly,
|
||||
showCursor: widget.showCursor,
|
||||
showSelectionHandles: _showSelectionHandles,
|
||||
controller: controller,
|
||||
focusNode: focusNode,
|
||||
keyboardType: widget.keyboardType,
|
||||
|
||||
@@ -84,6 +84,8 @@ class TextFormField extends FormField<String> {
|
||||
TextDirection textDirection,
|
||||
TextAlign textAlign = TextAlign.start,
|
||||
bool autofocus = false,
|
||||
bool readOnly = false,
|
||||
bool showCursor,
|
||||
bool obscureText = false,
|
||||
bool autocorrect = true,
|
||||
bool autovalidate = false,
|
||||
@@ -108,6 +110,7 @@ class TextFormField extends FormField<String> {
|
||||
}) : assert(initialValue == null || controller == null),
|
||||
assert(textAlign != null),
|
||||
assert(autofocus != null),
|
||||
assert(readOnly != null),
|
||||
assert(obscureText != null),
|
||||
assert(autocorrect != null),
|
||||
assert(autovalidate != null),
|
||||
@@ -149,6 +152,8 @@ class TextFormField extends FormField<String> {
|
||||
textDirection: textDirection,
|
||||
textCapitalization: textCapitalization,
|
||||
autofocus: autofocus,
|
||||
readOnly: readOnly,
|
||||
showCursor: showCursor,
|
||||
obscureText: obscureText,
|
||||
autocorrect: autocorrect,
|
||||
maxLengthEnforced: maxLengthEnforced,
|
||||
|
||||
@@ -238,6 +238,13 @@ class RenderEditable extends RenderBox {
|
||||
/// The default value of this property is false.
|
||||
bool ignorePointer;
|
||||
|
||||
/// Whether text is composed.
|
||||
///
|
||||
/// Text is composed when user selects it for editing. The [TextSpan] will have
|
||||
/// children with composing effect and leave text property to be null.
|
||||
@visibleForTesting
|
||||
bool get isComposingText => text.text == null;
|
||||
|
||||
/// The pixel ratio of the current device.
|
||||
///
|
||||
/// Should be obtained by querying MediaQuery for the devicePixelRatio.
|
||||
|
||||
@@ -585,6 +585,18 @@ abstract class TextSelectionDelegate {
|
||||
/// Brings the provided [TextPosition] into the visible area of the text
|
||||
/// input.
|
||||
void bringIntoView(TextPosition position);
|
||||
|
||||
/// Whether cut is enabled, must not be null.
|
||||
bool get cutEnabled => true;
|
||||
|
||||
/// Whether copy is enabled, must not be null.
|
||||
bool get copyEnabled => true;
|
||||
|
||||
/// Whether paste is enabled, must not be null.
|
||||
bool get pasteEnabled => true;
|
||||
|
||||
/// Whether select all is enabled, must not be null.
|
||||
bool get selectAllEnabled => true;
|
||||
}
|
||||
|
||||
/// An interface to receive information from [TextInput].
|
||||
|
||||
@@ -260,13 +260,17 @@ class EditableText extends StatefulWidget {
|
||||
/// [TextInputType.text] unless [maxLines] is greater than one, when it will
|
||||
/// default to [TextInputType.multiline].
|
||||
///
|
||||
/// The text cursor is not shown if [showCursor] is false or if [showCursor]
|
||||
/// is null (the default) and [readOnly] is true.
|
||||
///
|
||||
/// The [controller], [focusNode], [style], [cursorColor], [backgroundCursorColor],
|
||||
/// [textAlign], [dragStartBehavior] and [rendererIgnoresPointer] arguments
|
||||
/// must not be null.
|
||||
/// [textAlign], [dragStartBehavior], [rendererIgnoresPointer] and [readOnly]
|
||||
/// arguments must not be null.
|
||||
EditableText({
|
||||
Key key,
|
||||
@required this.controller,
|
||||
@required this.focusNode,
|
||||
this.readOnly = false,
|
||||
this.obscureText = false,
|
||||
this.autocorrect = true,
|
||||
@required this.style,
|
||||
@@ -281,6 +285,8 @@ class EditableText extends StatefulWidget {
|
||||
this.minLines,
|
||||
this.expands = false,
|
||||
this.autofocus = false,
|
||||
bool showCursor,
|
||||
this.showSelectionHandles = false,
|
||||
this.selectionColor,
|
||||
this.selectionControls,
|
||||
TextInputType keyboardType,
|
||||
@@ -308,6 +314,8 @@ class EditableText extends StatefulWidget {
|
||||
assert(focusNode != null),
|
||||
assert(obscureText != null),
|
||||
assert(autocorrect != null),
|
||||
assert(showSelectionHandles != null),
|
||||
assert(readOnly != null),
|
||||
assert(style != null),
|
||||
assert(cursorColor != null),
|
||||
assert(cursorOpacityAnimates != null),
|
||||
@@ -337,6 +345,7 @@ class EditableText extends StatefulWidget {
|
||||
..addAll(inputFormatters ?? const Iterable<TextInputFormatter>.empty())
|
||||
)
|
||||
: inputFormatters,
|
||||
showCursor = showCursor ?? !readOnly,
|
||||
super(key: key);
|
||||
|
||||
/// Controls the text being edited.
|
||||
@@ -355,6 +364,38 @@ class EditableText extends StatefulWidget {
|
||||
/// {@endtemplate}
|
||||
final bool obscureText;
|
||||
|
||||
/// {@template flutter.widgets.editableText.readOnly}
|
||||
/// Whether the text can be changed.
|
||||
///
|
||||
/// When this is set to true, the text cannot be modified
|
||||
/// by any shortcut or keyboard operation. The text is still selectable.
|
||||
///
|
||||
/// Defaults to false. Must not be null.
|
||||
/// {@endtemplate}
|
||||
final bool readOnly;
|
||||
|
||||
/// Whether to show selection handles.
|
||||
///
|
||||
/// When a selection is active, there will be two handles at each side of
|
||||
/// boundary, or one handle if the selection is collapsed. The handles can be
|
||||
/// dragged to adjust the selection.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [showCursor], which controls the visibility of the cursor..
|
||||
final bool showSelectionHandles;
|
||||
|
||||
/// {@template flutter.widgets.editableText.showCursor}
|
||||
/// Whether to show cursor.
|
||||
///
|
||||
/// The cursor refers to the blinking caret when the [EditableText] is focused.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [showSelectionHandles], which controls the visibility of the selection handles..
|
||||
/// {@endtemplate}
|
||||
final bool showCursor;
|
||||
|
||||
/// {@template flutter.widgets.editableText.autocorrect}
|
||||
/// Whether to enable autocorrection.
|
||||
///
|
||||
@@ -822,6 +863,18 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
|
||||
Color get _cursorColor => widget.cursorColor.withOpacity(_cursorBlinkOpacityController.value);
|
||||
|
||||
@override
|
||||
bool get cutEnabled => !widget.readOnly;
|
||||
|
||||
@override
|
||||
bool get copyEnabled => true;
|
||||
|
||||
@override
|
||||
bool get pasteEnabled => !widget.readOnly;
|
||||
|
||||
@override
|
||||
bool get selectAllEnabled => true;
|
||||
|
||||
// State lifecycle:
|
||||
|
||||
@override
|
||||
@@ -836,6 +889,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
_cursorBlinkOpacityController.addListener(_onCursorColorTick);
|
||||
_floatingCursorResetController = AnimationController(vsync: this);
|
||||
_floatingCursorResetController.addListener(_onFloatingCursorResetTick);
|
||||
_cursorVisibilityNotifier.value = widget.showCursor;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -855,6 +909,10 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
widget.controller.addListener(_didChangeTextEditingValue);
|
||||
_updateRemoteEditingValueIfNeeded();
|
||||
}
|
||||
if (widget.controller.selection != oldWidget.controller.selection) {
|
||||
_selectionOverlay?.update(_value);
|
||||
}
|
||||
_selectionOverlay?.handlesVisible = widget.showSelectionHandles;
|
||||
if (widget.focusNode != oldWidget.focusNode) {
|
||||
oldWidget.focusNode.removeListener(_handleFocusChanged);
|
||||
_focusAttachment?.detach();
|
||||
@@ -862,6 +920,12 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
widget.focusNode.addListener(_handleFocusChanged);
|
||||
updateKeepAlive();
|
||||
}
|
||||
if (widget.readOnly) {
|
||||
_closeInputConnectionIfNeeded();
|
||||
} else {
|
||||
if (oldWidget.readOnly && _hasFocus)
|
||||
_openInputConnection();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -886,6 +950,11 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
|
||||
@override
|
||||
void updateEditingValue(TextEditingValue value) {
|
||||
// Since we still have to support keyboard select, this is the best place
|
||||
// to disable text updating.
|
||||
if (widget.readOnly) {
|
||||
return;
|
||||
}
|
||||
if (value.text != _value.text) {
|
||||
_hideSelectionOverlayIfNeeded();
|
||||
_showCaretOnScreen();
|
||||
@@ -1067,6 +1136,9 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
bool get _hasInputConnection => _textInputConnection != null && _textInputConnection.attached;
|
||||
|
||||
void _openInputConnection() {
|
||||
if (widget.readOnly) {
|
||||
return;
|
||||
}
|
||||
if (!_hasInputConnection) {
|
||||
final TextEditingValue localValue = _value;
|
||||
_lastKnownRemoteTextEditingValue = localValue;
|
||||
@@ -1156,7 +1228,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
dragStartBehavior: widget.dragStartBehavior,
|
||||
onSelectionHandleTapped: widget.onSelectionHandleTapped,
|
||||
);
|
||||
|
||||
_selectionOverlay.handlesVisible = widget.showSelectionHandles;
|
||||
_selectionOverlay.showHandles();
|
||||
if (widget.onSelectionChanged != null)
|
||||
widget.onSelectionChanged(selection, cause);
|
||||
}
|
||||
@@ -1239,7 +1312,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
|
||||
void _onCursorColorTick() {
|
||||
renderEditable.cursorColor = widget.cursorColor.withOpacity(_cursorBlinkOpacityController.value);
|
||||
_cursorVisibilityNotifier.value = _cursorBlinkOpacityController.value > 0;
|
||||
_cursorVisibilityNotifier.value = widget.showCursor && _cursorBlinkOpacityController.value > 0;
|
||||
}
|
||||
|
||||
/// Whether the blinking cursor is actually visible at this precise moment
|
||||
@@ -1409,26 +1482,20 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
}
|
||||
}
|
||||
|
||||
/// Shows the handles at the location of the current selection.
|
||||
void showHandles() {
|
||||
assert(_selectionOverlay != null);
|
||||
_selectionOverlay.showHandles();
|
||||
}
|
||||
|
||||
VoidCallback _semanticsOnCopy(TextSelectionControls controls) {
|
||||
return widget.selectionEnabled && _hasFocus && controls?.canCopy(this) == true
|
||||
return widget.selectionEnabled && copyEnabled && _hasFocus && controls?.canCopy(this) == true
|
||||
? () => controls.handleCopy(this)
|
||||
: null;
|
||||
}
|
||||
|
||||
VoidCallback _semanticsOnCut(TextSelectionControls controls) {
|
||||
return widget.selectionEnabled && _hasFocus && controls?.canCut(this) == true
|
||||
return widget.selectionEnabled && cutEnabled && _hasFocus && controls?.canCut(this) == true
|
||||
? () => controls.handleCut(this)
|
||||
: null;
|
||||
}
|
||||
|
||||
VoidCallback _semanticsOnPaste(TextSelectionControls controls) {
|
||||
return widget.selectionEnabled &&_hasFocus && controls?.canPaste(this) == true
|
||||
return widget.selectionEnabled && pasteEnabled &&_hasFocus && controls?.canPaste(this) == true
|
||||
? () => controls.handlePaste(this)
|
||||
: null;
|
||||
}
|
||||
@@ -1460,7 +1527,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
cursorColor: _cursorColor,
|
||||
backgroundCursorColor: widget.backgroundCursorColor,
|
||||
showCursor: EditableText.debugDeterministicCursor
|
||||
? ValueNotifier<bool>(true)
|
||||
? ValueNotifier<bool>(widget.showCursor)
|
||||
: _cursorVisibilityNotifier,
|
||||
hasFocus: _hasFocus,
|
||||
maxLines: widget.maxLines,
|
||||
@@ -1497,11 +1564,11 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
/// By default makes text in composing range appear as underlined.
|
||||
/// Descendants can override this method to customize appearance of text.
|
||||
TextSpan buildTextSpan() {
|
||||
if (!widget.obscureText && _value.composing.isValid) {
|
||||
// Read only mode should not paint text composing.
|
||||
if (!widget.obscureText && _value.composing.isValid && !widget.readOnly) {
|
||||
final TextStyle composingStyle = widget.style.merge(
|
||||
const TextStyle(decoration: TextDecoration.underline),
|
||||
);
|
||||
|
||||
return TextSpan(
|
||||
style: widget.style,
|
||||
children: <TextSpan>[
|
||||
|
||||
@@ -20,6 +20,7 @@ import 'gesture_detector.dart';
|
||||
import 'overlay.dart';
|
||||
import 'ticker_provider.dart';
|
||||
import 'transitions.dart';
|
||||
import 'visibility.dart';
|
||||
|
||||
export 'package:flutter/services.dart' show TextSelectionDelegate;
|
||||
|
||||
@@ -130,7 +131,7 @@ abstract class TextSelectionControls {
|
||||
/// Subclasses can use this to decide if they should expose the cut
|
||||
/// functionality to the user.
|
||||
bool canCut(TextSelectionDelegate delegate) {
|
||||
return !delegate.textEditingValue.selection.isCollapsed;
|
||||
return delegate.cutEnabled && !delegate.textEditingValue.selection.isCollapsed;
|
||||
}
|
||||
|
||||
/// Whether the current selection of the text field managed by the given
|
||||
@@ -141,7 +142,7 @@ abstract class TextSelectionControls {
|
||||
/// Subclasses can use this to decide if they should expose the copy
|
||||
/// functionality to the user.
|
||||
bool canCopy(TextSelectionDelegate delegate) {
|
||||
return !delegate.textEditingValue.selection.isCollapsed;
|
||||
return delegate.copyEnabled && !delegate.textEditingValue.selection.isCollapsed;
|
||||
}
|
||||
|
||||
/// Whether the current [Clipboard] content can be pasted into the text field
|
||||
@@ -151,7 +152,7 @@ abstract class TextSelectionControls {
|
||||
/// functionality to the user.
|
||||
bool canPaste(TextSelectionDelegate delegate) {
|
||||
// TODO(goderbauer): return false when clipboard is empty, https://github.com/flutter/flutter/issues/11254
|
||||
return true;
|
||||
return delegate.pasteEnabled;
|
||||
}
|
||||
|
||||
/// Whether the current selection of the text field managed by the given
|
||||
@@ -161,7 +162,7 @@ abstract class TextSelectionControls {
|
||||
/// Subclasses can use this to decide if they should expose the select all
|
||||
/// functionality to the user.
|
||||
bool canSelectAll(TextSelectionDelegate delegate) {
|
||||
return delegate.textEditingValue.text.isNotEmpty && delegate.textEditingValue.selection.isCollapsed;
|
||||
return delegate.selectAllEnabled && delegate.textEditingValue.text.isNotEmpty && delegate.textEditingValue.selection.isCollapsed;
|
||||
}
|
||||
|
||||
/// Copy the current selection of the text field managed by the given
|
||||
@@ -267,11 +268,14 @@ class TextSelectionOverlay {
|
||||
@required this.layerLink,
|
||||
@required this.renderObject,
|
||||
this.selectionControls,
|
||||
bool handlesVisible = false,
|
||||
this.selectionDelegate,
|
||||
this.dragStartBehavior = DragStartBehavior.start,
|
||||
this.onSelectionHandleTapped,
|
||||
}) : assert(value != null),
|
||||
assert(context != null),
|
||||
assert(handlesVisible != null),
|
||||
_handlesVisible = handlesVisible,
|
||||
_value = value {
|
||||
final OverlayState overlay = Overlay.of(context);
|
||||
assert(overlay != null,
|
||||
@@ -337,6 +341,10 @@ class TextSelectionOverlay {
|
||||
AnimationController _toolbarController;
|
||||
Animation<double> get _toolbarOpacity => _toolbarController.view;
|
||||
|
||||
/// Retrieve current value.
|
||||
@visibleForTesting
|
||||
TextEditingValue get value => _value;
|
||||
|
||||
TextEditingValue _value;
|
||||
|
||||
/// A pair of handles. If this is non-null, there are always 2, though the
|
||||
@@ -348,16 +356,58 @@ class TextSelectionOverlay {
|
||||
|
||||
TextSelection get _selection => _value.selection;
|
||||
|
||||
/// Shows the handles by inserting them into the [context]'s overlay.
|
||||
/// Whether selection handles are visible.
|
||||
///
|
||||
/// Set to false if you want to hide the handles. Use this property to show or
|
||||
/// hide the handle without rebuilding them.
|
||||
///
|
||||
/// If this method is called while the [SchedulerBinding.schedulerPhase] is
|
||||
/// [SchedulerPhase.persistentCallbacks], i.e. during the build, layout, or
|
||||
/// paint phases (see [WidgetsBinding.drawFrame]), then the update is delayed
|
||||
/// until the post-frame callbacks phase. Otherwise the update is done
|
||||
/// synchronously. This means that it is safe to call during builds, but also
|
||||
/// that if you do call this during a build, the UI will not update until the
|
||||
/// next frame (i.e. many milliseconds later).
|
||||
///
|
||||
/// Defaults to false.
|
||||
bool get handlesVisible => _handlesVisible;
|
||||
bool _handlesVisible = false;
|
||||
set handlesVisible(bool visible) {
|
||||
assert(visible != null);
|
||||
if (_handlesVisible == visible)
|
||||
return;
|
||||
_handlesVisible = visible;
|
||||
// If we are in build state, it will be too late to update visibility.
|
||||
// We will need to schedule the build in next frame.
|
||||
if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.persistentCallbacks) {
|
||||
SchedulerBinding.instance.addPostFrameCallback(_markNeedsBuild);
|
||||
} else {
|
||||
_markNeedsBuild();
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds the handles by inserting them into the [context]'s overlay.
|
||||
void showHandles() {
|
||||
assert(_handles == null);
|
||||
_handles = <OverlayEntry>[
|
||||
OverlayEntry(builder: (BuildContext context) => _buildHandle(context, _TextSelectionHandlePosition.start)),
|
||||
OverlayEntry(builder: (BuildContext context) => _buildHandle(context, _TextSelectionHandlePosition.end)),
|
||||
];
|
||||
|
||||
|
||||
|
||||
Overlay.of(context, debugRequiredFor: debugRequiredFor).insertAll(_handles);
|
||||
}
|
||||
|
||||
/// Destroys the handles by removing them from overlay.
|
||||
void hideHandles() {
|
||||
if (_handles != null) {
|
||||
_handles[0].remove();
|
||||
_handles[1].remove();
|
||||
_handles = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Shows the toolbar by inserting it into the [context]'s overlay.
|
||||
void showToolbar() {
|
||||
assert(_toolbar == null);
|
||||
@@ -403,7 +453,7 @@ class TextSelectionOverlay {
|
||||
}
|
||||
|
||||
/// Whether the handles are currently visible.
|
||||
bool get handlesAreVisible => _handles != null;
|
||||
bool get handlesAreVisible => _handles != null && handlesVisible;
|
||||
|
||||
/// Whether the toolbar is currently visible.
|
||||
bool get toolbarIsVisible => _toolbar != null;
|
||||
@@ -440,16 +490,18 @@ class TextSelectionOverlay {
|
||||
if ((_selection.isCollapsed && position == _TextSelectionHandlePosition.end) ||
|
||||
selectionControls == null)
|
||||
return Container(); // hide the second handle when collapsed
|
||||
return _TextSelectionHandleOverlay(
|
||||
onSelectionHandleChanged: (TextSelection newSelection) { _handleSelectionHandleChanged(newSelection, position); },
|
||||
onSelectionHandleTapped: onSelectionHandleTapped,
|
||||
layerLink: layerLink,
|
||||
renderObject: renderObject,
|
||||
selection: _selection,
|
||||
selectionControls: selectionControls,
|
||||
position: position,
|
||||
dragStartBehavior: dragStartBehavior,
|
||||
);
|
||||
return Visibility(
|
||||
visible: handlesVisible,
|
||||
child: _TextSelectionHandleOverlay(
|
||||
onSelectionHandleChanged: (TextSelection newSelection) { _handleSelectionHandleChanged(newSelection, position); },
|
||||
onSelectionHandleTapped: onSelectionHandleTapped,
|
||||
layerLink: layerLink,
|
||||
renderObject: renderObject,
|
||||
selection: _selection,
|
||||
selectionControls: selectionControls,
|
||||
position: position,
|
||||
dragStartBehavior: dragStartBehavior,
|
||||
));
|
||||
}
|
||||
|
||||
Widget _buildToolbar(BuildContext context) {
|
||||
|
||||
@@ -1151,6 +1151,43 @@ void main() {
|
||||
expect(text.style.fontWeight, FontWeight.w300);
|
||||
});
|
||||
|
||||
testWidgets('Read only text field', (WidgetTester tester) async {
|
||||
final TextEditingController controller = TextEditingController(text: 'readonly');
|
||||
|
||||
await tester.pumpWidget(
|
||||
CupertinoApp(
|
||||
home: Column(
|
||||
children: <Widget>[
|
||||
CupertinoTextField(
|
||||
controller: controller,
|
||||
readOnly: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
// Read only text field cannot open keyboard.
|
||||
await tester.showKeyboard(find.byType(CupertinoTextField));
|
||||
expect(tester.testTextInput.hasAnyClients, false);
|
||||
|
||||
await tester.longPressAt(
|
||||
tester.getTopRight(find.text('readonly'))
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('Paste'), findsNothing);
|
||||
expect(find.text('Cut'), findsNothing);
|
||||
expect(find.text('Select All'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('Select All'));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('Copy'), findsOneWidget);
|
||||
expect(find.text('Paste'), findsNothing);
|
||||
expect(find.text('Cut'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('copy paste', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
CupertinoApp(
|
||||
@@ -2015,7 +2052,7 @@ void main() {
|
||||
expect(controller.selection.isCollapsed, isTrue);
|
||||
expect(controller.selection.baseOffset, 4);
|
||||
await tester.tapAt(ePos, pointer: 7);
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
await tester.pumpAndSettle();
|
||||
expect(controller.selection.baseOffset, 4);
|
||||
expect(controller.selection.extentOffset, 7);
|
||||
|
||||
@@ -2035,7 +2072,6 @@ void main() {
|
||||
await tester.pump();
|
||||
await gesture.moveTo(newHandlePos);
|
||||
await tester.pump();
|
||||
|
||||
expect(controller.selection.baseOffset, 4);
|
||||
expect(controller.selection.extentOffset, 5);
|
||||
|
||||
|
||||
@@ -56,6 +56,19 @@ class WidgetsLocalizationsDelegate extends LocalizationsDelegate<WidgetsLocaliza
|
||||
}
|
||||
|
||||
Widget overlay({ Widget child }) {
|
||||
final OverlayEntry entry = OverlayEntry(
|
||||
builder: (BuildContext context) {
|
||||
return Center(
|
||||
child: Material(
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
return overlayWithEntry(entry);
|
||||
}
|
||||
|
||||
Widget overlayWithEntry(OverlayEntry entry) {
|
||||
return Localizations(
|
||||
locale: const Locale('en', 'US'),
|
||||
delegates: <LocalizationsDelegate<dynamic>>[
|
||||
@@ -68,15 +81,7 @@ Widget overlay({ Widget child }) {
|
||||
data: const MediaQueryData(size: Size(800.0, 600.0)),
|
||||
child: Overlay(
|
||||
initialEntries: <OverlayEntry>[
|
||||
OverlayEntry(
|
||||
builder: (BuildContext context) {
|
||||
return Center(
|
||||
child: Material(
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
entry
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -473,7 +478,7 @@ void main() {
|
||||
expect(state.showToolbar(), true);
|
||||
|
||||
// This is needed for the AnimatedOpacity to turn from 0 to 1 so the toolbar is visible.
|
||||
await tester.pump();
|
||||
await tester.pumpAndSettle();
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
|
||||
// Sanity check that the toolbar widget exists.
|
||||
@@ -813,6 +818,194 @@ void main() {
|
||||
await gesture.removePointer();
|
||||
});
|
||||
|
||||
testWidgets('Read only text field basic', (WidgetTester tester) async {
|
||||
final TextEditingController controller = TextEditingController(text: 'readonly');
|
||||
|
||||
await tester.pumpWidget(
|
||||
overlay(
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
readOnly: true,
|
||||
),
|
||||
)
|
||||
);
|
||||
// Read only text field cannot open keyboard.
|
||||
await tester.showKeyboard(find.byType(TextField));
|
||||
expect(tester.testTextInput.hasAnyClients, false);
|
||||
await skipPastScrollingAnimation(tester);
|
||||
|
||||
expect(controller.selection.isCollapsed, true);
|
||||
|
||||
await tester.tap(find.byType(TextField));
|
||||
await tester.pump();
|
||||
expect(tester.testTextInput.hasAnyClients, false);
|
||||
final EditableTextState editableText = tester.state(find.byType(EditableText));
|
||||
// Collapse selection should not paint.
|
||||
expect(editableText.selectionOverlay.handlesAreVisible, isFalse);
|
||||
// Long press on the 'd' character of text 'readOnly' to show context menu.
|
||||
const int dIndex = 3;
|
||||
final Offset dPos = textOffsetToPosition(tester, dIndex);
|
||||
await tester.longPressAt(dPos);
|
||||
await tester.pump();
|
||||
|
||||
// Context menu should not have paste and cut.
|
||||
expect(find.text('COPY'), findsOneWidget);
|
||||
expect(find.text('PASTE'), findsNothing);
|
||||
expect(find.text('CUT'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('Sawping controllers should update selection', (WidgetTester tester) async {
|
||||
TextEditingController controller = TextEditingController(text: 'readonly');
|
||||
final OverlayEntry entry = OverlayEntry(
|
||||
builder: (BuildContext context) {
|
||||
return Center(
|
||||
child: Material(
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
readOnly: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
await tester.pumpWidget(overlayWithEntry(entry));
|
||||
const int dIndex = 3;
|
||||
final Offset dPos = textOffsetToPosition(tester, dIndex);
|
||||
await tester.longPressAt(dPos);
|
||||
await tester.pumpAndSettle();
|
||||
final EditableTextState state = tester.state(find.byType(EditableText));
|
||||
TextSelection currentOverlaySelection =
|
||||
state.selectionOverlay.value.selection;
|
||||
expect(currentOverlaySelection.baseOffset, 0);
|
||||
expect(currentOverlaySelection.extentOffset, 8);
|
||||
|
||||
// Update selection from [0 to 8] to [1 to 7].
|
||||
controller = TextEditingController.fromValue(
|
||||
controller.value.copyWith(selection: const TextSelection(
|
||||
baseOffset: 1, extentOffset: 7
|
||||
))
|
||||
);
|
||||
|
||||
// Mark entry to be dirty in order to trigger overlay update.
|
||||
entry.markNeedsBuild();
|
||||
|
||||
await tester.pump();
|
||||
currentOverlaySelection = state.selectionOverlay.value.selection;
|
||||
expect(currentOverlaySelection.baseOffset, 1);
|
||||
expect(currentOverlaySelection.extentOffset, 7);
|
||||
});
|
||||
|
||||
testWidgets('Read only text should not compose', (WidgetTester tester) async {
|
||||
final TextEditingController controller = TextEditingController.fromValue(
|
||||
const TextEditingValue(
|
||||
text: 'readonly',
|
||||
composing: TextRange(start: 0, end: 8) // Simulate text composing.
|
||||
)
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
overlay(
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
readOnly: true,
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
final RenderEditable renderEditable = findRenderEditable(tester);
|
||||
// There should be no composing.
|
||||
expect(renderEditable.isComposingText, false);
|
||||
});
|
||||
|
||||
testWidgets('Dynamically switching between read only and not read only should hide or show collapse cursor', (WidgetTester tester) async {
|
||||
final TextEditingController controller = TextEditingController(text: 'readonly');
|
||||
bool readOnly = true;
|
||||
final OverlayEntry entry = OverlayEntry(
|
||||
builder: (BuildContext context) {
|
||||
return Center(
|
||||
child: Material(
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
readOnly: readOnly,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
await tester.pumpWidget(overlayWithEntry(entry));
|
||||
await tester.tap(find.byType(TextField));
|
||||
await tester.pump();
|
||||
|
||||
final EditableTextState editableText = tester.state(find.byType(EditableText));
|
||||
// Collapse selection should not paint.
|
||||
expect(editableText.selectionOverlay.handlesAreVisible, isFalse);
|
||||
|
||||
readOnly = false;
|
||||
// Mark entry to be dirty in order to trigger overlay update.
|
||||
entry.markNeedsBuild();
|
||||
await tester.pumpAndSettle();
|
||||
expect(editableText.selectionOverlay.handlesAreVisible, isTrue);
|
||||
|
||||
readOnly = true;
|
||||
entry.markNeedsBuild();
|
||||
await tester.pumpAndSettle();
|
||||
expect(editableText.selectionOverlay.handlesAreVisible, isFalse);
|
||||
});
|
||||
|
||||
testWidgets('Dynamically switching to read only should close input connection', (WidgetTester tester) async {
|
||||
final TextEditingController controller = TextEditingController(text: 'readonly');
|
||||
bool readOnly = false;
|
||||
final OverlayEntry entry = OverlayEntry(
|
||||
builder: (BuildContext context) {
|
||||
return Center(
|
||||
child: Material(
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
readOnly: readOnly,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
await tester.pumpWidget(overlayWithEntry(entry));
|
||||
await tester.tap(find.byType(TextField));
|
||||
await tester.pump();
|
||||
expect(tester.testTextInput.hasAnyClients, true);
|
||||
|
||||
readOnly = true;
|
||||
// Mark entry to be dirty in order to trigger overlay update.
|
||||
entry.markNeedsBuild();
|
||||
await tester.pump();
|
||||
expect(tester.testTextInput.hasAnyClients, false);
|
||||
});
|
||||
|
||||
testWidgets('Dynamically switching to non read only should open input connection', (WidgetTester tester) async {
|
||||
final TextEditingController controller = TextEditingController(text: 'readonly');
|
||||
bool readOnly = true;
|
||||
final OverlayEntry entry = OverlayEntry(
|
||||
builder: (BuildContext context) {
|
||||
return Center(
|
||||
child: Material(
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
readOnly: readOnly,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
await tester.pumpWidget(overlayWithEntry(entry));
|
||||
await tester.tap(find.byType(TextField));
|
||||
await tester.pump();
|
||||
expect(tester.testTextInput.hasAnyClients, false);
|
||||
|
||||
readOnly = false;
|
||||
// Mark entry to be dirty in order to trigger overlay update.
|
||||
entry.markNeedsBuild();
|
||||
await tester.pump();
|
||||
expect(tester.testTextInput.hasAnyClients, true);
|
||||
});
|
||||
|
||||
testWidgets('enableInteractiveSelection = false, long-press', (WidgetTester tester) async {
|
||||
final TextEditingController controller = TextEditingController();
|
||||
|
||||
@@ -3038,10 +3231,12 @@ void main() {
|
||||
await tester.enterText(find.byType(TextField), testValue);
|
||||
|
||||
await tester.idle();
|
||||
// Need to wait for selection to catch up.
|
||||
await tester.pump();
|
||||
await tester.tap(find.byType(TextField));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
sendKeyEventWithCode(22, true, true, false); // RIGHT_ARROW keydown, SHIFT_ON
|
||||
sendKeyEventWithCode(21, true, true, false); // LEFT_ARROW keydown, SHIFT_ON
|
||||
expect(controller.selection.extentOffset - controller.selection.baseOffset, 1);
|
||||
});
|
||||
|
||||
@@ -3084,17 +3279,19 @@ void main() {
|
||||
await tester.enterText(find.byType(TextField), testValue);
|
||||
|
||||
await tester.idle();
|
||||
// Need to wait for selection to catch up.
|
||||
await tester.pump();
|
||||
await tester.tap(find.byType(TextField));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
sendKeyEventWithCode(20, true, true, false); // DOWN_ARROW keydown
|
||||
sendKeyEventWithCode(19, true, true, false); // UP_ARROW keydown
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(controller.selection.extentOffset - controller.selection.baseOffset, 11);
|
||||
|
||||
sendKeyEventWithCode(20, false, true, false); // DOWN_ARROW keyup
|
||||
sendKeyEventWithCode(19, false, true, false); // UP_ARROW keyup
|
||||
await tester.pumpAndSettle();
|
||||
sendKeyEventWithCode(19, true, true, false); // UP_ARROW keydown
|
||||
sendKeyEventWithCode(20, true, true, false); // DOWN_ARROW keydown
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(controller.selection.extentOffset - controller.selection.baseOffset, 0);
|
||||
@@ -3150,6 +3347,25 @@ void main() {
|
||||
|
||||
expect(controller.selection.extentOffset - controller.selection.baseOffset, 5);
|
||||
});
|
||||
|
||||
testWidgets('Read only keyboard selection test', (WidgetTester tester) async {
|
||||
final TextEditingController controller = TextEditingController(text: 'readonly');
|
||||
await tester.pumpWidget(
|
||||
overlay(
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
readOnly: true,
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
await tester.idle();
|
||||
await tester.tap(find.byType(TextField));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
sendKeyEventWithCode(21, true, true, false); // LEFT_ARROW keydown, SHIFT_ON
|
||||
expect(controller.selection.extentOffset - controller.selection.baseOffset, 1);
|
||||
});
|
||||
});
|
||||
|
||||
const int _kXKeyCode = 52;
|
||||
@@ -3450,11 +3666,13 @@ void main() {
|
||||
await tester.enterText(find.byType(TextField).first, testValue);
|
||||
|
||||
await tester.idle();
|
||||
// Need to wait for selection to catch up.
|
||||
await tester.pump();
|
||||
await tester.tap(find.byType(TextField).first);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
for (int i = 0; i < 5; i += 1) {
|
||||
sendKeyEventWithCode(22, true, true, false); // RIGHT_ARROW keydown
|
||||
sendKeyEventWithCode(21, true, true, false); // LEFT_ARROW keydown
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
@@ -3488,7 +3706,7 @@ void main() {
|
||||
);
|
||||
|
||||
for (int i = 0; i < 5; i += 1) {
|
||||
sendKeyEventWithCode(22, true, true, false); // RIGHT_ARROW keydown
|
||||
sendKeyEventWithCode(21, true, true, false); // LEFT_ARROW keydown
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
@@ -3532,31 +3750,34 @@ void main() {
|
||||
),
|
||||
);
|
||||
|
||||
await tester.idle();
|
||||
await tester.tap(find.byType(TextField).first);
|
||||
|
||||
const String testValue = 'a big house';
|
||||
await tester.enterText(find.byType(TextField).first, testValue);
|
||||
await tester.idle();
|
||||
await tester.pump();
|
||||
|
||||
await tester.idle();
|
||||
await tester.tap(find.byType(TextField).first);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
for (int i = 0; i < 5; i += 1) {
|
||||
sendKeyEventWithCode(22, true, true, false); // RIGHT_ARROW keydown
|
||||
sendKeyEventWithCode(21, true, true, false); // LEFT_ARROW keydown
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
expect(c1.selection.extentOffset - c1.selection.baseOffset, 5);
|
||||
expect(c2.selection.extentOffset - c2.selection.baseOffset, 0);
|
||||
|
||||
await tester.enterText(find.byType(TextField).last, testValue);
|
||||
await tester.idle();
|
||||
await tester.pump();
|
||||
|
||||
await tester.idle();
|
||||
await tester.tap(find.byType(TextField).last);
|
||||
|
||||
await tester.enterText(find.byType(TextField).last, testValue);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
for (int i = 0; i < 5; i += 1) {
|
||||
sendKeyEventWithCode(22, true, true, false); // RIGHT_ARROW keydown
|
||||
sendKeyEventWithCode(21, true, true, false); // LEFT_ARROW keydown
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
@@ -6338,6 +6559,7 @@ void main() {
|
||||
// Tap to position the cursor and show the selection handles.
|
||||
final Offset ePos = textOffsetToPosition(tester, 5); // Index of 'e'.
|
||||
await tester.tapAt(ePos, pointer: 7);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final EditableTextState editableText = tester.state(find.byType(EditableText));
|
||||
expect(editableText.selectionOverlay.toolbarIsVisible, isFalse);
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
import '../rendering/mock_canvas.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Passes textAlign to underlying TextField', (WidgetTester tester) async {
|
||||
@@ -213,4 +216,46 @@ void main() {
|
||||
|
||||
expect(find.text('5 of 10'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('readonly text form field will hide cursor by default', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Material(
|
||||
child: Center(
|
||||
child: TextFormField(
|
||||
initialValue: 'readonly',
|
||||
readOnly: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.showKeyboard(find.byType(TextFormField));
|
||||
expect(tester.testTextInput.hasAnyClients, false);
|
||||
|
||||
await tester.tap(find.byType(TextField));
|
||||
await tester.pump();
|
||||
expect(tester.testTextInput.hasAnyClients, false);
|
||||
|
||||
await tester.longPress(find.byType(TextFormField));
|
||||
await tester.pump();
|
||||
|
||||
// Context menu should not have paste.
|
||||
expect(find.text('SELECT ALL'), findsOneWidget);
|
||||
expect(find.text('PASTE'), findsNothing);
|
||||
|
||||
final EditableTextState editableTextState = tester.firstState(find.byType(EditableText));
|
||||
final RenderEditable renderEditable = editableTextState.renderEditable;
|
||||
|
||||
// Make sure it does not paint caret for a period of time.
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
expect(renderEditable, paintsExactlyCountTimes(#drawRect, 0));
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
expect(renderEditable, paintsExactlyCountTimes(#drawRect, 0));
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
expect(renderEditable, paintsExactlyCountTimes(#drawRect, 0));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -318,6 +318,34 @@ void main() {
|
||||
EditableText.debugDeterministicCursor = false;
|
||||
});
|
||||
|
||||
testWidgets('Cursor does not show when showCursor set to false', (WidgetTester tester) async {
|
||||
const Widget widget = MaterialApp(
|
||||
home: Material(
|
||||
child: TextField(
|
||||
showCursor: false,
|
||||
maxLines: 3,
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpWidget(widget);
|
||||
|
||||
await tester.tap(find.byType(TextField));
|
||||
await tester.pump();
|
||||
|
||||
final EditableTextState editableTextState = tester.firstState(find.byType(EditableText));
|
||||
final RenderEditable renderEditable = editableTextState.renderEditable;
|
||||
|
||||
// Make sure it does not paint for a period of time.
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
expect(renderEditable, paintsExactlyCountTimes(#drawRect, 0));
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
expect(renderEditable, paintsExactlyCountTimes(#drawRect, 0));
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
expect(renderEditable, paintsExactlyCountTimes(#drawRect, 0));
|
||||
});
|
||||
|
||||
testWidgets('Cursor radius is 2.0 on iOS', (WidgetTester tester) async {
|
||||
final Widget widget = MaterialApp(
|
||||
theme: ThemeData(platform: TargetPlatform.iOS),
|
||||
|
||||
@@ -1893,6 +1893,7 @@ void main() {
|
||||
child: SizedBox(
|
||||
width: 100,
|
||||
child: EditableText(
|
||||
showSelectionHandles: true,
|
||||
controller: controller,
|
||||
focusNode: FocusNode(),
|
||||
style: Typography(platform: TargetPlatform.android).black.subhead,
|
||||
@@ -2003,7 +2004,7 @@ void main() {
|
||||
throw TestFailure('HandlePositionInViewport can\'t be null.');
|
||||
}
|
||||
}
|
||||
|
||||
expect(state.selectionOverlay.handlesAreVisible, isTrue);
|
||||
testPosition(container[0].offset.dx, leftPosition);
|
||||
testPosition(container[1].offset.dx, rightPosition);
|
||||
}
|
||||
@@ -2011,7 +2012,6 @@ void main() {
|
||||
// Select the first word. Both handles should be visible.
|
||||
await tester.tapAt(const Offset(20, 10));
|
||||
renderEditable.selectWord(cause: SelectionChangedCause.longPress);
|
||||
state.showHandles();
|
||||
await tester.pump();
|
||||
await verifyVisibility(HandlePositionInViewport.leftEdge, true, HandlePositionInViewport.within, true);
|
||||
|
||||
@@ -2033,7 +2033,6 @@ void main() {
|
||||
// Now that the second word has been dragged fully into view, select it.
|
||||
await tester.tapAt(const Offset(80, 10));
|
||||
renderEditable.selectWord(cause: SelectionChangedCause.longPress);
|
||||
state.showHandles();
|
||||
await tester.pump();
|
||||
await verifyVisibility(HandlePositionInViewport.within, true, HandlePositionInViewport.within, true);
|
||||
|
||||
@@ -2060,6 +2059,7 @@ void main() {
|
||||
width: 100,
|
||||
child: EditableText(
|
||||
controller: controller,
|
||||
showSelectionHandles: true,
|
||||
focusNode: FocusNode(),
|
||||
style: Typography(platform: TargetPlatform.android).black.subhead,
|
||||
cursorColor: Colors.blue,
|
||||
@@ -2078,7 +2078,6 @@ void main() {
|
||||
// Select the first word. Both handles should be visible.
|
||||
await tester.tapAt(const Offset(20, 10));
|
||||
state.renderEditable.selectWord(cause: SelectionChangedCause.longPress);
|
||||
state.showHandles();
|
||||
await tester.pump();
|
||||
final List<CompositedTransformFollower> container =
|
||||
find.byType(CompositedTransformFollower)
|
||||
@@ -2100,6 +2099,7 @@ void main() {
|
||||
70.0 + kMinInteractiveSize,
|
||||
),
|
||||
);
|
||||
expect(state.selectionOverlay.handlesAreVisible, isTrue);
|
||||
expect(controller.selection.base.offset, 0);
|
||||
expect(controller.selection.extent.offset, 5);
|
||||
});
|
||||
@@ -2119,6 +2119,7 @@ void main() {
|
||||
child: SizedBox(
|
||||
width: 100,
|
||||
child: EditableText(
|
||||
showSelectionHandles: true,
|
||||
controller: controller,
|
||||
focusNode: FocusNode(),
|
||||
style: Typography(platform: TargetPlatform.iOS).black.subhead,
|
||||
@@ -2228,7 +2229,7 @@ void main() {
|
||||
throw TestFailure('HandlePositionInViewport can\'t be null.');
|
||||
}
|
||||
}
|
||||
|
||||
expect(state.selectionOverlay.handlesAreVisible, isTrue);
|
||||
testPosition(container[0].offset.dx, leftPosition);
|
||||
testPosition(container[1].offset.dx, rightPosition);
|
||||
}
|
||||
@@ -2236,7 +2237,6 @@ void main() {
|
||||
// Select the first word. Both handles should be visible.
|
||||
await tester.tapAt(const Offset(20, 10));
|
||||
renderEditable.selectWord(cause: SelectionChangedCause.longPress);
|
||||
state.showHandles();
|
||||
await tester.pump();
|
||||
await verifyVisibility(HandlePositionInViewport.leftEdge, true, HandlePositionInViewport.within, true);
|
||||
|
||||
@@ -2258,7 +2258,6 @@ void main() {
|
||||
// Now that the second word has been dragged fully into view, select it.
|
||||
await tester.tapAt(const Offset(80, 10));
|
||||
renderEditable.selectWord(cause: SelectionChangedCause.longPress);
|
||||
state.showHandles();
|
||||
await tester.pump();
|
||||
await verifyVisibility(HandlePositionInViewport.within, true, HandlePositionInViewport.within, true);
|
||||
|
||||
|
||||
@@ -58,6 +58,9 @@ class TestTextInput {
|
||||
bool get isRegistered => _isRegistered;
|
||||
bool _isRegistered = false;
|
||||
|
||||
/// Whether there are any active clients listening to text input.
|
||||
bool get hasAnyClients => _client > 0;
|
||||
|
||||
int _client = 0;
|
||||
|
||||
/// Arguments supplied to the TextInput.setClient method call.
|
||||
|
||||
Reference in New Issue
Block a user