Enable selection by default for password text field and expose api to turn on and off context menu options (#34676)
This commit is contained in:
parent
68fc7231b3
commit
0d0af31598
@ -209,6 +209,7 @@ class CupertinoTextField extends StatefulWidget {
|
|||||||
this.textAlign = TextAlign.start,
|
this.textAlign = TextAlign.start,
|
||||||
this.textAlignVertical,
|
this.textAlignVertical,
|
||||||
this.readOnly = false,
|
this.readOnly = false,
|
||||||
|
ToolbarOptions toolbarOptions,
|
||||||
this.showCursor,
|
this.showCursor,
|
||||||
this.autofocus = false,
|
this.autofocus = false,
|
||||||
this.obscureText = false,
|
this.obscureText = false,
|
||||||
@ -229,7 +230,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,
|
this.enableInteractiveSelection = true,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
this.scrollController,
|
this.scrollController,
|
||||||
this.scrollPhysics,
|
this.scrollPhysics,
|
||||||
@ -257,6 +258,17 @@ class CupertinoTextField extends StatefulWidget {
|
|||||||
assert(prefixMode != null),
|
assert(prefixMode != null),
|
||||||
assert(suffixMode != null),
|
assert(suffixMode != null),
|
||||||
keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline),
|
keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline),
|
||||||
|
toolbarOptions = toolbarOptions ?? obscureText ?
|
||||||
|
const ToolbarOptions(
|
||||||
|
selectAll: true,
|
||||||
|
paste: true,
|
||||||
|
) :
|
||||||
|
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.
|
||||||
@ -358,6 +370,13 @@ class CupertinoTextField extends StatefulWidget {
|
|||||||
/// {@macro flutter.widgets.editableText.textAlign}
|
/// {@macro flutter.widgets.editableText.textAlign}
|
||||||
final TextAlign textAlign;
|
final TextAlign textAlign;
|
||||||
|
|
||||||
|
/// Configuration of toolbar options.
|
||||||
|
///
|
||||||
|
/// If not set, select all and paste will default to be enabled. Copy and cut
|
||||||
|
/// will be disabled if [obscureText] is true. If [readOnly] is true,
|
||||||
|
/// paste and cut will be disabled regardless.
|
||||||
|
final ToolbarOptions toolbarOptions;
|
||||||
|
|
||||||
/// {@macro flutter.material.inputDecorator.textAlignVertical}
|
/// {@macro flutter.material.inputDecorator.textAlignVertical}
|
||||||
final TextAlignVertical textAlignVertical;
|
final TextAlignVertical textAlignVertical;
|
||||||
|
|
||||||
@ -498,9 +517,7 @@ class CupertinoTextField extends StatefulWidget {
|
|||||||
final ScrollPhysics scrollPhysics;
|
final ScrollPhysics scrollPhysics;
|
||||||
|
|
||||||
/// {@macro flutter.rendering.editable.selectionEnabled}
|
/// {@macro flutter.rendering.editable.selectionEnabled}
|
||||||
bool get selectionEnabled {
|
bool get selectionEnabled => enableInteractiveSelection;
|
||||||
return enableInteractiveSelection ?? !obscureText;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// {@macro flutter.material.textfield.onTap}
|
/// {@macro flutter.material.textfield.onTap}
|
||||||
final GestureTapCallback onTap;
|
final GestureTapCallback onTap;
|
||||||
@ -804,6 +821,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
|
|||||||
key: editableTextKey,
|
key: editableTextKey,
|
||||||
controller: controller,
|
controller: controller,
|
||||||
readOnly: widget.readOnly,
|
readOnly: widget.readOnly,
|
||||||
|
toolbarOptions: widget.toolbarOptions,
|
||||||
showCursor: widget.showCursor,
|
showCursor: widget.showCursor,
|
||||||
showSelectionHandles: _showSelectionHandles,
|
showSelectionHandles: _showSelectionHandles,
|
||||||
focusNode: _effectiveFocusNode,
|
focusNode: _effectiveFocusNode,
|
||||||
|
@ -195,6 +195,7 @@ class SelectableText extends StatefulWidget {
|
|||||||
this.textDirection,
|
this.textDirection,
|
||||||
this.showCursor = false,
|
this.showCursor = false,
|
||||||
this.autofocus = false,
|
this.autofocus = false,
|
||||||
|
ToolbarOptions toolbarOptions,
|
||||||
this.maxLines,
|
this.maxLines,
|
||||||
this.cursorWidth = 2.0,
|
this.cursorWidth = 2.0,
|
||||||
this.cursorRadius,
|
this.cursorRadius,
|
||||||
@ -213,6 +214,11 @@ class SelectableText extends StatefulWidget {
|
|||||||
'A non-null String must be provided to a SelectableText widget.',
|
'A non-null String must be provided to a SelectableText widget.',
|
||||||
),
|
),
|
||||||
textSpan = null,
|
textSpan = null,
|
||||||
|
toolbarOptions = toolbarOptions ??
|
||||||
|
const ToolbarOptions(
|
||||||
|
selectAll: true,
|
||||||
|
copy: true,
|
||||||
|
),
|
||||||
super(key: key);
|
super(key: key);
|
||||||
|
|
||||||
/// Creates a selectable text widget with a [TextSpan].
|
/// Creates a selectable text widget with a [TextSpan].
|
||||||
@ -229,6 +235,7 @@ class SelectableText extends StatefulWidget {
|
|||||||
this.textDirection,
|
this.textDirection,
|
||||||
this.showCursor = false,
|
this.showCursor = false,
|
||||||
this.autofocus = false,
|
this.autofocus = false,
|
||||||
|
ToolbarOptions toolbarOptions,
|
||||||
this.maxLines,
|
this.maxLines,
|
||||||
this.cursorWidth = 2.0,
|
this.cursorWidth = 2.0,
|
||||||
this.cursorRadius,
|
this.cursorRadius,
|
||||||
@ -247,6 +254,11 @@ class SelectableText extends StatefulWidget {
|
|||||||
'A non-null TextSpan must be provided to a SelectableText.rich widget.',
|
'A non-null TextSpan must be provided to a SelectableText.rich widget.',
|
||||||
),
|
),
|
||||||
data = null,
|
data = null,
|
||||||
|
toolbarOptions = toolbarOptions ??
|
||||||
|
const ToolbarOptions(
|
||||||
|
selectAll: true,
|
||||||
|
copy: true,
|
||||||
|
),
|
||||||
super(key: key);
|
super(key: key);
|
||||||
|
|
||||||
/// The text to display.
|
/// The text to display.
|
||||||
@ -325,6 +337,13 @@ class SelectableText extends StatefulWidget {
|
|||||||
/// {@macro flutter.widgets.scrollable.dragStartBehavior}
|
/// {@macro flutter.widgets.scrollable.dragStartBehavior}
|
||||||
final DragStartBehavior dragStartBehavior;
|
final DragStartBehavior dragStartBehavior;
|
||||||
|
|
||||||
|
/// Configuration of toolbar options.
|
||||||
|
///
|
||||||
|
/// Paste and cut will be disabled regardless.
|
||||||
|
///
|
||||||
|
/// If not set, select all and copy will be enabled by default.
|
||||||
|
final ToolbarOptions toolbarOptions;
|
||||||
|
|
||||||
/// {@macro flutter.rendering.editable.selectionEnabled}
|
/// {@macro flutter.rendering.editable.selectionEnabled}
|
||||||
bool get selectionEnabled {
|
bool get selectionEnabled {
|
||||||
return enableInteractiveSelection;
|
return enableInteractiveSelection;
|
||||||
@ -543,6 +562,7 @@ class _SelectableTextState extends State<SelectableText> with AutomaticKeepAlive
|
|||||||
textDirection: widget.textDirection,
|
textDirection: widget.textDirection,
|
||||||
autofocus: widget.autofocus,
|
autofocus: widget.autofocus,
|
||||||
forceLine: false,
|
forceLine: false,
|
||||||
|
toolbarOptions: widget.toolbarOptions,
|
||||||
maxLines: widget.maxLines ?? defaultTextStyle.maxLines,
|
maxLines: widget.maxLines ?? defaultTextStyle.maxLines,
|
||||||
selectionColor: themeData.textSelectionColor,
|
selectionColor: themeData.textSelectionColor,
|
||||||
selectionControls: widget.selectionEnabled ? textSelectionControls : null,
|
selectionControls: widget.selectionEnabled ? textSelectionControls : null,
|
||||||
|
@ -256,6 +256,7 @@ class TextField extends StatefulWidget {
|
|||||||
this.textAlignVertical,
|
this.textAlignVertical,
|
||||||
this.textDirection,
|
this.textDirection,
|
||||||
this.readOnly = false,
|
this.readOnly = false,
|
||||||
|
ToolbarOptions toolbarOptions,
|
||||||
this.showCursor,
|
this.showCursor,
|
||||||
this.autofocus = false,
|
this.autofocus = false,
|
||||||
this.obscureText = false,
|
this.obscureText = false,
|
||||||
@ -276,7 +277,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,
|
this.enableInteractiveSelection = true,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
this.buildCounter,
|
this.buildCounter,
|
||||||
this.scrollController,
|
this.scrollController,
|
||||||
@ -286,6 +287,7 @@ class TextField extends StatefulWidget {
|
|||||||
assert(autofocus != null),
|
assert(autofocus != null),
|
||||||
assert(obscureText != null),
|
assert(obscureText != null),
|
||||||
assert(autocorrect != null),
|
assert(autocorrect != null),
|
||||||
|
assert(enableInteractiveSelection != null),
|
||||||
assert(maxLengthEnforced != null),
|
assert(maxLengthEnforced != null),
|
||||||
assert(scrollPadding != null),
|
assert(scrollPadding != null),
|
||||||
assert(dragStartBehavior != null),
|
assert(dragStartBehavior != null),
|
||||||
@ -302,6 +304,17 @@ class TextField extends StatefulWidget {
|
|||||||
),
|
),
|
||||||
assert(maxLength == null || maxLength == TextField.noMaxLength || maxLength > 0),
|
assert(maxLength == null || maxLength == TextField.noMaxLength || maxLength > 0),
|
||||||
keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline),
|
keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline),
|
||||||
|
toolbarOptions = toolbarOptions ?? obscureText ?
|
||||||
|
const ToolbarOptions(
|
||||||
|
selectAll: true,
|
||||||
|
paste: true,
|
||||||
|
) :
|
||||||
|
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.
|
||||||
@ -410,6 +423,13 @@ class TextField extends StatefulWidget {
|
|||||||
/// {@macro flutter.widgets.editableText.readOnly}
|
/// {@macro flutter.widgets.editableText.readOnly}
|
||||||
final bool readOnly;
|
final bool readOnly;
|
||||||
|
|
||||||
|
/// Configuration of toolbar options.
|
||||||
|
///
|
||||||
|
/// If not set, select all and paste will default to be enabled. Copy and cut
|
||||||
|
/// will be disabled if [obscureText] is true. If [readOnly] is true,
|
||||||
|
/// paste and cut will be disabled regardless.
|
||||||
|
final ToolbarOptions toolbarOptions;
|
||||||
|
|
||||||
/// {@macro flutter.widgets.editableText.showCursor}
|
/// {@macro flutter.widgets.editableText.showCursor}
|
||||||
final bool showCursor;
|
final bool showCursor;
|
||||||
|
|
||||||
@ -538,9 +558,7 @@ class TextField extends StatefulWidget {
|
|||||||
final DragStartBehavior dragStartBehavior;
|
final DragStartBehavior dragStartBehavior;
|
||||||
|
|
||||||
/// {@macro flutter.rendering.editable.selectionEnabled}
|
/// {@macro flutter.rendering.editable.selectionEnabled}
|
||||||
bool get selectionEnabled {
|
bool get selectionEnabled => enableInteractiveSelection;
|
||||||
return enableInteractiveSelection ?? !obscureText;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// {@template flutter.material.textfield.onTap}
|
/// {@template flutter.material.textfield.onTap}
|
||||||
/// Called for each distinct tap except for every second tap of a double tap.
|
/// Called for each distinct tap except for every second tap of a double tap.
|
||||||
@ -952,6 +970,7 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
|
|||||||
child: EditableText(
|
child: EditableText(
|
||||||
key: editableTextKey,
|
key: editableTextKey,
|
||||||
readOnly: widget.readOnly,
|
readOnly: widget.readOnly,
|
||||||
|
toolbarOptions: widget.toolbarOptions,
|
||||||
showCursor: widget.showCursor,
|
showCursor: widget.showCursor,
|
||||||
showSelectionHandles: _showSelectionHandles,
|
showSelectionHandles: _showSelectionHandles,
|
||||||
controller: controller,
|
controller: controller,
|
||||||
|
@ -89,6 +89,7 @@ class TextFormField extends FormField<String> {
|
|||||||
TextAlign textAlign = TextAlign.start,
|
TextAlign textAlign = TextAlign.start,
|
||||||
bool autofocus = false,
|
bool autofocus = false,
|
||||||
bool readOnly = false,
|
bool readOnly = false,
|
||||||
|
ToolbarOptions toolbarOptions,
|
||||||
bool showCursor,
|
bool showCursor,
|
||||||
bool obscureText = false,
|
bool obscureText = false,
|
||||||
bool autocorrect = true,
|
bool autocorrect = true,
|
||||||
@ -163,6 +164,7 @@ class TextFormField extends FormField<String> {
|
|||||||
textDirection: textDirection,
|
textDirection: textDirection,
|
||||||
textCapitalization: textCapitalization,
|
textCapitalization: textCapitalization,
|
||||||
autofocus: autofocus,
|
autofocus: autofocus,
|
||||||
|
toolbarOptions: toolbarOptions,
|
||||||
readOnly: readOnly,
|
readOnly: readOnly,
|
||||||
showCursor: showCursor,
|
showCursor: showCursor,
|
||||||
obscureText: obscureText,
|
obscureText: obscureText,
|
||||||
|
@ -1554,6 +1554,10 @@ class RenderEditable extends RenderBox {
|
|||||||
// When long-pressing past the end of the text, we want a collapsed cursor.
|
// When long-pressing past the end of the text, we want a collapsed cursor.
|
||||||
if (position.offset >= word.end)
|
if (position.offset >= word.end)
|
||||||
return TextSelection.fromPosition(position);
|
return TextSelection.fromPosition(position);
|
||||||
|
// If text is obscured, the entire sentence should be treated as one word.
|
||||||
|
if (obscureText) {
|
||||||
|
return TextSelection(baseOffset: 0, extentOffset: text.toPlainText().length);
|
||||||
|
}
|
||||||
return TextSelection(baseOffset: word.start, extentOffset: word.end);
|
return TextSelection(baseOffset: word.start, extentOffset: word.end);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -221,6 +221,54 @@ class TextEditingController extends ValueNotifier<TextEditingValue> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Toolbar configuration for [EditableText].
|
||||||
|
///
|
||||||
|
/// Toolbar is a context menu that will show up when user right click or long
|
||||||
|
/// press the [EditableText]. It includes several options: cut, copy, paste,
|
||||||
|
/// and select all.
|
||||||
|
///
|
||||||
|
/// [EditableText] and its derived widgets have their own default [ToolbarOptions].
|
||||||
|
/// Create a custom [ToolbarOptions] if you want explicit control over the toolbar
|
||||||
|
/// option.
|
||||||
|
class ToolbarOptions {
|
||||||
|
/// Create a toolbar configuration with given options.
|
||||||
|
///
|
||||||
|
/// All options default to false if they are not explicitly set.
|
||||||
|
const ToolbarOptions({
|
||||||
|
this.copy = false,
|
||||||
|
this.cut = false,
|
||||||
|
this.paste = false,
|
||||||
|
this.selectAll = false,
|
||||||
|
}) : assert(copy != null),
|
||||||
|
assert(cut != null),
|
||||||
|
assert(paste != null),
|
||||||
|
assert(selectAll != null);
|
||||||
|
|
||||||
|
/// Whether to show copy option in toolbar.
|
||||||
|
///
|
||||||
|
/// Defaults to false. Must not be null.
|
||||||
|
final bool copy;
|
||||||
|
|
||||||
|
/// Whether to show cut option in toolbar.
|
||||||
|
///
|
||||||
|
/// If [EditableText.readOnly] is set to true, cut will be disabled regardless.
|
||||||
|
///
|
||||||
|
/// Defaults to false. Must not be null.
|
||||||
|
final bool cut;
|
||||||
|
|
||||||
|
/// Whether to show paste option in toolbar.
|
||||||
|
///
|
||||||
|
/// If [EditableText.readOnly] is set to true, paste will be disabled regardless.
|
||||||
|
///
|
||||||
|
/// Defaults to false. Must not be null.
|
||||||
|
final bool paste;
|
||||||
|
|
||||||
|
/// Whether to show select all option in toolbar.
|
||||||
|
///
|
||||||
|
/// Defaults to false. Must not be null.
|
||||||
|
final bool selectAll;
|
||||||
|
}
|
||||||
|
|
||||||
/// A basic text input field.
|
/// A basic text input field.
|
||||||
///
|
///
|
||||||
/// This widget interacts with the [TextInput] service to let the user edit the
|
/// This widget interacts with the [TextInput] service to let the user edit the
|
||||||
@ -336,14 +384,21 @@ 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,
|
this.enableInteractiveSelection = true,
|
||||||
this.scrollController,
|
this.scrollController,
|
||||||
this.scrollPhysics,
|
this.scrollPhysics,
|
||||||
|
this.toolbarOptions = const ToolbarOptions(
|
||||||
|
copy: true,
|
||||||
|
cut: true,
|
||||||
|
paste: true,
|
||||||
|
selectAll: true
|
||||||
|
)
|
||||||
}) : assert(controller != null),
|
}) : assert(controller != null),
|
||||||
assert(focusNode != null),
|
assert(focusNode != null),
|
||||||
assert(obscureText != null),
|
assert(obscureText != null),
|
||||||
assert(autocorrect != null),
|
assert(autocorrect != 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),
|
||||||
@ -367,6 +422,7 @@ 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),
|
||||||
_strutStyle = strutStyle,
|
_strutStyle = strutStyle,
|
||||||
keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline),
|
keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline),
|
||||||
inputFormatters = maxLines == 1
|
inputFormatters = maxLines == 1
|
||||||
@ -419,6 +475,12 @@ class EditableText extends StatefulWidget {
|
|||||||
/// * [textWidthBasis], which controls the calculation of text width.
|
/// * [textWidthBasis], which controls the calculation of text width.
|
||||||
final bool forceLine;
|
final bool forceLine;
|
||||||
|
|
||||||
|
/// Configuration of toolbar options.
|
||||||
|
///
|
||||||
|
/// By default, all options are enabled. If [readOnly] is true,
|
||||||
|
/// paste and cut will be disabled regardless.
|
||||||
|
final ToolbarOptions toolbarOptions;
|
||||||
|
|
||||||
/// Whether to show selection handles.
|
/// Whether to show selection handles.
|
||||||
///
|
///
|
||||||
/// When a selection is active, there will be two handles at each side of
|
/// When a selection is active, there will be two handles at each side of
|
||||||
@ -903,9 +965,7 @@ class EditableText extends StatefulWidget {
|
|||||||
final ScrollPhysics scrollPhysics;
|
final ScrollPhysics scrollPhysics;
|
||||||
|
|
||||||
/// {@macro flutter.rendering.editable.selectionEnabled}
|
/// {@macro flutter.rendering.editable.selectionEnabled}
|
||||||
bool get selectionEnabled {
|
bool get selectionEnabled => enableInteractiveSelection;
|
||||||
return enableInteractiveSelection ?? !obscureText;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
EditableTextState createState() => EditableTextState();
|
EditableTextState createState() => EditableTextState();
|
||||||
@ -969,16 +1029,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.readOnly;
|
bool get cutEnabled => widget.toolbarOptions.cut && !widget.readOnly;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool get copyEnabled => true;
|
bool get copyEnabled => widget.toolbarOptions.copy;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool get pasteEnabled => !widget.readOnly;
|
bool get pasteEnabled => widget.toolbarOptions.paste && !widget.readOnly;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool get selectAllEnabled => true;
|
bool get selectAllEnabled => widget.toolbarOptions.selectAll;
|
||||||
|
|
||||||
// State lifecycle:
|
// State lifecycle:
|
||||||
|
|
||||||
|
@ -872,7 +872,7 @@ class TextSelectionGestureDetectorBuilder {
|
|||||||
@protected
|
@protected
|
||||||
final TextSelectionGestureDetectorBuilderDelegate delegate;
|
final TextSelectionGestureDetectorBuilderDelegate delegate;
|
||||||
|
|
||||||
/// Whether to show the selection tool bar.
|
/// Whether to show the selection toolbar.
|
||||||
///
|
///
|
||||||
/// It is based on the signal source when a [onTapDown] is called. This getter
|
/// It is based on the signal source when a [onTapDown] is called. This getter
|
||||||
/// will return true if current [onTapDown] event is triggered by a touch or
|
/// will return true if current [onTapDown] event is triggered by a touch or
|
||||||
@ -937,7 +937,7 @@ class TextSelectionGestureDetectorBuilder {
|
|||||||
/// Handler for [TextSelectionGestureDetector.onForcePressEnd].
|
/// Handler for [TextSelectionGestureDetector.onForcePressEnd].
|
||||||
///
|
///
|
||||||
/// By default, it selects words in the range specified in [details] and shows
|
/// By default, it selects words in the range specified in [details] and shows
|
||||||
/// tool bar if it is necessary.
|
/// toolbar if it is necessary.
|
||||||
///
|
///
|
||||||
/// This callback is only applicable when force press is enabled.
|
/// This callback is only applicable when force press is enabled.
|
||||||
///
|
///
|
||||||
@ -1022,7 +1022,7 @@ class TextSelectionGestureDetectorBuilder {
|
|||||||
|
|
||||||
/// Handler for [TextSelectionGestureDetector.onSingleLongTapEnd].
|
/// Handler for [TextSelectionGestureDetector.onSingleLongTapEnd].
|
||||||
///
|
///
|
||||||
/// By default, it shows tool bar if necessary.
|
/// By default, it shows toolbar if necessary.
|
||||||
///
|
///
|
||||||
/// See also:
|
/// See also:
|
||||||
///
|
///
|
||||||
@ -1037,7 +1037,7 @@ class TextSelectionGestureDetectorBuilder {
|
|||||||
/// Handler for [TextSelectionGestureDetector.onDoubleTapDown].
|
/// Handler for [TextSelectionGestureDetector.onDoubleTapDown].
|
||||||
///
|
///
|
||||||
/// By default, it selects a word through [renderEditable.selectWord] if
|
/// By default, it selects a word through [renderEditable.selectWord] if
|
||||||
/// selectionEnabled and shows tool bar if necessary.
|
/// selectionEnabled and shows toolbar if necessary.
|
||||||
///
|
///
|
||||||
/// See also:
|
/// See also:
|
||||||
///
|
///
|
||||||
|
@ -1461,10 +1461,7 @@ void main() {
|
|||||||
|
|
||||||
// Long press to put the cursor after the "w".
|
// Long press to put the cursor after the "w".
|
||||||
const int index = 3;
|
const int index = 3;
|
||||||
final TestGesture gesture =
|
await tester.longPressAt(textOffsetToPosition(tester, index));
|
||||||
await tester.startGesture(textOffsetToPosition(tester, index));
|
|
||||||
await tester.pump(const Duration(milliseconds: 500));
|
|
||||||
await gesture.up();
|
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(
|
expect(
|
||||||
controller.selection,
|
controller.selection,
|
||||||
@ -1619,7 +1616,7 @@ void main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
testWidgets(
|
testWidgets(
|
||||||
'An obscured CupertinoTextField is not selectable by default',
|
'An obscured CupertinoTextField is not selectable when disabled',
|
||||||
(WidgetTester tester) async {
|
(WidgetTester tester) async {
|
||||||
final TextEditingController controller = TextEditingController(
|
final TextEditingController controller = TextEditingController(
|
||||||
text: 'Atwater Peel Sherbrooke Bonaventure',
|
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||||||
@ -1630,6 +1627,7 @@ void main() {
|
|||||||
child: CupertinoTextField(
|
child: CupertinoTextField(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
obscureText: true,
|
obscureText: true,
|
||||||
|
enableInteractiveSelection: false,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -1666,7 +1664,7 @@ void main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
testWidgets(
|
testWidgets(
|
||||||
'An obscured CupertinoTextField is selectable when enabled',
|
'An obscured CupertinoTextField is selectable by default',
|
||||||
(WidgetTester tester) async {
|
(WidgetTester tester) async {
|
||||||
final TextEditingController controller = TextEditingController(
|
final TextEditingController controller = TextEditingController(
|
||||||
text: 'Atwater Peel Sherbrooke Bonaventure',
|
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||||||
@ -1677,7 +1675,6 @@ void main() {
|
|||||||
child: CupertinoTextField(
|
child: CupertinoTextField(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
obscureText: true,
|
obscureText: true,
|
||||||
enableInteractiveSelection: true,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -1692,15 +1689,14 @@ void main() {
|
|||||||
// Hold the press.
|
// Hold the press.
|
||||||
await tester.pump(const Duration(milliseconds: 500));
|
await tester.pump(const Duration(milliseconds: 500));
|
||||||
|
|
||||||
// The obscured text is not broken into words, so only one letter is
|
// The obscured text is treated as one word, should select all
|
||||||
// selected at a time.
|
|
||||||
expect(
|
expect(
|
||||||
controller.selection,
|
controller.selection,
|
||||||
const TextSelection(baseOffset: 9, extentOffset: 10),
|
const TextSelection(baseOffset: 0, extentOffset: 35),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Selected text shows 3 toolbar buttons.
|
// Selected text shows paste toolbar buttons.
|
||||||
expect(find.byType(CupertinoButton), findsNWidgets(3));
|
expect(find.byType(CupertinoButton), findsNWidgets(1));
|
||||||
|
|
||||||
await gesture.up();
|
await gesture.up();
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
@ -1708,12 +1704,56 @@ void main() {
|
|||||||
// Still selected.
|
// Still selected.
|
||||||
expect(
|
expect(
|
||||||
controller.selection,
|
controller.selection,
|
||||||
const TextSelection(baseOffset: 9, extentOffset: 10),
|
const TextSelection(baseOffset: 0, extentOffset: 35),
|
||||||
);
|
);
|
||||||
expect(find.byType(CupertinoButton), findsNWidgets(3));
|
expect(find.byType(CupertinoButton), findsNWidgets(1));
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
testWidgets('An obscured TextField has correct default context menu', (WidgetTester tester) async {
|
||||||
|
final TextEditingController controller = TextEditingController(
|
||||||
|
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
CupertinoApp(
|
||||||
|
home: Center(
|
||||||
|
child: CupertinoTextField(
|
||||||
|
controller: controller,
|
||||||
|
obscureText: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Offset textfieldStart = tester.getCenter(find.byType(CupertinoTextField));
|
||||||
|
|
||||||
|
await tester.tapAt(textfieldStart + const Offset(150.0, 5.0));
|
||||||
|
await tester.pump(const Duration(milliseconds: 50));
|
||||||
|
await tester.longPressAt(textfieldStart + const Offset(150.0, 5.0));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
// Should only have paste option when whole obscure text is selected.
|
||||||
|
expect(find.text('Paste'), findsOneWidget);
|
||||||
|
expect(find.text('Copy'), findsNothing);
|
||||||
|
expect(find.text('Cut'), findsNothing);
|
||||||
|
expect(find.text('Select All'), findsNothing);
|
||||||
|
|
||||||
|
// Tap to cancel selection.
|
||||||
|
final Offset textfieldEnd = tester.getTopRight(find.byType(CupertinoTextField));
|
||||||
|
await tester.tapAt(textfieldEnd + const Offset(-10.0, 5.0));
|
||||||
|
await tester.pump(const Duration(milliseconds: 50));
|
||||||
|
// Long tap at the end.
|
||||||
|
await tester.longPressAt(textfieldEnd + const Offset(-10.0, 5.0));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
// Should have paste and select all options when collapse.
|
||||||
|
expect(find.text('Paste'), findsOneWidget);
|
||||||
|
expect(find.text('Select All'), findsOneWidget);
|
||||||
|
expect(find.text('Copy'), findsNothing);
|
||||||
|
expect(find.text('Cut'), findsNothing);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets(
|
testWidgets(
|
||||||
'long press moves cursor to the exact long press position and shows toolbar',
|
'long press moves cursor to the exact long press position and shows toolbar',
|
||||||
(WidgetTester tester) async {
|
(WidgetTester tester) async {
|
||||||
|
@ -742,9 +742,7 @@ void main() {
|
|||||||
|
|
||||||
// Long press the 'e' to select 'def'.
|
// Long press the 'e' to select 'def'.
|
||||||
final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e'));
|
final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e'));
|
||||||
final TestGesture gesture = await tester.startGesture(ePos, pointer: 7);
|
await tester.longPressAt(ePos, pointer: 7);
|
||||||
await tester.pump(const Duration(seconds: 2));
|
|
||||||
await gesture.up();
|
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
// 'def' is selected.
|
// 'def' is selected.
|
||||||
@ -867,7 +865,7 @@ void main() {
|
|||||||
expect(find.text('CUT'), findsNothing);
|
expect(find.text('CUT'), findsNothing);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('does not paint tool bar when no options available on ios', (WidgetTester tester) async {
|
testWidgets('does not paint toolbar when no options available on ios', (WidgetTester tester) async {
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
MaterialApp(
|
MaterialApp(
|
||||||
theme: ThemeData(platform: TargetPlatform.iOS),
|
theme: ThemeData(platform: TargetPlatform.iOS),
|
||||||
@ -889,7 +887,7 @@ void main() {
|
|||||||
expect(find.byType(CupertinoTextSelectionToolbar), paintsNothing);
|
expect(find.byType(CupertinoTextSelectionToolbar), paintsNothing);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('text field build empty tool bar when no options available android', (WidgetTester tester) async {
|
testWidgets('text field build empty toolbar when no options available android', (WidgetTester tester) async {
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
const MaterialApp(
|
const MaterialApp(
|
||||||
home: Material(
|
home: Material(
|
||||||
@ -1086,9 +1084,7 @@ void main() {
|
|||||||
|
|
||||||
// Long press the 'e' to select 'def'.
|
// Long press the 'e' to select 'def'.
|
||||||
final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e'));
|
final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e'));
|
||||||
final TestGesture gesture = await tester.startGesture(ePos, pointer: 7);
|
await tester.longPressAt(ePos, pointer: 7);
|
||||||
await tester.pump(const Duration(seconds: 2));
|
|
||||||
await gesture.up();
|
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
expect(controller.selection.isCollapsed, true);
|
expect(controller.selection.isCollapsed, true);
|
||||||
@ -1569,9 +1565,36 @@ void main() {
|
|||||||
// End the test here to ensure the animation is properly disposed of.
|
// End the test here to ensure the animation is properly disposed of.
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('An obscured TextField is not selectable by default', (WidgetTester tester) async {
|
testWidgets('An obscured TextField is selectable by default', (WidgetTester tester) async {
|
||||||
// This is a regression test for
|
// This is a regression test for
|
||||||
// https://github.com/flutter/flutter/issues/24100
|
// https://github.com/flutter/flutter/issues/32845
|
||||||
|
|
||||||
|
final TextEditingController controller = TextEditingController();
|
||||||
|
Widget buildFrame(bool obscureText) {
|
||||||
|
return overlay(
|
||||||
|
child: TextField(
|
||||||
|
controller: controller,
|
||||||
|
obscureText: obscureText,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obscure text and don't enable or disable selection.
|
||||||
|
await tester.pumpWidget(buildFrame(true));
|
||||||
|
await tester.enterText(find.byType(TextField), 'abcdefghi');
|
||||||
|
await skipPastScrollingAnimation(tester);
|
||||||
|
expect(controller.selection.isCollapsed, true);
|
||||||
|
|
||||||
|
// Long press does select text.
|
||||||
|
final Offset ePos = textOffsetToPosition(tester, 1);
|
||||||
|
await tester.longPressAt(ePos, pointer: 7);
|
||||||
|
await tester.pump();
|
||||||
|
expect(controller.selection.isCollapsed, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('An obscured TextField is not selectable when disabled', (WidgetTester tester) async {
|
||||||
|
// This is a regression test for
|
||||||
|
// https://github.com/flutter/flutter/issues/32845
|
||||||
|
|
||||||
final TextEditingController controller = TextEditingController();
|
final TextEditingController controller = TextEditingController();
|
||||||
Widget buildFrame(bool obscureText, bool enableInteractiveSelection) {
|
Widget buildFrame(bool obscureText, bool enableInteractiveSelection) {
|
||||||
@ -1584,49 +1607,75 @@ void main() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Obscure text and don't enable or disable selection
|
// Explicitly disabled selection on obscured text.
|
||||||
await tester.pumpWidget(buildFrame(true, null));
|
await tester.pumpWidget(buildFrame(true, false));
|
||||||
await tester.enterText(find.byType(TextField), 'abcdefghi');
|
await tester.enterText(find.byType(TextField), 'abcdefghi');
|
||||||
await skipPastScrollingAnimation(tester);
|
await skipPastScrollingAnimation(tester);
|
||||||
expect(controller.selection.isCollapsed, true);
|
expect(controller.selection.isCollapsed, true);
|
||||||
|
|
||||||
// Long press doesn't select anything
|
// Long press doesn't select text.
|
||||||
final Offset ePos = textOffsetToPosition(tester, 1);
|
final Offset ePos2 = textOffsetToPosition(tester, 1);
|
||||||
final TestGesture gesture = await tester.startGesture(ePos, pointer: 7);
|
await tester.longPressAt(ePos2, pointer: 7);
|
||||||
await tester.pump(const Duration(seconds: 2));
|
|
||||||
await gesture.up();
|
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(controller.selection.isCollapsed, true);
|
expect(controller.selection.isCollapsed, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('An obscured TextField is selectable when enabled', (WidgetTester tester) async {
|
testWidgets('An obscured TextField is selected as one word', (WidgetTester tester) async {
|
||||||
// This is a regression test for
|
|
||||||
// https://github.com/flutter/flutter/issues/24100
|
|
||||||
|
|
||||||
final TextEditingController controller = TextEditingController();
|
final TextEditingController controller = TextEditingController();
|
||||||
Widget buildFrame(bool obscureText, bool enableInteractiveSelection) {
|
|
||||||
return overlay(
|
await tester.pumpWidget(overlay(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
obscureText: obscureText,
|
obscureText: true,
|
||||||
enableInteractiveSelection: enableInteractiveSelection,
|
|
||||||
),
|
),
|
||||||
);
|
));
|
||||||
}
|
await tester.enterText(find.byType(TextField), 'abcde fghi');
|
||||||
|
|
||||||
// Explicitly allow selection on obscured text
|
|
||||||
await tester.pumpWidget(buildFrame(true, true));
|
|
||||||
await tester.enterText(find.byType(TextField), 'abcdefghi');
|
|
||||||
await skipPastScrollingAnimation(tester);
|
await skipPastScrollingAnimation(tester);
|
||||||
expect(controller.selection.isCollapsed, true);
|
|
||||||
|
|
||||||
// Long press does select text
|
// Long press does select text.
|
||||||
final Offset ePos2 = textOffsetToPosition(tester, 1);
|
final Offset bPos = textOffsetToPosition(tester, 1);
|
||||||
final TestGesture gesture2 = await tester.startGesture(ePos2, pointer: 7);
|
await tester.longPressAt(bPos, pointer: 7);
|
||||||
await tester.pump(const Duration(seconds: 2));
|
|
||||||
await gesture2.up();
|
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(controller.selection.isCollapsed, false);
|
final TextSelection selection = controller.selection;
|
||||||
|
expect(selection.isCollapsed, false);
|
||||||
|
expect(selection.baseOffset, 0);
|
||||||
|
expect(selection.extentOffset, 10);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('An obscured TextField has correct default context menu', (WidgetTester tester) async {
|
||||||
|
final TextEditingController controller = TextEditingController();
|
||||||
|
|
||||||
|
await tester.pumpWidget(overlay(
|
||||||
|
child: TextField(
|
||||||
|
controller: controller,
|
||||||
|
obscureText: true,
|
||||||
|
),
|
||||||
|
));
|
||||||
|
await tester.enterText(find.byType(TextField), 'abcde fghi');
|
||||||
|
await skipPastScrollingAnimation(tester);
|
||||||
|
|
||||||
|
// Long press to select text.
|
||||||
|
final Offset bPos = textOffsetToPosition(tester, 1);
|
||||||
|
await tester.longPressAt(bPos, pointer: 7);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
// Should only have paste option when whole obscure text is selected.
|
||||||
|
expect(find.text('PASTE'), findsOneWidget);
|
||||||
|
expect(find.text('COPY'), findsNothing);
|
||||||
|
expect(find.text('CUT'), findsNothing);
|
||||||
|
expect(find.text('SELECT ALL'), findsNothing);
|
||||||
|
|
||||||
|
// Long press at the end
|
||||||
|
final Offset iPos = textOffsetToPosition(tester, 10);
|
||||||
|
final Offset slightRight = iPos + const Offset(30.0, 0.0);
|
||||||
|
await tester.longPressAt(slightRight, pointer: 7);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
// Should have paste and select all options when collapse.
|
||||||
|
expect(find.text('PASTE'), findsOneWidget);
|
||||||
|
expect(find.text('SELECT ALL'), findsOneWidget);
|
||||||
|
expect(find.text('COPY'), findsNothing);
|
||||||
|
expect(find.text('CUT'), findsNothing);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('TextField height with minLines unset', (WidgetTester tester) async {
|
testWidgets('TextField height with minLines unset', (WidgetTester tester) async {
|
||||||
|
@ -533,6 +533,79 @@ void main() {
|
|||||||
expect(find.text('PASTE'), findsOneWidget);
|
expect(find.text('PASTE'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('can dynamically disable options in toolbar', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: EditableText(
|
||||||
|
backgroundCursorColor: Colors.grey,
|
||||||
|
controller: TextEditingController(text: 'blah blah'),
|
||||||
|
focusNode: focusNode,
|
||||||
|
toolbarOptions: const ToolbarOptions(
|
||||||
|
copy: true,
|
||||||
|
selectAll: true,
|
||||||
|
),
|
||||||
|
style: textStyle,
|
||||||
|
cursorColor: cursorColor,
|
||||||
|
selectionControls: materialTextSelectionControls,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final EditableTextState state =
|
||||||
|
tester.state<EditableTextState>(find.byType(EditableText));
|
||||||
|
|
||||||
|
// Select something. Doesn't really matter what.
|
||||||
|
state.renderEditable.selectWordsInRange(
|
||||||
|
from: const Offset(0, 0),
|
||||||
|
cause: SelectionChangedCause.tap,
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
expect(state.showToolbar(), true);
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.text('SELECT ALL'), findsOneWidget);
|
||||||
|
expect(find.text('COPY'), findsOneWidget);
|
||||||
|
expect(find.text('PASTE'), findsNothing);
|
||||||
|
expect(find.text('CUT'), findsNothing);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('cut and paste are disabled in read only mode even if explicit set', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: EditableText(
|
||||||
|
backgroundCursorColor: Colors.grey,
|
||||||
|
controller: TextEditingController(text: 'blah blah'),
|
||||||
|
focusNode: focusNode,
|
||||||
|
readOnly: true,
|
||||||
|
toolbarOptions: const ToolbarOptions(
|
||||||
|
paste: true,
|
||||||
|
cut: true,
|
||||||
|
selectAll: true,
|
||||||
|
copy: true,
|
||||||
|
),
|
||||||
|
style: textStyle,
|
||||||
|
cursorColor: cursorColor,
|
||||||
|
selectionControls: materialTextSelectionControls,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final EditableTextState state =
|
||||||
|
tester.state<EditableTextState>(find.byType(EditableText));
|
||||||
|
|
||||||
|
// Select something. Doesn't really matter what.
|
||||||
|
state.renderEditable.selectWordsInRange(
|
||||||
|
from: const Offset(0, 0),
|
||||||
|
cause: SelectionChangedCause.tap,
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
expect(state.showToolbar(), true);
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.text('SELECT ALL'), findsOneWidget);
|
||||||
|
expect(find.text('COPY'), findsOneWidget);
|
||||||
|
expect(find.text('PASTE'), findsNothing);
|
||||||
|
expect(find.text('CUT'), findsNothing);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('Fires onChanged when text changes via TextSelectionOverlay', (WidgetTester tester) async {
|
testWidgets('Fires onChanged when text changes via TextSelectionOverlay', (WidgetTester tester) async {
|
||||||
String changedValue;
|
String changedValue;
|
||||||
final Widget widget = MaterialApp(
|
final Widget widget = MaterialApp(
|
||||||
@ -1628,8 +1701,14 @@ void main() {
|
|||||||
SemanticsFlag.isObscured,
|
SemanticsFlag.isObscured,
|
||||||
SemanticsFlag.isFocused,
|
SemanticsFlag.isFocused,
|
||||||
],
|
],
|
||||||
|
actions: <SemanticsAction>[
|
||||||
|
SemanticsAction.moveCursorBackwardByCharacter,
|
||||||
|
SemanticsAction.setSelection,
|
||||||
|
SemanticsAction.moveCursorBackwardByWord
|
||||||
|
],
|
||||||
value: expectedValue,
|
value: expectedValue,
|
||||||
textDirection: TextDirection.ltr,
|
textDirection: TextDirection.ltr,
|
||||||
|
textSelection: const TextSelection.collapsed(offset: 24),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -592,6 +592,27 @@ void main() {
|
|||||||
expect(find.text('CUT'), findsNothing);
|
expect(find.text('CUT'), findsNothing);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('selectable text can disable toolbar options', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
overlay(
|
||||||
|
child: const SelectableText(
|
||||||
|
'a selectable text',
|
||||||
|
toolbarOptions: ToolbarOptions(
|
||||||
|
copy: false,
|
||||||
|
selectAll: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const int dIndex = 5;
|
||||||
|
final Offset dPos = textOffsetToPosition(tester, dIndex);
|
||||||
|
await tester.longPressAt(dPos);
|
||||||
|
await tester.pump();
|
||||||
|
// Context menu should not have copy.
|
||||||
|
expect(find.text('COPY'), findsNothing);
|
||||||
|
expect(find.text('SELECT ALL'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('Can select text by dragging with a mouse', (WidgetTester tester) async {
|
testWidgets('Can select text by dragging with a mouse', (WidgetTester tester) async {
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
const MaterialApp(
|
const MaterialApp(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user