class Balloon(Pmw
.MegaToplevel
):
def __init__(self
, parent
= None, **kw
):
# Define the megawidget options.
('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
)
interior
= self
.interior()
self
._label
= self
.createcomponent('label',
Tkinter
.Label
, (interior
,))
# 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.
# 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.
if self
._timer
is not None:
self
.after_cancel(self
._timer
)
Pmw
.MegaToplevel
.destroy(self
)
def bind(self
, widget
, balloonHelp
, statusHelp
= None):
# If a previous bind for this widget exists, remove it.
if balloonHelp
is None and statusHelp
is None:
enterId
= widget
.bind('<Enter>',
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
# Note: The Motion binding only works for basic widgets, and
# the hull of megawidgets but not for other megawidget components.
motionId
= widget
.bind('<Motion>',
lambda event
= None, self
= self
, statusHelp
= statusHelp
:
self
.showstatus(statusHelp
))
leaveId
= widget
.bind('<Leave>', self
._leave
)
buttonId
= widget
.bind('<ButtonPress>', 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('<Destroy>', 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('<Enter>', enterId
)
widget
.unbind('<Motion>', motionId
)
widget
.unbind('<Leave>', leaveId
)
widget
.unbind('<ButtonPress>', buttonId
)
widget
.unbind('<Destroy>', 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
triggerWidget
= self
._currentTrigger
[0]
if triggerWidget
== widget
:
if self
._timer
is not None:
self
.after_cancel(self
._timer
)
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:
enterId
= widget
.tag_bind(tagOrItem
, '<Enter>',
lambda event
, self
= self
, w
= widget
,
sHelp
= statusHelp
, bHelp
= balloonHelp
:
self
._enter
(event
, w
, sHelp
, bHelp
, 1))
motionId
= widget
.tag_bind(tagOrItem
, '<Motion>',
lambda event
= None, self
= self
, statusHelp
= statusHelp
:
self
.showstatus(statusHelp
))
leaveId
= widget
.tag_bind(tagOrItem
, '<Leave>', self
._leave
)
buttonId
= widget
.tag_bind(tagOrItem
, '<ButtonPress>', 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
, '<Enter>', enterId
)
widget
.tag_unbind(tagOrItem
, '<Motion>', motionId
)
widget
.tag_unbind(tagOrItem
, '<Leave>', leaveId
)
widget
.tag_unbind(tagOrItem
, '<ButtonPress>', buttonId
)
del widget
._Pmw
_BalloonBindIds
[tagOrItem
]
if self
._currentTrigger
is None:
# The balloon is not currently being displayed.
if len(self
._currentTrigger
) == 1:
# The current trigger is a widget.
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
._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
._currentTrigger
= None
def showstatus(self
, statusHelp
):
if self
['state'] in ('status', 'both'):
cmd
= self
['statuscommand']
if self
['state'] not in ('both', 'balloon', 'status', 'none'):
raise ValueError, 'bad state option ' + repr(self
['state']) + \
': should be one of \'both\', \'balloon\', ' + \
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
= self
.after(self
['initwait'],
lambda self
= self
, widget
= widget
, help = balloonHelp
,
self
._showBalloon
(widget
, help, isItem
))
if hasattr(widget
, 'canvasx'):
# The widget is a canvas.
item
= widget
.find_withtag('current')
self
._currentTrigger
= (widget
, item
)
# The widget is a text widget.
self
._currentTrigger
= (widget
, event
.x
, event
.y
)
self
._currentTrigger
= (widget
,)
self
.showstatus(statusHelp
)
if self
._timer
is not None:
self
.after_cancel(self
._timer
)
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
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
._currentTrigger
= None
def _buttonpress(self
, event
):
if self
._timer
is not None:
self
.after_cancel(self
._timer
)
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))
# Get the bounding box of the current item.
bbox
= widget
.bbox('current')
# The item that triggered the balloon has disappeared,
# perhaps by a user's timer event that occured between
# the <Enter> event and the 'initwait' timer calling
# 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)
# 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).
bottomrel
= bbox
[1] + bbox
[3]
bottomrel
= widget
.winfo_height()
xpointer
, ypointer
= widget
.winfo_pointerxy() # -1 if off screen
if xpointer
>= 0 and self
['relmouse'] in ('both', 'x'):
x
= leftrel
+ widget
.winfo_rootx()
if ypointer
>= 0 and self
['relmouse'] in ('both', 'y'):
y
= bottomrel
+ widget
.winfo_rooty()
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
= toprel
+ widget
.winfo_rooty()
y
= y
- self
._label
.winfo_reqheight() - self
['yoffset'] - edges
Pmw
.setgeometryanddeiconify(self
, '+%d+%d' % (x
, y
))