@@ -194,18 +194,41 @@ class Tooltip extends StatefulWidget {
194
194
/// * [Feedback] , for providing platform-specific feedback to certain actions.
195
195
final bool ? enableFeedback;
196
196
197
- static final Set <_TooltipState > _openedToolTips = < _TooltipState > {};
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
+ }
198
221
199
222
/// Dismiss all of the tooltips that are currently shown on the screen.
200
223
///
201
224
/// This method returns true if it successfully dismisses the tooltips. It
202
225
/// returns false if there is no tooltip shown on the screen.
203
226
static bool dismissAllToolTips () {
204
- if (_openedToolTips .isNotEmpty) {
227
+ if (_openedTooltips .isNotEmpty) {
205
228
// Avoid concurrent modification.
206
- final List <_TooltipState > openedToolTips = List < _TooltipState >. from (_openedToolTips );
207
- for (final _TooltipState state in openedToolTips ) {
208
- state._hideTooltip (immediately: true );
229
+ final List <_TooltipState > openedTooltips = _openedTooltips. toList ( );
230
+ for (final _TooltipState state in openedTooltips ) {
231
+ state._dismissTooltip (immediately: true );
209
232
}
210
233
return true ;
211
234
}
@@ -255,7 +278,7 @@ class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
255
278
late bool excludeFromSemantics;
256
279
late AnimationController _controller;
257
280
OverlayEntry ? _entry;
258
- Timer ? _hideTimer ;
281
+ Timer ? _dismissTimer ;
259
282
Timer ? _showTimer;
260
283
late Duration showDuration;
261
284
late Duration hoverShowDuration;
@@ -264,10 +287,14 @@ class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
264
287
bool _pressActivated = false ;
265
288
late TooltipTriggerMode triggerMode;
266
289
late bool enableFeedback;
290
+ late bool _isConcealed;
291
+ late bool _forceRemoval;
267
292
268
293
@override
269
294
void initState () {
270
295
super .initState ();
296
+ _isConcealed = false ;
297
+ _forceRemoval = false ;
271
298
_mouseIsConnected = RendererBinding .instance! .mouseTracker.mouseIsConnected;
272
299
_controller = AnimationController (
273
300
duration: _fadeInDuration,
@@ -333,47 +360,96 @@ class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
333
360
}
334
361
335
362
void _handleStatusChanged (AnimationStatus status) {
336
- if (status == AnimationStatus .dismissed) {
337
- _hideTooltip (immediately: true );
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 ();
338
367
}
339
368
}
340
369
341
- void _hideTooltip ({ bool immediately = false }) {
370
+ void _dismissTooltip ({ bool immediately = false }) {
342
371
_showTimer? .cancel ();
343
372
_showTimer = null ;
344
373
if (immediately) {
345
374
_removeEntry ();
346
375
return ;
347
376
}
377
+ // So it will be removed when it's done reversing, regardless of whether it is
378
+ // still concealed or not.
379
+ _forceRemoval = true ;
348
380
if (_pressActivated) {
349
- _hideTimer ?? = Timer (showDuration, _controller.reverse);
381
+ _dismissTimer ?? = Timer (showDuration, _controller.reverse);
350
382
} else {
351
- _hideTimer ?? = Timer (hoverShowDuration, _controller.reverse);
383
+ _dismissTimer ?? = Timer (hoverShowDuration, _controller.reverse);
352
384
}
353
385
_pressActivated = false ;
354
386
}
355
387
356
388
void _showTooltip ({ bool immediately = false }) {
357
- _hideTimer ? .cancel ();
358
- _hideTimer = null ;
389
+ _dismissTimer ? .cancel ();
390
+ _dismissTimer = null ;
359
391
if (immediately) {
360
392
ensureTooltipVisible ();
361
393
return ;
362
394
}
363
395
_showTimer ?? = Timer (waitDuration, ensureTooltipVisible);
364
396
}
365
397
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
+
366
435
/// Shows the tooltip if it is not already visible.
367
436
///
368
- /// Returns `false` when the tooltip was already visible or if the context has
369
- /// become null.
437
+ /// Returns `false` when the tooltip was already visible.
370
438
bool ensureTooltipVisible () {
371
439
_showTimer? .cancel ();
372
440
_showTimer = null ;
441
+ _forceRemoval = false ;
442
+ if (_isConcealed) {
443
+ if (_mouseIsConnected) {
444
+ Tooltip ._concealOtherTooltips (this );
445
+ }
446
+ _revealTooltip ();
447
+ return true ;
448
+ }
373
449
if (_entry != null ) {
374
450
// Stop trying to hide, if we were.
375
- _hideTimer ? .cancel ();
376
- _hideTimer = null ;
451
+ _dismissTimer ? .cancel ();
452
+ _dismissTimer = null ;
377
453
_controller.forward ();
378
454
return false ; // Already visible.
379
455
}
@@ -382,6 +458,17 @@ class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
382
458
return true ;
383
459
}
384
460
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
+
385
472
void _createNewEntry () {
386
473
final OverlayState overlayState = Overlay .of (
387
474
context,
@@ -404,8 +491,8 @@ class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
404
491
height: height,
405
492
padding: padding,
406
493
margin: margin,
407
- onEnter: _mouseIsConnected ? (PointerEnterEvent event ) => _showTooltip () : null ,
408
- onExit: _mouseIsConnected ? (PointerExitEvent event ) => _hideTooltip () : null ,
494
+ onEnter: _mouseIsConnected ? (_ ) => _handleMouseEnter () : null ,
495
+ onExit: _mouseIsConnected ? (_ ) => _handleMouseExit () : null ,
409
496
decoration: decoration,
410
497
textStyle: textStyle,
411
498
animation: CurvedAnimation (
@@ -418,36 +505,51 @@ class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
418
505
),
419
506
);
420
507
_entry = OverlayEntry (builder: (BuildContext context) => overlay);
508
+ _isConcealed = false ;
421
509
overlayState.insert (_entry! );
422
510
SemanticsService .tooltip (widget.message);
423
- Tooltip ._openedToolTips.add (this );
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 );
424
519
}
425
520
426
521
void _removeEntry () {
427
- Tooltip ._openedToolTips.remove (this );
428
- _hideTimer? .cancel ();
429
- _hideTimer = null ;
522
+ Tooltip ._openedTooltips.remove (this );
523
+ _mouseIn.remove (this );
524
+ _dismissTimer? .cancel ();
525
+ _dismissTimer = null ;
430
526
_showTimer? .cancel ();
431
527
_showTimer = null ;
432
- _entry? .remove ();
528
+ if (! _isConcealed) {
529
+ _entry? .remove ();
530
+ }
531
+ _isConcealed = false ;
433
532
_entry = null ;
533
+ if (_mouseIsConnected) {
534
+ Tooltip ._revealLastTooltip ();
535
+ }
434
536
}
435
537
436
538
void _handlePointerEvent (PointerEvent event) {
437
539
if (_entry == null ) {
438
540
return ;
439
541
}
440
542
if (event is PointerUpEvent || event is PointerCancelEvent ) {
441
- _hideTooltip ();
543
+ _handleMouseExit ();
442
544
} else if (event is PointerDownEvent ) {
443
- _hideTooltip (immediately: true );
545
+ _handleMouseExit (immediately: true );
444
546
}
445
547
}
446
548
447
549
@override
448
550
void deactivate () {
449
551
if (_entry != null ) {
450
- _hideTooltip (immediately: true );
552
+ _dismissTooltip (immediately: true );
451
553
}
452
554
_showTimer? .cancel ();
453
555
super .deactivate ();
@@ -535,8 +637,8 @@ class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
535
637
// Only check for hovering if there is a mouse connected.
536
638
if (_mouseIsConnected) {
537
639
result = MouseRegion (
538
- onEnter: (PointerEnterEvent event ) => _showTooltip (),
539
- onExit: (PointerExitEvent event ) => _hideTooltip (),
640
+ onEnter: (_ ) => _handleMouseEnter (),
641
+ onExit: (_ ) => _handleMouseExit (),
540
642
child: result,
541
643
);
542
644
}
0 commit comments