codekingpro/portable-devtools
114k
1"""Tools for displaying tool-tips.2 3This includes:4 * an abstract base-class for different kinds of tooltips5 * a simple text-only Tooltip class6"""7from tkinter import *8 9 10class TooltipBase: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 shown17 18 Note that a widget will only be shown when showtip() is called.19 """20 self.anchor_widget = anchor_widget21 self.tipwindow = None22 23 def __del__(self):24 self.hidetip()25 26 def showtip(self):27 """display the tooltip"""28 if self.tipwindow:29 return30 self.tipwindow = tw = Toplevel(self.anchor_widget)31 # show no border on the top level window32 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 grabbing36 # the focus.37 tw.tk.call("::tk::unsupported::MacWindowStyle", "style", tw._w,38 "help", "noActivates")39 except TclError:40 pass41 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() + x51 root_y = self.anchor_widget.winfo_rooty() + y52 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 get58 # a leave event and it disappears, and then we get an enter59 # event and it reappears, and so on forever :-(60 #61 # Note: This is a simplistic implementation; sub-classes will likely62 # want to override this.63 return 20, self.anchor_widget.winfo_height() + 164 65 def showcontents(self):66 """content display hook for sub-classes"""67 # See ToolTip for an example68 raise NotImplementedError69 70 def hidetip(self):71 """hide the tooltip"""72 # Note: This is called by __del__, so careful when overriding/extending73 tw = self.tipwindow74 self.tipwindow = None75 if tw:76 try:77 tw.destroy()78 except TclError: # pragma: no cover79 pass80 81 82class 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 shown89 hover_delay: time to delay before showing the tooltip, in milliseconds90 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 enough93 time.94 """95 super().__init__(anchor_widget)96 self.hover_delay = hover_delay97 98 self._after_id = None99 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) # pragma: no cover107 self.anchor_widget.unbind("<Button>", self._id3) # pragma: no cover108 except TclError:109 pass110 super().__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_id132 self._after_id = None133 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: # pragma: no cover141 pass142 super().hidetip()143 144 145class 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 foreground="#000000", background="#ffffe0"):149 """Create a text tooltip with a mouse hover delay.150 151 anchor_widget: the widget next to which the tooltip will be shown152 hover_delay: time to delay before showing the tooltip, in milliseconds153 154 Note that a widget will only be shown when showtip() is called,155 e.g. after hovering over the anchor widget with the mouse for enough156 time.157 """158 super().__init__(anchor_widget, hover_delay=hover_delay)159 self.text = text160 self.foreground = foreground161 self.background = background162 163 def showcontents(self):164 label = Label(self.tipwindow, text=self.text, justify=LEFT,165 relief=SOLID, borderwidth=1,166 foreground=self.foreground, background=self.background)167 label.pack()168 169 170def _tooltip(parent): # htest #171 top = Toplevel(parent)172 top.title("Test tooltip")173 x, y = map(int, parent.geometry().split('+')[1:])174 top.geometry("+%d+%d" % (x, y + 150))175 label = Label(top, text="Place your mouse over buttons")176 label.pack()177 button1 = Button(top, text="Button 1 -- 1/2 second hover delay")178 button1.pack()179 Hovertip(button1, "This is tooltip text for button1.", hover_delay=500)180 button2 = Button(top, text="Button 2 -- no hover delay")181 button2.pack()182 Hovertip(button2, "This is tooltip\ntext for button2.", hover_delay=None)183 184 185if __name__ == '__main__':186 from unittest import main187 main('idlelib.idle_test.test_tooltip', verbosity=2, exit=False)188 189 from idlelib.idle_test.htest import run190 run(_tooltip)191 