tooltip.py 6.3 KB

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