Miss Islington (bot) | e65ec49 | 2018-08-04 23:47:28 -0700 | [diff] [blame^] | 1 | """Tools for displaying tool-tips. |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 2 | |
Miss Islington (bot) | e65ec49 | 2018-08-04 23:47:28 -0700 | [diff] [blame^] | 3 | This includes: |
| 4 | * an abstract base-class for different kinds of tooltips |
| 5 | * a simple text-only Tooltip class |
| 6 | """ |
Georg Brandl | 14fc427 | 2008-05-17 18:39:55 +0000 | [diff] [blame] | 7 | from tkinter import * |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 8 | |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 9 | |
Miss Islington (bot) | e65ec49 | 2018-08-04 23:47:28 -0700 | [diff] [blame^] | 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 |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 21 | self.tipwindow = None |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 22 | |
Miss Islington (bot) | e65ec49 | 2018-08-04 23:47:28 -0700 | [diff] [blame^] | 23 | def __del__(self): |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 24 | self.hidetip() |
| 25 | |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 26 | def showtip(self): |
Miss Islington (bot) | e65ec49 | 2018-08-04 23:47:28 -0700 | [diff] [blame^] | 27 | """display the tooltip""" |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 28 | if self.tipwindow: |
| 29 | return |
Miss Islington (bot) | e65ec49 | 2018-08-04 23:47:28 -0700 | [diff] [blame^] | 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; |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 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 :-( |
Miss Islington (bot) | e65ec49 | 2018-08-04 23:47:28 -0700 | [diff] [blame^] | 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 |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 64 | |
Miss Islington (bot) | e65ec49 | 2018-08-04 23:47:28 -0700 | [diff] [blame^] | 65 | def showcontents(self): |
| 66 | """content display hook for sub-classes""" |
| 67 | # See ToolTip for an example |
| 68 | raise NotImplementedError |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 69 | |
| 70 | def hidetip(self): |
Miss Islington (bot) | e65ec49 | 2018-08-04 23:47:28 -0700 | [diff] [blame^] | 71 | """hide the tooltip""" |
| 72 | # Note: This is called by __del__, so careful when overriding/extending |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 73 | tw = self.tipwindow |
| 74 | self.tipwindow = None |
| 75 | if tw: |
Miss Islington (bot) | e65ec49 | 2018-08-04 23:47:28 -0700 | [diff] [blame^] | 76 | try: |
| 77 | tw.destroy() |
| 78 | except TclError: |
| 79 | pass |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 80 | |
Miss Islington (bot) | e65ec49 | 2018-08-04 23:47:28 -0700 | [diff] [blame^] | 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) |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 158 | self.text = text |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 159 | |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 160 | def showcontents(self): |
Miss Islington (bot) | e65ec49 | 2018-08-04 23:47:28 -0700 | [diff] [blame^] | 161 | label = Label(self.tipwindow, text=self.text, justify=LEFT, |
| 162 | background="#ffffe0", relief=SOLID, borderwidth=1) |
| 163 | label.pack() |
| 164 | |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 165 | |
Terry Jan Reedy | 6fa5bdc | 2016-05-28 13:22:31 -0400 | [diff] [blame] | 166 | def _tooltip(parent): # htest # |
Terry Jan Reedy | a748032 | 2016-07-10 17:28:10 -0400 | [diff] [blame] | 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") |
Terry Jan Reedy | 1b392ff | 2014-05-24 18:48:18 -0400 | [diff] [blame] | 172 | label.pack() |
Miss Islington (bot) | e65ec49 | 2018-08-04 23:47:28 -0700 | [diff] [blame^] | 173 | button1 = Button(top, text="Button 1 -- 1/2 second hover delay") |
Terry Jan Reedy | 1b392ff | 2014-05-24 18:48:18 -0400 | [diff] [blame] | 174 | button1.pack() |
Miss Islington (bot) | e65ec49 | 2018-08-04 23:47:28 -0700 | [diff] [blame^] | 175 | Hovertip(button1, "This is tooltip text for button1.", hover_delay=500) |
| 176 | button2 = Button(top, text="Button 2 -- no hover delay") |
Terry Jan Reedy | 1b392ff | 2014-05-24 18:48:18 -0400 | [diff] [blame] | 177 | button2.pack() |
Miss Islington (bot) | e65ec49 | 2018-08-04 23:47:28 -0700 | [diff] [blame^] | 178 | Hovertip(button2, "This is tooltip\ntext for button2.", hover_delay=None) |
| 179 | |
Kurt B. Kaiser | d5338a8 | 2001-07-14 01:14:09 +0000 | [diff] [blame] | 180 | |
Kurt B. Kaiser | 9df23ea | 2005-11-23 15:12:19 +0000 | [diff] [blame] | 181 | if __name__ == '__main__': |
Miss Islington (bot) | e65ec49 | 2018-08-04 23:47:28 -0700 | [diff] [blame^] | 182 | from unittest import main |
| 183 | main('idlelib.idle_test.test_tooltip', verbosity=2, exit=False) |
| 184 | |
Terry Jan Reedy | 1b392ff | 2014-05-24 18:48:18 -0400 | [diff] [blame] | 185 | from idlelib.idle_test.htest import run |
| 186 | run(_tooltip) |