Skip to content

Commit

Permalink
Revert "Disallow copy and cut when obscureText is set on TextField (
Browse files Browse the repository at this point in the history
  • Loading branch information
gspencergoog committed Jan 7, 2022
1 parent c5f108f commit 76cf452
Show file tree
Hide file tree
Showing 3 changed files with 172 additions and 160 deletions.
64 changes: 18 additions & 46 deletions packages/flutter/lib/src/widgets/editable_text.dart
Original file line number Diff line number Diff line change
Expand Up @@ -512,7 +512,12 @@ class EditableText extends StatefulWidget {
this.scrollController,
this.scrollPhysics,
this.autocorrectionTextRectColor,
ToolbarOptions? toolbarOptions,
this.toolbarOptions = const ToolbarOptions(
copy: true,
cut: true,
paste: true,
selectAll: true,
),
this.autofillHints = const <String>[],
this.autofillClient,
this.clipBehavior = Clip.hardEdge,
Expand Down Expand Up @@ -555,32 +560,8 @@ class EditableText extends StatefulWidget {
assert(rendererIgnoresPointer != null),
assert(scrollPadding != null),
assert(dragStartBehavior != null),
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(toolbarOptions != null),
assert(clipBehavior != null),
assert(enableIMEPersonalizedLearning != null),
_strutStyle = strutStyle,
keyboardType = keyboardType ?? _inferKeyboardType(autofillHints: autofillHints, maxLines: maxLines),
Expand Down Expand Up @@ -612,9 +593,7 @@ class EditableText extends StatefulWidget {
/// 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
/// 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.
/// replaced by [obscuringCharacter].
///
/// Defaults to false. Cannot be null.
/// {@endtemplate}
Expand Down Expand Up @@ -650,10 +629,8 @@ class EditableText extends StatefulWidget {

/// Configuration of toolbar options.
///
/// By default, all options are enabled. If [readOnly] is true, paste and cut
/// 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.
/// 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.
Expand Down Expand Up @@ -1596,16 +1573,16 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
Color get _cursorColor => widget.cursorColor.withOpacity(_cursorBlinkOpacityController!.value);

@override
bool get cutEnabled => widget.toolbarOptions.cut && !widget.readOnly && !widget.obscureText;
bool get cutEnabled => widget.toolbarOptions.cut && !widget.readOnly;

@override
bool get copyEnabled => widget.toolbarOptions.copy && !widget.obscureText;
bool get copyEnabled => widget.toolbarOptions.copy;

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

@override
bool get selectAllEnabled => widget.toolbarOptions.selectAll && (!widget.readOnly || !widget.obscureText) && widget.enableInteractiveSelection;
bool get selectAllEnabled => widget.toolbarOptions.selectAll;

void _onChangedClipboardStatus() {
setState(() {
Expand All @@ -1625,11 +1602,11 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
@override
void copySelection(SelectionChangedCause cause) {
final TextSelection selection = textEditingValue.selection;
final String text = textEditingValue.text;
assert(selection != null);
if (selection.isCollapsed || widget.obscureText) {
if (selection.isCollapsed) {
return;
}
final String text = textEditingValue.text;
Clipboard.setData(ClipboardData(text: selection.textInside(text)));
if (cause == SelectionChangedCause.toolbar) {
bringIntoView(textEditingValue.selection.extent);
Expand Down Expand Up @@ -1659,7 +1636,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
/// Cut current selection to [Clipboard].
@override
void cutSelection(SelectionChangedCause cause) {
if (widget.readOnly || widget.obscureText) {
if (widget.readOnly) {
return;
}
final TextSelection selection = textEditingValue.selection;
Expand Down Expand Up @@ -1704,11 +1681,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
/// Select the entire text value.
@override
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(
textEditingValue.copyWith(
selection: TextSelection(baseOffset: 0, extentOffset: textEditingValue.text.length),
Expand Down Expand Up @@ -3060,7 +3032,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
selectionHeightStyle: widget.selectionHeightStyle,
selectionWidthStyle: widget.selectionWidthStyle,
paintCursorAboveText: widget.paintCursorAboveText,
enableInteractiveSelection: widget.enableInteractiveSelection && (!widget.readOnly || !widget.obscureText),
enableInteractiveSelection: widget.enableInteractiveSelection,
textSelectionDelegate: this,
devicePixelRatio: _devicePixelRatio,
promptRectRange: _currentPromptRectRange,
Expand Down
153 changes: 153 additions & 0 deletions packages/flutter/test/material/text_field_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4970,6 +4970,82 @@ void main() {
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
testWidgets('Paste does not crash when the section is inValid', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
Expand Down Expand Up @@ -5098,6 +5174,83 @@ void main() {
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 {
final FocusNode focusNode = FocusNode();
final TextEditingController controller = TextEditingController();
Expand Down
Loading

0 comments on commit 76cf452

Please sign in to comment.