Skip to content

Commit a9e0dd4

Browse files
authored
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).
1 parent e25e1f9 commit a9e0dd4

File tree

8 files changed

+494
-213
lines changed

8 files changed

+494
-213
lines changed

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

Lines changed: 52 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,7 @@ class CupertinoTextField extends StatefulWidget {
289289
this.keyboardAppearance,
290290
this.scrollPadding = const EdgeInsets.all(20.0),
291291
this.dragStartBehavior = DragStartBehavior.start,
292-
this.enableInteractiveSelection = true,
292+
bool? enableInteractiveSelection,
293293
this.selectionControls,
294294
this.onTap,
295295
this.scrollController,
@@ -341,17 +341,31 @@ class CupertinoTextField extends StatefulWidget {
341341
),
342342
assert(enableIMEPersonalizedLearning != null),
343343
keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline),
344-
toolbarOptions = toolbarOptions ?? (obscureText ?
345-
const ToolbarOptions(
346-
selectAll: true,
347-
paste: true,
348-
) :
349-
const ToolbarOptions(
350-
copy: true,
351-
cut: true,
352-
selectAll: true,
353-
paste: true,
354-
)),
344+
enableInteractiveSelection = enableInteractiveSelection ?? (!readOnly || !obscureText),
345+
toolbarOptions = toolbarOptions ??
346+
(obscureText
347+
? (readOnly
348+
// No point in even offering "Select All" in a read-only obscured
349+
// field.
350+
? const ToolbarOptions()
351+
// Writable, but obscured.
352+
: const ToolbarOptions(
353+
selectAll: true,
354+
paste: true,
355+
))
356+
: (readOnly
357+
// Read-only, not obscured.
358+
? const ToolbarOptions(
359+
selectAll: true,
360+
copy: true,
361+
)
362+
// Writable, not obscured.
363+
: const ToolbarOptions(
364+
copy: true,
365+
cut: true,
366+
selectAll: true,
367+
paste: true,
368+
))),
355369
super(key: key);
356370

357371
/// Creates a borderless iOS-style text field.
@@ -446,7 +460,7 @@ class CupertinoTextField extends StatefulWidget {
446460
this.keyboardAppearance,
447461
this.scrollPadding = const EdgeInsets.all(20.0),
448462
this.dragStartBehavior = DragStartBehavior.start,
449-
this.enableInteractiveSelection = true,
463+
bool? enableInteractiveSelection,
450464
this.selectionControls,
451465
this.onTap,
452466
this.scrollController,
@@ -499,17 +513,31 @@ class CupertinoTextField extends StatefulWidget {
499513
assert(clipBehavior != null),
500514
assert(enableIMEPersonalizedLearning != null),
501515
keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline),
502-
toolbarOptions = toolbarOptions ?? (obscureText ?
503-
const ToolbarOptions(
504-
selectAll: true,
505-
paste: true,
506-
) :
507-
const ToolbarOptions(
508-
copy: true,
509-
cut: true,
510-
selectAll: true,
511-
paste: true,
512-
)),
516+
enableInteractiveSelection = enableInteractiveSelection ?? (!readOnly || !obscureText),
517+
toolbarOptions = toolbarOptions ??
518+
(obscureText
519+
? (readOnly
520+
// No point in even offering "Select All" in a read-only obscured
521+
// field.
522+
? const ToolbarOptions()
523+
// Writable, but obscured.
524+
: const ToolbarOptions(
525+
selectAll: true,
526+
paste: true,
527+
))
528+
: (readOnly
529+
// Read-only, not obscured.
530+
? const ToolbarOptions(
531+
selectAll: true,
532+
copy: true,
533+
)
534+
// Writable, not obscured.
535+
: const ToolbarOptions(
536+
copy: true,
537+
cut: true,
538+
selectAll: true,
539+
paste: true,
540+
))),
513541
super(key: key);
514542

515543
/// Controls the text being edited.

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

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,7 @@ class TextField extends StatefulWidget {
319319
this.keyboardAppearance,
320320
this.scrollPadding = const EdgeInsets.all(20.0),
321321
this.dragStartBehavior = DragStartBehavior.start,
322-
this.enableInteractiveSelection = true,
322+
bool? enableInteractiveSelection,
323323
this.selectionControls,
324324
this.onTap,
325325
this.mouseCursor,
@@ -339,7 +339,6 @@ class TextField extends StatefulWidget {
339339
smartDashesType = smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled),
340340
smartQuotesType = smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled),
341341
assert(enableSuggestions != null),
342-
assert(enableInteractiveSelection != null),
343342
assert(maxLengthEnforced != null),
344343
assert(
345344
maxLengthEnforced || maxLengthEnforcement == null,
@@ -372,17 +371,31 @@ class TextField extends StatefulWidget {
372371
assert(clipBehavior != null),
373372
assert(enableIMEPersonalizedLearning != null),
374373
keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline),
375-
toolbarOptions = toolbarOptions ?? (obscureText ?
376-
const ToolbarOptions(
377-
selectAll: true,
378-
paste: true,
379-
) :
380-
const ToolbarOptions(
381-
copy: true,
382-
cut: true,
383-
selectAll: true,
384-
paste: true,
385-
)),
374+
enableInteractiveSelection = enableInteractiveSelection ?? (!readOnly || !obscureText),
375+
toolbarOptions = toolbarOptions ??
376+
(obscureText
377+
? (readOnly
378+
// No point in even offering "Select All" in a read-only obscured
379+
// field.
380+
? const ToolbarOptions()
381+
// Writable, but obscured.
382+
: const ToolbarOptions(
383+
selectAll: true,
384+
paste: true,
385+
))
386+
: (readOnly
387+
// Read-only, not obscured.
388+
? const ToolbarOptions(
389+
selectAll: true,
390+
copy: true,
391+
)
392+
// Writable, not obscured.
393+
: const ToolbarOptions(
394+
copy: true,
395+
cut: true,
396+
selectAll: true,
397+
paste: true,
398+
))),
386399
super(key: key);
387400

388401
/// Controls the text being edited.

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ class TextFormField extends FormField<String> {
143143
Color? cursorColor,
144144
Brightness? keyboardAppearance,
145145
EdgeInsets scrollPadding = const EdgeInsets.all(20.0),
146-
bool enableInteractiveSelection = true,
146+
bool? enableInteractiveSelection,
147147
TextSelectionControls? selectionControls,
148148
InputCounterWidgetBuilder? buildCounter,
149149
ScrollPhysics? scrollPhysics,
@@ -179,7 +179,6 @@ class TextFormField extends FormField<String> {
179179
),
180180
assert(!obscureText || maxLines == 1, 'Obscured fields cannot be multiline.'),
181181
assert(maxLength == null || maxLength == TextField.noMaxLength || maxLength > 0),
182-
assert(enableInteractiveSelection != null),
183182
assert(enableIMEPersonalizedLearning != null),
184183
super(
185184
key: key,
@@ -243,7 +242,7 @@ class TextFormField extends FormField<String> {
243242
scrollPadding: scrollPadding,
244243
scrollPhysics: scrollPhysics,
245244
keyboardAppearance: keyboardAppearance,
246-
enableInteractiveSelection: enableInteractiveSelection,
245+
enableInteractiveSelection: enableInteractiveSelection ?? (!obscureText || !readOnly),
247246
selectionControls: selectionControls,
248247
buildCounter: buildCounter,
249248
autofillHints: autofillHints,

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

Lines changed: 49 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -508,16 +508,11 @@ class EditableText extends StatefulWidget {
508508
this.scrollPadding = const EdgeInsets.all(20.0),
509509
this.keyboardAppearance = Brightness.light,
510510
this.dragStartBehavior = DragStartBehavior.start,
511-
this.enableInteractiveSelection = true,
511+
bool? enableInteractiveSelection,
512512
this.scrollController,
513513
this.scrollPhysics,
514514
this.autocorrectionTextRectColor,
515-
this.toolbarOptions = const ToolbarOptions(
516-
copy: true,
517-
cut: true,
518-
paste: true,
519-
selectAll: true,
520-
),
515+
ToolbarOptions? toolbarOptions,
521516
this.autofillHints = const <String>[],
522517
this.autofillClient,
523518
this.clipBehavior = Clip.hardEdge,
@@ -533,7 +528,6 @@ class EditableText extends StatefulWidget {
533528
smartQuotesType = smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled),
534529
assert(enableSuggestions != null),
535530
assert(showSelectionHandles != null),
536-
assert(enableInteractiveSelection != null),
537531
assert(readOnly != null),
538532
assert(forceLine != null),
539533
assert(style != null),
@@ -560,7 +554,31 @@ class EditableText extends StatefulWidget {
560554
assert(rendererIgnoresPointer != null),
561555
assert(scrollPadding != null),
562556
assert(dragStartBehavior != null),
563-
assert(toolbarOptions != null),
557+
enableInteractiveSelection = enableInteractiveSelection ?? (!readOnly || !obscureText),
558+
toolbarOptions = toolbarOptions ??
559+
(obscureText
560+
? (readOnly
561+
// No point in even offering "Select All" in a read-only obscured
562+
// field.
563+
? const ToolbarOptions()
564+
// Writable, but obscured.
565+
: const ToolbarOptions(
566+
selectAll: true,
567+
paste: true,
568+
))
569+
: (readOnly
570+
// Read-only, not obscured.
571+
? const ToolbarOptions(
572+
selectAll: true,
573+
copy: true,
574+
)
575+
// Writable, not obscured.
576+
: const ToolbarOptions(
577+
copy: true,
578+
cut: true,
579+
selectAll: true,
580+
paste: true,
581+
))),
564582
assert(clipBehavior != null),
565583
assert(enableIMEPersonalizedLearning != null),
566584
_strutStyle = strutStyle,
@@ -593,7 +611,9 @@ class EditableText extends StatefulWidget {
593611
/// Whether to hide the text being edited (e.g., for passwords).
594612
///
595613
/// When this is set to true, all the characters in the text field are
596-
/// replaced by [obscuringCharacter].
614+
/// replaced by [obscuringCharacter], and the text in the field cannot be
615+
/// copied with copy or cut. If [readOnly] is also true, then the text cannot
616+
/// be selected.
597617
///
598618
/// Defaults to false. Cannot be null.
599619
/// {@endtemplate}
@@ -629,8 +649,10 @@ class EditableText extends StatefulWidget {
629649

630650
/// Configuration of toolbar options.
631651
///
632-
/// By default, all options are enabled. If [readOnly] is true,
633-
/// paste and cut will be disabled regardless.
652+
/// By default, all options are enabled. If [readOnly] is true, paste and cut
653+
/// will be disabled regardless. If [obscureText] is true, cut and copy will
654+
/// be disabled regardless. If [readOnly] and [obscureText] are both true,
655+
/// select all will also be disabled.
634656
final ToolbarOptions toolbarOptions;
635657

636658
/// Whether to show selection handles.
@@ -1492,6 +1514,7 @@ class EditableText extends StatefulWidget {
14921514
properties.add(DiagnosticsProperty<TextEditingController>('controller', controller));
14931515
properties.add(DiagnosticsProperty<FocusNode>('focusNode', focusNode));
14941516
properties.add(DiagnosticsProperty<bool>('obscureText', obscureText, defaultValue: false));
1517+
properties.add(DiagnosticsProperty<bool>('readOnly', readOnly, defaultValue: false));
14951518
properties.add(DiagnosticsProperty<bool>('autocorrect', autocorrect, defaultValue: true));
14961519
properties.add(EnumProperty<SmartDashesType>('smartDashesType', smartDashesType, defaultValue: obscureText ? SmartDashesType.disabled : SmartDashesType.enabled));
14971520
properties.add(EnumProperty<SmartQuotesType>('smartQuotesType', smartQuotesType, defaultValue: obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled));
@@ -1511,6 +1534,7 @@ class EditableText extends StatefulWidget {
15111534
properties.add(DiagnosticsProperty<Iterable<String>>('autofillHints', autofillHints, defaultValue: null));
15121535
properties.add(DiagnosticsProperty<TextHeightBehavior>('textHeightBehavior', textHeightBehavior, defaultValue: null));
15131536
properties.add(DiagnosticsProperty<bool>('enableIMEPersonalizedLearning', enableIMEPersonalizedLearning, defaultValue: true));
1537+
properties.add(DiagnosticsProperty<bool>('enableInteractiveSelection', enableInteractiveSelection, defaultValue: true));
15141538
}
15151539
}
15161540

@@ -1573,16 +1597,16 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
15731597
Color get _cursorColor => widget.cursorColor.withOpacity(_cursorBlinkOpacityController!.value);
15741598

15751599
@override
1576-
bool get cutEnabled => widget.toolbarOptions.cut && !widget.readOnly;
1600+
bool get cutEnabled => widget.toolbarOptions.cut && !widget.readOnly && !widget.obscureText;
15771601

15781602
@override
1579-
bool get copyEnabled => widget.toolbarOptions.copy;
1603+
bool get copyEnabled => widget.toolbarOptions.copy && !widget.obscureText;
15801604

15811605
@override
15821606
bool get pasteEnabled => widget.toolbarOptions.paste && !widget.readOnly;
15831607

15841608
@override
1585-
bool get selectAllEnabled => widget.toolbarOptions.selectAll;
1609+
bool get selectAllEnabled => widget.toolbarOptions.selectAll && (!widget.readOnly || !widget.obscureText) && widget.enableInteractiveSelection;
15861610

15871611
void _onChangedClipboardStatus() {
15881612
setState(() {
@@ -1602,11 +1626,11 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
16021626
@override
16031627
void copySelection(SelectionChangedCause cause) {
16041628
final TextSelection selection = textEditingValue.selection;
1605-
final String text = textEditingValue.text;
16061629
assert(selection != null);
1607-
if (selection.isCollapsed) {
1630+
if (selection.isCollapsed || widget.obscureText) {
16081631
return;
16091632
}
1633+
final String text = textEditingValue.text;
16101634
Clipboard.setData(ClipboardData(text: selection.textInside(text)));
16111635
if (cause == SelectionChangedCause.toolbar) {
16121636
bringIntoView(textEditingValue.selection.extent);
@@ -1636,7 +1660,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
16361660
/// Cut current selection to [Clipboard].
16371661
@override
16381662
void cutSelection(SelectionChangedCause cause) {
1639-
if (widget.readOnly) {
1663+
if (widget.readOnly || widget.obscureText) {
16401664
return;
16411665
}
16421666
final TextSelection selection = textEditingValue.selection;
@@ -1681,6 +1705,11 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
16811705
/// Select the entire text value.
16821706
@override
16831707
void selectAll(SelectionChangedCause cause) {
1708+
if (widget.readOnly && widget.obscureText) {
1709+
// If we can't modify it, and we can't copy it, there's no point in
1710+
// selecting it.
1711+
return;
1712+
}
16841713
userUpdateTextEditingValue(
16851714
textEditingValue.copyWith(
16861715
selection: TextSelection(baseOffset: 0, extentOffset: textEditingValue.text.length),
@@ -3057,7 +3086,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
30573086
selectionHeightStyle: widget.selectionHeightStyle,
30583087
selectionWidthStyle: widget.selectionWidthStyle,
30593088
paintCursorAboveText: widget.paintCursorAboveText,
3060-
enableInteractiveSelection: widget.enableInteractiveSelection,
3089+
enableInteractiveSelection: widget.enableInteractiveSelection && (!widget.readOnly || !widget.obscureText),
30613090
textSelectionDelegate: this,
30623091
devicePixelRatio: _devicePixelRatio,
30633092
promptRectRange: _currentPromptRectRange,
@@ -3287,6 +3316,7 @@ class _Editable extends MultiChildRenderObjectWidget {
32873316
..cursorOffset = cursorOffset
32883317
..selectionHeightStyle = selectionHeightStyle
32893318
..selectionWidthStyle = selectionWidthStyle
3319+
..enableInteractiveSelection = enableInteractiveSelection
32903320
..textSelectionDelegate = textSelectionDelegate
32913321
..devicePixelRatio = devicePixelRatio
32923322
..paintCursorAboveText = paintCursorAboveText

0 commit comments

Comments
 (0)