Skip to content

Commit ab51a02

Browse files
authored
Revert "Fix tooltip so only one shows at a time when hovering (flutter#90457)" (flutter#90909)
This reverts commit 885b2f5 to green up the build. Submitting on red to fix the build.
1 parent 777463c commit ab51a02

File tree

2 files changed

+31
-196
lines changed

2 files changed

+31
-196
lines changed

packages/flutter/lib/src/material/tooltip.dart

Lines changed: 29 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -194,41 +194,18 @@ class Tooltip extends StatefulWidget {
194194
/// * [Feedback], for providing platform-specific feedback to certain actions.
195195
final bool? enableFeedback;
196196

197-
static final List<_TooltipState> _openedTooltips = <_TooltipState>[];
198-
199-
// Causes any current tooltips to be concealed. Only called for mouse hover enter
200-
// detections. Won't conceal the supplied tooltip.
201-
static void _concealOtherTooltips(_TooltipState current) {
202-
if (_openedTooltips.isNotEmpty) {
203-
// Avoid concurrent modification.
204-
final List<_TooltipState> openedTooltips = _openedTooltips.toList();
205-
for (final _TooltipState state in openedTooltips) {
206-
if (state == current) {
207-
continue;
208-
}
209-
state._concealTooltip();
210-
}
211-
}
212-
}
213-
214-
// Causes the most recently concealed tooltip to be revealed. Only called for mouse
215-
// hover exit detections.
216-
static void _revealLastTooltip() {
217-
if (_openedTooltips.isNotEmpty) {
218-
_openedTooltips.last._revealTooltip();
219-
}
220-
}
197+
static final Set<_TooltipState> _openedToolTips = <_TooltipState>{};
221198

222199
/// Dismiss all of the tooltips that are currently shown on the screen.
223200
///
224201
/// This method returns true if it successfully dismisses the tooltips. It
225202
/// returns false if there is no tooltip shown on the screen.
226203
static bool dismissAllToolTips() {
227-
if (_openedTooltips.isNotEmpty) {
204+
if (_openedToolTips.isNotEmpty) {
228205
// Avoid concurrent modification.
229-
final List<_TooltipState> openedTooltips = _openedTooltips.toList();
230-
for (final _TooltipState state in openedTooltips) {
231-
state._dismissTooltip(immediately: true);
206+
final List<_TooltipState> openedToolTips = List<_TooltipState>.from(_openedToolTips);
207+
for (final _TooltipState state in openedToolTips) {
208+
state._hideTooltip(immediately: true);
232209
}
233210
return true;
234211
}
@@ -278,7 +255,7 @@ class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
278255
late bool excludeFromSemantics;
279256
late AnimationController _controller;
280257
OverlayEntry? _entry;
281-
Timer? _dismissTimer;
258+
Timer? _hideTimer;
282259
Timer? _showTimer;
283260
late Duration showDuration;
284261
late Duration hoverShowDuration;
@@ -287,14 +264,10 @@ class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
287264
bool _pressActivated = false;
288265
late TooltipTriggerMode triggerMode;
289266
late bool enableFeedback;
290-
late bool _isConcealed;
291-
late bool _forceRemoval;
292267

293268
@override
294269
void initState() {
295270
super.initState();
296-
_isConcealed = false;
297-
_forceRemoval = false;
298271
_mouseIsConnected = RendererBinding.instance!.mouseTracker.mouseIsConnected;
299272
_controller = AnimationController(
300273
duration: _fadeInDuration,
@@ -360,96 +333,47 @@ class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
360333
}
361334

362335
void _handleStatusChanged(AnimationStatus status) {
363-
// If this tip is concealed, don't remove it, even if it is dismissed, so that we can
364-
// reveal it later, unless it has explicitly been hidden with _dismissTooltip.
365-
if (status == AnimationStatus.dismissed && (_forceRemoval || !_isConcealed)) {
366-
_removeEntry();
336+
if (status == AnimationStatus.dismissed) {
337+
_hideTooltip(immediately: true);
367338
}
368339
}
369340

370-
void _dismissTooltip({ bool immediately = false }) {
341+
void _hideTooltip({ bool immediately = false }) {
371342
_showTimer?.cancel();
372343
_showTimer = null;
373344
if (immediately) {
374345
_removeEntry();
375346
return;
376347
}
377-
// So it will be removed when it's done reversing, regardless of whether it is
378-
// still concealed or not.
379-
_forceRemoval = true;
380348
if (_pressActivated) {
381-
_dismissTimer ??= Timer(showDuration, _controller.reverse);
349+
_hideTimer ??= Timer(showDuration, _controller.reverse);
382350
} else {
383-
_dismissTimer ??= Timer(hoverShowDuration, _controller.reverse);
351+
_hideTimer ??= Timer(hoverShowDuration, _controller.reverse);
384352
}
385353
_pressActivated = false;
386354
}
387355

388356
void _showTooltip({ bool immediately = false }) {
389-
_dismissTimer?.cancel();
390-
_dismissTimer = null;
357+
_hideTimer?.cancel();
358+
_hideTimer = null;
391359
if (immediately) {
392360
ensureTooltipVisible();
393361
return;
394362
}
395363
_showTimer ??= Timer(waitDuration, ensureTooltipVisible);
396364
}
397365

398-
void _concealTooltip() {
399-
if (_isConcealed || _forceRemoval) {
400-
// Already concealed, or it's being removed.
401-
return;
402-
}
403-
_isConcealed = true;
404-
_dismissTimer?.cancel();
405-
_dismissTimer = null;
406-
_showTimer?.cancel();
407-
_showTimer = null;
408-
if (_entry!= null) {
409-
_entry!.remove();
410-
}
411-
_controller.reverse();
412-
}
413-
414-
void _revealTooltip() {
415-
if (!_isConcealed) {
416-
// Already uncovered.
417-
return;
418-
}
419-
_isConcealed = false;
420-
_dismissTimer?.cancel();
421-
_dismissTimer = null;
422-
_showTimer?.cancel();
423-
_showTimer = null;
424-
if (!_entry!.mounted) {
425-
final OverlayState overlayState = Overlay.of(
426-
context,
427-
debugRequiredFor: widget,
428-
)!;
429-
overlayState.insert(_entry!);
430-
}
431-
SemanticsService.tooltip(widget.message);
432-
_controller.forward();
433-
}
434-
435366
/// Shows the tooltip if it is not already visible.
436367
///
437-
/// Returns `false` when the tooltip was already visible.
368+
/// Returns `false` when the tooltip was already visible or if the context has
369+
/// become null.
438370
bool ensureTooltipVisible() {
439371
_showTimer?.cancel();
440372
_showTimer = null;
441-
_forceRemoval = false;
442-
if (_isConcealed) {
443-
if (_mouseIsConnected) {
444-
Tooltip._concealOtherTooltips(this);
445-
}
446-
_revealTooltip();
447-
return true;
448-
}
449373
if (_entry != null) {
450374
// Stop trying to hide, if we were.
451-
_dismissTimer?.cancel();
452-
_dismissTimer = null;
375+
_hideTimer?.cancel();
376+
_hideTimer = null;
453377
_controller.forward();
454378
return false; // Already visible.
455379
}
@@ -458,17 +382,6 @@ class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
458382
return true;
459383
}
460384

461-
static final Set<_TooltipState> _mouseIn = <_TooltipState>{};
462-
463-
void _handleMouseEnter() {
464-
_showTooltip();
465-
}
466-
467-
void _handleMouseExit({bool immediately = false}) {
468-
// If the tip is currently covered, we can just remove it without waiting.
469-
_dismissTooltip(immediately: _isConcealed || immediately);
470-
}
471-
472385
void _createNewEntry() {
473386
final OverlayState overlayState = Overlay.of(
474387
context,
@@ -491,8 +404,8 @@ class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
491404
height: height,
492405
padding: padding,
493406
margin: margin,
494-
onEnter: _mouseIsConnected ? (_) => _handleMouseEnter() : null,
495-
onExit: _mouseIsConnected ? (_) => _handleMouseExit() : null,
407+
onEnter: _mouseIsConnected ? (PointerEnterEvent event) => _showTooltip() : null,
408+
onExit: _mouseIsConnected ? (PointerExitEvent event) => _hideTooltip() : null,
496409
decoration: decoration,
497410
textStyle: textStyle,
498411
animation: CurvedAnimation(
@@ -505,51 +418,36 @@ class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
505418
),
506419
);
507420
_entry = OverlayEntry(builder: (BuildContext context) => overlay);
508-
_isConcealed = false;
509421
overlayState.insert(_entry!);
510422
SemanticsService.tooltip(widget.message);
511-
if (_mouseIsConnected) {
512-
// Hovered tooltips shouldn't show more than one at once. For example, a chip with
513-
// a delete icon shouldn't show both the delete icon tooltip and the chip tooltip
514-
// at the same time.
515-
Tooltip._concealOtherTooltips(this);
516-
}
517-
assert(!Tooltip._openedTooltips.contains(this));
518-
Tooltip._openedTooltips.add(this);
423+
Tooltip._openedToolTips.add(this);
519424
}
520425

521426
void _removeEntry() {
522-
Tooltip._openedTooltips.remove(this);
523-
_mouseIn.remove(this);
524-
_dismissTimer?.cancel();
525-
_dismissTimer = null;
427+
Tooltip._openedToolTips.remove(this);
428+
_hideTimer?.cancel();
429+
_hideTimer = null;
526430
_showTimer?.cancel();
527431
_showTimer = null;
528-
if (!_isConcealed) {
529-
_entry?.remove();
530-
}
531-
_isConcealed = false;
432+
_entry?.remove();
532433
_entry = null;
533-
if (_mouseIsConnected) {
534-
Tooltip._revealLastTooltip();
535-
}
536434
}
537435

538436
void _handlePointerEvent(PointerEvent event) {
539437
if (_entry == null) {
540438
return;
541439
}
542440
if (event is PointerUpEvent || event is PointerCancelEvent) {
543-
_handleMouseExit();
441+
_hideTooltip();
544442
} else if (event is PointerDownEvent) {
545-
_handleMouseExit(immediately: true);
443+
_hideTooltip(immediately: true);
546444
}
547445
}
548446

549447
@override
550448
void deactivate() {
551449
if (_entry != null) {
552-
_dismissTooltip(immediately: true);
450+
_hideTooltip(immediately: true);
553451
}
554452
_showTimer?.cancel();
555453
super.deactivate();
@@ -637,8 +535,8 @@ class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
637535
// Only check for hovering if there is a mouse connected.
638536
if (_mouseIsConnected) {
639537
result = MouseRegion(
640-
onEnter: (_) => _handleMouseEnter(),
641-
onExit: (_) => _handleMouseExit(),
538+
onEnter: (PointerEnterEvent event) => _showTooltip(),
539+
onExit: (PointerExitEvent event) => _hideTooltip(),
642540
child: result,
643541
);
644542
}

packages/flutter/test/material/tooltip_test.dart

Lines changed: 2 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -919,7 +919,8 @@ void main() {
919919
const Duration waitDuration = Duration.zero;
920920
TestGesture? gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
921921
addTearDown(() async {
922-
gesture?.removePointer();
922+
if (gesture != null)
923+
return gesture.removePointer();
923924
});
924925
await gesture.addPointer();
925926
await gesture.moveTo(const Offset(1.0, 1.0));
@@ -969,70 +970,6 @@ void main() {
969970
expect(find.text(tooltipText), findsNothing);
970971
});
971972

972-
testWidgets('Tooltip should not show more than one tooltip when hovered', (WidgetTester tester) async {
973-
const Duration waitDuration = Duration(milliseconds: 500);
974-
final UniqueKey innerKey = UniqueKey();
975-
final UniqueKey outerKey = UniqueKey();
976-
await tester.pumpWidget(
977-
MaterialApp(
978-
home: Center(
979-
child: Tooltip(
980-
message: 'Outer',
981-
child: Container(
982-
key: outerKey,
983-
width: 100,
984-
height: 100,
985-
alignment: Alignment.centerRight,
986-
child: Tooltip(
987-
message: 'Inner',
988-
child: SizedBox(
989-
key: innerKey,
990-
width: 25,
991-
height: 100,
992-
),
993-
),
994-
),
995-
),
996-
),
997-
),
998-
);
999-
1000-
TestGesture? gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
1001-
addTearDown(() async { gesture?.removePointer(); });
1002-
1003-
// Both the inner and outer containers have tooltips associated with them, but only
1004-
// the currently hovered one should appear, even though the pointer is inside both.
1005-
final Finder outer = find.byKey(outerKey);
1006-
final Finder inner = find.byKey(innerKey);
1007-
await gesture.moveTo(Offset.zero);
1008-
await tester.pump();
1009-
await gesture.moveTo(tester.getCenter(outer));
1010-
await tester.pump();
1011-
await gesture.moveTo(tester.getCenter(inner));
1012-
await tester.pump();
1013-
1014-
// Wait for it to appear.
1015-
await tester.pump(waitDuration);
1016-
1017-
expect(find.text('Outer'), findsNothing);
1018-
expect(find.text('Inner'), findsOneWidget);
1019-
await gesture.moveTo(tester.getCenter(outer));
1020-
await tester.pump();
1021-
// Wait for it to switch.
1022-
await tester.pump(waitDuration);
1023-
expect(find.text('Outer'), findsOneWidget);
1024-
expect(find.text('Inner'), findsNothing);
1025-
1026-
await gesture.moveTo(Offset.zero);
1027-
1028-
// Wait for all tooltips to disappear.
1029-
await tester.pumpAndSettle();
1030-
await gesture.removePointer();
1031-
gesture = null;
1032-
expect(find.text('Outer'), findsNothing);
1033-
expect(find.text('Inner'), findsNothing);
1034-
});
1035-
1036973
testWidgets('Tooltip can be dismissed by escape key', (WidgetTester tester) async {
1037974
const Duration waitDuration = Duration.zero;
1038975
TestGesture? gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);

0 commit comments

Comments
 (0)