historical/toontown-classic.git/panda/Pmw/Pmw_1_3/lib/PmwBalloon.py
2024-01-16 11:20:27 -06:00

365 lines
14 KiB
Python

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('<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
# 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('<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
# 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, '<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.
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 <Enter> 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))