Disallow copy and cut when text field is obscured. (#96309)
Before this change, it was possible to select and copy obscured text from a text field. This changes things so that: - Obscured text fields don't allow copy or cut. - If a field is both obscured and read-only, then selection is disabled as well (if you can't modify it, and can't copy it, there's no point in selecting it).
This commit is contained in:
parent
e25e1f9037
commit
a9e0dd40dc
@ -289,7 +289,7 @@ class CupertinoTextField extends StatefulWidget {
|
|||||||
this.keyboardAppearance,
|
this.keyboardAppearance,
|
||||||
this.scrollPadding = const EdgeInsets.all(20.0),
|
this.scrollPadding = const EdgeInsets.all(20.0),
|
||||||
this.dragStartBehavior = DragStartBehavior.start,
|
this.dragStartBehavior = DragStartBehavior.start,
|
||||||
this.enableInteractiveSelection = true,
|
bool? enableInteractiveSelection,
|
||||||
this.selectionControls,
|
this.selectionControls,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
this.scrollController,
|
this.scrollController,
|
||||||
@ -341,17 +341,31 @@ class CupertinoTextField extends StatefulWidget {
|
|||||||
),
|
),
|
||||||
assert(enableIMEPersonalizedLearning != null),
|
assert(enableIMEPersonalizedLearning != null),
|
||||||
keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline),
|
keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline),
|
||||||
toolbarOptions = toolbarOptions ?? (obscureText ?
|
enableInteractiveSelection = enableInteractiveSelection ?? (!readOnly || !obscureText),
|
||||||
const ToolbarOptions(
|
toolbarOptions = toolbarOptions ??
|
||||||
selectAll: true,
|
(obscureText
|
||||||
paste: true,
|
? (readOnly
|
||||||
) :
|
// No point in even offering "Select All" in a read-only obscured
|
||||||
const ToolbarOptions(
|
// field.
|
||||||
copy: true,
|
? const ToolbarOptions()
|
||||||
cut: true,
|
// Writable, but obscured.
|
||||||
selectAll: true,
|
: const ToolbarOptions(
|
||||||
paste: true,
|
selectAll: true,
|
||||||
)),
|
paste: true,
|
||||||
|
))
|
||||||
|
: (readOnly
|
||||||
|
// Read-only, not obscured.
|
||||||
|
? const ToolbarOptions(
|
||||||
|
selectAll: true,
|
||||||
|
copy: true,
|
||||||
|
)
|
||||||
|
// Writable, not obscured.
|
||||||
|
: const ToolbarOptions(
|
||||||
|
copy: true,
|
||||||
|
cut: true,
|
||||||
|
selectAll: true,
|
||||||
|
paste: true,
|
||||||
|
))),
|
||||||
super(key: key);
|
super(key: key);
|
||||||
|
|
||||||
/// Creates a borderless iOS-style text field.
|
/// Creates a borderless iOS-style text field.
|
||||||
@ -446,7 +460,7 @@ class CupertinoTextField extends StatefulWidget {
|
|||||||
this.keyboardAppearance,
|
this.keyboardAppearance,
|
||||||
this.scrollPadding = const EdgeInsets.all(20.0),
|
this.scrollPadding = const EdgeInsets.all(20.0),
|
||||||
this.dragStartBehavior = DragStartBehavior.start,
|
this.dragStartBehavior = DragStartBehavior.start,
|
||||||
this.enableInteractiveSelection = true,
|
bool? enableInteractiveSelection,
|
||||||
this.selectionControls,
|
this.selectionControls,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
this.scrollController,
|
this.scrollController,
|
||||||
@ -499,17 +513,31 @@ class CupertinoTextField extends StatefulWidget {
|
|||||||
assert(clipBehavior != null),
|
assert(clipBehavior != null),
|
||||||
assert(enableIMEPersonalizedLearning != null),
|
assert(enableIMEPersonalizedLearning != null),
|
||||||
keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline),
|
keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline),
|
||||||
toolbarOptions = toolbarOptions ?? (obscureText ?
|
enableInteractiveSelection = enableInteractiveSelection ?? (!readOnly || !obscureText),
|
||||||
const ToolbarOptions(
|
toolbarOptions = toolbarOptions ??
|
||||||
selectAll: true,
|
(obscureText
|
||||||
paste: true,
|
? (readOnly
|
||||||
) :
|
// No point in even offering "Select All" in a read-only obscured
|
||||||
const ToolbarOptions(
|
// field.
|
||||||
copy: true,
|
? const ToolbarOptions()
|
||||||
cut: true,
|
// Writable, but obscured.
|
||||||
selectAll: true,
|
: const ToolbarOptions(
|
||||||
paste: true,
|
selectAll: true,
|
||||||
)),
|
paste: true,
|
||||||
|
))
|
||||||
|
: (readOnly
|
||||||
|
// Read-only, not obscured.
|
||||||
|
? const ToolbarOptions(
|
||||||
|
selectAll: true,
|
||||||
|
copy: true,
|
||||||
|
)
|
||||||
|
// Writable, not obscured.
|
||||||
|
: const ToolbarOptions(
|
||||||
|
copy: true,
|
||||||
|
cut: true,
|
||||||
|
selectAll: true,
|
||||||
|
paste: true,
|
||||||
|
))),
|
||||||
super(key: key);
|
super(key: key);
|
||||||
|
|
||||||
/// Controls the text being edited.
|
/// Controls the text being edited.
|
||||||
|
@ -319,7 +319,7 @@ class TextField extends StatefulWidget {
|
|||||||
this.keyboardAppearance,
|
this.keyboardAppearance,
|
||||||
this.scrollPadding = const EdgeInsets.all(20.0),
|
this.scrollPadding = const EdgeInsets.all(20.0),
|
||||||
this.dragStartBehavior = DragStartBehavior.start,
|
this.dragStartBehavior = DragStartBehavior.start,
|
||||||
this.enableInteractiveSelection = true,
|
bool? enableInteractiveSelection,
|
||||||
this.selectionControls,
|
this.selectionControls,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
this.mouseCursor,
|
this.mouseCursor,
|
||||||
@ -339,7 +339,6 @@ class TextField extends StatefulWidget {
|
|||||||
smartDashesType = smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled),
|
smartDashesType = smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled),
|
||||||
smartQuotesType = smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled),
|
smartQuotesType = smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled),
|
||||||
assert(enableSuggestions != null),
|
assert(enableSuggestions != null),
|
||||||
assert(enableInteractiveSelection != null),
|
|
||||||
assert(maxLengthEnforced != null),
|
assert(maxLengthEnforced != null),
|
||||||
assert(
|
assert(
|
||||||
maxLengthEnforced || maxLengthEnforcement == null,
|
maxLengthEnforced || maxLengthEnforcement == null,
|
||||||
@ -372,17 +371,31 @@ class TextField extends StatefulWidget {
|
|||||||
assert(clipBehavior != null),
|
assert(clipBehavior != null),
|
||||||
assert(enableIMEPersonalizedLearning != null),
|
assert(enableIMEPersonalizedLearning != null),
|
||||||
keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline),
|
keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline),
|
||||||
toolbarOptions = toolbarOptions ?? (obscureText ?
|
enableInteractiveSelection = enableInteractiveSelection ?? (!readOnly || !obscureText),
|
||||||
const ToolbarOptions(
|
toolbarOptions = toolbarOptions ??
|
||||||
selectAll: true,
|
(obscureText
|
||||||
paste: true,
|
? (readOnly
|
||||||
) :
|
// No point in even offering "Select All" in a read-only obscured
|
||||||
const ToolbarOptions(
|
// field.
|
||||||
copy: true,
|
? const ToolbarOptions()
|
||||||
cut: true,
|
// Writable, but obscured.
|
||||||
selectAll: true,
|
: const ToolbarOptions(
|
||||||
paste: true,
|
selectAll: true,
|
||||||
)),
|
paste: true,
|
||||||
|
))
|
||||||
|
: (readOnly
|
||||||
|
// Read-only, not obscured.
|
||||||
|
? const ToolbarOptions(
|
||||||
|
selectAll: true,
|
||||||
|
copy: true,
|
||||||
|
)
|
||||||
|
// Writable, not obscured.
|
||||||
|
: const ToolbarOptions(
|
||||||
|
copy: true,
|
||||||
|
cut: true,
|
||||||
|
selectAll: true,
|
||||||
|
paste: true,
|
||||||
|
))),
|
||||||
super(key: key);
|
super(key: key);
|
||||||
|
|
||||||
/// Controls the text being edited.
|
/// Controls the text being edited.
|
||||||
|
@ -143,7 +143,7 @@ class TextFormField extends FormField<String> {
|
|||||||
Color? cursorColor,
|
Color? cursorColor,
|
||||||
Brightness? keyboardAppearance,
|
Brightness? keyboardAppearance,
|
||||||
EdgeInsets scrollPadding = const EdgeInsets.all(20.0),
|
EdgeInsets scrollPadding = const EdgeInsets.all(20.0),
|
||||||
bool enableInteractiveSelection = true,
|
bool? enableInteractiveSelection,
|
||||||
TextSelectionControls? selectionControls,
|
TextSelectionControls? selectionControls,
|
||||||
InputCounterWidgetBuilder? buildCounter,
|
InputCounterWidgetBuilder? buildCounter,
|
||||||
ScrollPhysics? scrollPhysics,
|
ScrollPhysics? scrollPhysics,
|
||||||
@ -179,7 +179,6 @@ class TextFormField extends FormField<String> {
|
|||||||
),
|
),
|
||||||
assert(!obscureText || maxLines == 1, 'Obscured fields cannot be multiline.'),
|
assert(!obscureText || maxLines == 1, 'Obscured fields cannot be multiline.'),
|
||||||
assert(maxLength == null || maxLength == TextField.noMaxLength || maxLength > 0),
|
assert(maxLength == null || maxLength == TextField.noMaxLength || maxLength > 0),
|
||||||
assert(enableInteractiveSelection != null),
|
|
||||||
assert(enableIMEPersonalizedLearning != null),
|
assert(enableIMEPersonalizedLearning != null),
|
||||||
super(
|
super(
|
||||||
key: key,
|
key: key,
|
||||||
@ -243,7 +242,7 @@ class TextFormField extends FormField<String> {
|
|||||||
scrollPadding: scrollPadding,
|
scrollPadding: scrollPadding,
|
||||||
scrollPhysics: scrollPhysics,
|
scrollPhysics: scrollPhysics,
|
||||||
keyboardAppearance: keyboardAppearance,
|
keyboardAppearance: keyboardAppearance,
|
||||||
enableInteractiveSelection: enableInteractiveSelection,
|
enableInteractiveSelection: enableInteractiveSelection ?? (!obscureText || !readOnly),
|
||||||
selectionControls: selectionControls,
|
selectionControls: selectionControls,
|
||||||
buildCounter: buildCounter,
|
buildCounter: buildCounter,
|
||||||
autofillHints: autofillHints,
|
autofillHints: autofillHints,
|
||||||
|
@ -508,16 +508,11 @@ class EditableText extends StatefulWidget {
|
|||||||
this.scrollPadding = const EdgeInsets.all(20.0),
|
this.scrollPadding = const EdgeInsets.all(20.0),
|
||||||
this.keyboardAppearance = Brightness.light,
|
this.keyboardAppearance = Brightness.light,
|
||||||
this.dragStartBehavior = DragStartBehavior.start,
|
this.dragStartBehavior = DragStartBehavior.start,
|
||||||
this.enableInteractiveSelection = true,
|
bool? enableInteractiveSelection,
|
||||||
this.scrollController,
|
this.scrollController,
|
||||||
this.scrollPhysics,
|
this.scrollPhysics,
|
||||||
this.autocorrectionTextRectColor,
|
this.autocorrectionTextRectColor,
|
||||||
this.toolbarOptions = const ToolbarOptions(
|
ToolbarOptions? toolbarOptions,
|
||||||
copy: true,
|
|
||||||
cut: true,
|
|
||||||
paste: true,
|
|
||||||
selectAll: true,
|
|
||||||
),
|
|
||||||
this.autofillHints = const <String>[],
|
this.autofillHints = const <String>[],
|
||||||
this.autofillClient,
|
this.autofillClient,
|
||||||
this.clipBehavior = Clip.hardEdge,
|
this.clipBehavior = Clip.hardEdge,
|
||||||
@ -533,7 +528,6 @@ class EditableText extends StatefulWidget {
|
|||||||
smartQuotesType = smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled),
|
smartQuotesType = smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled),
|
||||||
assert(enableSuggestions != null),
|
assert(enableSuggestions != null),
|
||||||
assert(showSelectionHandles != null),
|
assert(showSelectionHandles != null),
|
||||||
assert(enableInteractiveSelection != null),
|
|
||||||
assert(readOnly != null),
|
assert(readOnly != null),
|
||||||
assert(forceLine != null),
|
assert(forceLine != null),
|
||||||
assert(style != null),
|
assert(style != null),
|
||||||
@ -560,7 +554,31 @@ class EditableText extends StatefulWidget {
|
|||||||
assert(rendererIgnoresPointer != null),
|
assert(rendererIgnoresPointer != null),
|
||||||
assert(scrollPadding != null),
|
assert(scrollPadding != null),
|
||||||
assert(dragStartBehavior != null),
|
assert(dragStartBehavior != null),
|
||||||
assert(toolbarOptions != null),
|
enableInteractiveSelection = enableInteractiveSelection ?? (!readOnly || !obscureText),
|
||||||
|
toolbarOptions = toolbarOptions ??
|
||||||
|
(obscureText
|
||||||
|
? (readOnly
|
||||||
|
// No point in even offering "Select All" in a read-only obscured
|
||||||
|
// field.
|
||||||
|
? const ToolbarOptions()
|
||||||
|
// Writable, but obscured.
|
||||||
|
: const ToolbarOptions(
|
||||||
|
selectAll: true,
|
||||||
|
paste: true,
|
||||||
|
))
|
||||||
|
: (readOnly
|
||||||
|
// Read-only, not obscured.
|
||||||
|
? const ToolbarOptions(
|
||||||
|
selectAll: true,
|
||||||
|
copy: true,
|
||||||
|
)
|
||||||
|
// Writable, not obscured.
|
||||||
|
: const ToolbarOptions(
|
||||||
|
copy: true,
|
||||||
|
cut: true,
|
||||||
|
selectAll: true,
|
||||||
|
paste: true,
|
||||||
|
))),
|
||||||
assert(clipBehavior != null),
|
assert(clipBehavior != null),
|
||||||
assert(enableIMEPersonalizedLearning != null),
|
assert(enableIMEPersonalizedLearning != null),
|
||||||
_strutStyle = strutStyle,
|
_strutStyle = strutStyle,
|
||||||
@ -593,7 +611,9 @@ class EditableText extends StatefulWidget {
|
|||||||
/// Whether to hide the text being edited (e.g., for passwords).
|
/// Whether to hide the text being edited (e.g., for passwords).
|
||||||
///
|
///
|
||||||
/// When this is set to true, all the characters in the text field are
|
/// When this is set to true, all the characters in the text field are
|
||||||
/// replaced by [obscuringCharacter].
|
/// replaced by [obscuringCharacter], and the text in the field cannot be
|
||||||
|
/// copied with copy or cut. If [readOnly] is also true, then the text cannot
|
||||||
|
/// be selected.
|
||||||
///
|
///
|
||||||
/// Defaults to false. Cannot be null.
|
/// Defaults to false. Cannot be null.
|
||||||
/// {@endtemplate}
|
/// {@endtemplate}
|
||||||
@ -629,8 +649,10 @@ class EditableText extends StatefulWidget {
|
|||||||
|
|
||||||
/// Configuration of toolbar options.
|
/// Configuration of toolbar options.
|
||||||
///
|
///
|
||||||
/// By default, all options are enabled. If [readOnly] is true,
|
/// By default, all options are enabled. If [readOnly] is true, paste and cut
|
||||||
/// paste and cut will be disabled regardless.
|
/// will be disabled regardless. If [obscureText] is true, cut and copy will
|
||||||
|
/// be disabled regardless. If [readOnly] and [obscureText] are both true,
|
||||||
|
/// select all will also be disabled.
|
||||||
final ToolbarOptions toolbarOptions;
|
final ToolbarOptions toolbarOptions;
|
||||||
|
|
||||||
/// Whether to show selection handles.
|
/// Whether to show selection handles.
|
||||||
@ -1492,6 +1514,7 @@ class EditableText extends StatefulWidget {
|
|||||||
properties.add(DiagnosticsProperty<TextEditingController>('controller', controller));
|
properties.add(DiagnosticsProperty<TextEditingController>('controller', controller));
|
||||||
properties.add(DiagnosticsProperty<FocusNode>('focusNode', focusNode));
|
properties.add(DiagnosticsProperty<FocusNode>('focusNode', focusNode));
|
||||||
properties.add(DiagnosticsProperty<bool>('obscureText', obscureText, defaultValue: false));
|
properties.add(DiagnosticsProperty<bool>('obscureText', obscureText, defaultValue: false));
|
||||||
|
properties.add(DiagnosticsProperty<bool>('readOnly', readOnly, defaultValue: false));
|
||||||
properties.add(DiagnosticsProperty<bool>('autocorrect', autocorrect, defaultValue: true));
|
properties.add(DiagnosticsProperty<bool>('autocorrect', autocorrect, defaultValue: true));
|
||||||
properties.add(EnumProperty<SmartDashesType>('smartDashesType', smartDashesType, defaultValue: obscureText ? SmartDashesType.disabled : SmartDashesType.enabled));
|
properties.add(EnumProperty<SmartDashesType>('smartDashesType', smartDashesType, defaultValue: obscureText ? SmartDashesType.disabled : SmartDashesType.enabled));
|
||||||
properties.add(EnumProperty<SmartQuotesType>('smartQuotesType', smartQuotesType, defaultValue: obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled));
|
properties.add(EnumProperty<SmartQuotesType>('smartQuotesType', smartQuotesType, defaultValue: obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled));
|
||||||
@ -1511,6 +1534,7 @@ class EditableText extends StatefulWidget {
|
|||||||
properties.add(DiagnosticsProperty<Iterable<String>>('autofillHints', autofillHints, defaultValue: null));
|
properties.add(DiagnosticsProperty<Iterable<String>>('autofillHints', autofillHints, defaultValue: null));
|
||||||
properties.add(DiagnosticsProperty<TextHeightBehavior>('textHeightBehavior', textHeightBehavior, defaultValue: null));
|
properties.add(DiagnosticsProperty<TextHeightBehavior>('textHeightBehavior', textHeightBehavior, defaultValue: null));
|
||||||
properties.add(DiagnosticsProperty<bool>('enableIMEPersonalizedLearning', enableIMEPersonalizedLearning, defaultValue: true));
|
properties.add(DiagnosticsProperty<bool>('enableIMEPersonalizedLearning', enableIMEPersonalizedLearning, defaultValue: true));
|
||||||
|
properties.add(DiagnosticsProperty<bool>('enableInteractiveSelection', enableInteractiveSelection, defaultValue: true));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1573,16 +1597,16 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
Color get _cursorColor => widget.cursorColor.withOpacity(_cursorBlinkOpacityController!.value);
|
Color get _cursorColor => widget.cursorColor.withOpacity(_cursorBlinkOpacityController!.value);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool get cutEnabled => widget.toolbarOptions.cut && !widget.readOnly;
|
bool get cutEnabled => widget.toolbarOptions.cut && !widget.readOnly && !widget.obscureText;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool get copyEnabled => widget.toolbarOptions.copy;
|
bool get copyEnabled => widget.toolbarOptions.copy && !widget.obscureText;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool get pasteEnabled => widget.toolbarOptions.paste && !widget.readOnly;
|
bool get pasteEnabled => widget.toolbarOptions.paste && !widget.readOnly;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool get selectAllEnabled => widget.toolbarOptions.selectAll;
|
bool get selectAllEnabled => widget.toolbarOptions.selectAll && (!widget.readOnly || !widget.obscureText) && widget.enableInteractiveSelection;
|
||||||
|
|
||||||
void _onChangedClipboardStatus() {
|
void _onChangedClipboardStatus() {
|
||||||
setState(() {
|
setState(() {
|
||||||
@ -1602,11 +1626,11 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
@override
|
@override
|
||||||
void copySelection(SelectionChangedCause cause) {
|
void copySelection(SelectionChangedCause cause) {
|
||||||
final TextSelection selection = textEditingValue.selection;
|
final TextSelection selection = textEditingValue.selection;
|
||||||
final String text = textEditingValue.text;
|
|
||||||
assert(selection != null);
|
assert(selection != null);
|
||||||
if (selection.isCollapsed) {
|
if (selection.isCollapsed || widget.obscureText) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
final String text = textEditingValue.text;
|
||||||
Clipboard.setData(ClipboardData(text: selection.textInside(text)));
|
Clipboard.setData(ClipboardData(text: selection.textInside(text)));
|
||||||
if (cause == SelectionChangedCause.toolbar) {
|
if (cause == SelectionChangedCause.toolbar) {
|
||||||
bringIntoView(textEditingValue.selection.extent);
|
bringIntoView(textEditingValue.selection.extent);
|
||||||
@ -1636,7 +1660,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
/// Cut current selection to [Clipboard].
|
/// Cut current selection to [Clipboard].
|
||||||
@override
|
@override
|
||||||
void cutSelection(SelectionChangedCause cause) {
|
void cutSelection(SelectionChangedCause cause) {
|
||||||
if (widget.readOnly) {
|
if (widget.readOnly || widget.obscureText) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final TextSelection selection = textEditingValue.selection;
|
final TextSelection selection = textEditingValue.selection;
|
||||||
@ -1681,6 +1705,11 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
/// Select the entire text value.
|
/// Select the entire text value.
|
||||||
@override
|
@override
|
||||||
void selectAll(SelectionChangedCause cause) {
|
void selectAll(SelectionChangedCause cause) {
|
||||||
|
if (widget.readOnly && widget.obscureText) {
|
||||||
|
// If we can't modify it, and we can't copy it, there's no point in
|
||||||
|
// selecting it.
|
||||||
|
return;
|
||||||
|
}
|
||||||
userUpdateTextEditingValue(
|
userUpdateTextEditingValue(
|
||||||
textEditingValue.copyWith(
|
textEditingValue.copyWith(
|
||||||
selection: TextSelection(baseOffset: 0, extentOffset: textEditingValue.text.length),
|
selection: TextSelection(baseOffset: 0, extentOffset: textEditingValue.text.length),
|
||||||
@ -3057,7 +3086,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
selectionHeightStyle: widget.selectionHeightStyle,
|
selectionHeightStyle: widget.selectionHeightStyle,
|
||||||
selectionWidthStyle: widget.selectionWidthStyle,
|
selectionWidthStyle: widget.selectionWidthStyle,
|
||||||
paintCursorAboveText: widget.paintCursorAboveText,
|
paintCursorAboveText: widget.paintCursorAboveText,
|
||||||
enableInteractiveSelection: widget.enableInteractiveSelection,
|
enableInteractiveSelection: widget.enableInteractiveSelection && (!widget.readOnly || !widget.obscureText),
|
||||||
textSelectionDelegate: this,
|
textSelectionDelegate: this,
|
||||||
devicePixelRatio: _devicePixelRatio,
|
devicePixelRatio: _devicePixelRatio,
|
||||||
promptRectRange: _currentPromptRectRange,
|
promptRectRange: _currentPromptRectRange,
|
||||||
@ -3287,6 +3316,7 @@ class _Editable extends MultiChildRenderObjectWidget {
|
|||||||
..cursorOffset = cursorOffset
|
..cursorOffset = cursorOffset
|
||||||
..selectionHeightStyle = selectionHeightStyle
|
..selectionHeightStyle = selectionHeightStyle
|
||||||
..selectionWidthStyle = selectionWidthStyle
|
..selectionWidthStyle = selectionWidthStyle
|
||||||
|
..enableInteractiveSelection = enableInteractiveSelection
|
||||||
..textSelectionDelegate = textSelectionDelegate
|
..textSelectionDelegate = textSelectionDelegate
|
||||||
..devicePixelRatio = devicePixelRatio
|
..devicePixelRatio = devicePixelRatio
|
||||||
..paintCursorAboveText = paintCursorAboveText
|
..paintCursorAboveText = paintCursorAboveText
|
||||||
|
@ -1747,6 +1747,44 @@ void main() {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'double tap does not select word on read-only obscured field',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
final TextEditingController controller = TextEditingController(
|
||||||
|
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||||||
|
);
|
||||||
|
await tester.pumpWidget(
|
||||||
|
CupertinoApp(
|
||||||
|
home: Center(
|
||||||
|
child: CupertinoTextField(
|
||||||
|
readOnly: true,
|
||||||
|
obscureText: true,
|
||||||
|
controller: controller,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Long press to put the cursor after the "w".
|
||||||
|
const int index = 3;
|
||||||
|
await tester.longPressAt(textOffsetToPosition(tester, index));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
// Second tap doesn't select anything.
|
||||||
|
await tester.tapAt(textOffsetToPosition(tester, index));
|
||||||
|
await tester.pump(const Duration(milliseconds: 50));
|
||||||
|
await tester.tapAt(textOffsetToPosition(tester, index));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(
|
||||||
|
controller.selection,
|
||||||
|
const TextSelection.collapsed(offset: 35),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Selected text shows nothing.
|
||||||
|
expect(find.byType(CupertinoButton), findsNothing);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
testWidgets('Readonly text field does not have tap action', (WidgetTester tester) async {
|
testWidgets('Readonly text field does not have tap action', (WidgetTester tester) async {
|
||||||
final SemanticsTester semantics = SemanticsTester(tester);
|
final SemanticsTester semantics = SemanticsTester(tester);
|
||||||
|
|
||||||
@ -2132,6 +2170,54 @@ void main() {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'A read-only obscured CupertinoTextField is not selectable',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
final TextEditingController controller = TextEditingController(
|
||||||
|
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||||||
|
);
|
||||||
|
await tester.pumpWidget(
|
||||||
|
CupertinoApp(
|
||||||
|
home: Center(
|
||||||
|
child: CupertinoTextField(
|
||||||
|
controller: controller,
|
||||||
|
obscureText: true,
|
||||||
|
readOnly: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Offset textFieldStart = tester.getTopLeft(find.byType(CupertinoTextField));
|
||||||
|
|
||||||
|
await tester.tapAt(textFieldStart + const Offset(150.0, 5.0));
|
||||||
|
await tester.pump(const Duration(milliseconds: 50));
|
||||||
|
final TestGesture gesture =
|
||||||
|
await tester.startGesture(textFieldStart + const Offset(150.0, 5.0));
|
||||||
|
// Hold the press.
|
||||||
|
await tester.pump(const Duration(milliseconds: 500));
|
||||||
|
|
||||||
|
// Nothing is selected despite the double tap long press gesture.
|
||||||
|
expect(
|
||||||
|
controller.selection,
|
||||||
|
const TextSelection(baseOffset: 35, extentOffset: 35),
|
||||||
|
);
|
||||||
|
|
||||||
|
// The selection menu is not present.
|
||||||
|
expect(find.byType(CupertinoButton), findsNWidgets(0));
|
||||||
|
|
||||||
|
await gesture.up();
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
// Still nothing selected and no selection menu.
|
||||||
|
expect(
|
||||||
|
controller.selection,
|
||||||
|
const TextSelection.collapsed(offset: 35),
|
||||||
|
);
|
||||||
|
expect(find.byType(CupertinoButton), findsNWidgets(0));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
testWidgets(
|
testWidgets(
|
||||||
'An obscured CupertinoTextField is selectable by default',
|
'An obscured CupertinoTextField is selectable by default',
|
||||||
(WidgetTester tester) async {
|
(WidgetTester tester) async {
|
||||||
|
@ -2454,6 +2454,34 @@ void main() {
|
|||||||
expect(controller.selection.isCollapsed, true);
|
expect(controller.selection.isCollapsed, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('An obscured TextField is not selectable when read-only', (WidgetTester tester) async {
|
||||||
|
// This is a regression test for
|
||||||
|
// https://github.com/flutter/flutter/issues/32845
|
||||||
|
|
||||||
|
final TextEditingController controller = TextEditingController();
|
||||||
|
Widget buildFrame(bool obscureText, bool readOnly) {
|
||||||
|
return overlay(
|
||||||
|
child: TextField(
|
||||||
|
controller: controller,
|
||||||
|
obscureText: obscureText,
|
||||||
|
readOnly: readOnly,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Explicitly disabled selection on obscured text that is read-only.
|
||||||
|
await tester.pumpWidget(buildFrame(true, true));
|
||||||
|
await tester.enterText(find.byType(TextField), 'abcdefghi');
|
||||||
|
await skipPastScrollingAnimation(tester);
|
||||||
|
expect(controller.selection.isCollapsed, true);
|
||||||
|
|
||||||
|
// Long press doesn't select text.
|
||||||
|
final Offset ePos2 = textOffsetToPosition(tester, 1);
|
||||||
|
await tester.longPressAt(ePos2, pointer: 7);
|
||||||
|
await tester.pump();
|
||||||
|
expect(controller.selection.isCollapsed, true);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('An obscured TextField is selected as one word', (WidgetTester tester) async {
|
testWidgets('An obscured TextField is selected as one word', (WidgetTester tester) async {
|
||||||
final TextEditingController controller = TextEditingController();
|
final TextEditingController controller = TextEditingController();
|
||||||
|
|
||||||
@ -4970,82 +4998,6 @@ void main() {
|
|||||||
variant: KeySimulatorTransitModeVariant.all()
|
variant: KeySimulatorTransitModeVariant.all()
|
||||||
);
|
);
|
||||||
|
|
||||||
testWidgets('Copy paste obscured text test', (WidgetTester tester) async {
|
|
||||||
final FocusNode focusNode = FocusNode();
|
|
||||||
final TextEditingController controller = TextEditingController();
|
|
||||||
final TextField textField =
|
|
||||||
TextField(
|
|
||||||
controller: controller,
|
|
||||||
obscureText: true,
|
|
||||||
);
|
|
||||||
|
|
||||||
String clipboardContent = '';
|
|
||||||
tester.binding.defaultBinaryMessenger
|
|
||||||
.setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async {
|
|
||||||
if (methodCall.method == 'Clipboard.setData')
|
|
||||||
// ignore: avoid_dynamic_calls
|
|
||||||
clipboardContent = methodCall.arguments['text'] as String;
|
|
||||||
else if (methodCall.method == 'Clipboard.getData')
|
|
||||||
return <String, dynamic>{'text': clipboardContent};
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
|
|
||||||
await tester.pumpWidget(
|
|
||||||
MaterialApp(
|
|
||||||
home: Material(
|
|
||||||
child: RawKeyboardListener(
|
|
||||||
focusNode: focusNode,
|
|
||||||
child: textField,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
focusNode.requestFocus();
|
|
||||||
await tester.pump();
|
|
||||||
|
|
||||||
const String testValue = 'a big house jumped over a mouse';
|
|
||||||
await tester.enterText(find.byType(TextField), testValue);
|
|
||||||
|
|
||||||
await tester.idle();
|
|
||||||
await tester.tap(find.byType(TextField));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
// Select the first 5 characters
|
|
||||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
|
|
||||||
for (int i = 0; i < 5; i += 1) {
|
|
||||||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
}
|
|
||||||
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
|
|
||||||
|
|
||||||
// Copy them
|
|
||||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlRight);
|
|
||||||
await tester.sendKeyEvent(LogicalKeyboardKey.keyC);
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlRight);
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
expect(clipboardContent, 'a big');
|
|
||||||
|
|
||||||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
// Paste them
|
|
||||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlRight);
|
|
||||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyV);
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
await tester.pump(const Duration(milliseconds: 200));
|
|
||||||
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyV);
|
|
||||||
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlRight);
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
const String expected = 'a biga big house jumped over a mouse';
|
|
||||||
expect(find.text(expected), findsOneWidget, reason: 'Because text contains ${controller.text}');
|
|
||||||
},
|
|
||||||
skip: areKeyEventsHandledByPlatform, // [intended] only applies to platforms where we handle key events.
|
|
||||||
variant: KeySimulatorTransitModeVariant.all()
|
|
||||||
);
|
|
||||||
|
|
||||||
// Regressing test for https://github.com/flutter/flutter/issues/78219
|
// Regressing test for https://github.com/flutter/flutter/issues/78219
|
||||||
testWidgets('Paste does not crash when the section is inValid', (WidgetTester tester) async {
|
testWidgets('Paste does not crash when the section is inValid', (WidgetTester tester) async {
|
||||||
final FocusNode focusNode = FocusNode();
|
final FocusNode focusNode = FocusNode();
|
||||||
@ -5174,83 +5126,6 @@ void main() {
|
|||||||
variant: KeySimulatorTransitModeVariant.all()
|
variant: KeySimulatorTransitModeVariant.all()
|
||||||
);
|
);
|
||||||
|
|
||||||
testWidgets('Cut obscured text test', (WidgetTester tester) async {
|
|
||||||
final FocusNode focusNode = FocusNode();
|
|
||||||
final TextEditingController controller = TextEditingController();
|
|
||||||
final TextField textField = TextField(
|
|
||||||
controller: controller,
|
|
||||||
obscureText: true,
|
|
||||||
);
|
|
||||||
String clipboardContent = '';
|
|
||||||
tester.binding.defaultBinaryMessenger
|
|
||||||
.setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async {
|
|
||||||
if (methodCall.method == 'Clipboard.setData')
|
|
||||||
// ignore: avoid_dynamic_calls
|
|
||||||
clipboardContent = methodCall.arguments['text'] as String;
|
|
||||||
else if (methodCall.method == 'Clipboard.getData')
|
|
||||||
return <String, dynamic>{'text': clipboardContent};
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
|
|
||||||
await tester.pumpWidget(
|
|
||||||
MaterialApp(
|
|
||||||
home: Material(
|
|
||||||
child: RawKeyboardListener(
|
|
||||||
focusNode: focusNode,
|
|
||||||
child: textField,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
focusNode.requestFocus();
|
|
||||||
await tester.pump();
|
|
||||||
|
|
||||||
const String testValue = 'a big house jumped over a mouse';
|
|
||||||
await tester.enterText(find.byType(TextField), testValue);
|
|
||||||
|
|
||||||
await tester.idle();
|
|
||||||
await tester.tap(find.byType(TextField));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
// Select the first 5 characters
|
|
||||||
for (int i = 0; i < 5; i += 1) {
|
|
||||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
|
|
||||||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cut them
|
|
||||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlRight);
|
|
||||||
await tester.sendKeyEvent(LogicalKeyboardKey.keyX);
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlRight);
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
expect(clipboardContent, 'a big');
|
|
||||||
|
|
||||||
for (int i = 0; i < 5; i += 1) {
|
|
||||||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Paste them
|
|
||||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlRight);
|
|
||||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyV);
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
await tester.pump(const Duration(milliseconds: 200));
|
|
||||||
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyV);
|
|
||||||
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlRight);
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
const String expected = ' housa bige jumped over a mouse';
|
|
||||||
expect(find.text(expected), findsOneWidget);
|
|
||||||
},
|
|
||||||
skip: areKeyEventsHandledByPlatform, // [intended] only applies to platforms where we handle key events.
|
|
||||||
variant: KeySimulatorTransitModeVariant.all()
|
|
||||||
);
|
|
||||||
|
|
||||||
testWidgets('Select all test', (WidgetTester tester) async {
|
testWidgets('Select all test', (WidgetTester tester) async {
|
||||||
final FocusNode focusNode = FocusNode();
|
final FocusNode focusNode = FocusNode();
|
||||||
final TextEditingController controller = TextEditingController();
|
final TextEditingController controller = TextEditingController();
|
||||||
@ -7125,6 +7000,55 @@ void main() {
|
|||||||
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
|
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'double tap does not select word on read-only obscured field',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
final TextEditingController controller = TextEditingController(
|
||||||
|
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||||||
|
);
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: Material(
|
||||||
|
child: Center(
|
||||||
|
child: TextField(
|
||||||
|
obscureText: true,
|
||||||
|
readOnly: true,
|
||||||
|
controller: controller,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));
|
||||||
|
|
||||||
|
// This tap just puts the cursor somewhere different than where the double
|
||||||
|
// tap will occur to test that the double tap moves the existing cursor first.
|
||||||
|
await tester.tapAt(textfieldStart + const Offset(50.0, 9.0));
|
||||||
|
await tester.pump(const Duration(milliseconds: 500));
|
||||||
|
|
||||||
|
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
|
||||||
|
await tester.pump(const Duration(milliseconds: 50));
|
||||||
|
// First tap moved the cursor.
|
||||||
|
expect(
|
||||||
|
controller.selection,
|
||||||
|
const TextSelection.collapsed(offset: 35),
|
||||||
|
);
|
||||||
|
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Second tap doesn't select anything.
|
||||||
|
expect(
|
||||||
|
controller.selection,
|
||||||
|
const TextSelection.collapsed(offset: 35),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Selected text shows nothing.
|
||||||
|
expect(find.byType(CupertinoButton), findsNothing);
|
||||||
|
},
|
||||||
|
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
|
||||||
|
);
|
||||||
|
|
||||||
testWidgets(
|
testWidgets(
|
||||||
'double tap selects word and first tap of double tap moves cursor and shows toolbar',
|
'double tap selects word and first tap of double tap moves cursor and shows toolbar',
|
||||||
(WidgetTester tester) async {
|
(WidgetTester tester) async {
|
||||||
|
@ -102,6 +102,94 @@ void main() {
|
|||||||
skip: kIsWeb, // [intended] we don't supply the cut/copy/paste buttons on the web.
|
skip: kIsWeb, // [intended] we don't supply the cut/copy/paste buttons on the web.
|
||||||
);
|
);
|
||||||
|
|
||||||
|
testWidgets('the desktop cut/copy/paste buttons are disabled for read-only obscured form fields', (WidgetTester tester) async {
|
||||||
|
final TextEditingController controller = TextEditingController(
|
||||||
|
text: 'blah1 blah2',
|
||||||
|
);
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: Material(
|
||||||
|
child: Center(
|
||||||
|
child: TextFormField(
|
||||||
|
readOnly: true,
|
||||||
|
obscureText: true,
|
||||||
|
controller: controller,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Initially, the menu is not shown and there is no selection.
|
||||||
|
expect(find.byType(CupertinoButton), findsNothing);
|
||||||
|
const TextSelection invalidSelection = TextSelection(baseOffset: -1, extentOffset: -1);
|
||||||
|
expect(controller.selection, invalidSelection);
|
||||||
|
|
||||||
|
final Offset midBlah1 = textOffsetToPosition(tester, 2);
|
||||||
|
|
||||||
|
// Right clicking shows the menu.
|
||||||
|
final TestGesture gesture = await tester.startGesture(
|
||||||
|
midBlah1,
|
||||||
|
kind: PointerDeviceKind.mouse,
|
||||||
|
buttons: kSecondaryMouseButton,
|
||||||
|
);
|
||||||
|
addTearDown(gesture.removePointer);
|
||||||
|
await tester.pump();
|
||||||
|
await gesture.up();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(controller.selection, invalidSelection);
|
||||||
|
expect(find.text('Copy'), findsNothing);
|
||||||
|
expect(find.text('Cut'), findsNothing);
|
||||||
|
expect(find.text('Paste'), findsNothing);
|
||||||
|
expect(find.byType(CupertinoButton), findsNothing);
|
||||||
|
},
|
||||||
|
variant: TargetPlatformVariant.desktop(),
|
||||||
|
skip: kIsWeb, // [intended] we don't supply the cut/copy/paste buttons on the web.
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets('the desktop cut/copy buttons are disabled for obscured form fields', (WidgetTester tester) async {
|
||||||
|
final TextEditingController controller = TextEditingController(
|
||||||
|
text: 'blah1 blah2',
|
||||||
|
);
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: Material(
|
||||||
|
child: Center(
|
||||||
|
child: TextFormField(
|
||||||
|
obscureText: true,
|
||||||
|
controller: controller,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Initially, the menu is not shown and there is no selection.
|
||||||
|
expect(find.byType(CupertinoButton), findsNothing);
|
||||||
|
const TextSelection invalidSelection = TextSelection(baseOffset: -1, extentOffset: -1);
|
||||||
|
expect(controller.selection, invalidSelection);
|
||||||
|
|
||||||
|
final Offset midBlah1 = textOffsetToPosition(tester, 2);
|
||||||
|
|
||||||
|
// Right clicking shows the menu.
|
||||||
|
final TestGesture gesture = await tester.startGesture(
|
||||||
|
midBlah1,
|
||||||
|
kind: PointerDeviceKind.mouse,
|
||||||
|
buttons: kSecondaryMouseButton,
|
||||||
|
);
|
||||||
|
addTearDown(gesture.removePointer);
|
||||||
|
await tester.pump();
|
||||||
|
await gesture.up();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 11));
|
||||||
|
expect(find.text('Copy'), findsNothing);
|
||||||
|
expect(find.text('Cut'), findsNothing);
|
||||||
|
expect(find.text('Paste'), findsOneWidget);
|
||||||
|
},
|
||||||
|
variant: TargetPlatformVariant.desktop(),
|
||||||
|
skip: kIsWeb, // [intended] we don't supply the cut/copy/paste buttons on the web.
|
||||||
|
);
|
||||||
|
|
||||||
testWidgets('TextFormField accepts TextField.noMaxLength as value to maxLength parameter', (WidgetTester tester) async {
|
testWidgets('TextFormField accepts TextField.noMaxLength as value to maxLength parameter', (WidgetTester tester) async {
|
||||||
bool asserted;
|
bool asserted;
|
||||||
try {
|
try {
|
||||||
|
@ -1506,7 +1506,7 @@ void main() {
|
|||||||
expect(find.text('Cut'), findsNothing);
|
expect(find.text('Cut'), findsNothing);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('cut and paste are disabled in read only mode even if explicit set', (WidgetTester tester) async {
|
testWidgets('cut and paste are disabled in read only mode even if explicitly set', (WidgetTester tester) async {
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
MaterialApp(
|
MaterialApp(
|
||||||
home: EditableText(
|
home: EditableText(
|
||||||
@ -1514,6 +1514,12 @@ void main() {
|
|||||||
controller: TextEditingController(text: 'blah blah'),
|
controller: TextEditingController(text: 'blah blah'),
|
||||||
focusNode: focusNode,
|
focusNode: focusNode,
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
|
toolbarOptions: const ToolbarOptions(
|
||||||
|
copy: true,
|
||||||
|
cut: true,
|
||||||
|
paste: true,
|
||||||
|
selectAll: true,
|
||||||
|
),
|
||||||
style: textStyle,
|
style: textStyle,
|
||||||
cursorColor: cursorColor,
|
cursorColor: cursorColor,
|
||||||
selectionControls: materialTextSelectionControls,
|
selectionControls: materialTextSelectionControls,
|
||||||
@ -1539,6 +1545,113 @@ void main() {
|
|||||||
expect(find.text('Cut'), findsNothing);
|
expect(find.text('Cut'), findsNothing);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('cut and copy are disabled in obscured mode even if explicitly set', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: EditableText(
|
||||||
|
backgroundCursorColor: Colors.grey,
|
||||||
|
controller: TextEditingController(text: 'blah blah'),
|
||||||
|
focusNode: focusNode,
|
||||||
|
obscureText: true,
|
||||||
|
toolbarOptions: const ToolbarOptions(
|
||||||
|
copy: true,
|
||||||
|
cut: true,
|
||||||
|
paste: true,
|
||||||
|
selectAll: true,
|
||||||
|
),
|
||||||
|
style: textStyle,
|
||||||
|
cursorColor: cursorColor,
|
||||||
|
selectionControls: materialTextSelectionControls,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final EditableTextState state =
|
||||||
|
tester.state<EditableTextState>(find.byType(EditableText));
|
||||||
|
await tester.tap(find.byType(EditableText));
|
||||||
|
await tester.pump();
|
||||||
|
// Select something, but not the whole thing.
|
||||||
|
state.renderEditable.selectWord(cause: SelectionChangedCause.tap);
|
||||||
|
await tester.pump();
|
||||||
|
expect(state.selectAllEnabled, isTrue);
|
||||||
|
expect(state.pasteEnabled, isTrue);
|
||||||
|
expect(state.cutEnabled, isFalse);
|
||||||
|
expect(state.copyEnabled, isFalse);
|
||||||
|
|
||||||
|
// On web, we don't let Flutter show the toolbar.
|
||||||
|
expect(state.showToolbar(), kIsWeb ? isFalse : isTrue);
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.text('Select all'), kIsWeb ? findsNothing : findsOneWidget);
|
||||||
|
expect(find.text('Copy'), findsNothing);
|
||||||
|
expect(find.text('Paste'), kIsWeb ? findsNothing : findsOneWidget);
|
||||||
|
expect(find.text('Cut'), findsNothing);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('cut and copy do nothing in obscured mode even if explicitly called', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: EditableText(
|
||||||
|
backgroundCursorColor: Colors.grey,
|
||||||
|
controller: TextEditingController(text: 'blah blah'),
|
||||||
|
focusNode: focusNode,
|
||||||
|
obscureText: true,
|
||||||
|
style: textStyle,
|
||||||
|
cursorColor: cursorColor,
|
||||||
|
selectionControls: materialTextSelectionControls,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final EditableTextState state =
|
||||||
|
tester.state<EditableTextState>(find.byType(EditableText));
|
||||||
|
expect(state.selectAllEnabled, isTrue);
|
||||||
|
expect(state.pasteEnabled, isTrue);
|
||||||
|
expect(state.cutEnabled, isFalse);
|
||||||
|
expect(state.copyEnabled, isFalse);
|
||||||
|
|
||||||
|
// Select all.
|
||||||
|
state.selectAll(SelectionChangedCause.toolbar);
|
||||||
|
await tester.pump();
|
||||||
|
await Clipboard.setData(const ClipboardData(text: ''));
|
||||||
|
state.cutSelection(SelectionChangedCause.toolbar);
|
||||||
|
ClipboardData? data = await Clipboard.getData('text/plain');
|
||||||
|
expect(data, isNotNull);
|
||||||
|
expect(data!.text, isEmpty);
|
||||||
|
|
||||||
|
state.selectAll(SelectionChangedCause.toolbar);
|
||||||
|
await tester.pump();
|
||||||
|
await Clipboard.setData(const ClipboardData(text: ''));
|
||||||
|
state.copySelection(SelectionChangedCause.toolbar);
|
||||||
|
data = await Clipboard.getData('text/plain');
|
||||||
|
expect(data, isNotNull);
|
||||||
|
expect(data!.text, isEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('select all does nothing if obscured and read-only, even if explicitly called', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: EditableText(
|
||||||
|
backgroundCursorColor: Colors.grey,
|
||||||
|
controller: TextEditingController(text: 'blah blah'),
|
||||||
|
focusNode: focusNode,
|
||||||
|
obscureText: true,
|
||||||
|
readOnly: true,
|
||||||
|
style: textStyle,
|
||||||
|
cursorColor: cursorColor,
|
||||||
|
selectionControls: materialTextSelectionControls,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final EditableTextState state =
|
||||||
|
tester.state<EditableTextState>(find.byType(EditableText));
|
||||||
|
|
||||||
|
// Select all.
|
||||||
|
state.selectAll(SelectionChangedCause.toolbar);
|
||||||
|
expect(state.selectAllEnabled, isFalse);
|
||||||
|
expect(state.textEditingValue.selection.isCollapsed, isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('Handles the read-only flag correctly', (WidgetTester tester) async {
|
testWidgets('Handles the read-only flag correctly', (WidgetTester tester) async {
|
||||||
final TextEditingController controller =
|
final TextEditingController controller =
|
||||||
TextEditingController(text: 'Lorem ipsum dolor sit amet');
|
TextEditingController(text: 'Lorem ipsum dolor sit amet');
|
||||||
|
Loading…
x
Reference in New Issue
Block a user