Module TkZero.Tooltip

Attach a tool tip to widgets.

Expand source code
"""
Attach a tool tip to widgets.
"""

import tkinter as tk


# The following classes were copied directly from idlelib.tooltip.py
# You can usually find the implementation if you installed it at:
# <python install directory>/Python/Python39/Lib/idlelib/tooltip.py
# On Windows, I found it at:
# C:\Users\<USERNAME>\AppData\Local\Programs\Python\Python39\Lib\idlelib\tooltip.py
# They were then modified to have underscores in front and to not import
# stuff from the global namespace. Then Black did it's thing...
class _TooltipBase(object):
    """abstract base class for tooltips"""

    def __init__(self, anchor_widget):
        """Create a tooltip.

        anchor_widget: the widget next to which the tooltip will be shown

        Note that a widget will only be shown when showtip() is called.
        """
        self.anchor_widget = anchor_widget
        self.tipwindow = None

    def __del__(self):
        self.hidetip()

    def showtip(self):
        """display the tooltip"""
        if self.tipwindow:
            return
        self.tipwindow = tw = tk.Toplevel(self.anchor_widget)
        # show no border on the top level window
        tw.wm_overrideredirect(1)
        try:
            # This command is only needed and available on Tk >= 8.4.0 for OSX.
            # Without it, call tips intrude on the typing process by grabbing
            # the focus.
            tw.tk.call(
                "::tk::unsupported::MacWindowStyle",
                "style",
                tw._w,
                "help",
                "noActivates",
            )
        except tk.TclError:
            pass

        self.position_window()
        self.showcontents()
        self.tipwindow.update_idletasks()  # Needed on MacOS -- see #34275.
        self.tipwindow.lift()  # work around bug in Tk 8.5.18+ (issue #24570)

    def position_window(self):
        """(re)-set the tooltip's screen position"""
        x, y = self.get_position()
        root_x = self.anchor_widget.winfo_rootx() + x
        root_y = self.anchor_widget.winfo_rooty() + y
        self.tipwindow.wm_geometry("+%d+%d" % (root_x, root_y))

    def get_position(self):
        """choose a screen position for the tooltip"""
        # The tip window must be completely outside the anchor widget;
        # otherwise when the mouse enters the tip window we get
        # a leave event and it disappears, and then we get an enter
        # event and it reappears, and so on forever :-(
        #
        # Note: This is a simplistic implementation; sub-classes will likely
        # want to override this.
        return 20, self.anchor_widget.winfo_height() + 1

    def showcontents(self):
        """content display hook for sub-classes"""
        # See ToolTip for an example
        raise NotImplementedError

    def hidetip(self):
        """hide the tooltip"""
        # Note: This is called by __del__, so careful when overriding/extending
        tw = self.tipwindow
        self.tipwindow = None
        if tw:
            try:
                tw.destroy()
            except tk.TclError:  # pragma: no cover
                pass


class _OnHoverTooltipBase(_TooltipBase):
    """abstract base class for tooltips, with delayed on-hover display"""

    def __init__(self, anchor_widget, hover_delay=1000):
        """Create a tooltip with a mouse hover delay.

        anchor_widget: the widget next to which the tooltip will be shown
        hover_delay: time to delay before showing the tooltip, in milliseconds

        Note that a widget will only be shown when showtip() is called,
        e.g. after hovering over the anchor widget with the mouse for enough
        time.
        """
        super(_OnHoverTooltipBase, self).__init__(anchor_widget)
        self.hover_delay = hover_delay

        self._after_id = None
        self._id1 = self.anchor_widget.bind("<Enter>", self._show_event)
        self._id2 = self.anchor_widget.bind("<Leave>", self._hide_event)
        self._id3 = self.anchor_widget.bind("<Button>", self._hide_event)

    def __del__(self):
        try:
            self.anchor_widget.unbind("<Enter>", self._id1)
            self.anchor_widget.unbind("<Leave>", self._id2)  # pragma: no cover
            self.anchor_widget.unbind("<Button>", self._id3)  # pragma: no cover
        except tk.TclError:
            pass
        super(_OnHoverTooltipBase, self).__del__()

    def _show_event(self, event=None):
        """event handler to display the tooltip"""
        if self.hover_delay:
            self.schedule()
        else:
            self.showtip()

    def _hide_event(self, event=None):
        """event handler to hide the tooltip"""
        self.hidetip()

    def schedule(self):
        """schedule the future display of the tooltip"""
        self.unschedule()
        self._after_id = self.anchor_widget.after(self.hover_delay, self.showtip)

    def unschedule(self):
        """cancel the future display of the tooltip"""
        after_id = self._after_id
        self._after_id = None
        if after_id:
            self.anchor_widget.after_cancel(after_id)

    def hidetip(self):
        """hide the tooltip"""
        try:
            self.unschedule()
        except tk.TclError:  # pragma: no cover
            pass
        super(_OnHoverTooltipBase, self).hidetip()


class _Hovertip(_OnHoverTooltipBase):
    "A tooltip that pops up when a mouse hovers over an anchor widget."

    def __init__(self, anchor_widget, text, hover_delay=1000):
        """Create a text tooltip with a mouse hover delay.

        anchor_widget: the widget next to which the tooltip will be shown
        hover_delay: time to delay before showing the tooltip, in milliseconds

        Note that a widget will only be shown when showtip() is called,
        e.g. after hovering over the anchor widget with the mouse for enough
        time.
        """
        super(_Hovertip, self).__init__(anchor_widget, hover_delay=hover_delay)
        self.text = text

    def showcontents(self):
        label = tk.Label(
            self.tipwindow,
            text=self.text,
            justify=tk.LEFT,
            background="#ffffe0",
            relief=tk.SOLID,
            borderwidth=1,
        )
        label.pack()


def add_tooltip(widget: tk.Widget, text: str, hold_time: int = 1000) -> None:
    """
    Add a tooltip to a widget using IDLE-style tooltips.

    :param widget: A tk.Widget to attach the tooltip to.
    :param text: The text to attach to the tooltip.
    :param hold_time: How long to hold the cursor over the widget before the
     tooltip pops up.
    :return: None.
    """
    if not isinstance(widget, tk.Widget):
        raise TypeError(
            f"widget is not a tk.Widget! " f"(type passed in: {repr(type(widget))})"
        )
    if not isinstance(text, str):
        raise TypeError(f"text is not a str! " f"(type passed in: {repr(type(text))})")
    if not isinstance(hold_time, int):
        raise TypeError(
            f"hold_time is not a int! " f"(type passed in: {repr(type(hold_time))})"
        )
    _Hovertip(anchor_widget=widget, text=text, hover_delay=hold_time)

Functions

def add_tooltip(widget: tkinter.Widget, text: str, hold_time: int = 1000) ‑> NoneType

Add a tooltip to a widget using IDLE-style tooltips.

:param widget: A tk.Widget to attach the tooltip to. :param text: The text to attach to the tooltip. :param hold_time: How long to hold the cursor over the widget before the tooltip pops up. :return: None.

Expand source code
def add_tooltip(widget: tk.Widget, text: str, hold_time: int = 1000) -> None:
    """
    Add a tooltip to a widget using IDLE-style tooltips.

    :param widget: A tk.Widget to attach the tooltip to.
    :param text: The text to attach to the tooltip.
    :param hold_time: How long to hold the cursor over the widget before the
     tooltip pops up.
    :return: None.
    """
    if not isinstance(widget, tk.Widget):
        raise TypeError(
            f"widget is not a tk.Widget! " f"(type passed in: {repr(type(widget))})"
        )
    if not isinstance(text, str):
        raise TypeError(f"text is not a str! " f"(type passed in: {repr(type(text))})")
    if not isinstance(hold_time, int):
        raise TypeError(
            f"hold_time is not a int! " f"(type passed in: {repr(type(hold_time))})"
        )
    _Hovertip(anchor_widget=widget, text=text, hover_delay=hold_time)