# Display a message in a window when the mouse enters a widget and
# remains there for a set time (default 1 second).  The window closes
# when the mouse leaves the widget, or any buttons are pressed while
# over the widget.

# This class has one method 'bind', which takes a widget and a message
# to display for that widget.

import os
import string
import Tkinter
import Pmw

class Balloon(Pmw.MegaToplevel):
    def __init__(self, parent=None, **kw):

	# Define the megawidget options.
	optiondefs = (
	    ('initwait',          500,            None), # milliseconds
	    ('label_background',  'lightyellow',  None),
	    ('label_justify',     'left',         None),
            ('master',            'parent',       None),
	    ('state',             'both',         self._state),
	    ('statuscommand',     None,           None),
	    ('xoffset',           20,             None), # pixels
	    ('yoffset',           1,              None), # pixels
	)
	self.defineoptions(kw, optiondefs)

	# Initialise the base class (after defining the options).
	Pmw.MegaToplevel.__init__(self, parent)

	self.withdraw()
	self.overrideredirect(1)
	self.configure(hull_borderwidth=1, hull_background="black")

	# Create the components.
	interior = self.interior()
	self._label = self.createcomponent('label',
		(), None,
		Tkinter.Label, (interior,))
	self._label.pack()

	# Initialise instance variables.
	self._timer = None
	
	# Check keywords and initialise options.
	self.initialiseoptions(Balloon)

    def destroy(self):
	if self._timer is not None:
	    self.after_cancel(self._timer)
	    self._timer = None
	Pmw.MegaToplevel.destroy(self)

    def bind(self, widget, balloonHelp, statusHelp = None):
	if balloonHelp is None and statusHelp is None:
	    self.unbind(widget)
	    return

	if statusHelp is None:
	    statusHelp = balloonHelp
	statusHelp = string.replace(statusHelp, '\n', ' ')
	widget.bind('<Enter>', 
		lambda event=None, self=self, w=widget,
			sHelp=statusHelp, bHelp=balloonHelp:
				self._enter(w, sHelp, bHelp, 0))
	# Note: The Motion binding only works for basic widgets,
	# not megawidgets.
	widget.bind('<Motion>', 
		lambda event=None, self=self, statusHelp=statusHelp:
			self.showstatus(statusHelp))
	widget.bind('<Leave>', self._leave)
	widget.bind('<ButtonPress>', self._buttonpress)

    def unbind(self, widget):
	widget.unbind('<Motion>')
	widget.unbind('<Enter>')
	widget.unbind('<Leave>')
	widget.unbind('<ButtonPress>')

    def tagbind(self, widget, tagOrItem, balloonHelp, statusHelp = None):
	if balloonHelp is None and statusHelp is None:
	    self.tagunbind(widget)
	    return

	if statusHelp is None:
	    statusHelp = balloonHelp
	statusHelp = string.replace(statusHelp, '\n', ' ')
	widget.tag_bind(tagOrItem, '<Enter>', 
		lambda event=None, self=self, w=widget,
			sHelp=statusHelp, bHelp=balloonHelp:
				self._enter(w, sHelp, bHelp, 1))
	widget.tag_bind(tagOrItem, '<Motion>', 
		lambda event=None, self=self, statusHelp=statusHelp:
			self.showstatus(statusHelp))
	widget.tag_bind(tagOrItem, '<Leave>', self._leave)
	widget.tag_bind(tagOrItem, '<ButtonPress>', self._buttonpress)

    def tagunbind(self, widget, tagOrItem):
	widget.tag_unbind(tagOrItem, '<Motion>')
	widget.tag_unbind(tagOrItem, '<Enter>')
	widget.tag_unbind(tagOrItem, '<Leave>')
	widget.tag_unbind(tagOrItem, '<ButtonPress>')

    def showstatus(self, statusHelp):
	if self['state'] in ('status', 'both'):
	    cmd = self['statuscommand']
	    if callable(cmd):
		cmd(statusHelp)

    def clearstatus(self):
	if self['state'] in ('status', 'both'):
	    cmd = self['statuscommand']
	    if callable(cmd):
		cmd(None)

    def _state(self):
	if self['state'] not in ('both', 'balloon', 'status', 'none'):
	    raise ValueError, 'bad state option ' + repr(self['state']) + \
		': should be one of \'both\', \'balloon\', ' + \
		'\'status\' or \'none\''

    def _enter(self, widget, statusHelp, balloonHelp, isItem):
	if balloonHelp is not None and self['state'] in ('balloon', 'both'):
	    if self._timer is not None:
		self.after_cancel(self._timer)
		self._timer = None

	    self._timer = self.after(self['initwait'], 
		    lambda self=self, widget=widget, help=balloonHelp,
			    isItem=isItem:
			    self._showBalloon(widget, help, isItem))

	self.showstatus(statusHelp)

    def _leave(self, event):
	if self._timer is not None:
	    self.after_cancel(self._timer)
	    self._timer = None
	self.withdraw()
	self.clearstatus()

    def _buttonpress(self, event):
	if self._timer is not None:
	    self.after_cancel(self._timer)
	    self._timer = None
	self.withdraw()

    def _showBalloon(self, widget, balloonHelp, isItem):
	if isItem:
	    # The widget is either a text or canvas.  The meaning of
	    # the values returned by the bbox method is different for
	    # each, so use the existence of the 'canvasx' method to
	    # distinguish between them.
	    if hasattr(widget, 'canvasx'):
		# The widget is a canvas.  Place balloon under canvas
                # item.  The positions returned by bbox are relative
                # to the entire canvas, not just the visible part, so
                # need to convert to window coordinates.
                bbox = widget.bbox('current')
                windowx = bbox[0] - widget.canvasx(0)
                windowy = bbox[3] - widget.canvasy(0)
	    else:
		# The widget is a text widget.  Place balloon under
                # the character closest to the mouse.  The positions
                # returned by bbox are relative to the text widget
                # window (ie the visible part of the text only).
                bbox = widget.bbox('current')
		windowx = bbox[0]
		windowy = bbox[1] + bbox[3]
	else:
	    windowx = 0
	    windowy = widget.winfo_height()

        x = windowx + widget.winfo_rootx() + self['xoffset']
        y = windowy + widget.winfo_rooty() + self['yoffset']

	self._label.configure(text=balloonHelp)

	# To avoid flashes on X and to position the window
	# correctly on Win95 (caused by Tk bugs):
	if os.name != "nt":
	    self.geometry('%+d%+d' % (x, y))
	self.deiconify()
	self.tkraise()
	if os.name == "nt":
	    self.geometry('%+d%+d' % (x, y))