From ef5abcc6bfe93f0e66f9fea9e56b50cd2634139f Mon Sep 17 00:00:00 2001 From: Mouad Debbar Date: Tue, 14 May 2019 09:33:04 -0700 Subject: [PATCH] Revert "Show/hide toolbar and handles based on device kind (#29683)" (#32702) This reverts commit 18ca37542d8e946724f1ccc41b6d7c98c28d7164. --- .../flutter/lib/src/cupertino/text_field.dart | 55 +--- .../flutter/lib/src/gestures/multitap.dart | 17 +- .../flutter/lib/src/gestures/recognizer.dart | 36 +-- packages/flutter/lib/src/gestures/tap.dart | 24 +- .../flutter/lib/src/material/text_field.dart | 58 +--- .../lib/src/widgets/editable_text.dart | 25 +- .../lib/src/widgets/text_selection.dart | 42 ++- .../test/cupertino/text_field_test.dart | 189 ------------ .../flutter/test/gestures/debug_test.dart | 2 +- .../test/material/text_field_test.dart | 282 +----------------- .../test/widgets/editable_text_test.dart | 50 +++- 11 files changed, 93 insertions(+), 687 deletions(-) diff --git a/packages/flutter/lib/src/cupertino/text_field.dart b/packages/flutter/lib/src/cupertino/text_field.dart index b0067f3405d..25164e48e81 100644 --- a/packages/flutter/lib/src/cupertino/text_field.dart +++ b/packages/flutter/lib/src/cupertino/text_field.dart @@ -469,12 +469,6 @@ class _CupertinoTextFieldState extends State with AutomaticK FocusNode _focusNode; FocusNode get _effectiveFocusNode => widget.focusNode ?? (_focusNode ??= FocusNode()); - // The selection overlay should only be shown when the user is interacting - // through a touch screen (via either a finger or a stylus). A mouse shouldn't - // trigger the selection overlay. - // For backwards-compatibility, we treat a null kind the same as touch. - bool _shouldShowSelectionToolbar = true; - @override void initState() { super.initState(); @@ -507,26 +501,14 @@ class _CupertinoTextFieldState extends State with AutomaticK super.dispose(); } - EditableTextState get _editableText => _editableTextKey.currentState; - void _requestKeyboard() { - _editableText?.requestKeyboard(); + _editableTextKey.currentState?.requestKeyboard(); } - RenderEditable get _renderEditable => _editableText.renderEditable; + RenderEditable get _renderEditable => _editableTextKey.currentState.renderEditable; void _handleTapDown(TapDownDetails details) { _renderEditable.handleTapDown(details); - - // The selection overlay should only be shown when the user is interacting - // through a touch screen (via either a finger or a stylus). A mouse shouldn't - // trigger the selection overlay. - // For backwards-compatibility, we treat a null kind the same as touch. - final PointerDeviceKind kind = details.kind; - _shouldShowSelectionToolbar = - kind == null || - kind == PointerDeviceKind.touch || - kind == PointerDeviceKind.stylus; } void _handleForcePressStarted(ForcePressDetails details) { @@ -541,8 +523,7 @@ class _CupertinoTextFieldState extends State with AutomaticK from: details.globalPosition, cause: SelectionChangedCause.forcePress, ); - if (_shouldShowSelectionToolbar) - _editableText.showToolbar(); + _editableTextKey.currentState.showToolbar(); } void _handleSingleTapUp(TapUpDetails details) { @@ -565,33 +546,12 @@ class _CupertinoTextFieldState extends State with AutomaticK } void _handleSingleLongTapEnd(LongPressEndDetails details) { - if (_shouldShowSelectionToolbar) - _editableText.showToolbar(); + _editableTextKey.currentState.showToolbar(); } void _handleDoubleTapDown(TapDownDetails details) { _renderEditable.selectWord(cause: SelectionChangedCause.tap); - if (_shouldShowSelectionToolbar) - _editableText.showToolbar(); - } - - bool _shouldShowSelectionHandles(SelectionChangedCause cause) { - // When the text field is activated by something that doesn't trigger the - // selection overlay, we shouldn't show the handles either. - if (!_shouldShowSelectionToolbar) - return false; - - // On iOS, we don't show handles when the selection is collapsed. - if (_effectiveController.selection.isCollapsed) - return false; - - if (cause == SelectionChangedCause.keyboard) - return false; - - if (_effectiveController.text.isNotEmpty) - return true; - - return false; + _editableTextKey.currentState.showToolbar(); } void _handleMouseDragSelectionStart(DragStartDetails details) { @@ -618,10 +578,7 @@ class _CupertinoTextFieldState extends State with AutomaticK void _handleSelectionChanged(TextSelection selection, SelectionChangedCause cause) { if (cause == SelectionChangedCause.longPress) { - _editableText?.bringIntoView(selection.base); - } - if (_shouldShowSelectionHandles(cause)) { - _editableText?.showHandles(); + _editableTextKey.currentState?.bringIntoView(selection.base); } } diff --git a/packages/flutter/lib/src/gestures/multitap.dart b/packages/flutter/lib/src/gestures/multitap.dart index 7e0df8ccced..57e5a5eed14 100644 --- a/packages/flutter/lib/src/gestures/multitap.dart +++ b/packages/flutter/lib/src/gestures/multitap.dart @@ -397,12 +397,7 @@ class MultiTapGestureRecognizer extends GestureRecognizer { longTapDelay: longTapDelay, ); if (onTapDown != null) - invokeCallback('onTapDown', () { - onTapDown(event.pointer, TapDownDetails( - globalPosition: event.position, - kind: event.kind, - )); - }); + invokeCallback('onTapDown', () => onTapDown(event.pointer, TapDownDetails(globalPosition: event.position))); } @override @@ -437,15 +432,7 @@ class MultiTapGestureRecognizer extends GestureRecognizer { void _dispatchLongTap(int pointer, Offset lastPosition) { assert(_gestureMap.containsKey(pointer)); if (onLongTapDown != null) - invokeCallback('onLongTapDown', () { - onLongTapDown( - pointer, - TapDownDetails( - globalPosition: lastPosition, - kind: getKindForPointer(pointer), - ), - ); - }); + invokeCallback('onLongTapDown', () => onLongTapDown(pointer, TapDownDetails(globalPosition: lastPosition))); } @override diff --git a/packages/flutter/lib/src/gestures/recognizer.dart b/packages/flutter/lib/src/gestures/recognizer.dart index 3d807f8c690..a6d277fceea 100644 --- a/packages/flutter/lib/src/gestures/recognizer.dart +++ b/packages/flutter/lib/src/gestures/recognizer.dart @@ -64,7 +64,7 @@ abstract class GestureRecognizer extends GestureArenaMember with DiagnosticableT /// by providing the optional [kind] argument. If [kind] is null, /// the recognizer will accept pointer events from all device kinds. /// {@endtemplate} - GestureRecognizer({ this.debugOwner, PointerDeviceKind kind }) : _kindFilter = kind; + GestureRecognizer({ this.debugOwner, PointerDeviceKind kind }) : _kind = kind; /// The recognizer's owner. /// @@ -74,11 +74,7 @@ abstract class GestureRecognizer extends GestureArenaMember with DiagnosticableT /// The kind of device that's allowed to be recognized. If null, events from /// all device kinds will be tracked and recognized. - final PointerDeviceKind _kindFilter; - - /// Holds a mapping between pointer IDs and the kind of devices they are - /// coming from. - final Map _pointerToKind = {}; + final PointerDeviceKind _kind; /// Registers a new pointer that might be relevant to this gesture /// detector. @@ -96,7 +92,6 @@ abstract class GestureRecognizer extends GestureArenaMember with DiagnosticableT /// This method is called for each and all pointers being added. In /// most cases, you want to override [addAllowedPointer] instead. void addPointer(PointerDownEvent event) { - _pointerToKind[event.pointer] = event.kind; if (isPointerAllowed(event)) { addAllowedPointer(event); } else { @@ -128,17 +123,7 @@ abstract class GestureRecognizer extends GestureArenaMember with DiagnosticableT bool isPointerAllowed(PointerDownEvent event) { // Currently, it only checks for device kind. But in the future we could check // for other things e.g. mouse button. - return _kindFilter == null || _kindFilter == event.kind; - } - - /// For a given pointer ID, returns the device kind associated with it. - /// - /// The pointer ID is expected to be a valid one i.e. an event was received - /// with that pointer ID. - @protected - PointerDeviceKind getKindForPointer(int pointer) { - assert(_pointerToKind.containsKey(pointer)); - return _pointerToKind[pointer]; + return _kind == null || _kind == event.kind; } /// Releases any resources used by the object. @@ -426,7 +411,7 @@ abstract class PrimaryPointerGestureRecognizer extends OneSequenceGestureRecogni primaryPointer = event.pointer; initialPosition = event.position; if (deadline != null) - _timer = Timer(deadline, () => didExceedDeadlineWithEvent(event)); + _timer = Timer(deadline, didExceedDeadline); } } @@ -459,23 +444,12 @@ abstract class PrimaryPointerGestureRecognizer extends OneSequenceGestureRecogni /// Override to be notified when [deadline] is exceeded. /// - /// You must override this method or [didExceedDeadlineWithEvent] if you - /// supply a [deadline]. + /// You must override this method if you supply a [deadline]. @protected void didExceedDeadline() { assert(deadline == null); } - /// Same as [didExceedDeadline] but receives the [event] that initiated the - /// gesture. - /// - /// You must override this method or [didExceedDeadline] if you supply a - /// [deadline]. - @protected - void didExceedDeadlineWithEvent(PointerDownEvent event) { - didExceedDeadline(); - } - @override void acceptGesture(int pointer) { _gestureAccepted = true; diff --git a/packages/flutter/lib/src/gestures/tap.dart b/packages/flutter/lib/src/gestures/tap.dart index 75c109bd5a5..d5672255ad2 100644 --- a/packages/flutter/lib/src/gestures/tap.dart +++ b/packages/flutter/lib/src/gestures/tap.dart @@ -19,16 +19,11 @@ class TapDownDetails { /// Creates details for a [GestureTapDownCallback]. /// /// The [globalPosition] argument must not be null. - TapDownDetails({ - this.globalPosition = Offset.zero, - this.kind, - }) : assert(globalPosition != null); + TapDownDetails({ this.globalPosition = Offset.zero }) + : assert(globalPosition != null); /// The global position at which the pointer contacted the screen. final Offset globalPosition; - - /// The kind of the device that initiated the event. - final PointerDeviceKind kind; } /// Signature for when a pointer that might cause a tap has contacted the @@ -203,15 +198,15 @@ class TapGestureRecognizer extends PrimaryPointerGestureRecognizer { } @override - void didExceedDeadlineWithEvent(PointerDownEvent event) { - _checkDown(event.pointer); + void didExceedDeadline() { + _checkDown(); } @override void acceptGesture(int pointer) { super.acceptGesture(pointer); if (pointer == primaryPointer) { - _checkDown(pointer); + _checkDown(); _wonArenaForPrimaryPointer = true; _checkUp(); } @@ -229,15 +224,10 @@ class TapGestureRecognizer extends PrimaryPointerGestureRecognizer { } } - void _checkDown(int pointer) { + void _checkDown() { if (!_sentTapDown) { if (onTapDown != null) - invokeCallback('onTapDown', () { - onTapDown(TapDownDetails( - globalPosition: initialPosition, - kind: getKindForPointer(pointer), - )); - }); + invokeCallback('onTapDown', () { onTapDown(TapDownDetails(globalPosition: initialPosition)); }); _sentTapDown = true; } } diff --git a/packages/flutter/lib/src/material/text_field.dart b/packages/flutter/lib/src/material/text_field.dart index 86fc99810bf..0eed7c86d06 100644 --- a/packages/flutter/lib/src/material/text_field.dart +++ b/packages/flutter/lib/src/material/text_field.dart @@ -513,8 +513,6 @@ class _TextFieldState extends State with AutomaticKeepAliveClientMixi && widget.decoration != null && widget.decoration.counterText == null; - bool _shouldShowSelectionToolbar = true; - InputDecoration _getEffectiveDecoration() { final MaterialLocalizations localizations = MaterialLocalizations.of(context); final ThemeData themeData = Theme.of(context); @@ -607,41 +605,17 @@ class _TextFieldState extends State with AutomaticKeepAliveClientMixi super.dispose(); } - EditableTextState get _editableText => _editableTextKey.currentState; - void _requestKeyboard() { - _editableText?.requestKeyboard(); - } - - bool _shouldShowSelectionHandles(SelectionChangedCause cause) { - // When the text field is activated by something that doesn't trigger the - // selection overlay, we shouldn't show the handles either. - if (!_shouldShowSelectionToolbar) - return false; - - if (cause == SelectionChangedCause.keyboard) - return false; - - if (cause == SelectionChangedCause.longPress) - return true; - - if (_effectiveController.text.isNotEmpty) - return true; - - return false; + _editableTextKey.currentState?.requestKeyboard(); } void _handleSelectionChanged(TextSelection selection, SelectionChangedCause cause) { // iOS cursor doesn't move via a selection handle. The scroll happens // directly from new text selection changes. - if (_shouldShowSelectionHandles(cause)) { - _editableText?.showHandles(); - } - switch (Theme.of(context).platform) { case TargetPlatform.iOS: if (cause == SelectionChangedCause.longPress) { - _editableText?.bringIntoView(selection.base); + _editableTextKey.currentState?.bringIntoView(selection.base); } return; case TargetPlatform.android: @@ -650,13 +624,6 @@ class _TextFieldState extends State with AutomaticKeepAliveClientMixi } } - /// Toggle the toolbar when a selection handle is tapped. - void _handleSelectionHandleTapped() { - if (_effectiveController.selection.isCollapsed) { - _editableText.toggleToolbar(); - } - } - InteractiveInkFeature _createInkFeature(Offset globalPosition) { final MaterialInkController inkController = Material.of(context); final ThemeData themeData = Theme.of(context); @@ -696,16 +663,6 @@ class _TextFieldState extends State with AutomaticKeepAliveClientMixi void _handleTapDown(TapDownDetails details) { _renderEditable.handleTapDown(details); _startSplash(details.globalPosition); - - // The selection overlay should only be shown when the user is interacting - // through a touch screen (via either a finger or a stylus). A mouse shouldn't - // trigger the selection overlay. - // For backwards-compatibility, we treat a null kind the same as touch. - final PointerDeviceKind kind = details.kind; - _shouldShowSelectionToolbar = - kind == null || - kind == PointerDeviceKind.touch || - kind == PointerDeviceKind.stylus; } void _handleForcePressStarted(ForcePressDetails details) { @@ -714,8 +671,7 @@ class _TextFieldState extends State with AutomaticKeepAliveClientMixi from: details.globalPosition, cause: SelectionChangedCause.forcePress, ); - if (_shouldShowSelectionToolbar) - _editableTextKey.currentState.showToolbar(); + _editableTextKey.currentState.showToolbar(); } } @@ -782,16 +738,13 @@ class _TextFieldState extends State with AutomaticKeepAliveClientMixi } void _handleSingleLongTapEnd(LongPressEndDetails details) { - print('long tap end'); - if (_shouldShowSelectionToolbar) - _editableTextKey.currentState.showToolbar(); + _editableTextKey.currentState.showToolbar(); } void _handleDoubleTapDown(TapDownDetails details) { if (widget.selectionEnabled) { _renderEditable.selectWord(cause: SelectionChangedCause.doubleTap); - if (_shouldShowSelectionToolbar) - _editableTextKey.currentState.showToolbar(); + _editableTextKey.currentState.showToolbar(); } } @@ -931,7 +884,6 @@ class _TextFieldState extends State with AutomaticKeepAliveClientMixi onSelectionChanged: _handleSelectionChanged, onEditingComplete: widget.onEditingComplete, onSubmitted: widget.onSubmitted, - onSelectionHandleTapped: _handleSelectionHandleTapped, inputFormatters: formatters, rendererIgnoresPointer: true, cursorWidth: widget.cursorWidth, diff --git a/packages/flutter/lib/src/widgets/editable_text.dart b/packages/flutter/lib/src/widgets/editable_text.dart index 72e20219a30..6676571f564 100644 --- a/packages/flutter/lib/src/widgets/editable_text.dart +++ b/packages/flutter/lib/src/widgets/editable_text.dart @@ -290,7 +290,6 @@ class EditableText extends StatefulWidget { this.onEditingComplete, this.onSubmitted, this.onSelectionChanged, - this.onSelectionHandleTapped, List inputFormatters, this.rendererIgnoresPointer = false, this.cursorWidth = 2.0, @@ -646,9 +645,6 @@ class EditableText extends StatefulWidget { /// location). final SelectionChangedCallback onSelectionChanged; - /// {@macro flutter.widgets.textSelection.onSelectionHandleTapped} - final VoidCallback onSelectionHandleTapped; - /// {@template flutter.widgets.editableText.inputFormatters} /// Optional input validation and formatting overrides. /// @@ -1139,9 +1135,10 @@ class EditableTextState extends State with AutomaticKeepAliveClien selectionControls: widget.selectionControls, selectionDelegate: this, dragStartBehavior: widget.dragStartBehavior, - onSelectionHandleTapped: widget.onSelectionHandleTapped, ); - + final bool longPress = cause == SelectionChangedCause.longPress; + if (cause != SelectionChangedCause.keyboard && (_value.text.isNotEmpty || longPress)) + _selectionOverlay.showHandles(); if (widget.onSelectionChanged != null) widget.onSelectionChanged(selection, cause); } @@ -1384,22 +1381,6 @@ class EditableTextState extends State with AutomaticKeepAliveClien _selectionOverlay?.hide(); } - /// Toggles the visibility of the toolbar. - void toggleToolbar() { - assert(_selectionOverlay != null); - if (_selectionOverlay.toolbarIsVisible) { - hideToolbar(); - } else { - showToolbar(); - } - } - - /// Shows the handles at the location of the current selection. - void showHandles() { - assert(_selectionOverlay != null); - _selectionOverlay.showHandles(); - } - VoidCallback _semanticsOnCopy(TextSelectionControls controls) { return widget.selectionEnabled && _hasFocus && controls?.canCopy(this) == true ? () => controls.handleCopy(this) diff --git a/packages/flutter/lib/src/widgets/text_selection.dart b/packages/flutter/lib/src/widgets/text_selection.dart index fd9660bcfbd..9eb6163c289 100644 --- a/packages/flutter/lib/src/widgets/text_selection.dart +++ b/packages/flutter/lib/src/widgets/text_selection.dart @@ -263,7 +263,6 @@ class TextSelectionOverlay { this.selectionControls, this.selectionDelegate, this.dragStartBehavior = DragStartBehavior.start, - this.onSelectionHandleTapped, }) : assert(value != null), assert(context != null), _value = value { @@ -317,14 +316,6 @@ class TextSelectionOverlay { /// * [DragGestureRecognizer.dragStartBehavior], which gives an example for the different behaviors. final DragStartBehavior dragStartBehavior; - /// {@template flutter.widgets.textSelection.onSelectionHandleTapped} - /// A callback that's invoked when a selection handle is tapped. - /// - /// Both regular taps and long presses invoke this callback, but a drag - /// gesture won't. - /// {@endtemplate} - final VoidCallback onSelectionHandleTapped; - /// Controls the fade-in and fade-out animations for the toolbar and handles. static const Duration fadeDuration = Duration(milliseconds: 150); @@ -402,26 +393,17 @@ class TextSelectionOverlay { /// Whether the toolbar is currently visible. bool get toolbarIsVisible => _toolbar != null; - /// Hides the entire overlay including the toolbar and the handles. + /// Hides the overlay. void hide() { if (_handles != null) { _handles[0].remove(); _handles[1].remove(); _handles = null; } - if (_toolbar != null) { - hideToolbar(); - } - } - - /// Hides the toolbar part of the overlay. - /// - /// To hide the whole overlay, see [hide]. - void hideToolbar() { - assert(_toolbar != null); - _toolbarController.stop(); - _toolbar.remove(); + _toolbar?.remove(); _toolbar = null; + + _toolbarController.stop(); } /// Final cleanup. @@ -436,7 +418,7 @@ class TextSelectionOverlay { return Container(); // hide the second handle when collapsed return _TextSelectionHandleOverlay( onSelectionHandleChanged: (TextSelection newSelection) { _handleSelectionHandleChanged(newSelection, position); }, - onSelectionHandleTapped: onSelectionHandleTapped, + onSelectionHandleTapped: _handleSelectionHandleTapped, layerLink: layerLink, renderObject: renderObject, selection: _selection, @@ -494,6 +476,17 @@ class TextSelectionOverlay { selectionDelegate.textEditingValue = _value.copyWith(selection: newSelection, composing: TextRange.empty); selectionDelegate.bringIntoView(textPosition); } + + void _handleSelectionHandleTapped() { + if (_value.selection.isCollapsed) { + if (_toolbar != null) { + _toolbar?.remove(); + _toolbar = null; + } else { + showToolbar(); + } + } + } } /// This widget represents a single draggable text selection handle. @@ -609,8 +602,7 @@ class _TextSelectionHandleOverlayState } void _handleTap() { - if (widget.onSelectionHandleTapped != null) - widget.onSelectionHandleTapped(); + widget.onSelectionHandleTapped(); } @override diff --git a/packages/flutter/test/cupertino/text_field_test.dart b/packages/flutter/test/cupertino/text_field_test.dart index 705104215cd..161efee17d9 100644 --- a/packages/flutter/test/cupertino/text_field_test.dart +++ b/packages/flutter/test/cupertino/text_field_test.dart @@ -2008,195 +2008,6 @@ void main() { await gesture.removePointer(); }); - testWidgets('Tap does not show handles nor toolbar', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( - text: 'abc def ghi', - ); - - await tester.pumpWidget( - CupertinoApp( - home: Center( - child: CupertinoTextField(controller: controller), - ), - ), - ); - - // Tap to trigger the text field. - await tester.tap(find.byType(CupertinoTextField)); - await tester.pump(); - - final EditableTextState editableText = tester.state(find.byType(EditableText)); - expect(editableText.selectionOverlay.handlesAreVisible, isFalse); - expect(editableText.selectionOverlay.toolbarIsVisible, isFalse); - }); - - testWidgets('Long press shows toolbar but not handles', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( - text: 'abc def ghi', - ); - - await tester.pumpWidget( - CupertinoApp( - home: Center( - child: CupertinoTextField(controller: controller), - ), - ), - ); - - // Long press to trigger the text field. - await tester.longPress(find.byType(CupertinoTextField)); - await tester.pump(); - // A long press in Cupertino should position the cursor without any selection. - expect(controller.selection.isCollapsed, isTrue); - - final EditableTextState editableText = tester.state(find.byType(EditableText)); - expect(editableText.selectionOverlay.handlesAreVisible, isFalse); - expect(editableText.selectionOverlay.toolbarIsVisible, isTrue); - }); - - testWidgets( - 'Double tap shows handles and toolbar if selection is not collapsed', - (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( - text: 'abc def ghi', - ); - - await tester.pumpWidget( - CupertinoApp( - home: Center( - child: CupertinoTextField(controller: controller), - ), - ), - ); - - final Offset hPos = textOffsetToPosition(tester, 9); // Position of 'h'. - - // Double tap on 'h' to select 'ghi'. - await tester.tapAt(hPos); - await tester.pump(const Duration(milliseconds: 50)); - await tester.tapAt(hPos); - await tester.pump(); - - final EditableTextState editableText = tester.state(find.byType(EditableText)); - expect(editableText.selectionOverlay.handlesAreVisible, isTrue); - expect(editableText.selectionOverlay.toolbarIsVisible, isTrue); - }, - ); - - testWidgets( - 'Double tap shows toolbar but not handles if selection is collapsed', - (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( - text: 'abc def ghi', - ); - - await tester.pumpWidget( - CupertinoApp( - home: Center( - child: CupertinoTextField(controller: controller), - ), - ), - ); - - final Offset textEndPos = textOffsetToPosition(tester, 11); // Position at the end of text. - - // Double tap to place the cursor at the end. - await tester.tapAt(textEndPos); - await tester.pump(const Duration(milliseconds: 50)); - await tester.tapAt(textEndPos); - await tester.pump(); - - final EditableTextState editableText = tester.state(find.byType(EditableText)); - expect(editableText.selectionOverlay.handlesAreVisible, isFalse); - expect(editableText.selectionOverlay.toolbarIsVisible, isTrue); - }, - ); - - testWidgets( - 'Mouse long press does not show handles nor toolbar', - (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( - text: 'abc def ghi', - ); - - await tester.pumpWidget( - CupertinoApp( - home: Center( - child: CupertinoTextField(controller: controller), - ), - ), - ); - - // Long press to trigger the text field. - final Offset textFieldPos = tester.getCenter(find.byType(CupertinoTextField)); - final TestGesture gesture = await tester.startGesture( - textFieldPos, - kind: PointerDeviceKind.mouse, - ); - await tester.pump(const Duration(seconds: 2)); - await gesture.up(); - await tester.pump(); - - final EditableTextState editableText = tester.state(find.byType(EditableText)); - expect(editableText.selectionOverlay.toolbarIsVisible, isFalse); - expect(editableText.selectionOverlay.handlesAreVisible, isFalse); - }, - ); - - testWidgets( - 'Mouse double tap does not show handles nor toolbar', - (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( - text: 'abc def ghi', - ); - - await tester.pumpWidget( - CupertinoApp( - home: Center( - child: CupertinoTextField(controller: controller), - ), - ), - ); - - final EditableTextState editableText = tester.state(find.byType(EditableText)); - - // Double tap at the end of text. - final Offset textEndPos = textOffsetToPosition(tester, 11); // Position at the end of text. - TestGesture gesture = await tester.startGesture( - textEndPos, - kind: PointerDeviceKind.mouse, - ); - await tester.pump(const Duration(milliseconds: 50)); - await gesture.up(); - await tester.pump(); - await gesture.down(textEndPos); - await tester.pump(); - await gesture.up(); - await tester.pump(); - - expect(editableText.selectionOverlay.toolbarIsVisible, isFalse); - expect(editableText.selectionOverlay.handlesAreVisible, isFalse); - - final Offset hPos = textOffsetToPosition(tester, 9); // Position of 'h'. - - // Double tap on 'h' to select 'ghi'. - gesture = await tester.startGesture( - hPos, - kind: PointerDeviceKind.mouse, - ); - await tester.pump(const Duration(milliseconds: 50)); - await gesture.up(); - await tester.pump(); - await gesture.down(hPos); - await tester.pump(); - await gesture.up(); - await tester.pump(); - - expect(editableText.selectionOverlay.handlesAreVisible, isFalse); - expect(editableText.selectionOverlay.toolbarIsVisible, isFalse); - }, - ); - testWidgets( 'text field respects theme', (WidgetTester tester) async { diff --git a/packages/flutter/test/gestures/debug_test.dart b/packages/flutter/test/gestures/debug_test.dart index 656daf50a86..1a323758c53 100644 --- a/packages/flutter/test/gestures/debug_test.dart +++ b/packages/flutter/test/gestures/debug_test.dart @@ -151,7 +151,7 @@ void main() { expect(tap.toString(), equalsIgnoringHashCodes('TapGestureRecognizer#00000(state: ready)')); const PointerEvent event = PointerDownEvent(pointer: 1, position: Offset(10.0, 10.0)); tap.addPointer(event); - tap.didExceedDeadlineWithEvent(event); + tap.didExceedDeadline(); expect(tap.toString(), equalsIgnoringHashCodes('TapGestureRecognizer#00000(state: possible, sent tap down)')); }); } diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart index b22b7e1e16d..8d75c4158b4 100644 --- a/packages/flutter/test/material/text_field_test.dart +++ b/packages/flutter/test/material/text_field_test.dart @@ -5909,9 +5909,8 @@ void main() { ), ); - final EditableTextState state = - tester.state(find.byType(EditableText)); - final RenderEditable renderEditable = state.renderEditable; + final RenderEditable renderEditable = + tester.state(find.byType(EditableText)).renderEditable; await tester.tapAt(const Offset(20, 10)); renderEditable.selectWord(cause: SelectionChangedCause.longPress); @@ -5962,281 +5961,4 @@ void main() { debugDefaultTargetPlatformOverride = null; }); - - testWidgets('Tap shows handles but not toolbar', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( - text: 'abc def ghi', - ); - - await tester.pumpWidget( - MaterialApp( - home: Material( - child: TextField(controller: controller), - ), - ), - ); - - // Tap to trigger the text field. - await tester.tap(find.byType(TextField)); - await tester.pump(); - - final EditableTextState editableText = tester.state(find.byType(EditableText)); - expect(editableText.selectionOverlay.handlesAreVisible, isTrue); - expect(editableText.selectionOverlay.toolbarIsVisible, isFalse); - }); - - testWidgets( - 'Tap in empty text field does not show handles nor toolbar', - (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); - - await tester.pumpWidget( - MaterialApp( - home: Material( - child: TextField(controller: controller), - ), - ), - ); - - // Tap to trigger the text field. - await tester.tap(find.byType(TextField)); - await tester.pump(); - - final EditableTextState editableText = tester.state(find.byType(EditableText)); - expect(editableText.selectionOverlay.handlesAreVisible, isFalse); - expect(editableText.selectionOverlay.toolbarIsVisible, isFalse); - }, - ); - - testWidgets('Long press shows handles and toolbar', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( - text: 'abc def ghi', - ); - - await tester.pumpWidget( - MaterialApp( - home: Material( - child: TextField(controller: controller), - ), - ), - ); - - // Long press to trigger the text field. - await tester.longPress(find.byType(TextField)); - await tester.pump(); - - final EditableTextState editableText = tester.state(find.byType(EditableText)); - expect(editableText.selectionOverlay.handlesAreVisible, isTrue); - expect(editableText.selectionOverlay.toolbarIsVisible, isTrue); - }); - - testWidgets( - 'Long press in empty text field shows handles and toolbar', - (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); - - await tester.pumpWidget( - MaterialApp( - home: Material( - child: TextField(controller: controller), - ), - ), - ); - - // Tap to trigger the text field. - await tester.longPress(find.byType(TextField)); - await tester.pump(); - - final EditableTextState editableText = tester.state(find.byType(EditableText)); - expect(editableText.selectionOverlay.handlesAreVisible, isTrue); - expect(editableText.selectionOverlay.toolbarIsVisible, isTrue); - }, - ); - - testWidgets('Double tap shows handles and toolbar', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( - text: 'abc def ghi', - ); - - await tester.pumpWidget( - MaterialApp( - home: Material( - child: TextField(controller: controller), - ), - ), - ); - - // Double tap to trigger the text field. - await tester.tap(find.byType(TextField)); - await tester.pump(const Duration(milliseconds: 50)); - await tester.tap(find.byType(TextField)); - await tester.pump(); - - final EditableTextState editableText = tester.state(find.byType(EditableText)); - expect(editableText.selectionOverlay.handlesAreVisible, isTrue); - expect(editableText.selectionOverlay.toolbarIsVisible, isTrue); - }); - - testWidgets( - 'Double tap in empty text field shows toolbar but not handles', - (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); - - await tester.pumpWidget( - MaterialApp( - home: Material( - child: TextField(controller: controller), - ), - ), - ); - - // Double tap to trigger the text field. - await tester.tap(find.byType(TextField)); - await tester.pump(const Duration(milliseconds: 50)); - await tester.tap(find.byType(TextField)); - await tester.pump(); - - final EditableTextState editableText = tester.state(find.byType(EditableText)); - expect(editableText.selectionOverlay.handlesAreVisible, isFalse); - expect(editableText.selectionOverlay.toolbarIsVisible, isTrue); - }, - ); - - testWidgets( - 'Mouse tap does not show handles nor toolbar', - (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( - text: 'abc def ghi', - ); - - await tester.pumpWidget( - MaterialApp( - home: Material( - child: TextField(controller: controller), - ), - ), - ); - - // Long press to trigger the text field. - final Offset textFieldPos = tester.getCenter(find.byType(TextField)); - final TestGesture gesture = await tester.startGesture( - textFieldPos, - pointer: 7, - kind: PointerDeviceKind.mouse, - ); - await tester.pump(); - await gesture.up(); - await tester.pump(); - - final EditableTextState editableText = tester.state(find.byType(EditableText)); - expect(editableText.selectionOverlay.toolbarIsVisible, isFalse); - expect(editableText.selectionOverlay.handlesAreVisible, isFalse); - }, - ); - - testWidgets( - 'Mouse long press does not show handles nor toolbar', - (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( - text: 'abc def ghi', - ); - - await tester.pumpWidget( - MaterialApp( - home: Material( - child: TextField(controller: controller), - ), - ), - ); - - // Long press to trigger the text field. - final Offset textFieldPos = tester.getCenter(find.byType(TextField)); - final TestGesture gesture = await tester.startGesture( - textFieldPos, - pointer: 7, - kind: PointerDeviceKind.mouse, - ); - await tester.pump(const Duration(seconds: 2)); - await gesture.up(); - await tester.pump(); - - final EditableTextState editableText = tester.state(find.byType(EditableText)); - expect(editableText.selectionOverlay.toolbarIsVisible, isFalse); - expect(editableText.selectionOverlay.handlesAreVisible, isFalse); - }, - ); - - testWidgets( - 'Mouse double tap does not show handles nor toolbar', - (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( - text: 'abc def ghi', - ); - - await tester.pumpWidget( - MaterialApp( - home: Material( - child: TextField(controller: controller), - ), - ), - ); - - // Double tap to trigger the text field. - final Offset textFieldPos = tester.getCenter(find.byType(TextField)); - final TestGesture gesture = await tester.startGesture( - textFieldPos, - pointer: 7, - kind: PointerDeviceKind.mouse, - ); - await tester.pump(const Duration(milliseconds: 50)); - await gesture.up(); - await tester.pump(); - await gesture.down(textFieldPos); - await tester.pump(); - await gesture.up(); - await tester.pump(); - - final EditableTextState editableText = tester.state(find.byType(EditableText)); - expect(editableText.selectionOverlay.toolbarIsVisible, isFalse); - expect(editableText.selectionOverlay.handlesAreVisible, isFalse); - }, - ); - - testWidgets('Tapping selection handles toggles the toolbar', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( - text: 'abc def ghi', - ); - - await tester.pumpWidget( - MaterialApp( - home: Material( - child: TextField(controller: controller), - ), - ), - ); - - // Tap to position the cursor and show the selection handles. - final Offset ePos = textOffsetToPosition(tester, 5); // Index of 'e'. - await tester.tapAt(ePos, pointer: 7); - - final EditableTextState editableText = tester.state(find.byType(EditableText)); - expect(editableText.selectionOverlay.toolbarIsVisible, isFalse); - expect(editableText.selectionOverlay.handlesAreVisible, isTrue); - - final RenderEditable renderEditable = findRenderEditable(tester); - final List endpoints = globalize( - renderEditable.getEndpointsForSelection(controller.selection), - renderEditable, - ); - expect(endpoints.length, 1); - - // Tap the handle to show the toolbar. - final Offset handlePos = endpoints[0].point + const Offset(0.0, 1.0); - await tester.tapAt(handlePos, pointer: 7); - expect(editableText.selectionOverlay.toolbarIsVisible, isTrue); - - // Tap the handle again to hide the toolbar. - await tester.tapAt(handlePos, pointer: 7); - expect(editableText.selectionOverlay.toolbarIsVisible, isFalse); - }); } diff --git a/packages/flutter/test/widgets/editable_text_test.dart b/packages/flutter/test/widgets/editable_text_test.dart index be24411c9a0..8f45c3342c1 100644 --- a/packages/flutter/test/widgets/editable_text_test.dart +++ b/packages/flutter/test/widgets/editable_text_test.dart @@ -898,6 +898,51 @@ void main() { semantics.dispose(); }); + testWidgets('changing selection with keyboard does not show handles', (WidgetTester tester) async { + const String value1 = 'Hello World'; + + controller.text = value1; + + await tester.pumpWidget( + MaterialApp( + home: EditableText( + backgroundCursorColor: Colors.grey, + controller: controller, + selectionControls: materialTextSelectionControls, + focusNode: focusNode, + style: textStyle, + cursorColor: cursorColor, + ), + ), + ); + + // Simulate selection change via tap to show handles. + final RenderEditable render = tester.allRenderObjects + .firstWhere((RenderObject o) => o.runtimeType == RenderEditable); + render.onSelectionChanged(const TextSelection.collapsed(offset: 4), render, + SelectionChangedCause.tap); + + await tester.pumpAndSettle(); + final EditableTextState textState = tester.state(find.byType(EditableText)); + + expect(textState.selectionOverlay.handlesAreVisible, isTrue); + expect( + textState.selectionOverlay.selectionDelegate.textEditingValue.selection, + const TextSelection.collapsed(offset: 4), + ); + + // Simulate selection change via keyboard and expect handles to disappear. + render.onSelectionChanged(const TextSelection.collapsed(offset: 10), render, + SelectionChangedCause.keyboard); + await tester.pumpAndSettle(); + + expect(textState.selectionOverlay.handlesAreVisible, isFalse); + expect( + textState.selectionOverlay.selectionDelegate.textEditingValue.selection, + const TextSelection.collapsed(offset: 10), + ); + }); + testWidgets('exposes correct cursor movement semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); @@ -1989,7 +2034,6 @@ void main() { // Select the first word. Both handles should be visible. await tester.tapAt(const Offset(20, 10)); renderEditable.selectWord(cause: SelectionChangedCause.longPress); - state.showHandles(); await tester.pump(); await verifyVisibility(HandlePositionInViewport.leftEdge, true, HandlePositionInViewport.within, true); @@ -2011,7 +2055,6 @@ void main() { // Now that the second word has been dragged fully into view, select it. await tester.tapAt(const Offset(80, 10)); renderEditable.selectWord(cause: SelectionChangedCause.longPress); - state.showHandles(); await tester.pump(); await verifyVisibility(HandlePositionInViewport.within, true, HandlePositionInViewport.within, true); @@ -2056,7 +2099,6 @@ void main() { // Select the first word. Both handles should be visible. await tester.tapAt(const Offset(20, 10)); state.renderEditable.selectWord(cause: SelectionChangedCause.longPress); - state.showHandles(); await tester.pump(); final List positioned = find.byType(Positioned).evaluate().map((Element e) => e.widget).cast().toList(); @@ -2176,7 +2218,6 @@ void main() { // Select the first word. Both handles should be visible. await tester.tapAt(const Offset(20, 10)); renderEditable.selectWord(cause: SelectionChangedCause.longPress); - state.showHandles(); await tester.pump(); await verifyVisibility(HandlePositionInViewport.leftEdge, true, HandlePositionInViewport.within, true); @@ -2198,7 +2239,6 @@ void main() { // Now that the second word has been dragged fully into view, select it. await tester.tapAt(const Offset(80, 10)); renderEditable.selectWord(cause: SelectionChangedCause.longPress); - state.showHandles(); await tester.pump(); await verifyVisibility(HandlePositionInViewport.within, true, HandlePositionInViewport.within, true);