Home | History | Annotate | Download | only in idlelib
      1 """Tools for displaying tool-tips.
      2 
      3 This includes:
      4  * an abstract base-class for different kinds of tooltips
      5  * a simple text-only Tooltip class
      6 """
      7 from tkinter import *
      8 
      9 
     10 class TooltipBase(object):
     11     """abstract base class for tooltips"""
     12 
     13     def __init__(self, anchor_widget):
     14         """Create a tooltip.
     15 
     16         anchor_widget: the widget next to which the tooltip will be shown
     17 
     18         Note that a widget will only be shown when showtip() is called.
     19         """
     20         self.anchor_widget = anchor_widget
     21         self.tipwindow = None
     22 
     23     def __del__(self):
     24         self.hidetip()
     25 
     26     def showtip(self):
     27         """display the tooltip"""
     28         if self.tipwindow:
     29             return
     30         self.tipwindow = tw = Toplevel(self.anchor_widget)
     31         # show no border on the top level window
     32         tw.wm_overrideredirect(1)
     33         try:
     34             # This command is only needed and available on Tk >= 8.4.0 for OSX.
     35             # Without it, call tips intrude on the typing process by grabbing
     36             # the focus.
     37             tw.tk.call("::tk::unsupported::MacWindowStyle", "style", tw._w,
     38                        "help", "noActivates")
     39         except TclError:
     40             pass
     41 
     42         self.position_window()
     43         self.showcontents()
     44         self.tipwindow.update_idletasks()  # Needed on MacOS -- see #34275.
     45         self.tipwindow.lift()  # work around bug in Tk 8.5.18+ (issue #24570)
     46 
     47     def position_window(self):
     48         """(re)-set the tooltip's screen position"""
     49         x, y = self.get_position()
     50         root_x = self.anchor_widget.winfo_rootx() + x
     51         root_y = self.anchor_widget.winfo_rooty() + y
     52         self.tipwindow.wm_geometry("+%d+%d" % (root_x, root_y))
     53 
     54     def get_position(self):
     55         """choose a screen position for the tooltip"""
     56         # The tip window must be completely outside the anchor widget;
     57         # otherwise when the mouse enters the tip window we get
     58         # a leave event and it disappears, and then we get an enter
     59         # event and it reappears, and so on forever :-(
     60         #
     61         # Note: This is a simplistic implementation; sub-classes will likely
     62         # want to override this.
     63         return 20, self.anchor_widget.winfo_height() + 1
     64 
     65     def showcontents(self):
     66         """content display hook for sub-classes"""
     67         # See ToolTip for an example
     68         raise NotImplementedError
     69 
     70     def hidetip(self):
     71         """hide the tooltip"""
     72         # Note: This is called by __del__, so careful when overriding/extending
     73         tw = self.tipwindow
     74         self.tipwindow = None
     75         if tw:
     76             try:
     77                 tw.destroy()
     78             except TclError:
     79                 pass
     80 
     81 
     82 class OnHoverTooltipBase(TooltipBase):
     83     """abstract base class for tooltips, with delayed on-hover display"""
     84 
     85     def __init__(self, anchor_widget, hover_delay=1000):
     86         """Create a tooltip with a mouse hover delay.
     87 
     88         anchor_widget: the widget next to which the tooltip will be shown
     89         hover_delay: time to delay before showing the tooltip, in milliseconds
     90 
     91         Note that a widget will only be shown when showtip() is called,
     92         e.g. after hovering over the anchor widget with the mouse for enough
     93         time.
     94         """
     95         super(OnHoverTooltipBase, self).__init__(anchor_widget)
     96         self.hover_delay = hover_delay
     97 
     98         self._after_id = None
     99         self._id1 = self.anchor_widget.bind("<Enter>", self._show_event)
    100         self._id2 = self.anchor_widget.bind("<Leave>", self._hide_event)
    101         self._id3 = self.anchor_widget.bind("<Button>", self._hide_event)
    102 
    103     def __del__(self):
    104         try:
    105             self.anchor_widget.unbind("<Enter>", self._id1)
    106             self.anchor_widget.unbind("<Leave>", self._id2)
    107             self.anchor_widget.unbind("<Button>", self._id3)
    108         except TclError:
    109             pass
    110         super(OnHoverTooltipBase, self).__del__()
    111 
    112     def _show_event(self, event=None):
    113         """event handler to display the tooltip"""
    114         if self.hover_delay:
    115             self.schedule()
    116         else:
    117             self.showtip()
    118 
    119     def _hide_event(self, event=None):
    120         """event handler to hide the tooltip"""
    121         self.hidetip()
    122 
    123     def schedule(self):
    124         """schedule the future display of the tooltip"""
    125         self.unschedule()
    126         self._after_id = self.anchor_widget.after(self.hover_delay,
    127                                                   self.showtip)
    128 
    129     def unschedule(self):
    130         """cancel the future display of the tooltip"""
    131         after_id = self._after_id
    132         self._after_id = None
    133         if after_id:
    134             self.anchor_widget.after_cancel(after_id)
    135 
    136     def hidetip(self):
    137         """hide the tooltip"""
    138         try:
    139             self.unschedule()
    140         except TclError:
    141             pass
    142         super(OnHoverTooltipBase, self).hidetip()
    143 
    144 
    145 class Hovertip(OnHoverTooltipBase):
    146     "A tooltip that pops up when a mouse hovers over an anchor widget."
    147     def __init__(self, anchor_widget, text, hover_delay=1000):
    148         """Create a text tooltip with a mouse hover delay.
    149 
    150         anchor_widget: the widget next to which the tooltip will be shown
    151         hover_delay: time to delay before showing the tooltip, in milliseconds
    152 
    153         Note that a widget will only be shown when showtip() is called,
    154         e.g. after hovering over the anchor widget with the mouse for enough
    155         time.
    156         """
    157         super(Hovertip, self).__init__(anchor_widget, hover_delay=hover_delay)
    158         self.text = text
    159 
    160     def showcontents(self):
    161         label = Label(self.tipwindow, text=self.text, justify=LEFT,
    162                       background="#ffffe0", relief=SOLID, borderwidth=1)
    163         label.pack()
    164 
    165 
    166 def _tooltip(parent):  # htest #
    167     top = Toplevel(parent)
    168     top.title("Test tooltip")
    169     x, y = map(int, parent.geometry().split('+')[1:])
    170     top.geometry("+%d+%d" % (x, y + 150))
    171     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     Hovertip(button1, "This is tooltip text for button1.", hover_delay=500)
    176     button2 = Button(top, text="Button 2 -- no hover delay")
    177     button2.pack()
    178     Hovertip(button2, "This is tooltip\ntext for button2.", hover_delay=None)
    179 
    180 
    181 if __name__ == '__main__':
    182     from unittest import main
    183     main('idlelib.idle_test.test_tooltip', verbosity=2, exit=False)
    184 
    185     from idlelib.idle_test.htest import run
    186     run(_tooltip)
    187