diff --git a/packages/flutter/lib/src/cupertino/text_field.dart b/packages/flutter/lib/src/cupertino/text_field.dart index 39cda8988e..d7237ef307 100644 --- a/packages/flutter/lib/src/cupertino/text_field.dart +++ b/packages/flutter/lib/src/cupertino/text_field.dart @@ -456,15 +456,18 @@ class _CupertinoTextFieldState extends State with AutomaticK } void _handleForcePressStarted(ForcePressDetails details) { - // The cause is not keyboard press but we would still like to just - // highlight the word without showing any handles or toolbar. - _renderEditable.selectWordsInRange(from: details.globalPosition, cause: SelectionChangedCause.keyboard); + _renderEditable.selectWordsInRange( + from: details.globalPosition, + cause: SelectionChangedCause.forcePress, + ); } void _handleForcePressEnded(ForcePressDetails details) { - // The cause is not technically double tap, but we would still like to show - // the toolbar and handles. - _renderEditable.selectWordsInRange(from: details.globalPosition, cause: SelectionChangedCause.doubleTap); + _renderEditable.selectWordsInRange( + from: details.globalPosition, + cause: SelectionChangedCause.forcePress, + ); + _editableTextKey.currentState.showToolbar(); } void _handleSingleTapUp(TapUpDetails details) { @@ -474,10 +477,12 @@ class _CupertinoTextFieldState extends State with AutomaticK void _handleSingleLongTapDown() { _renderEditable.selectPosition(cause: SelectionChangedCause.longPress); + _editableTextKey.currentState.showToolbar(); } void _handleDoubleTapDown(TapDownDetails details) { - _renderEditable.selectWord(cause: SelectionChangedCause.doubleTap); + _renderEditable.selectWord(cause: SelectionChangedCause.tap); + _editableTextKey.currentState.showToolbar(); } @override diff --git a/packages/flutter/lib/src/material/text_field.dart b/packages/flutter/lib/src/material/text_field.dart index 438225bea0..e0e0e969d7 100644 --- a/packages/flutter/lib/src/material/text_field.dart +++ b/packages/flutter/lib/src/material/text_field.dart @@ -582,11 +582,6 @@ class _TextFieldState extends State with AutomaticKeepAliveClientMixi _editableTextKey.currentState?.requestKeyboard(); } - void _handleSelectionChanged(TextSelection selection, SelectionChangedCause cause) { - if (cause == SelectionChangedCause.longPress) - Feedback.forLongPress(context); - } - InteractiveInkFeature _createInkFeature(TapDownDetails details) { final MaterialInkController inkController = Material.of(context); final ThemeData themeData = Theme.of(context); @@ -630,9 +625,11 @@ class _TextFieldState extends State with AutomaticKeepAliveClientMixi void _handleForcePressStarted(ForcePressDetails details) { if (widget.selectionEnabled) { - // The cause is not technically double tap, but we would like to show - // the toolbar. - _renderEditable.selectWordsInRange(from: details.globalPosition, cause: SelectionChangedCause.doubleTap); + _renderEditable.selectWordsInRange( + from: details.globalPosition, + cause: SelectionChangedCause.forcePress, + ); + _editableTextKey.currentState.showToolbar(); } } @@ -667,14 +664,19 @@ class _TextFieldState extends State with AutomaticKeepAliveClientMixi case TargetPlatform.android: case TargetPlatform.fuchsia: _renderEditable.selectWord(cause: SelectionChangedCause.longPress); + Feedback.forLongPress(context); break; } + _editableTextKey.currentState.showToolbar(); } _confirmCurrentSplash(); } void _handleDoubleTapDown(TapDownDetails details) { - _renderEditable.selectWord(cause: SelectionChangedCause.doubleTap); + if (widget.selectionEnabled) { + _renderEditable.selectWord(cause: SelectionChangedCause.doubleTap); + _editableTextKey.currentState.showToolbar(); + } } void _startSplash(TapDownDetails details) { @@ -784,7 +786,6 @@ class _TextFieldState extends State with AutomaticKeepAliveClientMixi onChanged: widget.onChanged, onEditingComplete: widget.onEditingComplete, onSubmitted: widget.onSubmitted, - onSelectionChanged: _handleSelectionChanged, inputFormatters: formatters, rendererIgnoresPointer: true, cursorWidth: widget.cursorWidth, diff --git a/packages/flutter/lib/src/rendering/editable.dart b/packages/flutter/lib/src/rendering/editable.dart index 00f082eecf..0e94e95a0f 100644 --- a/packages/flutter/lib/src/rendering/editable.dart +++ b/packages/flutter/lib/src/rendering/editable.dart @@ -45,6 +45,10 @@ enum SelectionChangedCause { /// location of the cursor) to change. longPress, + /// The user force-pressed the text and that caused the selection (or the + /// location of the cursor) to change. + forcePress, + /// The user used the keyboard to change the selection or the location of the /// cursor. /// @@ -737,12 +741,12 @@ class RenderEditable extends RenderBox { markNeedsLayout(); } - ///{@template flutter.rendering.editable.paintCursorOnTop} + /// {@template flutter.rendering.editable.paintCursorOnTop} /// If the cursor should be painted on top of the text or underneath it. /// /// By default, the cursor should be painted on top for iOS platforms and /// underneath for Android platforms. - /// {@end template} + /// {@endtemplate} bool get paintCursorAboveText => _paintCursorOnTop; bool _paintCursorOnTop; set paintCursorAboveText(bool value) { @@ -759,7 +763,7 @@ class RenderEditable extends RenderBox { /// (-[cursorWidth] * 0.5, 0.0) on iOS platforms and (0, 0) on Android /// platforms. The origin from where the offset is applied to is the arbitrary /// location where the cursor ends up being rendered from by default. - /// {@end template} + /// {@endtemplate} Offset get cursorOffset => _cursorOffset; Offset _cursorOffset; set cursorOffset(Offset value) { @@ -1233,6 +1237,15 @@ class RenderEditable extends RenderBox { } /// Move selection to the location of the last tap down. + /// + /// {@template flutter.rendering.editable.select} + /// This method is mainly used to translate user inputs in global positions + /// into a [TextSelection]. When used in conjunction with a [EditableText], + /// the selection change is fed back into [TextEditingController.selection]. + /// + /// If you have a [TextEditingController], it's generally easier to + /// programmatically manipulate its `value` or `selection` directly. + /// {@endtemplate} void selectPosition({@required SelectionChangedCause cause}) { assert(cause != null); _layoutText(constraints.maxWidth); @@ -1244,6 +1257,8 @@ class RenderEditable extends RenderBox { } /// Select a word around the location of the last tap down. + /// + /// {@macro flutter.rendering.editable.select} void selectWord({@required SelectionChangedCause cause}) { selectWordsInRange(from: _lastTapDownPosition, cause: cause); } @@ -1252,6 +1267,8 @@ class RenderEditable extends RenderBox { /// /// The first and last endpoints of the selection will always be at the /// beginning and end of a word respectively. + /// + /// {@macro flutter.rendering.editable.select} void selectWordsInRange({@required Offset from, Offset to, @required SelectionChangedCause cause}) { assert(cause != null); _layoutText(constraints.maxWidth); @@ -1272,6 +1289,8 @@ class RenderEditable extends RenderBox { } /// Move the selection to the beginning or end of a word. + /// + /// {@macro flutter.rendering.editable.select} void selectWordEdge({@required SelectionChangedCause cause}) { assert(cause != null); _layoutText(constraints.maxWidth); diff --git a/packages/flutter/lib/src/widgets/editable_text.dart b/packages/flutter/lib/src/widgets/editable_text.dart index 01f17027c4..fa2f816900 100644 --- a/packages/flutter/lib/src/widgets/editable_text.dart +++ b/packages/flutter/lib/src/widgets/editable_text.dart @@ -745,7 +745,7 @@ class EditableTextState extends State with AutomaticKeepAliveClien renderEditable.setFloatingCursor(FloatingCursorDragState.End, finalPosition, _lastTextPosition); if (_lastTextPosition.offset != renderEditable.selection.baseOffset) // The cause is technically the force cursor, but the cause is listed as tap as the desired functionality is the same. - _handleSelectionChanged(TextSelection.collapsed(offset: _lastTextPosition.offset), renderEditable, SelectionChangedCause.tap); + _handleSelectionChanged(TextSelection.collapsed(offset: _lastTextPosition.offset), renderEditable, SelectionChangedCause.forcePress); _startCaretRect = null; _lastTextPosition = null; _pointOffsetOrigin = null; @@ -923,8 +923,6 @@ class EditableTextState extends State with AutomaticKeepAliveClien final bool longPress = cause == SelectionChangedCause.longPress; if (cause != SelectionChangedCause.keyboard && (_value.text.isNotEmpty || longPress)) _selectionOverlay.showHandles(); - if (longPress || cause == SelectionChangedCause.doubleTap) - _selectionOverlay.showToolbar(); if (widget.onSelectionChanged != null) widget.onSelectionChanged(selection, cause); } @@ -1150,6 +1148,18 @@ class EditableTextState extends State with AutomaticKeepAliveClien _scrollController.jumpTo(_getScrollOffsetForCaret(renderEditable.getLocalRectForCaret(position))); } + /// Shows the selection toolbar at the location of the current cursor. + /// + /// Returns `false` if a toolbar couldn't be shown such as when no text + /// selection currently exists. + bool showToolbar() { + if (_selectionOverlay == null) + return false; + + _selectionOverlay.showToolbar(); + return true; + } + @override void hideToolbar() { _selectionOverlay?.hide(); diff --git a/packages/flutter/test/widgets/editable_text_cursor_test.dart b/packages/flutter/test/widgets/editable_text_cursor_test.dart index dea8cb150b..9f5c4a8e0c 100644 --- a/packages/flutter/test/widgets/editable_text_cursor_test.dart +++ b/packages/flutter/test/widgets/editable_text_cursor_test.dart @@ -71,6 +71,7 @@ void main() { // Long-press to bring up the text editing controls. final Finder textFinder = find.byKey(editableTextKey); await tester.longPress(textFinder); + tester.state(textFinder).showToolbar(); await tester.pump(); await tester.pump(const Duration(milliseconds: 500)); await tester.pump(const Duration(milliseconds: 500)); @@ -125,6 +126,7 @@ void main() { // Long-press to bring up the text editing controls. final Finder textFinder = find.byKey(editableTextKey); await tester.longPress(textFinder); + tester.state(textFinder).showToolbar(); await tester.pump(); await tester.tap(find.text('PASTE')); diff --git a/packages/flutter/test/widgets/editable_text_test.dart b/packages/flutter/test/widgets/editable_text_test.dart index 88f630aaf0..4a5b4a94d6 100644 --- a/packages/flutter/test/widgets/editable_text_test.dart +++ b/packages/flutter/test/widgets/editable_text_test.dart @@ -478,6 +478,45 @@ void main() { equals('TextInputAction.done')); }); + testWidgets('can only show toolbar when there is text and a selection', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: EditableText( + backgroundCursorColor: Colors.grey, + controller: controller, + focusNode: focusNode, + style: textStyle, + cursorColor: cursorColor, + selectionControls: materialTextSelectionControls, + ), + ), + ); + + final EditableTextState state = + tester.state(find.byType(EditableText)); + + expect(state.showToolbar(), false); + await tester.pump(); + expect(find.text('PASTE'), findsNothing); + + controller.text = 'blah'; + await tester.pump(); + expect(state.showToolbar(), false); + await tester.pump(); + expect(find.text('PASTE'), findsNothing); + + // 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('PASTE'), findsOneWidget); + }); + testWidgets('Fires onChanged when text changes via TextSelectionOverlay', (WidgetTester tester) async { final GlobalKey editableTextKey = @@ -513,6 +552,7 @@ void main() { // Long-press to bring up the text editing controls. final Finder textFinder = find.byKey(editableTextKey); await tester.longPress(textFinder); + tester.state(textFinder).showToolbar(); await tester.pump(); await tester.tap(find.text('PASTE'));