| 
									
										
										
										
											2018-08-05 09:21:08 +03:00
										 |  |  | """Tools for displaying tool-tips.
 | 
					
						
							| 
									
										
										
										
											2000-08-15 01:13:23 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-08-05 09:21:08 +03:00
										 |  |  | This includes: | 
					
						
							|  |  |  |  * an abstract base-class for different kinds of tooltips | 
					
						
							|  |  |  |  * a simple text-only Tooltip class | 
					
						
							|  |  |  | """
 | 
					
						
							| 
									
										
										
										
											2008-05-17 18:39:55 +00:00
										 |  |  | from tkinter import * | 
					
						
							| 
									
										
										
										
											2000-08-15 01:13:23 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-01-24 14:08:50 -05:00
										 |  |  | class TooltipBase: | 
					
						
							| 
									
										
										
										
											2018-08-05 09:21:08 +03:00
										 |  |  |     """abstract base class for tooltips""" | 
					
						
							| 
									
										
										
										
											2000-08-15 01:13:23 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-08-05 09:21:08 +03:00
										 |  |  |     def __init__(self, anchor_widget): | 
					
						
							|  |  |  |         """Create a tooltip.
 | 
					
						
							| 
									
										
										
										
											2000-08-15 01:13:23 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-08-05 09:21:08 +03:00
										 |  |  |         anchor_widget: the widget next to which the tooltip will be shown | 
					
						
							| 
									
										
										
										
											2000-08-15 01:13:23 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-08-05 09:21:08 +03:00
										 |  |  |         Note that a widget will only be shown when showtip() is called. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         self.anchor_widget = anchor_widget | 
					
						
							|  |  |  |         self.tipwindow = None | 
					
						
							| 
									
										
										
										
											2000-08-15 01:13:23 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-08-05 09:21:08 +03:00
										 |  |  |     def __del__(self): | 
					
						
							|  |  |  |         self.hidetip() | 
					
						
							| 
									
										
										
										
											2000-08-15 01:13:23 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  |     def showtip(self): | 
					
						
							| 
									
										
										
										
											2018-08-05 09:21:08 +03:00
										 |  |  |         """display the tooltip""" | 
					
						
							| 
									
										
										
										
											2000-08-15 01:13:23 +00:00
										 |  |  |         if self.tipwindow: | 
					
						
							|  |  |  |             return | 
					
						
							| 
									
										
										
										
											2018-08-05 09:21:08 +03:00
										 |  |  |         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; | 
					
						
							| 
									
										
										
										
											2000-08-15 01:13:23 +00:00
										 |  |  |         # 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 :-( | 
					
						
							| 
									
										
										
										
											2018-08-05 09:21:08 +03:00
										 |  |  |         # | 
					
						
							|  |  |  |         # Note: This is a simplistic implementation; sub-classes will likely | 
					
						
							|  |  |  |         # want to override this. | 
					
						
							|  |  |  |         return 20, self.anchor_widget.winfo_height() + 1 | 
					
						
							| 
									
										
										
										
											2000-08-15 01:13:23 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-08-05 09:21:08 +03:00
										 |  |  |     def showcontents(self): | 
					
						
							|  |  |  |         """content display hook for sub-classes""" | 
					
						
							|  |  |  |         # See ToolTip for an example | 
					
						
							|  |  |  |         raise NotImplementedError | 
					
						
							| 
									
										
										
										
											2000-08-15 01:13:23 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  |     def hidetip(self): | 
					
						
							| 
									
										
										
										
											2018-08-05 09:21:08 +03:00
										 |  |  |         """hide the tooltip""" | 
					
						
							|  |  |  |         # Note: This is called by __del__, so careful when overriding/extending | 
					
						
							| 
									
										
										
										
											2000-08-15 01:13:23 +00:00
										 |  |  |         tw = self.tipwindow | 
					
						
							|  |  |  |         self.tipwindow = None | 
					
						
							|  |  |  |         if tw: | 
					
						
							| 
									
										
										
										
											2018-08-05 09:21:08 +03:00
										 |  |  |             try: | 
					
						
							|  |  |  |                 tw.destroy() | 
					
						
							| 
									
										
										
										
											2019-09-03 08:17:00 +03:00
										 |  |  |             except TclError:  # pragma: no cover | 
					
						
							| 
									
										
										
										
											2018-08-05 09:21:08 +03:00
										 |  |  |                 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 | 
					
						
							| 
									
										
										
										
											2000-08-15 01:13:23 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-08-05 09:21:08 +03:00
										 |  |  |         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. | 
					
						
							|  |  |  |         """
 | 
					
						
							| 
									
										
										
										
											2023-04-24 02:25:08 +03:00
										 |  |  |         super().__init__(anchor_widget) | 
					
						
							| 
									
										
										
										
											2018-08-05 09:21:08 +03:00
										 |  |  |         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) | 
					
						
							| 
									
										
										
										
											2019-09-03 08:17:00 +03:00
										 |  |  |             self.anchor_widget.unbind("<Leave>", self._id2)  # pragma: no cover | 
					
						
							|  |  |  |             self.anchor_widget.unbind("<Button>", self._id3) # pragma: no cover | 
					
						
							| 
									
										
										
										
											2018-08-05 09:21:08 +03:00
										 |  |  |         except TclError: | 
					
						
							|  |  |  |             pass | 
					
						
							| 
									
										
										
										
											2023-04-24 02:25:08 +03:00
										 |  |  |         super().__del__() | 
					
						
							| 
									
										
										
										
											2018-08-05 09:21:08 +03:00
										 |  |  | 
 | 
					
						
							|  |  |  |     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() | 
					
						
							| 
									
										
										
										
											2019-09-03 08:17:00 +03:00
										 |  |  |         except TclError:  # pragma: no cover | 
					
						
							| 
									
										
										
										
											2018-08-05 09:21:08 +03:00
										 |  |  |             pass | 
					
						
							| 
									
										
										
										
											2023-04-24 02:25:08 +03:00
										 |  |  |         super().hidetip() | 
					
						
							| 
									
										
										
										
											2018-08-05 09:21:08 +03:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 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. | 
					
						
							|  |  |  |         """
 | 
					
						
							| 
									
										
										
										
											2023-04-24 02:25:08 +03:00
										 |  |  |         super().__init__(anchor_widget, hover_delay=hover_delay) | 
					
						
							| 
									
										
										
										
											2000-08-15 01:13:23 +00:00
										 |  |  |         self.text = text | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def showcontents(self): | 
					
						
							| 
									
										
										
										
											2018-08-05 09:21:08 +03:00
										 |  |  |         label = Label(self.tipwindow, text=self.text, justify=LEFT, | 
					
						
							|  |  |  |                       background="#ffffe0", relief=SOLID, borderwidth=1) | 
					
						
							|  |  |  |         label.pack() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2000-08-15 01:13:23 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-05-28 13:22:31 -04:00
										 |  |  | def _tooltip(parent):  # htest # | 
					
						
							| 
									
										
										
										
											2016-07-10 17:28:10 -04:00
										 |  |  |     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") | 
					
						
							| 
									
										
										
										
											2014-05-24 18:48:18 -04:00
										 |  |  |     label.pack() | 
					
						
							| 
									
										
										
										
											2018-08-05 09:21:08 +03:00
										 |  |  |     button1 = Button(top, text="Button 1 -- 1/2 second hover delay") | 
					
						
							| 
									
										
										
										
											2014-05-24 18:48:18 -04:00
										 |  |  |     button1.pack() | 
					
						
							| 
									
										
										
										
											2018-08-05 09:21:08 +03:00
										 |  |  |     Hovertip(button1, "This is tooltip text for button1.", hover_delay=500) | 
					
						
							|  |  |  |     button2 = Button(top, text="Button 2 -- no hover delay") | 
					
						
							| 
									
										
										
										
											2014-05-24 18:48:18 -04:00
										 |  |  |     button2.pack() | 
					
						
							| 
									
										
										
										
											2018-08-05 09:21:08 +03:00
										 |  |  |     Hovertip(button2, "This is tooltip\ntext for button2.", hover_delay=None) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2001-07-14 01:14:09 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2005-11-23 15:12:19 +00:00
										 |  |  | if __name__ == '__main__': | 
					
						
							| 
									
										
										
										
											2018-08-05 09:21:08 +03:00
										 |  |  |     from unittest import main | 
					
						
							|  |  |  |     main('idlelib.idle_test.test_tooltip', verbosity=2, exit=False) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2014-05-24 18:48:18 -04:00
										 |  |  |     from idlelib.idle_test.htest import run | 
					
						
							|  |  |  |     run(_tooltip) |