From de8cf8b530dea716d778bdec41c6ec4b32a2071d Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Fri, 24 Apr 2020 17:59:02 -0700 Subject: [PATCH] Customizable obscuringCharacter (#55415) --- .../flutter/lib/src/cupertino/text_field.dart | 7 ++++++ .../flutter/lib/src/material/text_field.dart | 7 ++++++ .../lib/src/material/text_form_field.dart | 3 +++ .../flutter/lib/src/rendering/editable.dart | 20 +++++++++++++--- .../lib/src/widgets/editable_text.dart | 20 ++++++++++++++-- .../test/widgets/editable_text_test.dart | 23 ++++++++++++++++++- 6 files changed, 74 insertions(+), 6 deletions(-) diff --git a/packages/flutter/lib/src/cupertino/text_field.dart b/packages/flutter/lib/src/cupertino/text_field.dart index a00410fd8f..b36c6cc890 100644 --- a/packages/flutter/lib/src/cupertino/text_field.dart +++ b/packages/flutter/lib/src/cupertino/text_field.dart @@ -241,6 +241,7 @@ class CupertinoTextField extends StatefulWidget { ToolbarOptions toolbarOptions, this.showCursor, this.autofocus = false, + this.obscuringCharacter = '•', this.obscureText = false, this.autocorrect = true, SmartDashesType smartDashesType, @@ -272,6 +273,7 @@ class CupertinoTextField extends StatefulWidget { }) : assert(textAlign != null), assert(readOnly != null), assert(autofocus != null), + assert(obscuringCharacter != null && obscuringCharacter.length == 1), assert(obscureText != null), assert(autocorrect != null), smartDashesType = smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled), @@ -428,6 +430,9 @@ class CupertinoTextField extends StatefulWidget { /// {@macro flutter.widgets.editableText.autofocus} final bool autofocus; + /// {@macro flutter.widgets.editableText.obscuringCharacter} + final String obscuringCharacter; + /// {@macro flutter.widgets.editableText.obscureText} final bool obscureText; @@ -601,6 +606,7 @@ class CupertinoTextField extends StatefulWidget { properties.add(DiagnosticsProperty('keyboardType', keyboardType, defaultValue: TextInputType.text)); properties.add(DiagnosticsProperty('style', style, defaultValue: null)); properties.add(DiagnosticsProperty('autofocus', autofocus, defaultValue: false)); + properties.add(DiagnosticsProperty('obscuringCharacter', obscuringCharacter, defaultValue: '•')); properties.add(DiagnosticsProperty('obscureText', obscureText, defaultValue: false)); properties.add(DiagnosticsProperty('autocorrect', autocorrect, defaultValue: true)); properties.add(EnumProperty('smartDashesType', smartDashesType, defaultValue: obscureText ? SmartDashesType.disabled : SmartDashesType.enabled)); @@ -921,6 +927,7 @@ class _CupertinoTextFieldState extends State with AutomaticK strutStyle: widget.strutStyle, textAlign: widget.textAlign, autofocus: widget.autofocus, + obscuringCharacter: widget.obscuringCharacter, obscureText: widget.obscureText, autocorrect: widget.autocorrect, smartDashesType: widget.smartDashesType, diff --git a/packages/flutter/lib/src/material/text_field.dart b/packages/flutter/lib/src/material/text_field.dart index 62a6fe3d0c..23d7893ab7 100644 --- a/packages/flutter/lib/src/material/text_field.dart +++ b/packages/flutter/lib/src/material/text_field.dart @@ -318,6 +318,7 @@ class TextField extends StatefulWidget { ToolbarOptions toolbarOptions, this.showCursor, this.autofocus = false, + this.obscuringCharacter = '•', this.obscureText = false, this.autocorrect = true, SmartDashesType smartDashesType, @@ -350,6 +351,7 @@ class TextField extends StatefulWidget { }) : assert(textAlign != null), assert(readOnly != null), assert(autofocus != null), + assert(obscuringCharacter != null && obscuringCharacter.length == 1), assert(obscureText != null), assert(autocorrect != null), smartDashesType = smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled), @@ -476,6 +478,9 @@ class TextField extends StatefulWidget { /// {@macro flutter.widgets.editableText.autofocus} final bool autofocus; + /// {@macro flutter.widgets.editableText.obscuringCharacter} + final String obscuringCharacter; + /// {@macro flutter.widgets.editableText.obscureText} final bool obscureText; @@ -727,6 +732,7 @@ class TextField extends StatefulWidget { properties.add(DiagnosticsProperty('keyboardType', keyboardType, defaultValue: TextInputType.text)); properties.add(DiagnosticsProperty('style', style, defaultValue: null)); properties.add(DiagnosticsProperty('autofocus', autofocus, defaultValue: false)); + properties.add(DiagnosticsProperty('obscuringCharacter', obscuringCharacter, defaultValue: '•')); properties.add(DiagnosticsProperty('obscureText', obscureText, defaultValue: false)); properties.add(DiagnosticsProperty('autocorrect', autocorrect, defaultValue: true)); properties.add(EnumProperty('smartDashesType', smartDashesType, defaultValue: obscureText ? SmartDashesType.disabled : SmartDashesType.enabled)); @@ -1021,6 +1027,7 @@ class _TextFieldState extends State implements TextSelectionGestureDe textAlign: widget.textAlign, textDirection: widget.textDirection, autofocus: widget.autofocus, + obscuringCharacter: widget.obscuringCharacter, obscureText: widget.obscureText, autocorrect: widget.autocorrect, smartDashesType: widget.smartDashesType, diff --git a/packages/flutter/lib/src/material/text_form_field.dart b/packages/flutter/lib/src/material/text_form_field.dart index ee5fa95827..8d235754f7 100644 --- a/packages/flutter/lib/src/material/text_form_field.dart +++ b/packages/flutter/lib/src/material/text_form_field.dart @@ -146,6 +146,7 @@ class TextFormField extends FormField { bool readOnly = false, ToolbarOptions toolbarOptions, bool showCursor, + String obscuringCharacter = '•', bool obscureText = false, bool autocorrect = true, SmartDashesType smartDashesType, @@ -177,6 +178,7 @@ class TextFormField extends FormField { assert(textAlign != null), assert(autofocus != null), assert(readOnly != null), + assert(obscuringCharacter != null && obscuringCharacter.length == 1), assert(obscureText != null), assert(autocorrect != null), assert(enableSuggestions != null), @@ -230,6 +232,7 @@ class TextFormField extends FormField { toolbarOptions: toolbarOptions, readOnly: readOnly, showCursor: showCursor, + obscuringCharacter: obscuringCharacter, obscureText: obscureText, autocorrect: autocorrect, smartDashesType: smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled), diff --git a/packages/flutter/lib/src/rendering/editable.dart b/packages/flutter/lib/src/rendering/editable.dart index df5a7c5c5a..0f31f6ab57 100644 --- a/packages/flutter/lib/src/rendering/editable.dart +++ b/packages/flutter/lib/src/rendering/editable.dart @@ -213,6 +213,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin { bool readOnly = false, bool forceLine = true, TextWidthBasis textWidthBasis = TextWidthBasis.parent, + String obscuringCharacter = '•', bool obscureText = false, Locale locale, double cursorWidth = 1.0, @@ -247,6 +248,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin { assert(ignorePointer != null), assert(textWidthBasis != null), assert(paintCursorAboveText != null), + assert(obscuringCharacter != null && obscuringCharacter.length == 1), assert(obscureText != null), assert(textSelectionDelegate != null), assert(cursorWidth != null && cursorWidth >= 0.0), @@ -284,6 +286,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin { _selectionWidthStyle = selectionWidthStyle, _startHandleLayerLink = startHandleLayerLink, _endHandleLayerLink = endHandleLayerLink, + _obscuringCharacter = obscuringCharacter, _obscureText = obscureText, _readOnly = readOnly, _forceLine = forceLine, @@ -295,9 +298,6 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin { _promptRectPaint.color = promptRectColor; } - /// Character used to obscure text if [obscureText] is true. - static const String obscuringCharacter = '•'; - /// Called when the selection changes. /// /// If this is null, then selection changes will be ignored. @@ -344,6 +344,20 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin { markNeedsTextLayout(); } + /// Character used for obscuring text if [obscureText] is true. + /// + /// Cannot be null, and must have a length of exactly one. + String get obscuringCharacter => _obscuringCharacter; + String _obscuringCharacter; + set obscuringCharacter(String value) { + if (_obscuringCharacter == value) { + return; + } + assert(value != null && value.length == 1); + _obscuringCharacter = value; + markNeedsLayout(); + } + /// Whether to hide the text being edited (e.g., for passwords). bool get obscureText => _obscureText; bool _obscureText; diff --git a/packages/flutter/lib/src/widgets/editable_text.dart b/packages/flutter/lib/src/widgets/editable_text.dart index a1792bb436..ef0d5b14e1 100644 --- a/packages/flutter/lib/src/widgets/editable_text.dart +++ b/packages/flutter/lib/src/widgets/editable_text.dart @@ -357,6 +357,7 @@ class EditableText extends StatefulWidget { @required this.controller, @required this.focusNode, this.readOnly = false, + this.obscuringCharacter = '•', this.obscureText = false, this.autocorrect = true, SmartDashesType smartDashesType, @@ -413,6 +414,7 @@ class EditableText extends StatefulWidget { this.autofillHints, }) : assert(controller != null), assert(focusNode != null), + assert(obscuringCharacter != null && obscuringCharacter.length == 1), assert(obscureText != null), assert(autocorrect != null), smartDashesType = smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled), @@ -464,11 +466,20 @@ class EditableText extends StatefulWidget { /// Controls whether this widget has keyboard focus. final FocusNode focusNode; + /// {@template flutter.widgets.editableText.obscuringCharacter} + /// Character used for obscuring text if [obscureText] is true. + /// + /// Must be only a single character. + /// + /// Defaults to the character U+2022 BULLET (•). + /// {@endtemplate} + final String obscuringCharacter; + /// {@template flutter.widgets.editableText.obscureText} /// 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 U+2022 BULLET characters (•). + /// replaced by [obscuringCharacter]. /// /// Defaults to false. Cannot be null. /// {@endtemplate} @@ -2029,6 +2040,7 @@ class EditableTextState extends State with AutomaticKeepAliveClien textDirection: _textDirection, locale: widget.locale, textWidthBasis: widget.textWidthBasis, + obscuringCharacter: widget.obscuringCharacter, obscureText: widget.obscureText, autocorrect: widget.autocorrect, smartDashesType: widget.smartDashesType, @@ -2063,7 +2075,7 @@ class EditableTextState extends State with AutomaticKeepAliveClien TextSpan buildTextSpan() { if (widget.obscureText) { String text = _value.text; - text = RenderEditable.obscuringCharacter * text.length; + text = widget.obscuringCharacter * text.length; final int o = _obscureShowCharTicksPending > 0 ? _obscureLatestCharIndex : null; if (o != null && o >= 0 && o < text.length) @@ -2101,6 +2113,7 @@ class _Editable extends LeafRenderObjectWidget { this.textAlign, @required this.textDirection, this.locale, + this.obscuringCharacter, this.obscureText, this.autocorrect, this.smartDashesType, @@ -2144,6 +2157,7 @@ class _Editable extends LeafRenderObjectWidget { final TextAlign textAlign; final TextDirection textDirection; final Locale locale; + final String obscuringCharacter; final bool obscureText; final TextWidthBasis textWidthBasis; final bool autocorrect; @@ -2192,6 +2206,7 @@ class _Editable extends LeafRenderObjectWidget { onSelectionChanged: onSelectionChanged, onCaretChanged: onCaretChanged, ignorePointer: rendererIgnoresPointer, + obscuringCharacter: obscuringCharacter, obscureText: obscureText, textWidthBasis: textWidthBasis, cursorWidth: cursorWidth, @@ -2234,6 +2249,7 @@ class _Editable extends LeafRenderObjectWidget { ..onCaretChanged = onCaretChanged ..ignorePointer = rendererIgnoresPointer ..textWidthBasis = textWidthBasis + ..obscuringCharacter = obscuringCharacter ..obscureText = obscureText ..cursorWidth = cursorWidth ..cursorRadius = cursorRadius diff --git a/packages/flutter/test/widgets/editable_text_test.dart b/packages/flutter/test/widgets/editable_text_test.dart index 69457bbf79..409c46098b 100644 --- a/packages/flutter/test/widgets/editable_text_test.dart +++ b/packages/flutter/test/widgets/editable_text_test.dart @@ -2281,7 +2281,7 @@ void main() { ), )); - const String expectedValue = '••••••••••••••••••••••••'; + final String expectedValue = '•' * originalText.length; expect( semantics, @@ -2368,6 +2368,27 @@ void main() { semantics.dispose(); }); + testWidgets('password fields can have their obscuring character customized', (WidgetTester tester) async { + const String originalText = 'super-secret-password!!1'; + controller.text = originalText; + + const String obscuringCharacter = '#'; + await tester.pumpWidget(MaterialApp( + home: EditableText( + backgroundCursorColor: Colors.grey, + controller: controller, + obscuringCharacter: obscuringCharacter, + obscureText: true, + focusNode: focusNode, + style: textStyle, + cursorColor: cursorColor, + ), + )); + + final String expectedValue = obscuringCharacter * originalText.length; + expect(findRenderEditable(tester).text.text, expectedValue); + }); + group('a11y copy/cut/paste', () { Future _buildApp(MockTextSelectionControls controls, WidgetTester tester) { return tester.pumpWidget(MaterialApp(