Unverified Commit 730025fa authored by chunhtai's avatar chunhtai Committed by GitHub

fix issue 14014 read only text field (#32059)

parent 54410441
......@@ -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);
......
......@@ -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.
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment