tooltip.py 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. #! /usr/bin/python3
  2. # -*- coding:Utf-8 -*-
  3. """
  4. tkcalendar - System tray unread mail checker
  5. Copyright 2016-2018 Juliette Monsel <j_4321@protonmail.com>
  6. tkcalendar is free software: you can redistribute it and/or modify
  7. it under the terms of the GNU General Public License as published by
  8. the Free Software Foundation, either version 3 of the License, or
  9. (at your option) any later version.
  10. tkcalendar is distributed in the hope that it will be useful,
  11. but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. GNU General Public License for more details.
  14. You should have received a copy of the GNU General Public License
  15. along with this program. If not, see <http://www.gnu.org/licenses/>.
  16. Tooltip and TooltipWrapper
  17. """
  18. from sys import platform
  19. try:
  20. import tkinter as tk
  21. from tkinter import ttk
  22. except ImportError:
  23. import Tkinter as tk
  24. import ttk
  25. class Tooltip(tk.Toplevel):
  26. """Tooltip widget displays a ttk.Label in a Toplevel without window decoration."""
  27. _initialized = False
  28. def __init__(self, parent, **kwargs):
  29. """
  30. Construct a Tooltip with parent master.
  31. Keyword Options
  32. ---------------
  33. ttk.Label options,
  34. alpha: float. Tooltip opacity between 0 and 1.
  35. """
  36. tk.Toplevel.__init__(self, parent, padx=0, pady=0)
  37. self.transient(parent)
  38. self.overrideredirect(True)
  39. self.update_idletasks()
  40. self.attributes('-alpha', kwargs.pop('alpha', 0.8))
  41. if platform == 'linux':
  42. self.attributes('-type', 'tooltip')
  43. if not Tooltip._initialized:
  44. # default tooltip style
  45. style = ttk.Style(self)
  46. style.configure('tooltip.TLabel',
  47. foreground='gray90',
  48. background='black',
  49. font='TkDefaultFont 9 bold')
  50. Tooltip._initialized = True
  51. # default options
  52. kw = {'compound': 'left', 'style': 'tooltip.TLabel', 'padding': 4}
  53. # update with given options
  54. kw.update(kwargs)
  55. self.label = ttk.Label(self, **kw)
  56. self.label.pack(fill='both')
  57. self.config = self.configure
  58. def __setitem__(self, key, value):
  59. self.configure(**{key: value})
  60. def __getitem__(self, key):
  61. return self.cget(key)
  62. def cget(self, key):
  63. if key == 'alpha':
  64. return self.attributes('-alpha')
  65. else:
  66. return self.label.cget(key)
  67. def configure(self, **kwargs):
  68. if 'alpha' in kwargs:
  69. self.attributes('-alpha', kwargs.pop('alpha'))
  70. self.label.configure(**kwargs)
  71. def keys(self):
  72. keys = list(self.label.keys())
  73. keys.insert(0, 'alpha')
  74. return keys
  75. class TooltipWrapper:
  76. """
  77. Tooltip wrapper widget handle tooltip display when the mouse hovers over
  78. widgets.
  79. """
  80. def __init__(self, master, **kwargs):
  81. """
  82. Construct a Tooltip wrapper with parent master.
  83. Keyword Options
  84. ---------------
  85. Tooltip options,
  86. delay: time (ms) the mouse has to stay still over the widget before
  87. the Tooltip is displayed.
  88. """
  89. self.widgets = {} # {widget name: tooltip text, ...}
  90. # keep track of binding ids to cleanly remove them
  91. self.bind_enter_ids = {} # {widget name: bind id, ...}
  92. self.bind_leave_ids = {} # {widget name: bind id, ...}
  93. # time delay before displaying the tooltip
  94. self._delay = 2000
  95. self._timer_id = None
  96. self.tooltip = Tooltip(master)
  97. self.tooltip.withdraw()
  98. # widget currently under the mouse if among wrapped widgets:
  99. self.current_widget = None
  100. self.configure(**kwargs)
  101. self.config = self.configure
  102. self.tooltip.bind('<Leave>', self._on_leave_tooltip)
  103. def __setitem__(self, key, value):
  104. self.configure(**{key: value})
  105. def __getitem__(self, key):
  106. return self.cget(key)
  107. def cget(self, key):
  108. if key == 'delay':
  109. return self._delay
  110. else:
  111. return self.tooltip.cget(key)
  112. def configure(self, **kwargs):
  113. try:
  114. self._delay = int(kwargs.pop('delay', self._delay))
  115. except ValueError:
  116. raise ValueError('expected integer for the delay option.')
  117. self.tooltip.configure(**kwargs)
  118. def add_tooltip(self, widget, text):
  119. """Add new widget to wrapper."""
  120. self.widgets[str(widget)] = text
  121. self.bind_enter_ids[str(widget)] = widget.bind('<Enter>', self._on_enter)
  122. self.bind_leave_ids[str(widget)] = widget.bind('<Leave>', self._on_leave)
  123. def set_tooltip_text(self, widget, text):
  124. """Change tooltip text for given widget."""
  125. self.widgets[str(widget)] = text
  126. def remove_all(self):
  127. """Remove all tooltips."""
  128. for name in self.widgets:
  129. widget = self.tooltip.nametowidget(name)
  130. widget.unbind('<Enter>', self.bind_enter_ids[name])
  131. widget.unbind('<Leave>', self.bind_leave_ids[name])
  132. self.widgets.clear()
  133. self.bind_enter_ids.clear()
  134. self.bind_leave_ids.clear()
  135. def remove_tooltip(self, widget):
  136. """Remove widget from wrapper."""
  137. try:
  138. name = str(widget)
  139. del self.widgets[name]
  140. widget.unbind('<Enter>', self.bind_enter_ids[name])
  141. widget.unbind('<Leave>', self.bind_leave_ids[name])
  142. del self.bind_enter_ids[name]
  143. del self.bind_leave_ids[name]
  144. except KeyError:
  145. pass
  146. def _on_enter(self, event):
  147. """Change current widget and launch timer to display tooltip."""
  148. if not self.tooltip.winfo_ismapped():
  149. self._timer_id = event.widget.after(self._delay, self.display_tooltip)
  150. self.current_widget = event.widget
  151. def _on_leave(self, event):
  152. """Hide tooltip if visible or cancel tooltip display."""
  153. if self.tooltip.winfo_ismapped():
  154. x, y = event.widget.winfo_pointerxy()
  155. if not event.widget.winfo_containing(x, y) in [event.widget, self.tooltip]:
  156. self.tooltip.withdraw()
  157. else:
  158. try:
  159. event.widget.after_cancel(self._timer_id)
  160. except ValueError:
  161. pass
  162. self.current_widget = None
  163. def _on_leave_tooltip(self, event):
  164. """Hide tooltip."""
  165. x, y = event.widget.winfo_pointerxy()
  166. if not event.widget.winfo_containing(x, y) in [self.current_widget, self.tooltip]:
  167. self.tooltip.withdraw()
  168. def display_tooltip(self):
  169. """Display tooltip with text corresponding to current widget."""
  170. if self.current_widget is None:
  171. return
  172. try:
  173. disabled = "disabled" in self.current_widget.state()
  174. except AttributeError:
  175. disabled = self.current_widget.cget('state') == "disabled"
  176. if not disabled:
  177. self.tooltip['text'] = self.widgets[str(self.current_widget)]
  178. self.tooltip.deiconify()
  179. x = self.current_widget.winfo_pointerx() + 14
  180. y = self.current_widget.winfo_rooty() + self.current_widget.winfo_height() + 2
  181. self.tooltip.geometry('+%i+%i' % (x, y))