Source code for pyworkflow.gui.tooltip

# **************************************************************************
# *
# * Authors:     J.M. De la Rosa Trevin ( [1]
# *
# * [1] SciLifeLab, Stockholm University
# *
# * This program is free software: you can redistribute it and/or modify
# * it under the terms of the GNU General Public License as published by
# * the Free Software Foundation, either version 3 of the License, or
# * (at your option) any later version.
# *
# * This program is distributed in the hope that it will be useful,
# * but WITHOUT ANY WARRANTY; without even the implied warranty of
# * GNU General Public License for more details.
# *
# * You should have received a copy of the GNU General Public License
# * along with this program.  If not, see <>.
# *
# *  All comments concerning this program package may be sent to the
# *  e-mail address ''
# *
# **************************************************************************
Tooltip widget, taken from web.
import tkinter as tk

'''Michael Lange <klappnase (at) freakmail (dot) de>
The ToolTip class provides a flexible tooltip widget for Tkinter; it is based on IDLE's ToolTip
module which unfortunately seems to be broken (at least the version I saw).
anchor :        where the text should be positioned inside the widget, must be on of "n", "s", "e", "w", "nw" and so on;
                default is "center"
bd :            borderwidth of the widget; default is 1 (NOTE: don't use "borderwidth" here)
bg :            background color to use for the widget; default is "lightyellow" (NOTE: don't use "background")
delay :         time in ms that it takes for the widget to appear on the screen when the mouse pointer has
                entered the parent widget; default is 1500
fg :            foreground (i.e. text) color to use; default is "black" (NOTE: don't use "foreground")
follow_mouse :  if set to 1 the tooltip will follow the mouse pointer instead of being displayed
                outside of the parent widget; this may be useful if you want to use tooltips for
                large widgets like listboxes or canvases; default is 0
font :          font to use for the widget; default is system specific
justify :       how multiple lines of text will be aligned, must be "left", "right" or "center"; default is "left"
padx :          extra space added to the left and right within the widget; default is 4
pady :          extra space above and below the text; default is 2
relief :        one of "flat", "ridge", "groove", "raised", "sunken" or "solid"; default is "solid"
state :         must be "normal" or "disabled"; if set to "disabled" the tooltip will not appear; default is "normal"
text :          the text that is displayed inside the widget
textvariable :  if set to an instance of Tkinter.StringVar() the variable's value will be used as text for the widget
width :         width of the widget; the default is 0, which means that "wraplength" will be used to limit the widgets width
wraplength :    limits the number of characters in each line; default is 150

configure(**opts) : change one or more of the widget's options as described above; the changes will take effect the
                    next time the tooltip shows up; NOTE: follow_mouse cannot be changed after widget initialization

Other widget methods that might be useful if you want to subclass ToolTip:
enter() :           callback when the mouse pointer enters the parent widget
leave() :           called when the mouse pointer leaves the parent widget
motion() :          is called when the mouse pointer moves inside the parent widget if follow_mouse is set to 1 and the
                    tooltip has shown up to continually update the coordinates of the tooltip window
coords() :          calculates the screen coordinates of the tooltip window
create_contents() : creates the contents of the tooltip window (by default a Tkinter.Label)
# Ideas gleaned from PySol

[docs]class ToolTip: def __init__(self, master, text='Your text here', delay=1500, **opts): self.master = master self._opts = {'anchor': 'center', 'bd': 1, 'bg': 'lightyellow', 'delay': delay, 'fg': 'black', 'follow_mouse': 0, 'font': None, 'justify': 'left', 'padx': 4, 'pady': 2, 'relief': 'solid', 'state': 'normal', 'text': text, 'textvariable': None, 'width': 0, 'wraplength': 150} self.configure(**opts) self._tipwindow = None self._id = None self._id1 = self.master.bind("<Enter>", self.enter, '+') self._id2 = self.master.bind("<Leave>", self.leave, '+') self._id3 = self.master.bind("<ButtonPress>", self.leave, '+') self._follow_mouse = 0 if self._opts['follow_mouse']: self._id4 = self.master.bind("<Motion>", self.motion, '+') self._follow_mouse = 1
[docs] def configure(self, **opts): for key in opts: if key in self._opts: self._opts[key] = opts[key] else: KeyError = 'KeyError: Unknown option: "%s"' % key raise KeyError
# ----these methods handle the callbacks on "<Enter>", "<Leave>" and "<Motion>"------------- # ----events on the parent widget; override them if you want to change the widget's behavior
[docs] def enter(self, event=None): self._schedule()
[docs] def leave(self, event=None): self._unschedule() self._hide()
[docs] def motion(self, event=None): if self._tipwindow and self._follow_mouse: x, y = self.coords() self._tipwindow.wm_geometry("+%d+%d" % (x, y))
# ------the methods that do the work:---------------------------------------------------------## def _schedule(self): self._unschedule() if self._opts['state'] == 'disabled': return self._id = self.master.after(self._opts['delay'], self._show) def _unschedule(self): id = self._id self._id = None if id: self.master.after_cancel(id) def _show(self): if self._opts['state'] == 'disabled': self._unschedule() return if not self._tipwindow: self._tipwindow = tw = tk.Toplevel(self.master) # hide the window until we know the geometry tw.withdraw() tw.wm_overrideredirect(1) if"tk", "windowingsystem") == 'aqua':"::tk::unsupported::MacWindowStyle", "style", tw._w, "help", "none") self.create_contents() tw.update_idletasks() x, y = self.coords() tw.wm_geometry("+%d+%d" % (x, y)) tw.deiconify() def _hide(self): tw = self._tipwindow self._tipwindow = None if tw: tw.destroy() ##----these methods might be overridden in derived classes:----------------------------------##
[docs] def coords(self): # The tip window must be completely outside the master widget; # 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 :-( # or we take care that the mouse pointer is always outside the tipwindow :-) tw = self._tipwindow twx, twy = tw.winfo_reqwidth(), tw.winfo_reqheight() w, h = tw.winfo_screenwidth(), tw.winfo_screenheight() # calculate the y coordinate: if self._follow_mouse: y = tw.winfo_pointery() + 20 # make sure the tipwindow is never outside the screen: if y + twy > h: y = y - twy - 30 else: y = self.master.winfo_rooty() + self.master.winfo_height() + 3 if y + twy > h: y = self.master.winfo_rooty() - twy - 3 # we can use the same x coord in both cases: x = tw.winfo_pointerx() - twx / 2 if x < 0: x = 0 elif x + twx > w: x = w - twx return x, y
[docs] def create_contents(self): opts = self._opts.copy() for opt in ('delay', 'follow_mouse', 'state'): del opts[opt] label = tk.Label(self._tipwindow, **opts) label.pack()