From ab51a0260d29d50800ae5c9665ecb01883a9b503 Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Tue, 28 Sep 2021 15:18:53 -0700 Subject: [PATCH] Revert "Fix tooltip so only one shows at a time when hovering (#90457)" (#90909) This reverts commit 885b2f56e180faac645c7ec9b60b3809131dc628 to green up the build. Submitting on red to fix the build. --- .../flutter/lib/src/material/tooltip.dart | 160 ++++-------------- .../flutter/test/material/tooltip_test.dart | 67 +------- 2 files changed, 31 insertions(+), 196 deletions(-) diff --git a/packages/flutter/lib/src/material/tooltip.dart b/packages/flutter/lib/src/material/tooltip.dart index 9f8dcd3923..13cc4a5bbb 100644 --- a/packages/flutter/lib/src/material/tooltip.dart +++ b/packages/flutter/lib/src/material/tooltip.dart @@ -194,41 +194,18 @@ class Tooltip extends StatefulWidget { /// * [Feedback], for providing platform-specific feedback to certain actions. final bool? enableFeedback; - static final List<_TooltipState> _openedTooltips = <_TooltipState>[]; - - // Causes any current tooltips to be concealed. Only called for mouse hover enter - // detections. Won't conceal the supplied tooltip. - static void _concealOtherTooltips(_TooltipState current) { - if (_openedTooltips.isNotEmpty) { - // Avoid concurrent modification. - final List<_TooltipState> openedTooltips = _openedTooltips.toList(); - for (final _TooltipState state in openedTooltips) { - if (state == current) { - continue; - } - state._concealTooltip(); - } - } - } - - // Causes the most recently concealed tooltip to be revealed. Only called for mouse - // hover exit detections. - static void _revealLastTooltip() { - if (_openedTooltips.isNotEmpty) { - _openedTooltips.last._revealTooltip(); - } - } + static final Set<_TooltipState> _openedToolTips = <_TooltipState>{}; /// Dismiss all of the tooltips that are currently shown on the screen. /// /// This method returns true if it successfully dismisses the tooltips. It /// returns false if there is no tooltip shown on the screen. static bool dismissAllToolTips() { - if (_openedTooltips.isNotEmpty) { + if (_openedToolTips.isNotEmpty) { // Avoid concurrent modification. - final List<_TooltipState> openedTooltips = _openedTooltips.toList(); - for (final _TooltipState state in openedTooltips) { - state._dismissTooltip(immediately: true); + final List<_TooltipState> openedToolTips = List<_TooltipState>.from(_openedToolTips); + for (final _TooltipState state in openedToolTips) { + state._hideTooltip(immediately: true); } return true; } @@ -278,7 +255,7 @@ class _TooltipState extends State with SingleTickerProviderStateMixin { late bool excludeFromSemantics; late AnimationController _controller; OverlayEntry? _entry; - Timer? _dismissTimer; + Timer? _hideTimer; Timer? _showTimer; late Duration showDuration; late Duration hoverShowDuration; @@ -287,14 +264,10 @@ class _TooltipState extends State with SingleTickerProviderStateMixin { bool _pressActivated = false; late TooltipTriggerMode triggerMode; late bool enableFeedback; - late bool _isConcealed; - late bool _forceRemoval; @override void initState() { super.initState(); - _isConcealed = false; - _forceRemoval = false; _mouseIsConnected = RendererBinding.instance!.mouseTracker.mouseIsConnected; _controller = AnimationController( duration: _fadeInDuration, @@ -360,34 +333,29 @@ class _TooltipState extends State with SingleTickerProviderStateMixin { } void _handleStatusChanged(AnimationStatus status) { - // If this tip is concealed, don't remove it, even if it is dismissed, so that we can - // reveal it later, unless it has explicitly been hidden with _dismissTooltip. - if (status == AnimationStatus.dismissed && (_forceRemoval || !_isConcealed)) { - _removeEntry(); + if (status == AnimationStatus.dismissed) { + _hideTooltip(immediately: true); } } - void _dismissTooltip({ bool immediately = false }) { + void _hideTooltip({ bool immediately = false }) { _showTimer?.cancel(); _showTimer = null; if (immediately) { _removeEntry(); return; } - // So it will be removed when it's done reversing, regardless of whether it is - // still concealed or not. - _forceRemoval = true; if (_pressActivated) { - _dismissTimer ??= Timer(showDuration, _controller.reverse); + _hideTimer ??= Timer(showDuration, _controller.reverse); } else { - _dismissTimer ??= Timer(hoverShowDuration, _controller.reverse); + _hideTimer ??= Timer(hoverShowDuration, _controller.reverse); } _pressActivated = false; } void _showTooltip({ bool immediately = false }) { - _dismissTimer?.cancel(); - _dismissTimer = null; + _hideTimer?.cancel(); + _hideTimer = null; if (immediately) { ensureTooltipVisible(); return; @@ -395,61 +363,17 @@ class _TooltipState extends State with SingleTickerProviderStateMixin { _showTimer ??= Timer(waitDuration, ensureTooltipVisible); } - void _concealTooltip() { - if (_isConcealed || _forceRemoval) { - // Already concealed, or it's being removed. - return; - } - _isConcealed = true; - _dismissTimer?.cancel(); - _dismissTimer = null; - _showTimer?.cancel(); - _showTimer = null; - if (_entry!= null) { - _entry!.remove(); - } - _controller.reverse(); - } - - void _revealTooltip() { - if (!_isConcealed) { - // Already uncovered. - return; - } - _isConcealed = false; - _dismissTimer?.cancel(); - _dismissTimer = null; - _showTimer?.cancel(); - _showTimer = null; - if (!_entry!.mounted) { - final OverlayState overlayState = Overlay.of( - context, - debugRequiredFor: widget, - )!; - overlayState.insert(_entry!); - } - SemanticsService.tooltip(widget.message); - _controller.forward(); - } - /// Shows the tooltip if it is not already visible. /// - /// Returns `false` when the tooltip was already visible. + /// Returns `false` when the tooltip was already visible or if the context has + /// become null. bool ensureTooltipVisible() { _showTimer?.cancel(); _showTimer = null; - _forceRemoval = false; - if (_isConcealed) { - if (_mouseIsConnected) { - Tooltip._concealOtherTooltips(this); - } - _revealTooltip(); - return true; - } if (_entry != null) { // Stop trying to hide, if we were. - _dismissTimer?.cancel(); - _dismissTimer = null; + _hideTimer?.cancel(); + _hideTimer = null; _controller.forward(); return false; // Already visible. } @@ -458,17 +382,6 @@ class _TooltipState extends State with SingleTickerProviderStateMixin { return true; } - static final Set<_TooltipState> _mouseIn = <_TooltipState>{}; - - void _handleMouseEnter() { - _showTooltip(); - } - - void _handleMouseExit({bool immediately = false}) { - // If the tip is currently covered, we can just remove it without waiting. - _dismissTooltip(immediately: _isConcealed || immediately); - } - void _createNewEntry() { final OverlayState overlayState = Overlay.of( context, @@ -491,8 +404,8 @@ class _TooltipState extends State with SingleTickerProviderStateMixin { height: height, padding: padding, margin: margin, - onEnter: _mouseIsConnected ? (_) => _handleMouseEnter() : null, - onExit: _mouseIsConnected ? (_) => _handleMouseExit() : null, + onEnter: _mouseIsConnected ? (PointerEnterEvent event) => _showTooltip() : null, + onExit: _mouseIsConnected ? (PointerExitEvent event) => _hideTooltip() : null, decoration: decoration, textStyle: textStyle, animation: CurvedAnimation( @@ -505,34 +418,19 @@ class _TooltipState extends State with SingleTickerProviderStateMixin { ), ); _entry = OverlayEntry(builder: (BuildContext context) => overlay); - _isConcealed = false; overlayState.insert(_entry!); SemanticsService.tooltip(widget.message); - if (_mouseIsConnected) { - // Hovered tooltips shouldn't show more than one at once. For example, a chip with - // a delete icon shouldn't show both the delete icon tooltip and the chip tooltip - // at the same time. - Tooltip._concealOtherTooltips(this); - } - assert(!Tooltip._openedTooltips.contains(this)); - Tooltip._openedTooltips.add(this); + Tooltip._openedToolTips.add(this); } void _removeEntry() { - Tooltip._openedTooltips.remove(this); - _mouseIn.remove(this); - _dismissTimer?.cancel(); - _dismissTimer = null; + Tooltip._openedToolTips.remove(this); + _hideTimer?.cancel(); + _hideTimer = null; _showTimer?.cancel(); _showTimer = null; - if (!_isConcealed) { - _entry?.remove(); - } - _isConcealed = false; + _entry?.remove(); _entry = null; - if (_mouseIsConnected) { - Tooltip._revealLastTooltip(); - } } void _handlePointerEvent(PointerEvent event) { @@ -540,16 +438,16 @@ class _TooltipState extends State with SingleTickerProviderStateMixin { return; } if (event is PointerUpEvent || event is PointerCancelEvent) { - _handleMouseExit(); + _hideTooltip(); } else if (event is PointerDownEvent) { - _handleMouseExit(immediately: true); + _hideTooltip(immediately: true); } } @override void deactivate() { if (_entry != null) { - _dismissTooltip(immediately: true); + _hideTooltip(immediately: true); } _showTimer?.cancel(); super.deactivate(); @@ -637,8 +535,8 @@ class _TooltipState extends State with SingleTickerProviderStateMixin { // Only check for hovering if there is a mouse connected. if (_mouseIsConnected) { result = MouseRegion( - onEnter: (_) => _handleMouseEnter(), - onExit: (_) => _handleMouseExit(), + onEnter: (PointerEnterEvent event) => _showTooltip(), + onExit: (PointerExitEvent event) => _hideTooltip(), child: result, ); } diff --git a/packages/flutter/test/material/tooltip_test.dart b/packages/flutter/test/material/tooltip_test.dart index 35a02a693b..8e7e113168 100644 --- a/packages/flutter/test/material/tooltip_test.dart +++ b/packages/flutter/test/material/tooltip_test.dart @@ -919,7 +919,8 @@ void main() { const Duration waitDuration = Duration.zero; TestGesture? gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); addTearDown(() async { - gesture?.removePointer(); + if (gesture != null) + return gesture.removePointer(); }); await gesture.addPointer(); await gesture.moveTo(const Offset(1.0, 1.0)); @@ -969,70 +970,6 @@ void main() { expect(find.text(tooltipText), findsNothing); }); - testWidgets('Tooltip should not show more than one tooltip when hovered', (WidgetTester tester) async { - const Duration waitDuration = Duration(milliseconds: 500); - final UniqueKey innerKey = UniqueKey(); - final UniqueKey outerKey = UniqueKey(); - await tester.pumpWidget( - MaterialApp( - home: Center( - child: Tooltip( - message: 'Outer', - child: Container( - key: outerKey, - width: 100, - height: 100, - alignment: Alignment.centerRight, - child: Tooltip( - message: 'Inner', - child: SizedBox( - key: innerKey, - width: 25, - height: 100, - ), - ), - ), - ), - ), - ), - ); - - TestGesture? gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); - addTearDown(() async { gesture?.removePointer(); }); - - // Both the inner and outer containers have tooltips associated with them, but only - // the currently hovered one should appear, even though the pointer is inside both. - final Finder outer = find.byKey(outerKey); - final Finder inner = find.byKey(innerKey); - await gesture.moveTo(Offset.zero); - await tester.pump(); - await gesture.moveTo(tester.getCenter(outer)); - await tester.pump(); - await gesture.moveTo(tester.getCenter(inner)); - await tester.pump(); - - // Wait for it to appear. - await tester.pump(waitDuration); - - expect(find.text('Outer'), findsNothing); - expect(find.text('Inner'), findsOneWidget); - await gesture.moveTo(tester.getCenter(outer)); - await tester.pump(); - // Wait for it to switch. - await tester.pump(waitDuration); - expect(find.text('Outer'), findsOneWidget); - expect(find.text('Inner'), findsNothing); - - await gesture.moveTo(Offset.zero); - - // Wait for all tooltips to disappear. - await tester.pumpAndSettle(); - await gesture.removePointer(); - gesture = null; - expect(find.text('Outer'), findsNothing); - expect(find.text('Inner'), findsNothing); - }); - testWidgets('Tooltip can be dismissed by escape key', (WidgetTester tester) async { const Duration waitDuration = Duration.zero; TestGesture? gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);