17db96d56Sopenharmony_ci"""Tools for displaying tool-tips. 27db96d56Sopenharmony_ci 37db96d56Sopenharmony_ciThis includes: 47db96d56Sopenharmony_ci * an abstract base-class for different kinds of tooltips 57db96d56Sopenharmony_ci * a simple text-only Tooltip class 67db96d56Sopenharmony_ci""" 77db96d56Sopenharmony_cifrom tkinter import * 87db96d56Sopenharmony_ci 97db96d56Sopenharmony_ci 107db96d56Sopenharmony_ciclass TooltipBase: 117db96d56Sopenharmony_ci """abstract base class for tooltips""" 127db96d56Sopenharmony_ci 137db96d56Sopenharmony_ci def __init__(self, anchor_widget): 147db96d56Sopenharmony_ci """Create a tooltip. 157db96d56Sopenharmony_ci 167db96d56Sopenharmony_ci anchor_widget: the widget next to which the tooltip will be shown 177db96d56Sopenharmony_ci 187db96d56Sopenharmony_ci Note that a widget will only be shown when showtip() is called. 197db96d56Sopenharmony_ci """ 207db96d56Sopenharmony_ci self.anchor_widget = anchor_widget 217db96d56Sopenharmony_ci self.tipwindow = None 227db96d56Sopenharmony_ci 237db96d56Sopenharmony_ci def __del__(self): 247db96d56Sopenharmony_ci self.hidetip() 257db96d56Sopenharmony_ci 267db96d56Sopenharmony_ci def showtip(self): 277db96d56Sopenharmony_ci """display the tooltip""" 287db96d56Sopenharmony_ci if self.tipwindow: 297db96d56Sopenharmony_ci return 307db96d56Sopenharmony_ci self.tipwindow = tw = Toplevel(self.anchor_widget) 317db96d56Sopenharmony_ci # show no border on the top level window 327db96d56Sopenharmony_ci tw.wm_overrideredirect(1) 337db96d56Sopenharmony_ci try: 347db96d56Sopenharmony_ci # This command is only needed and available on Tk >= 8.4.0 for OSX. 357db96d56Sopenharmony_ci # Without it, call tips intrude on the typing process by grabbing 367db96d56Sopenharmony_ci # the focus. 377db96d56Sopenharmony_ci tw.tk.call("::tk::unsupported::MacWindowStyle", "style", tw._w, 387db96d56Sopenharmony_ci "help", "noActivates") 397db96d56Sopenharmony_ci except TclError: 407db96d56Sopenharmony_ci pass 417db96d56Sopenharmony_ci 427db96d56Sopenharmony_ci self.position_window() 437db96d56Sopenharmony_ci self.showcontents() 447db96d56Sopenharmony_ci self.tipwindow.update_idletasks() # Needed on MacOS -- see #34275. 457db96d56Sopenharmony_ci self.tipwindow.lift() # work around bug in Tk 8.5.18+ (issue #24570) 467db96d56Sopenharmony_ci 477db96d56Sopenharmony_ci def position_window(self): 487db96d56Sopenharmony_ci """(re)-set the tooltip's screen position""" 497db96d56Sopenharmony_ci x, y = self.get_position() 507db96d56Sopenharmony_ci root_x = self.anchor_widget.winfo_rootx() + x 517db96d56Sopenharmony_ci root_y = self.anchor_widget.winfo_rooty() + y 527db96d56Sopenharmony_ci self.tipwindow.wm_geometry("+%d+%d" % (root_x, root_y)) 537db96d56Sopenharmony_ci 547db96d56Sopenharmony_ci def get_position(self): 557db96d56Sopenharmony_ci """choose a screen position for the tooltip""" 567db96d56Sopenharmony_ci # The tip window must be completely outside the anchor widget; 577db96d56Sopenharmony_ci # otherwise when the mouse enters the tip window we get 587db96d56Sopenharmony_ci # a leave event and it disappears, and then we get an enter 597db96d56Sopenharmony_ci # event and it reappears, and so on forever :-( 607db96d56Sopenharmony_ci # 617db96d56Sopenharmony_ci # Note: This is a simplistic implementation; sub-classes will likely 627db96d56Sopenharmony_ci # want to override this. 637db96d56Sopenharmony_ci return 20, self.anchor_widget.winfo_height() + 1 647db96d56Sopenharmony_ci 657db96d56Sopenharmony_ci def showcontents(self): 667db96d56Sopenharmony_ci """content display hook for sub-classes""" 677db96d56Sopenharmony_ci # See ToolTip for an example 687db96d56Sopenharmony_ci raise NotImplementedError 697db96d56Sopenharmony_ci 707db96d56Sopenharmony_ci def hidetip(self): 717db96d56Sopenharmony_ci """hide the tooltip""" 727db96d56Sopenharmony_ci # Note: This is called by __del__, so careful when overriding/extending 737db96d56Sopenharmony_ci tw = self.tipwindow 747db96d56Sopenharmony_ci self.tipwindow = None 757db96d56Sopenharmony_ci if tw: 767db96d56Sopenharmony_ci try: 777db96d56Sopenharmony_ci tw.destroy() 787db96d56Sopenharmony_ci except TclError: # pragma: no cover 797db96d56Sopenharmony_ci pass 807db96d56Sopenharmony_ci 817db96d56Sopenharmony_ci 827db96d56Sopenharmony_ciclass OnHoverTooltipBase(TooltipBase): 837db96d56Sopenharmony_ci """abstract base class for tooltips, with delayed on-hover display""" 847db96d56Sopenharmony_ci 857db96d56Sopenharmony_ci def __init__(self, anchor_widget, hover_delay=1000): 867db96d56Sopenharmony_ci """Create a tooltip with a mouse hover delay. 877db96d56Sopenharmony_ci 887db96d56Sopenharmony_ci anchor_widget: the widget next to which the tooltip will be shown 897db96d56Sopenharmony_ci hover_delay: time to delay before showing the tooltip, in milliseconds 907db96d56Sopenharmony_ci 917db96d56Sopenharmony_ci Note that a widget will only be shown when showtip() is called, 927db96d56Sopenharmony_ci e.g. after hovering over the anchor widget with the mouse for enough 937db96d56Sopenharmony_ci time. 947db96d56Sopenharmony_ci """ 957db96d56Sopenharmony_ci super().__init__(anchor_widget) 967db96d56Sopenharmony_ci self.hover_delay = hover_delay 977db96d56Sopenharmony_ci 987db96d56Sopenharmony_ci self._after_id = None 997db96d56Sopenharmony_ci self._id1 = self.anchor_widget.bind("<Enter>", self._show_event) 1007db96d56Sopenharmony_ci self._id2 = self.anchor_widget.bind("<Leave>", self._hide_event) 1017db96d56Sopenharmony_ci self._id3 = self.anchor_widget.bind("<Button>", self._hide_event) 1027db96d56Sopenharmony_ci 1037db96d56Sopenharmony_ci def __del__(self): 1047db96d56Sopenharmony_ci try: 1057db96d56Sopenharmony_ci self.anchor_widget.unbind("<Enter>", self._id1) 1067db96d56Sopenharmony_ci self.anchor_widget.unbind("<Leave>", self._id2) # pragma: no cover 1077db96d56Sopenharmony_ci self.anchor_widget.unbind("<Button>", self._id3) # pragma: no cover 1087db96d56Sopenharmony_ci except TclError: 1097db96d56Sopenharmony_ci pass 1107db96d56Sopenharmony_ci super().__del__() 1117db96d56Sopenharmony_ci 1127db96d56Sopenharmony_ci def _show_event(self, event=None): 1137db96d56Sopenharmony_ci """event handler to display the tooltip""" 1147db96d56Sopenharmony_ci if self.hover_delay: 1157db96d56Sopenharmony_ci self.schedule() 1167db96d56Sopenharmony_ci else: 1177db96d56Sopenharmony_ci self.showtip() 1187db96d56Sopenharmony_ci 1197db96d56Sopenharmony_ci def _hide_event(self, event=None): 1207db96d56Sopenharmony_ci """event handler to hide the tooltip""" 1217db96d56Sopenharmony_ci self.hidetip() 1227db96d56Sopenharmony_ci 1237db96d56Sopenharmony_ci def schedule(self): 1247db96d56Sopenharmony_ci """schedule the future display of the tooltip""" 1257db96d56Sopenharmony_ci self.unschedule() 1267db96d56Sopenharmony_ci self._after_id = self.anchor_widget.after(self.hover_delay, 1277db96d56Sopenharmony_ci self.showtip) 1287db96d56Sopenharmony_ci 1297db96d56Sopenharmony_ci def unschedule(self): 1307db96d56Sopenharmony_ci """cancel the future display of the tooltip""" 1317db96d56Sopenharmony_ci after_id = self._after_id 1327db96d56Sopenharmony_ci self._after_id = None 1337db96d56Sopenharmony_ci if after_id: 1347db96d56Sopenharmony_ci self.anchor_widget.after_cancel(after_id) 1357db96d56Sopenharmony_ci 1367db96d56Sopenharmony_ci def hidetip(self): 1377db96d56Sopenharmony_ci """hide the tooltip""" 1387db96d56Sopenharmony_ci try: 1397db96d56Sopenharmony_ci self.unschedule() 1407db96d56Sopenharmony_ci except TclError: # pragma: no cover 1417db96d56Sopenharmony_ci pass 1427db96d56Sopenharmony_ci super().hidetip() 1437db96d56Sopenharmony_ci 1447db96d56Sopenharmony_ci 1457db96d56Sopenharmony_ciclass Hovertip(OnHoverTooltipBase): 1467db96d56Sopenharmony_ci "A tooltip that pops up when a mouse hovers over an anchor widget." 1477db96d56Sopenharmony_ci def __init__(self, anchor_widget, text, hover_delay=1000): 1487db96d56Sopenharmony_ci """Create a text tooltip with a mouse hover delay. 1497db96d56Sopenharmony_ci 1507db96d56Sopenharmony_ci anchor_widget: the widget next to which the tooltip will be shown 1517db96d56Sopenharmony_ci hover_delay: time to delay before showing the tooltip, in milliseconds 1527db96d56Sopenharmony_ci 1537db96d56Sopenharmony_ci Note that a widget will only be shown when showtip() is called, 1547db96d56Sopenharmony_ci e.g. after hovering over the anchor widget with the mouse for enough 1557db96d56Sopenharmony_ci time. 1567db96d56Sopenharmony_ci """ 1577db96d56Sopenharmony_ci super().__init__(anchor_widget, hover_delay=hover_delay) 1587db96d56Sopenharmony_ci self.text = text 1597db96d56Sopenharmony_ci 1607db96d56Sopenharmony_ci def showcontents(self): 1617db96d56Sopenharmony_ci label = Label(self.tipwindow, text=self.text, justify=LEFT, 1627db96d56Sopenharmony_ci background="#ffffe0", relief=SOLID, borderwidth=1) 1637db96d56Sopenharmony_ci label.pack() 1647db96d56Sopenharmony_ci 1657db96d56Sopenharmony_ci 1667db96d56Sopenharmony_cidef _tooltip(parent): # htest # 1677db96d56Sopenharmony_ci top = Toplevel(parent) 1687db96d56Sopenharmony_ci top.title("Test tooltip") 1697db96d56Sopenharmony_ci x, y = map(int, parent.geometry().split('+')[1:]) 1707db96d56Sopenharmony_ci top.geometry("+%d+%d" % (x, y + 150)) 1717db96d56Sopenharmony_ci label = Label(top, text="Place your mouse over buttons") 1727db96d56Sopenharmony_ci label.pack() 1737db96d56Sopenharmony_ci button1 = Button(top, text="Button 1 -- 1/2 second hover delay") 1747db96d56Sopenharmony_ci button1.pack() 1757db96d56Sopenharmony_ci Hovertip(button1, "This is tooltip text for button1.", hover_delay=500) 1767db96d56Sopenharmony_ci button2 = Button(top, text="Button 2 -- no hover delay") 1777db96d56Sopenharmony_ci button2.pack() 1787db96d56Sopenharmony_ci Hovertip(button2, "This is tooltip\ntext for button2.", hover_delay=None) 1797db96d56Sopenharmony_ci 1807db96d56Sopenharmony_ci 1817db96d56Sopenharmony_ciif __name__ == '__main__': 1827db96d56Sopenharmony_ci from unittest import main 1837db96d56Sopenharmony_ci main('idlelib.idle_test.test_tooltip', verbosity=2, exit=False) 1847db96d56Sopenharmony_ci 1857db96d56Sopenharmony_ci from idlelib.idle_test.htest import run 1867db96d56Sopenharmony_ci run(_tooltip) 187