Skip to content

Commit 7eb09a8

Browse files
authored
reland Enable selection by default for password text field and expose api to turn on and off context menu options (#34676) (#37183)
This reverts commit 48c7090.
1 parent bd02e4f commit 7eb09a8

File tree

12 files changed

+376
-62
lines changed

12 files changed

+376
-62
lines changed

dev/integration_tests/android_semantics_testing/test_driver/main_test.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,8 @@ void main() {
112112
actions: <AndroidSemanticsAction>[
113113
AndroidSemanticsAction.click,
114114
AndroidSemanticsAction.accessibilityFocus,
115+
AndroidSemanticsAction.setSelection,
116+
AndroidSemanticsAction.copy,
115117
],
116118
));
117119

packages/flutter/lib/src/cupertino/text_field.dart

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@ class CupertinoTextField extends StatefulWidget {
209209
this.textAlign = TextAlign.start,
210210
this.textAlignVertical,
211211
this.readOnly = false,
212+
ToolbarOptions toolbarOptions,
212213
this.showCursor,
213214
this.autofocus = false,
214215
this.obscureText = false,
@@ -229,7 +230,7 @@ class CupertinoTextField extends StatefulWidget {
229230
this.keyboardAppearance,
230231
this.scrollPadding = const EdgeInsets.all(20.0),
231232
this.dragStartBehavior = DragStartBehavior.start,
232-
this.enableInteractiveSelection,
233+
this.enableInteractiveSelection = true,
233234
this.onTap,
234235
this.scrollController,
235236
this.scrollPhysics,
@@ -257,6 +258,17 @@ class CupertinoTextField extends StatefulWidget {
257258
assert(prefixMode != null),
258259
assert(suffixMode != null),
259260
keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline),
261+
toolbarOptions = toolbarOptions ?? obscureText ?
262+
const ToolbarOptions(
263+
selectAll: true,
264+
paste: true,
265+
) :
266+
const ToolbarOptions(
267+
copy: true,
268+
cut: true,
269+
selectAll: true,
270+
paste: true,
271+
),
260272
super(key: key);
261273

262274
/// Controls the text being edited.
@@ -358,6 +370,13 @@ class CupertinoTextField extends StatefulWidget {
358370
/// {@macro flutter.widgets.editableText.textAlign}
359371
final TextAlign textAlign;
360372

373+
/// Configuration of toolbar options.
374+
///
375+
/// If not set, select all and paste will default to be enabled. Copy and cut
376+
/// will be disabled if [obscureText] is true. If [readOnly] is true,
377+
/// paste and cut will be disabled regardless.
378+
final ToolbarOptions toolbarOptions;
379+
361380
/// {@macro flutter.material.inputDecorator.textAlignVertical}
362381
final TextAlignVertical textAlignVertical;
363382

@@ -498,9 +517,7 @@ class CupertinoTextField extends StatefulWidget {
498517
final ScrollPhysics scrollPhysics;
499518

500519
/// {@macro flutter.rendering.editable.selectionEnabled}
501-
bool get selectionEnabled {
502-
return enableInteractiveSelection ?? !obscureText;
503-
}
520+
bool get selectionEnabled => enableInteractiveSelection;
504521

505522
/// {@macro flutter.material.textfield.onTap}
506523
final GestureTapCallback onTap;
@@ -804,6 +821,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
804821
key: editableTextKey,
805822
controller: controller,
806823
readOnly: widget.readOnly,
824+
toolbarOptions: widget.toolbarOptions,
807825
showCursor: widget.showCursor,
808826
showSelectionHandles: _showSelectionHandles,
809827
focusNode: _effectiveFocusNode,

packages/flutter/lib/src/material/selectable_text.dart

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ class SelectableText extends StatefulWidget {
195195
this.textDirection,
196196
this.showCursor = false,
197197
this.autofocus = false,
198+
ToolbarOptions toolbarOptions,
198199
this.maxLines,
199200
this.cursorWidth = 2.0,
200201
this.cursorRadius,
@@ -213,6 +214,11 @@ class SelectableText extends StatefulWidget {
213214
'A non-null String must be provided to a SelectableText widget.',
214215
),
215216
textSpan = null,
217+
toolbarOptions = toolbarOptions ??
218+
const ToolbarOptions(
219+
selectAll: true,
220+
copy: true,
221+
),
216222
super(key: key);
217223

218224
/// Creates a selectable text widget with a [TextSpan].
@@ -229,6 +235,7 @@ class SelectableText extends StatefulWidget {
229235
this.textDirection,
230236
this.showCursor = false,
231237
this.autofocus = false,
238+
ToolbarOptions toolbarOptions,
232239
this.maxLines,
233240
this.cursorWidth = 2.0,
234241
this.cursorRadius,
@@ -247,6 +254,11 @@ class SelectableText extends StatefulWidget {
247254
'A non-null TextSpan must be provided to a SelectableText.rich widget.',
248255
),
249256
data = null,
257+
toolbarOptions = toolbarOptions ??
258+
const ToolbarOptions(
259+
selectAll: true,
260+
copy: true,
261+
),
250262
super(key: key);
251263

252264
/// The text to display.
@@ -325,6 +337,13 @@ class SelectableText extends StatefulWidget {
325337
/// {@macro flutter.widgets.scrollable.dragStartBehavior}
326338
final DragStartBehavior dragStartBehavior;
327339

340+
/// Configuration of toolbar options.
341+
///
342+
/// Paste and cut will be disabled regardless.
343+
///
344+
/// If not set, select all and copy will be enabled by default.
345+
final ToolbarOptions toolbarOptions;
346+
328347
/// {@macro flutter.rendering.editable.selectionEnabled}
329348
bool get selectionEnabled {
330349
return enableInteractiveSelection;
@@ -543,6 +562,7 @@ class _SelectableTextState extends State<SelectableText> with AutomaticKeepAlive
543562
textDirection: widget.textDirection,
544563
autofocus: widget.autofocus,
545564
forceLine: false,
565+
toolbarOptions: widget.toolbarOptions,
546566
maxLines: widget.maxLines ?? defaultTextStyle.maxLines,
547567
selectionColor: themeData.textSelectionColor,
548568
selectionControls: widget.selectionEnabled ? textSelectionControls : null,

packages/flutter/lib/src/material/text_field.dart

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@ class TextField extends StatefulWidget {
256256
this.textAlignVertical,
257257
this.textDirection,
258258
this.readOnly = false,
259+
ToolbarOptions toolbarOptions,
259260
this.showCursor,
260261
this.autofocus = false,
261262
this.obscureText = false,
@@ -276,7 +277,7 @@ class TextField extends StatefulWidget {
276277
this.keyboardAppearance,
277278
this.scrollPadding = const EdgeInsets.all(20.0),
278279
this.dragStartBehavior = DragStartBehavior.start,
279-
this.enableInteractiveSelection,
280+
this.enableInteractiveSelection = true,
280281
this.onTap,
281282
this.buildCounter,
282283
this.scrollController,
@@ -286,6 +287,7 @@ class TextField extends StatefulWidget {
286287
assert(autofocus != null),
287288
assert(obscureText != null),
288289
assert(autocorrect != null),
290+
assert(enableInteractiveSelection != null),
289291
assert(maxLengthEnforced != null),
290292
assert(scrollPadding != null),
291293
assert(dragStartBehavior != null),
@@ -302,6 +304,17 @@ class TextField extends StatefulWidget {
302304
),
303305
assert(maxLength == null || maxLength == TextField.noMaxLength || maxLength > 0),
304306
keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline),
307+
toolbarOptions = toolbarOptions ?? obscureText ?
308+
const ToolbarOptions(
309+
selectAll: true,
310+
paste: true,
311+
) :
312+
const ToolbarOptions(
313+
copy: true,
314+
cut: true,
315+
selectAll: true,
316+
paste: true,
317+
),
305318
super(key: key);
306319

307320
/// Controls the text being edited.
@@ -410,6 +423,13 @@ class TextField extends StatefulWidget {
410423
/// {@macro flutter.widgets.editableText.readOnly}
411424
final bool readOnly;
412425

426+
/// Configuration of toolbar options.
427+
///
428+
/// If not set, select all and paste will default to be enabled. Copy and cut
429+
/// will be disabled if [obscureText] is true. If [readOnly] is true,
430+
/// paste and cut will be disabled regardless.
431+
final ToolbarOptions toolbarOptions;
432+
413433
/// {@macro flutter.widgets.editableText.showCursor}
414434
final bool showCursor;
415435

@@ -538,9 +558,7 @@ class TextField extends StatefulWidget {
538558
final DragStartBehavior dragStartBehavior;
539559

540560
/// {@macro flutter.rendering.editable.selectionEnabled}
541-
bool get selectionEnabled {
542-
return enableInteractiveSelection ?? !obscureText;
543-
}
561+
bool get selectionEnabled => enableInteractiveSelection;
544562

545563
/// {@template flutter.material.textfield.onTap}
546564
/// 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
952970
child: EditableText(
953971
key: editableTextKey,
954972
readOnly: widget.readOnly,
973+
toolbarOptions: widget.toolbarOptions,
955974
showCursor: widget.showCursor,
956975
showSelectionHandles: _showSelectionHandles,
957976
controller: controller,

packages/flutter/lib/src/material/text_form_field.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ class TextFormField extends FormField<String> {
8989
TextAlign textAlign = TextAlign.start,
9090
bool autofocus = false,
9191
bool readOnly = false,
92+
ToolbarOptions toolbarOptions,
9293
bool showCursor,
9394
bool obscureText = false,
9495
bool autocorrect = true,
@@ -163,6 +164,7 @@ class TextFormField extends FormField<String> {
163164
textDirection: textDirection,
164165
textCapitalization: textCapitalization,
165166
autofocus: autofocus,
167+
toolbarOptions: toolbarOptions,
166168
readOnly: readOnly,
167169
showCursor: showCursor,
168170
obscureText: obscureText,

packages/flutter/lib/src/rendering/editable.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1554,6 +1554,10 @@ class RenderEditable extends RenderBox {
15541554
// When long-pressing past the end of the text, we want a collapsed cursor.
15551555
if (position.offset >= word.end)
15561556
return TextSelection.fromPosition(position);
1557+
// If text is obscured, the entire sentence should be treated as one word.
1558+
if (obscureText) {
1559+
return TextSelection(baseOffset: 0, extentOffset: text.toPlainText().length);
1560+
}
15571561
return TextSelection(baseOffset: word.start, extentOffset: word.end);
15581562
}
15591563

packages/flutter/lib/src/widgets/editable_text.dart

Lines changed: 68 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,54 @@ class TextEditingController extends ValueNotifier<TextEditingValue> {
221221
}
222222
}
223223

224+
/// Toolbar configuration for [EditableText].
225+
///
226+
/// Toolbar is a context menu that will show up when user right click or long
227+
/// press the [EditableText]. It includes several options: cut, copy, paste,
228+
/// and select all.
229+
///
230+
/// [EditableText] and its derived widgets have their own default [ToolbarOptions].
231+
/// Create a custom [ToolbarOptions] if you want explicit control over the toolbar
232+
/// option.
233+
class ToolbarOptions {
234+
/// Create a toolbar configuration with given options.
235+
///
236+
/// All options default to false if they are not explicitly set.
237+
const ToolbarOptions({
238+
this.copy = false,
239+
this.cut = false,
240+
this.paste = false,
241+
this.selectAll = false,
242+
}) : assert(copy != null),
243+
assert(cut != null),
244+
assert(paste != null),
245+
assert(selectAll != null);
246+
247+
/// Whether to show copy option in toolbar.
248+
///
249+
/// Defaults to false. Must not be null.
250+
final bool copy;
251+
252+
/// Whether to show cut option in toolbar.
253+
///
254+
/// If [EditableText.readOnly] is set to true, cut will be disabled regardless.
255+
///
256+
/// Defaults to false. Must not be null.
257+
final bool cut;
258+
259+
/// Whether to show paste option in toolbar.
260+
///
261+
/// If [EditableText.readOnly] is set to true, paste will be disabled regardless.
262+
///
263+
/// Defaults to false. Must not be null.
264+
final bool paste;
265+
266+
/// Whether to show select all option in toolbar.
267+
///
268+
/// Defaults to false. Must not be null.
269+
final bool selectAll;
270+
}
271+
224272
/// A basic text input field.
225273
///
226274
/// This widget interacts with the [TextInput] service to let the user edit the
@@ -336,14 +384,21 @@ class EditableText extends StatefulWidget {
336384
this.scrollPadding = const EdgeInsets.all(20.0),
337385
this.keyboardAppearance = Brightness.light,
338386
this.dragStartBehavior = DragStartBehavior.start,
339-
this.enableInteractiveSelection,
387+
this.enableInteractiveSelection = true,
340388
this.scrollController,
341389
this.scrollPhysics,
390+
this.toolbarOptions = const ToolbarOptions(
391+
copy: true,
392+
cut: true,
393+
paste: true,
394+
selectAll: true
395+
)
342396
}) : assert(controller != null),
343397
assert(focusNode != null),
344398
assert(obscureText != null),
345399
assert(autocorrect != null),
346400
assert(showSelectionHandles != null),
401+
assert(enableInteractiveSelection != null),
347402
assert(readOnly != null),
348403
assert(forceLine != null),
349404
assert(style != null),
@@ -367,6 +422,7 @@ class EditableText extends StatefulWidget {
367422
assert(rendererIgnoresPointer != null),
368423
assert(scrollPadding != null),
369424
assert(dragStartBehavior != null),
425+
assert(toolbarOptions != null),
370426
_strutStyle = strutStyle,
371427
keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline),
372428
inputFormatters = maxLines == 1
@@ -419,6 +475,12 @@ class EditableText extends StatefulWidget {
419475
/// * [textWidthBasis], which controls the calculation of text width.
420476
final bool forceLine;
421477

478+
/// Configuration of toolbar options.
479+
///
480+
/// By default, all options are enabled. If [readOnly] is true,
481+
/// paste and cut will be disabled regardless.
482+
final ToolbarOptions toolbarOptions;
483+
422484
/// Whether to show selection handles.
423485
///
424486
/// When a selection is active, there will be two handles at each side of
@@ -903,9 +965,7 @@ class EditableText extends StatefulWidget {
903965
final ScrollPhysics scrollPhysics;
904966

905967
/// {@macro flutter.rendering.editable.selectionEnabled}
906-
bool get selectionEnabled {
907-
return enableInteractiveSelection ?? !obscureText;
908-
}
968+
bool get selectionEnabled => enableInteractiveSelection;
909969

910970
@override
911971
EditableTextState createState() => EditableTextState();
@@ -969,16 +1029,16 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
9691029
Color get _cursorColor => widget.cursorColor.withOpacity(_cursorBlinkOpacityController.value);
9701030

9711031
@override
972-
bool get cutEnabled => !widget.readOnly;
1032+
bool get cutEnabled => widget.toolbarOptions.cut && !widget.readOnly;
9731033

9741034
@override
975-
bool get copyEnabled => true;
1035+
bool get copyEnabled => widget.toolbarOptions.copy;
9761036

9771037
@override
978-
bool get pasteEnabled => !widget.readOnly;
1038+
bool get pasteEnabled => widget.toolbarOptions.paste && !widget.readOnly;
9791039

9801040
@override
981-
bool get selectAllEnabled => true;
1041+
bool get selectAllEnabled => widget.toolbarOptions.selectAll;
9821042

9831043
// State lifecycle:
9841044

0 commit comments

Comments
 (0)