1313import tkinter as tk
1414from platform import system as platform_system
1515from tkinter import messagebox , ttk
16- from typing import Any , NamedTuple , Optional , cast
16+ from typing import Any , ClassVar , NamedTuple , Optional , cast
1717from weakref import WeakKeyDictionary
1818
1919from ardupilot_methodic_configurator import _
2020
2121# Tooltip positioning constants
2222TOOLTIP_MAX_OFFSET = 100 # Maximum horizontal offset from widget edge
2323TOOLTIP_VERTICAL_OFFSET = 10 # Vertical offset when positioning above widget
24+ TOOLTIP_SHOW_DELAY_MS = 250 # Delay before showing to avoid flicker while moving across dense UIs
25+ TOOLTIP_HIDE_DELAY_MS = 75 # Small delay prevents leave/enter jitter from leaving stale tooltips behind
2426
2527
2628class MonitorBounds (NamedTuple ):
@@ -456,6 +458,8 @@ class Tooltip:
456458 Creates a tooltip that appears when the mouse hovers over a widget and disappears when the mouse leaves the widget.
457459 """
458460
461+ _active_tooltip : ClassVar [Optional ["Tooltip" ]] = None
462+
459463 def __init__ ( # pylint: disable=too-many-arguments, too-many-positional-arguments
460464 self ,
461465 widget : tk .Widget ,
@@ -470,15 +474,17 @@ def __init__( # pylint: disable=too-many-arguments, too-many-positional-argumen
470474 self .position_below : bool = position_below
471475 self .toplevel_class = toplevel_class or tk .Toplevel
472476 self .hide_timer : Optional [str ] = None
477+ self .show_timer : Optional [str ] = None
473478
474479 # Bind the <Enter> and <Leave> events to show and hide the tooltip
475480 if platform_system () == "Darwin" :
476- # On macOS, only create the tooltip when the mouse enters the widget
481+ # On macOS, defer tooltip creation slightly to avoid flashing while
482+ # moving through dense tables and controls.
477483 if tag_name and isinstance (self .widget , tk .Text ):
478- self .widget .tag_bind (tag_name , "<Enter>" , self .create_show , "+" )
484+ self .widget .tag_bind (tag_name , "<Enter>" , self .schedule_show , "+" )
479485 self .widget .tag_bind (tag_name , "<Leave>" , self .destroy_hide , "+" )
480486 else :
481- self .widget .bind ("<Enter>" , self .create_show , "+" )
487+ self .widget .bind ("<Enter>" , self .schedule_show , "+" )
482488 self .widget .bind ("<Leave>" , self .destroy_hide , "+" )
483489 else :
484490 if tag_name and isinstance (self .widget , tk .Text ):
@@ -499,25 +505,50 @@ def __init__( # pylint: disable=too-many-arguments, too-many-positional-argumen
499505 self .tooltip .bind ("<Enter>" , self ._cancel_hide )
500506 self .tooltip .bind ("<Leave>" , self .hide )
501507
508+ def _cancel_show (self ) -> None :
509+ """Cancel any pending show timer."""
510+ if self .show_timer :
511+ self .widget .after_cancel (self .show_timer )
512+ self .show_timer = None
513+
502514 def show (self , event : Optional [tk .Event ] = None ) -> None : # noqa: ARG002 # pylint: disable=unused-argument
503515 """On non-macOS, tooltip already exists, show it on events."""
504516 self ._cancel_hide ()
517+ self ._hide_active_tooltip ()
505518 if self .tooltip :
506519 self .position_tooltip ()
507520 self .tooltip .deiconify ()
521+ Tooltip ._active_tooltip = self
508522
509523 def _cancel_hide (self , event : Optional [tk .Event ] = None ) -> None : # noqa: ARG002 # pylint: disable=unused-argument
510524 """Cancel the hide timer."""
511525 if self .hide_timer :
512526 self .widget .after_cancel (self .hide_timer )
513527 self .hide_timer = None
514528
529+ def _hide_active_tooltip (self ) -> None :
530+ """Hide another active tooltip before showing this one."""
531+ if Tooltip ._active_tooltip and Tooltip ._active_tooltip is not self :
532+ Tooltip ._active_tooltip .force_hide ()
533+
534+ def schedule_show (self , event : Optional [tk .Event ] = None ) -> None : # noqa: ARG002 # pylint: disable=unused-argument
535+ """Delay tooltip creation slightly to avoid flicker during pointer movement."""
536+ self ._cancel_hide ()
537+ self ._cancel_show ()
538+ self .show_timer = self .widget .after (TOOLTIP_SHOW_DELAY_MS , self .create_show )
539+
515540 def create_show (self , event : Optional [tk .Event ] = None ) -> None : # noqa: ARG002 # pylint: disable=unused-argument
516541 """On macOS, only create the tooltip when the mouse enters the widget."""
542+ self ._cancel_show ()
543+ self ._cancel_hide ()
544+ self ._hide_active_tooltip ()
545+
517546 if self .tooltip :
547+ Tooltip ._active_tooltip = self
518548 return # Avoid redundant tooltip creation
519549
520550 self .tooltip = cast ("tk.Toplevel" , self .toplevel_class (self .widget ))
551+ self .tooltip .withdraw ()
521552
522553 try :
523554 self .tooltip .tk .call (
@@ -537,9 +568,8 @@ def create_show(self, event: Optional[tk.Event] = None) -> None: # noqa: ARG002
537568 )
538569 tooltip_label .pack ()
539570 self .position_tooltip ()
540- # Bind to tooltip to prevent hiding when mouse is over it
541- self .tooltip .bind ("<Enter>" , self ._cancel_hide )
542- self .tooltip .bind ("<Leave>" , self .destroy_hide )
571+ self .tooltip .deiconify ()
572+ Tooltip ._active_tooltip = self
543573
544574 def position_tooltip (self ) -> None :
545575 """Position tooltip within monitor bounds, handling widget destruction gracefully."""
@@ -580,20 +610,32 @@ def position_tooltip(self) -> None:
580610 def hide (self , event : Optional [tk .Event ] = None ) -> None : # noqa: ARG002 # pylint: disable=unused-argument
581611 """Hide the tooltip after a delay on non-macOS."""
582612 self ._cancel_hide ()
583- self .hide_timer = self .widget .after (10 , self ._do_hide )
613+ self .hide_timer = self .widget .after (TOOLTIP_HIDE_DELAY_MS , self ._do_hide )
584614
585615 def _do_hide (self ) -> None :
586616 """Actually hide or destroy the tooltip depending on platform."""
587617 if self .tooltip :
588618 self .tooltip .withdraw ()
619+ if Tooltip ._active_tooltip is self :
620+ Tooltip ._active_tooltip = None
589621 self .hide_timer = None
590622
591- def destroy_hide (self , event : Optional [tk .Event ] = None ) -> None : # noqa: ARG002 # pylint: disable=unused-argument
592- """On macOS, fully destroy the tooltip when the mouse leaves the widget."""
623+ def force_hide (self ) -> None :
624+ """Immediately hide or destroy the tooltip, depending on platform."""
625+ self ._cancel_show ()
593626 self ._cancel_hide ()
594627 if self .tooltip :
595- self .tooltip .destroy ()
596- self .tooltip = None
628+ if platform_system () == "Darwin" :
629+ self .tooltip .destroy ()
630+ self .tooltip = None
631+ else :
632+ self .tooltip .withdraw ()
633+ if Tooltip ._active_tooltip is self :
634+ Tooltip ._active_tooltip = None
635+
636+ def destroy_hide (self , event : Optional [tk .Event ] = None ) -> None : # noqa: ARG002 # pylint: disable=unused-argument
637+ """On macOS, fully destroy the tooltip when the mouse leaves the widget."""
638+ self .force_hide ()
597639
598640
599641def show_tooltip (widget : tk .Widget , text : str , position_below : bool = True ) -> Tooltip :
0 commit comments