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_foreground', 'black', None), ('label_justify', 'left', None), ('master', 'parent', None), ('relmouse', 'none', self._relmouse), ('state', 'both', self._state), ('statuscommand', None, None), ('xoffset', 20, None), # pixels ('yoffset', 1, None), # pixels ('hull_highlightthickness', 1, None), ('hull_highlightbackground', 'black', None), ) self.defineoptions(kw, optiondefs) # Initialise the base class (after defining the options). Pmw.MegaToplevel.__init__(self, parent) self.withdraw() self.overrideredirect(1) # Create the components. interior = self.interior() self._label = self.createcomponent('label', (), None, Tkinter.Label, (interior,)) self._label.pack() # The default hull configuration options give a black border # around the balloon, but avoids a black 'flash' when the # balloon is deiconified, before the text appears. if not kw.has_key('hull_background'): self.configure(hull_background = \ str(self._label.cget('background'))) # Initialise instance variables. self._timer = None # The widget or item that is currently triggering the balloon. # It is None if the balloon is not being displayed. It is a # one-tuple if the balloon is being displayed in response to a # widget binding (value is the widget). It is a two-tuple if # the balloon is being displayed in response to a canvas or # text item binding (value is the widget and the item). self._currentTrigger = None # Check keywords and initialise options. self.initialiseoptions() 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 a previous bind for this widget exists, remove it. self.unbind(widget) if balloonHelp is None and statusHelp is None: return if statusHelp is None: statusHelp = balloonHelp enterId = widget.bind('', lambda event, self = self, w = widget, sHelp = statusHelp, bHelp = balloonHelp: self._enter(event, w, sHelp, bHelp, 0)) # Set Motion binding so that if the pointer remains at rest # within the widget until the status line removes the help and # then the pointer moves again, then redisplay the help in the # status line. # Note: The Motion binding only works for basic widgets, and # the hull of megawidgets but not for other megawidget components. motionId = widget.bind('', lambda event = None, self = self, statusHelp = statusHelp: self.showstatus(statusHelp)) leaveId = widget.bind('', self._leave) buttonId = widget.bind('', self._buttonpress) # Set Destroy binding so that the balloon can be withdrawn and # the timer can be cancelled if the widget is destroyed. destroyId = widget.bind('', self._destroy) # Use the None item in the widget's private Pmw dictionary to # store the widget's bind callbacks, for later clean up. if not hasattr(widget, '_Pmw_BalloonBindIds'): widget._Pmw_BalloonBindIds = {} widget._Pmw_BalloonBindIds[None] = \ (enterId, motionId, leaveId, buttonId, destroyId) def unbind(self, widget): if hasattr(widget, '_Pmw_BalloonBindIds'): if widget._Pmw_BalloonBindIds.has_key(None): (enterId, motionId, leaveId, buttonId, destroyId) = \ widget._Pmw_BalloonBindIds[None] # Need to pass in old bindings, so that Tkinter can # delete the commands. Otherwise, memory is leaked. widget.unbind('', enterId) widget.unbind('', motionId) widget.unbind('', leaveId) widget.unbind('', buttonId) widget.unbind('', destroyId) del widget._Pmw_BalloonBindIds[None] if self._currentTrigger is not None and len(self._currentTrigger) == 1: # The balloon is currently being displayed and the current # trigger is a widget. triggerWidget = self._currentTrigger[0] if triggerWidget == widget: if self._timer is not None: self.after_cancel(self._timer) self._timer = None self.withdraw() self.clearstatus() self._currentTrigger = None def tagbind(self, widget, tagOrItem, balloonHelp, statusHelp = None): # If a previous bind for this widget's tagOrItem exists, remove it. self.tagunbind(widget, tagOrItem) if balloonHelp is None and statusHelp is None: return if statusHelp is None: statusHelp = balloonHelp enterId = widget.tag_bind(tagOrItem, '', lambda event, self = self, w = widget, sHelp = statusHelp, bHelp = balloonHelp: self._enter(event, w, sHelp, bHelp, 1)) motionId = widget.tag_bind(tagOrItem, '', lambda event = None, self = self, statusHelp = statusHelp: self.showstatus(statusHelp)) leaveId = widget.tag_bind(tagOrItem, '', self._leave) buttonId = widget.tag_bind(tagOrItem, '', self._buttonpress) # Use the tagOrItem item in the widget's private Pmw dictionary to # store the tagOrItem's bind callbacks, for later clean up. if not hasattr(widget, '_Pmw_BalloonBindIds'): widget._Pmw_BalloonBindIds = {} widget._Pmw_BalloonBindIds[tagOrItem] = \ (enterId, motionId, leaveId, buttonId) def tagunbind(self, widget, tagOrItem): if hasattr(widget, '_Pmw_BalloonBindIds'): if widget._Pmw_BalloonBindIds.has_key(tagOrItem): (enterId, motionId, leaveId, buttonId) = \ widget._Pmw_BalloonBindIds[tagOrItem] widget.tag_unbind(tagOrItem, '', enterId) widget.tag_unbind(tagOrItem, '', motionId) widget.tag_unbind(tagOrItem, '', leaveId) widget.tag_unbind(tagOrItem, '', buttonId) del widget._Pmw_BalloonBindIds[tagOrItem] if self._currentTrigger is None: # The balloon is not currently being displayed. return if len(self._currentTrigger) == 1: # The current trigger is a widget. return if len(self._currentTrigger) == 2: # The current trigger is a canvas item. (triggerWidget, triggerItem) = self._currentTrigger if triggerWidget == widget and triggerItem == tagOrItem: if self._timer is not None: self.after_cancel(self._timer) self._timer = None self.withdraw() self.clearstatus() self._currentTrigger = None else: # The current trigger is a text item. (triggerWidget, x, y) = self._currentTrigger if triggerWidget == widget: currentPos = widget.index('@%d,%d' % (x, y)) currentTags = widget.tag_names(currentPos) if tagOrItem in currentTags: if self._timer is not None: self.after_cancel(self._timer) self._timer = None self.withdraw() self.clearstatus() self._currentTrigger = None def showstatus(self, statusHelp): if self['state'] in ('status', 'both'): cmd = self['statuscommand'] if callable(cmd): cmd(statusHelp) def clearstatus(self): self.showstatus(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 _relmouse(self): if self['relmouse'] not in ('both', 'x', 'y', 'none'): raise ValueError, 'bad relmouse option ' + repr(self['relmouse'])+ \ ': should be one of \'both\', \'x\', ' + '\'y\' or \'none\'' def _enter(self, event, widget, statusHelp, balloonHelp, isItem): # Do not display balloon if mouse button is pressed. This # will only occur if the button was pressed inside a widget, # then the mouse moved out of and then back into the widget, # with the button still held down. The number 0x1f00 is the # button mask for the 5 possible buttons in X. buttonPressed = (event.state & 0x1f00) != 0 if not buttonPressed and 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)) if isItem: if hasattr(widget, 'canvasx'): # The widget is a canvas. item = widget.find_withtag('current') if len(item) > 0: item = item[0] else: item = None self._currentTrigger = (widget, item) else: # The widget is a text widget. self._currentTrigger = (widget, event.x, event.y) else: self._currentTrigger = (widget,) 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() self._currentTrigger = None def _destroy(self, event): # Only withdraw the balloon and cancel the timer if the widget # being destroyed is the widget that triggered the balloon. # Note that in a Tkinter Destroy event, the widget field is a # string and not a widget as usual. if self._currentTrigger is None: # The balloon is not currently being displayed return if len(self._currentTrigger) == 1: # The current trigger is a widget (not an item) triggerWidget = self._currentTrigger[0] if str(triggerWidget) == event.widget: if self._timer is not None: self.after_cancel(self._timer) self._timer = None self.withdraw() self.clearstatus() self._currentTrigger = None def _buttonpress(self, event): if self._timer is not None: self.after_cancel(self._timer) self._timer = None self.withdraw() self._currentTrigger = None def _showBalloon(self, widget, balloonHelp, isItem): self._label.configure(text = balloonHelp) # First, display the balloon offscreen to get dimensions. screenWidth = self.winfo_screenwidth() screenHeight = self.winfo_screenheight() self.geometry('+%d+0' % (screenWidth + 1)) self.update_idletasks() if isItem: # Get the bounding box of the current item. bbox = widget.bbox('current') if bbox is None: # The item that triggered the balloon has disappeared, # perhaps by a user's timer event that occured between # the event and the 'initwait' timer calling # this method. return # 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. leftrel = bbox[0] - widget.canvasx(0) toprel = bbox[1] - widget.canvasy(0) bottomrel = 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). leftrel = bbox[0] toprel = bbox[1] bottomrel = bbox[1] + bbox[3] else: leftrel = 0 toprel = 0 bottomrel = widget.winfo_height() xpointer, ypointer = widget.winfo_pointerxy() # -1 if off screen if xpointer >= 0 and self['relmouse'] in ('both', 'x'): x = xpointer else: x = leftrel + widget.winfo_rootx() x = x + self['xoffset'] if ypointer >= 0 and self['relmouse'] in ('both', 'y'): y = ypointer else: y = bottomrel + widget.winfo_rooty() y = y + self['yoffset'] edges = (string.atoi(str(self.cget('hull_highlightthickness'))) + string.atoi(str(self.cget('hull_borderwidth')))) * 2 if x + self._label.winfo_reqwidth() + edges > screenWidth: x = screenWidth - self._label.winfo_reqwidth() - edges if y + self._label.winfo_reqheight() + edges > screenHeight: if ypointer >= 0 and self['relmouse'] in ('both', 'y'): y = ypointer else: y = toprel + widget.winfo_rooty() y = y - self._label.winfo_reqheight() - self['yoffset'] - edges Pmw.setgeometryanddeiconify(self, '+%d+%d' % (x, y))