tooltip.py 6.33 KB
Newer Older
1
"""Tools for displaying tool-tips.
David Scherer's avatar
David Scherer committed
2

3 4 5 6
This includes:
 * an abstract base-class for different kinds of tooltips
 * a simple text-only Tooltip class
"""
7
from tkinter import *
David Scherer's avatar
David Scherer committed
8 9


10 11
class TooltipBase(object):
    """abstract base class for tooltips"""
David Scherer's avatar
David Scherer committed
12

13 14
    def __init__(self, anchor_widget):
        """Create a tooltip.
David Scherer's avatar
David Scherer committed
15

16
        anchor_widget: the widget next to which the tooltip will be shown
David Scherer's avatar
David Scherer committed
17

18 19 20 21
        Note that a widget will only be shown when showtip() is called.
        """
        self.anchor_widget = anchor_widget
        self.tipwindow = None
David Scherer's avatar
David Scherer committed
22

23 24
    def __del__(self):
        self.hidetip()
David Scherer's avatar
David Scherer committed
25 26

    def showtip(self):
27
        """display the tooltip"""
David Scherer's avatar
David Scherer committed
28 29
        if self.tipwindow:
            return
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
        self.tipwindow = tw = 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 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;
David Scherer's avatar
David Scherer committed
57 58 59
        # 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 :-(
60 61 62 63
        #
        # Note: This is a simplistic implementation; sub-classes will likely
        # want to override this.
        return 20, self.anchor_widget.winfo_height() + 1
David Scherer's avatar
David Scherer committed
64

65 66 67 68
    def showcontents(self):
        """content display hook for sub-classes"""
        # See ToolTip for an example
        raise NotImplementedError
David Scherer's avatar
David Scherer committed
69 70

    def hidetip(self):
71 72
        """hide the tooltip"""
        # Note: This is called by __del__, so careful when overriding/extending
David Scherer's avatar
David Scherer committed
73 74 75
        tw = self.tipwindow
        self.tipwindow = None
        if tw:
76 77 78 79 80 81 82 83 84 85 86 87 88 89
            try:
                tw.destroy()
            except TclError:
                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
David Scherer's avatar
David Scherer committed
90

91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157
        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)
            self.anchor_widget.unbind("<Button>", self._id3)
        except 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 TclError:
            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)
David Scherer's avatar
David Scherer committed
158 159 160
        self.text = text

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

David Scherer's avatar
David Scherer committed
165

166
def _tooltip(parent):  # htest #
167 168 169 170 171
    top = Toplevel(parent)
    top.title("Test tooltip")
    x, y = map(int, parent.geometry().split('+')[1:])
    top.geometry("+%d+%d" % (x, y + 150))
    label = Label(top, text="Place your mouse over buttons")
172
    label.pack()
173
    button1 = Button(top, text="Button 1 -- 1/2 second hover delay")
174
    button1.pack()
175 176
    Hovertip(button1, "This is tooltip text for button1.", hover_delay=500)
    button2 = Button(top, text="Button 2 -- no hover delay")
177
    button2.pack()
178 179
    Hovertip(button2, "This is tooltip\ntext for button2.", hover_delay=None)

180

Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
181
if __name__ == '__main__':
182 183 184
    from unittest import main
    main('idlelib.idle_test.test_tooltip', verbosity=2, exit=False)

185 186
    from idlelib.idle_test.htest import run
    run(_tooltip)