# Pmw megawidget framework.
# This module provides a framework for building megawidgets. It
# contains the MegaArchetype class which manages component widgets and
# configuration options. Also provided are the MegaToplevel and
# MegaWidget classes, derived from the MegaArchetype class. The
# MegaToplevel class contains a Tkinter Toplevel widget to act as the
# container of the megawidget. This is used as the base class of all
# megawidgets that are contained in their own top level window, such
# as a Dialog window. The MegaWidget class contains a Tkinter Frame
# to act as the container of the megawidget. This is used as the base
# class of all other megawidgets, such as a ComboBox or ButtonBox.
#
# Megawidgets are built by creating a class that inherits from either
# the MegaToplevel or MegaWidget class.
import string
import sys
import traceback
import types
import Tkinter
# Constant used to indicate that an option can only be set by a call
# to the constructor.
INITOPT = [42]
_DEFAULT_OPTION_VALUE = [69]
_useTkOptionDb = 0
# Symbolic constants for the indexes into an optionInfo list.
_OPT_DEFAULT = 0
_OPT_VALUE = 1
_OPT_FUNCTION = 2
#=============================================================================
# Functions used to forward methods from a class to a component.
# Fill in a flattened method resolution dictionary for a class (attributes are
# filtered out). Flattening honours the MI method resolution rules
# (depth-first search of bases in order). The dictionary has method names
# for keys and functions for values.
def __methodDict(cls, dict):
# the strategy is to traverse the class in the _reverse_ of the normal
# order, and overwrite any duplicates.
baseList = list(cls.__bases__)
baseList.reverse()
# do bases in reverse order, so first base overrides last base
for super in baseList:
__methodDict(super, dict)
# do my methods last to override base classes
for key, value in cls.__dict__.items():
# ignore class attributes
if type(value) == types.FunctionType:
dict[key] = value
def __methods(cls):
# Return all method names for a class.
# Return all method names for a class (attributes are filtered
# out). Base classes are searched recursively.
dict = {}
__methodDict(cls, dict)
return dict.keys()
# Function body to resolve a forwarding given the target method name and the
# attribute name. The resulting lambda requires only self, but will forward
# any other parameters.
__stringBody = (
'def %(method)s(this, *args, **kw): return ' +
'apply(this.%(attribute)s.%(method)s, args, kw)')
# Get a unique id
__counter = 0
def __unique():
global __counter
__counter = __counter + 1
return str(__counter)
# Function body to resolve a forwarding given the target method name and the
# index of the resolution function. The resulting lambda requires only self,
# but will forward any other parameters. The target instance is identified
# by invoking the resolution function.
__funcBody = (
'def %(method)s(this, *args, **kw): return ' +
'apply(this.%(forwardFunc)s().%(method)s, args, kw)')
def forwardmethods(fromClass, toClass, toPart, exclude = []):
# Forward all methods from one class to another.
# Forwarders will be created in fromClass to forward method
# invocations to toClass. The methods to be forwarded are
# identified by flattening the interface of toClass, and excluding
# methods identified in the exclude list. Methods already defined
# in fromClass, or special methods with one or more leading or
# trailing underscores will not be forwarded.
# For a given object of class fromClass, the corresponding toClass
# object is identified using toPart. This can either be a String
# denoting an attribute of fromClass objects, or a function taking
# a fromClass object and returning a toClass object.
# Example:
# class MyClass:
# ...
# def __init__(self):
# ...
# self.__target = TargetClass()
# ...
# def findtarget(self):
# return self.__target
# forwardmethods(MyClass, TargetClass, '__target', ['dangerous1', 'dangerous2'])
# # ...or...
# forwardmethods(MyClass, TargetClass, MyClass.findtarget,
# ['dangerous1', 'dangerous2'])
# In both cases, all TargetClass methods will be forwarded from
# MyClass except for dangerous1, dangerous2, special methods like
# __str__, and pre-existing methods like findtarget.
# Allow an attribute name (String) or a function to determine the instance
if type(toPart) != types.StringType:
# check that it is something like a function
if callable(toPart):
# If a method is passed, use the function within it
if hasattr(toPart, 'im_func'):
toPart = toPart.im_func
# After this is set up, forwarders in this class will use
# the forwarding function. The forwarding function name is
# guaranteed to be unique, so that it can't be hidden by subclasses
forwardName = '__fwdfunc__' + __unique()
fromClass.__dict__[forwardName] = toPart
# It's not a valid type
else:
raise TypeError, 'toPart must be attribute name, function or method'
# get the full set of candidate methods
dict = {}
__methodDict(toClass, dict)
# discard special methods
for ex in dict.keys():
if ex[:1] == '_' or ex[-1:] == '_':
del dict[ex]
# discard dangerous methods supplied by the caller
for ex in exclude:
if dict.has_key(ex):
del dict[ex]
# discard methods already defined in fromClass
for ex in __methods(fromClass):
if dict.has_key(ex):
del dict[ex]
for method, func in dict.items():
d = {'method': method, 'func': func}
if type(toPart) == types.StringType:
execString = \
__stringBody % {'method' : method, 'attribute' : toPart}
else:
execString = \
__funcBody % {'forwardFunc' : forwardName, 'method' : method}
exec execString in d
# this creates a method
fromClass.__dict__[method] = d[method]
#=============================================================================
class MegaArchetype:
# Megawidget abstract root class.
# This class provides methods which are inherited by classes
# implementing useful bases (this class doesn't provide a
# container widget inside which the megawidget can be built).
def __init__(self, parent = None, hullClass = None):
# Mapping from each megawidget option to a list of information
# about the option
# - default value
# - current value
# - function to call when the option is initialised in the
# call to initialiseoptions() in the constructor or
# modified via configure(). If this is INITOPT, the
# option is an initialisation option (an option that can
# be set by the call to the constructor but can not be
# used with configure).
# This mapping is not initialised here, but in the call to
# defineoptions() which precedes construction of this base class.
#
# self._optionInfo = {}
# Mapping from each component name to a tuple of information
# about the component.
# - component widget instance
# - configure function of widget instance
# - the class of the widget (Frame, EntryField, etc)
# - cget function of widget instance
# - the name of the component group of this component, if any
self.__componentInfo = {}
# Mapping from alias names to the names of components or
# sub-components.
self.__componentAliases = {}
# Contains information about the keywords provided to the
# constructor. It is a mapping from the keyword to a tuple
# containing:
# - value of keyword
# - a boolean indicating if the keyword has been used.
# A keyword is used if, during the construction of a megawidget,
# - it is defined in a call to defineoptions() or addoptions(), or
# - it references, by name, a component of the megawidget, or
# - it references, by group, at least one component
# At the end of megawidget construction, a call is made to
# initialiseoptions() which reports an error if there are
# unused options given to the constructor.
#
# self._constructorKeywords = {}
if hullClass is None:
self._hull = None
else:
if parent is None:
parent = Tkinter._default_root
# Create the hull.
self._hull = self.createcomponent('hull',
(), None,
hullClass, (parent,))
_hullToMegaWidget[self._hull] = self
if _useTkOptionDb:
# Now that a widget has been created, query the Tk
# option database to get the default values for the
# options which have not been set in the call to the
# constructor. This assumes that defineoptions() is
# called before the __init__().
option_get = self.option_get
VALUE = _OPT_VALUE
DEFAULT = _OPT_DEFAULT
for name, info in self._optionInfo.items():
value = info[VALUE]
if value is _DEFAULT_OPTION_VALUE:
resourceClass = string.upper(name[0]) + name[1:]
value = option_get(name, resourceClass)
if value != '':
try:
# Convert the string to int/float/tuple, etc
value = eval(value, {'__builtins__': {}})
except:
pass
info[VALUE] = value
else:
info[VALUE] = info[DEFAULT]
#======================================================================
# Methods used (mainly) during the construction of the megawidget.
def defineoptions(self, keywords, optionDefs):
# Create options, providing the default value and the method
# to call when the value is changed. If any option created by
# base classes has the same name as one in <optionDefs>, the
# base class's value and function will be overriden.
# This should be called before the constructor of the base
# class, so that default values defined in the derived class
# override those in the base class.
if not hasattr(self, '_constructorKeywords'):
tmp = {}
for option, value in keywords.items():
tmp[option] = [value, 0]
self._constructorKeywords = tmp
self._optionInfo = {}
self.addoptions(optionDefs)
def addoptions(self, optionDefs):
# Add additional options, providing the default value and the
# method to call when the value is changed. See
# "defineoptions" for more details
# optimisations:
optionInfo = self._optionInfo
optionInfo_has_key = optionInfo.has_key
keywords = self._constructorKeywords
keywords_has_key = keywords.has_key
FUNCTION = _OPT_FUNCTION
for name, default, function in optionDefs:
if '_' not in name:
# The option will already exist if it has been defined
# in a derived class. In this case, do not override the
# default value of the option or the callback function
# if it is not None.
if not optionInfo_has_key(name):
if keywords_has_key(name):
value = keywords[name][0]
optionInfo[name] = [default, value, function]
del keywords[name]
else:
if _useTkOptionDb:
optionInfo[name] = \
[default, _DEFAULT_OPTION_VALUE, function]
else:
optionInfo[name] = [default, default, function]
elif optionInfo[name][FUNCTION] is None:
optionInfo[name][FUNCTION] = function
else:
# This option is of the form "component_option". If this is
# not already defined in self._constructorKeywords add it.
# This allows a derived class to override the default value
# of an option of a component of a base class.
if not keywords_has_key(name):
keywords[name] = [default, 0]
def createcomponent(self, name, aliases, group, widgetClass, widgetArgs, **kw):
# Create a component (during construction or later).
if '_' in name:
raise ValueError, 'Component name "%s" must not contain "_"' % name
if hasattr(self, '_constructorKeywords'):
keywords = self._constructorKeywords
else:
keywords = {}
for alias, component in aliases:
# Create aliases to the component and its sub-components.
index = string.find(component, '_')
if index < 0:
self.__componentAliases[alias] = (component, None)
else:
mainComponent = component[:index]
subComponent = component[(index + 1):]
self.__componentAliases[alias] = (mainComponent, subComponent)
# Remove aliases from the constructor keyword arguments by
# replacing any keyword arguments that begin with *alias*
# with corresponding keys beginning with *component*.
alias = alias + '_'
aliasLen = len(alias)
for option in keywords.keys():
if len(option) > aliasLen and option[:aliasLen] == alias:
newkey = component + '_' + option[aliasLen:]
keywords[newkey] = keywords[option]
del keywords[option]
componentName = name + '_'
nameLen = len(componentName)
for option in keywords.keys():
if len(option) > nameLen and option[:nameLen] == componentName:
# The keyword argument refers to this component, so add
# this to the options to use when constructing the widget.
kw[option[nameLen:]] = keywords[option][0]
del keywords[option]
else:
# Check if this keyword argument refers to the group
# of this component. If so, add this to the options
# to use when constructing the widget. Mark the
# keyword argument as being used, but do not remove it
# since it may be required when creating another
# component.
index = string.find(option, '_')
if index >= 0 and group == option[:index]:
rest = option[(index + 1):]
kw[rest] = keywords[option][0]
keywords[option][1] = 1
if kw.has_key('pyclass'):
widgetClass = kw['pyclass']
del kw['pyclass']
if widgetClass is None:
return None
widget = apply(widgetClass, widgetArgs, kw)
componentClass = widget.__class__.__name__
self.__componentInfo[name] = (widget, widget.configure,
componentClass, widget.cget, group)
return widget
def destroycomponent(self, name):
# Remove a megawidget component.
# This command is for use by megawidget designers to destroy a
# megawidget component.
self.__componentInfo[name][0].destroy()
del self.__componentInfo[name]
def createlabel(self, parent, childCols = 1, childRows = 1):
labelpos = self['labelpos']
labelmargin = self['labelmargin']
if labelpos is None:
return
label = self.createcomponent('label',
(), None,
Tkinter.Label, (parent,))
if labelpos[0] in 'ns':
# vertical layout
if labelpos[0] == 'n':
row = 0
margin = 1
else:
row = childRows + 3
margin = row - 1
label.grid(column=2, row=row, columnspan=childCols, sticky=labelpos)
parent.grid_rowconfigure(margin, minsize=labelmargin)
else:
# horizontal layout
if labelpos[0] == 'w':
col = 0
margin = 1
else:
col = childCols + 3
margin = col - 1
label.grid(column=col, row=2, rowspan=childRows, sticky=labelpos)
parent.grid_columnconfigure(margin, minsize=labelmargin)
def initialiseoptions(self, myClass):
if self.__class__ is myClass:
unusedOptions = []
keywords = self._constructorKeywords
for name in keywords.keys():
used = keywords[name][1]
if not used:
unusedOptions.append(name)
self._constructorKeywords = {}
if len(unusedOptions) > 0:
if len(unusedOptions) == 1:
text = 'Unknown option "'
else:
text = 'Unknown options "'
raise TypeError, text + string.join(unusedOptions, ', ') + \
'" for ' + myClass.__name__
# Call the configuration callback function for every option.
FUNCTION = _OPT_FUNCTION
for info in self._optionInfo.values():
func = info[FUNCTION]
if func is not None and func is not INITOPT:
func()
#======================================================================
# Method used to configure the megawidget.
def configure(self, option=None, **kw):
# Query or configure the megawidget options.
#
# If not empty, *kw* is a dictionary giving new
# values for some of the options of this megawidget or its
# components. For options defined for this megawidget, set
# the value of the option to the new value and call the
# configuration callback function, if any. For options of the
# form <component>_<option>, where <component> is a component
# of this megawidget, call the configure method of the
# component giving it the new value of the option. The
# <component> part may be an alias or a component group name.
#
# If *option* is None, return all megawidget configuration
# options and settings. Options are returned as standard 5
# element tuples
#
# If *option* is a string, return the 5 element tuple for the
# given configuration option.
# First, deal with the option queries.
if len(kw) == 0:
# This configure call is querying the values of one or all options.
# Return 5-tuples:
# (optionName, resourceName, resourceClass, default, value)
if option is None:
rtn = {}
for option, config in self._optionInfo.items():
resourceClass = string.upper(option[0]) + option[1:]
rtn[option] = (option, option, resourceClass,
config[_OPT_DEFAULT], config[_OPT_VALUE])
return rtn
else:
config = self._optionInfo[option]
resourceClass = string.upper(option[0]) + option[1:]
return (option, option, resourceClass, config[_OPT_DEFAULT],
config[_OPT_VALUE])
# optimisations:
optionInfo = self._optionInfo
optionInfo_has_key = optionInfo.has_key
componentInfo = self.__componentInfo
componentInfo_has_key = componentInfo.has_key
componentAliases = self.__componentAliases
componentAliases_has_key = componentAliases.has_key
VALUE = _OPT_VALUE
FUNCTION = _OPT_FUNCTION
# This will contain a list of options in *kw* which
# are known to this megawidget.
directOptions = []
# This will contain information about the options in
# *kw* of the form <component>_<option>, where
# <component> is a component of this megawidget. It is a
# dictionary whose keys are the configure method of each
# component and whose values are a dictionary of options and
# values for the component.
indirectOptions = {}
indirectOptions_has_key = indirectOptions.has_key
for option, value in kw.items():
if optionInfo_has_key(option):
# This is one of the options of this megawidget.
# Check it is an initialisation option.
if optionInfo[option][FUNCTION] is INITOPT:
raise IndexError, \
'Cannot configure initialisation option "' \
+ option + '" for ' + self.__class__.__name__
optionInfo[option][VALUE] = value
directOptions.append(option)
else:
index = string.find(option, '_')
if index >= 0:
# This option may be of the form <component>_<option>.
component = option[:index]
componentOption = option[(index + 1):]
# Expand component alias
if componentAliases_has_key(component):
component, subComponent = componentAliases[component]
if subComponent is not None:
componentOption = subComponent + '_' \
+ componentOption
# Expand option string to write on error
option = component + '_' + componentOption
if componentInfo_has_key(component):
# Configure the named component
componentConfigFuncs = [componentInfo[component][1]]
else:
# Check if this is a group name and configure all
# components in the group.
componentConfigFuncs = []
for info in componentInfo.values():
if info[4] == component:
componentConfigFuncs.append(info[1])
if len(componentConfigFuncs) == 0:
raise IndexError, 'Unknown option "' + option + \
'" for ' + self.__class__.__name__
# Add the configure method(s) (may be more than
# one if this is configuring a component group)
# and option/value to dictionary.
for componentConfigFunc in componentConfigFuncs:
if not indirectOptions_has_key(componentConfigFunc):
indirectOptions[componentConfigFunc] = {}
indirectOptions[componentConfigFunc][componentOption] \
= value
else:
raise IndexError, 'Unknown option "' + option + \
'" for ' + self.__class__.__name__
# Call the configure methods for any components.
map(apply, indirectOptions.keys(),
((),) * len(indirectOptions), indirectOptions.values())
# Call the configuration callback function for each option.
for option in directOptions:
info = optionInfo[option]
func = info[_OPT_FUNCTION]
if func is not None:
func()
#======================================================================
# Methods used to query the megawidget.
def component(self, name):
# Return a component widget of the megawidget given the
# component's name
# This allows the user of a megawidget to access and configure
# widget components directly.
# Find the main component and any subcomponents
index = string.find(name, '_')
if index < 0:
component = name
remainingComponents = None
else:
component = name[:index]
remainingComponents = name[(index + 1):]
# Expand component alias
if self.__componentAliases.has_key(component):
component, subComponent = self.__componentAliases[component]
if subComponent is not None:
if remainingComponents is None:
remainingComponents = subComponent
else:
remainingComponents = subComponent + '_' \
+ remainingComponents
widget = self.__componentInfo[component][0]
if remainingComponents is None:
return widget
else:
return widget.component(remainingComponents)
def interior(self):
return self._hull
def hulldestroyed(self):
return not _hullToMegaWidget.has_key(self._hull)
def __str__(self):
return str(self._hull)
def cget(self, option):
# Get current configuration setting.
# Return the value of an option, for example myWidget['font'].
if self._optionInfo.has_key(option):
return self._optionInfo[option][_OPT_VALUE]
else:
index = string.find(option, '_')
if index >= 0:
component = option[:index]
componentOption = option[(index + 1):]
# Expand component alias
if self.__componentAliases.has_key(component):
component, subComponent = self.__componentAliases[component]
if subComponent is not None:
componentOption = subComponent + '_' + componentOption
# Expand option string to write on error
option = component + '_' + componentOption
if self.__componentInfo.has_key(component):
# Call cget on the component.
componentCget = self.__componentInfo[component][3]
return componentCget(componentOption)
else:
# If this is a group name, call cget for one of
# the components in the group.
for info in self.__componentInfo.values():
if info[4] == component:
componentCget = info[3]
return componentCget(componentOption)
raise IndexError, 'Unknown option "' + option + \
'" for ' + self.__class__.__name__
__getitem__ = cget
def isinitoption(self, option):
return self._optionInfo[option][_OPT_FUNCTION] is INITOPT
def options(self):
options = []
if hasattr(self, '_optionInfo'):
for option, info in self._optionInfo.items():
isinit = info[_OPT_FUNCTION] is INITOPT
default = info[_OPT_DEFAULT]
options.append((option, default, isinit))
options.sort()
return options
def components(self):
# Return a list of all components.
# This list includes the 'hull' component and all widget subcomponents
names = self.__componentInfo.keys()
names.sort()
return names
def componentaliases(self):
# Return a list of all component aliases.
componentAliases = self.__componentAliases
names = componentAliases.keys()
names.sort()
rtn = []
for alias in names:
(mainComponent, subComponent) = componentAliases[alias]
if subComponent is None:
rtn.append((alias, mainComponent))
else:
rtn.append((alias, mainComponent + '_' + subComponent))
return rtn
def componentgroup(self, name):
return self.__componentInfo[name][4]
#=============================================================================
class MegaToplevel(MegaArchetype):
# <_grabStack> is a list of tuples. Each tuple contains the
# active widget and a boolean indicating whether the window was
# activated in global mode.
_grabStack = []
def __init__(self, parent = None, **kw):
# Define the options for this megawidget.
optiondefs = (
('activatecommand', None, None),
('deactivatecommand', None, None),
('title', None, self._settitle),
('hull_class', self.__class__.__name__, None),
)
self.defineoptions(kw, optiondefs)
# Initialise the base class (after defining the options).
MegaArchetype.__init__(self, parent, Tkinter.Toplevel)
# Initialise instance.
self.protocol('WM_DELETE_WINDOW', self._userDeleteWindow)
# Initialise instance variables.
self._firstShowing = 1
# Used by show() to ensure window retains previous position on screen.
# The IntVar() variable to wait on during a modal dialog.
self._wait = None
# Attribute _active can be 'no', 'yes', or 'waiting'. The
# latter means that the window has been deiconified but has
# not yet become visible.
self._active = 'no'
self._userDeleteFunc = self.destroy
self._userModalDeleteFunc = self.deactivate
# Check keywords and initialise options.
self.initialiseoptions(MegaToplevel)
def _settitle(self):
title = self['title']
if title is not None:
self.title(title)
def userdeletefunc(self, func=None):
if func:
self._userDeleteFunc = func
else:
return self._userDeleteFunc
def usermodaldeletefunc(self, func=None):
if func:
self._userModalDeleteFunc = func
else:
return self._userModalDeleteFunc
def _userDeleteWindow(self):
if self.active():
self._userModalDeleteFunc()
else:
self._userDeleteFunc()
def destroy(self):
# Allow this to be called more than once.
if _hullToMegaWidget.has_key(self._hull):
del _hullToMegaWidget[self._hull]
self.deactivate()
self._hull.destroy()
def show(self):
if self.state() == 'normal':
self.tkraise()
else:
if self._firstShowing:
# Just let the window manager determine the window
# position for the first time.
self._firstShowing = 0
else:
# Position the window at the same place it was last time.
geometry = self.geometry()
index = string.find(geometry, '+')
if index >= 0:
self.geometry(geometry[index:])
self.deiconify()
def _centreonscreen(self):
# Centre the window on the screen. (Actually halfway across
# and one third down.)
self.update_idletasks()
# I'm not sure what the winfo_vroot[xy] stuff does, but tk_dialog
# does it, so...
#x = (self.winfo_screenwidth() - self.winfo_reqwidth()) / 2
#y = (self.winfo_screenheight() - self.winfo_reqheight()) / 3
x = (self.winfo_screenwidth() - self.winfo_reqwidth()) / 2 \
- self.winfo_vrootx()
y = (self.winfo_screenheight() - self.winfo_reqheight()) / 3 \
- self.winfo_vrooty()
if x < 0:
x = 0
if y < 0:
y = 0
self.geometry('+%s+%s' % (x, y))
def _sameposition(self):
# Position the window at the same place it was last time.
geometry = self.geometry()
index = string.find(geometry, '+')
if index >= 0:
self.geometry(geometry[index:])
def activate(self, globalMode=0, master=None,
geometry = 'centerscreenfirst'):
if self.state() == 'normal':
self.withdraw()
if self._active == 'yes':
raise ValueError, 'Window is already active'
if self._active == 'waiting':
return
if master is not None:
self.transient(master)
if len(MegaToplevel._grabStack) > 0:
widget = MegaToplevel._grabStack[-1][0]
widget.grab_release()
MegaToplevel._grabStack.append(self, globalMode)
showbusycursor()
if self._wait is None:
self._wait = Tkinter.IntVar()
self._wait.set(0)
self._active = 'waiting'
if geometry == 'centerscreenalways':
self._centreonscreen()
elif geometry == 'centerscreenfirst':
if self._firstShowing:
# Centre the window the first time it is displayed.
self._centreonscreen()
else:
# Position the window at the same place it was last time.
self._sameposition()
elif geometry[:5] == 'first':
if self._firstShowing:
self.geometry(geometry[5:])
else:
# Position the window at the same place it was last time.
self._sameposition()
elif geometry is not None:
self.geometry(geometry)
self._firstShowing = 0
self.deiconify()
self.wait_visibility()
if self._active == 'no':
# The deactivate() method was called during the call to
# wait_visibility() (perhaps from a timer).
return self._result
self._active = 'yes'
while 1:
try:
if globalMode:
self.grab_set_global()
else:
self.grab_set()
break
except Tkinter.TclError:
# Another application has grab. Keep trying until
# grab can succeed.
self.after(100)
self.focus_set()
command = self['activatecommand']
if callable(command):
command()
self.wait_variable(self._wait)
return self._result
# TBD
# This is how tk_dialog handles the focus and grab:
# # 7. Set a grab and claim the focus too.
#
# set oldFocus [focus]
# set oldGrab [grab current $w]
# if {$oldGrab != ""} {
# set grabStatus [grab status $oldGrab]
# }
# grab $w
# if {$default >= 0} {
# focus $w.button$default
# } else {
# focus $w
# }
#
# # 8. Wait for the user to respond, then restore the focus and
# # return the index of the selected button. Restore the focus
# # before deleting the window, since otherwise the window manager
# # may take the focus away so we can't redirect it. Finally,
# # restore any grab that was in effect.
#
# tkwait variable tkPriv(button)
# catch {focus $oldFocus}
# catch {
# # It's possible that the window has already been destroyed,
# # hence this "catch". Delete the Destroy handler so that
# # tkPriv(button) doesn't get reset by it.
#
# bind $w <Destroy> {}
# destroy $w
# }
# if {$oldGrab != ""} {
# if {$grabStatus == "global"} {
# grab -global $oldGrab
# } else {
# grab $oldGrab
# }
# }
def deactivate(self, result=None):
if self._active == 'no':
return
self._active = 'no'
# Deactivate any active windows above this on the stack.
while len(MegaToplevel._grabStack) > 0:
if MegaToplevel._grabStack[-1][0] == self:
break
else:
MegaToplevel._grabStack[-1][0].deactivate()
# Clean up this window.
hidebusycursor()
self.withdraw()
self.grab_release()
# Return the grab to the next active window in the stack, if any.
del MegaToplevel._grabStack[-1]
if len(MegaToplevel._grabStack) > 0:
widget, globalMode = MegaToplevel._grabStack[-1]
while 1:
try:
if globalMode:
widget.grab_set_global()
else:
widget.grab_set()
break
except Tkinter.TclError:
# Another application has grab. Keep trying until
# grab can succeed.
self.after(100)
command = self['deactivatecommand']
if callable(command):
command()
self._result = result
self._wait.set(1)
def active(self):
return self._active != 'no'
forwardmethods(MegaToplevel, Tkinter.Toplevel, '_hull')
#=============================================================================
class MegaWidget(MegaArchetype):
def __init__(self, parent = None, **kw):
# Define the options for this megawidget.
optiondefs = (
('hull_class', self.__class__.__name__, None),
)
self.defineoptions(kw, optiondefs)
# Initialise the base class (after defining the options).
MegaArchetype.__init__(self, parent, Tkinter.Frame)
def destroy(self):
del _hullToMegaWidget[self._hull]
self._hull.destroy()
forwardmethods(MegaWidget, Tkinter.Frame, '_hull')
#=============================================================================
# Public functions
#-----------------
def tracetk(root, on, withStackTrace = 0, file=None):
global _withStackTrace
_withStackTrace = withStackTrace
if on:
if hasattr(root.tk, '__class__'):
# Tracing already on
return
tk = _TraceTk(root.tk, file)
else:
if not hasattr(root.tk, '__class__'):
# Tracing already off
return
tk = root.tk.getTclInterp()
_setTkInterps(root, tk)
def showbusycursor():
__addRootToToplevelBusyCount()
doUpdate = 0
for window in _toplevelBusyInfo.keys():
if window.state() != 'withdrawn':
_toplevelBusyInfo[window][0] = _toplevelBusyInfo[window][0] + 1
if _haveblt(window):
if _toplevelBusyInfo[window][0] == 1:
_busy_hold(window)
# Make sure that no events for the busy window get
# through to Tkinter, otherwise it will crash in
# _nametowidget with a 'KeyError: _Busy' if there is
# a binding on the toplevel window.
if window._w == '.':
busyWindow = '._Busy'
else:
busyWindow = window._w + '._Busy'
window.tk.call('bindtags', busyWindow, 'Pmw_Dummy_Tag')
# Remember previous focus window and set focus to
# the busy window, which should ignore all events.
lastFocus = window.tk.call('focus')
_toplevelBusyInfo[window][1] = \
window.tk.call('focus', '-lastfor', window._w)
window.tk.call('focus', busyWindow)
if _toplevelBusyInfo[window][1] != lastFocus:
window.tk.call('focus', lastFocus)
doUpdate = 1
if doUpdate:
window.update_idletasks()
def hidebusycursor():
__addRootToToplevelBusyCount()
for window in _toplevelBusyInfo.keys():
if _toplevelBusyInfo[window][0] > 0:
_toplevelBusyInfo[window][0] = _toplevelBusyInfo[window][0] - 1
if _haveblt(window):
if _toplevelBusyInfo[window][0] == 0:
_busy_release(window)
lastFocus = window.tk.call('focus')
try:
window.tk.call('focus', _toplevelBusyInfo[window][1])
except Tkinter.TclError:
# Previous focus widget has been deleted. Set focus
# to toplevel window instead (can't leave focus on
# busy window).
window.focus_set()
if window._w == '.':
busyWindow = '._Busy'
else:
busyWindow = window._w + '._Busy'
if lastFocus != busyWindow:
window.tk.call('focus', lastFocus)
def clearbusycursor():
__addRootToToplevelBusyCount()
for window in _toplevelBusyInfo.keys():
if _toplevelBusyInfo[window][0] > 0:
_toplevelBusyInfo[window][0] = 0
if _haveblt(window):
_busy_release(window)
try:
window.tk.call('focus', _toplevelBusyInfo[window][1])
except Tkinter.TclError:
# Previous focus widget has been deleted. Set focus
# to toplevel window instead (can't leave focus on
# busy window).
window.focus_set()
def busycallback(command, updateFunction = None):
if not callable(command):
raise RuntimeError, \
'cannot register non-command busy callback %s %s' % \
(repr(command), type(command))
wrapper = _BusyWrapper(command, updateFunction)
return wrapper.callback
_errorReportFile = None
_errorWindow = None
def reporterrorstofile(file = None):
global _errorReportFile
_errorReportFile = file
def displayerror(text):
global _errorWindow
if _errorReportFile is not None:
_errorReportFile.write(text + '\n')
else:
if _errorWindow is None:
# The error window has not yet been created.
_errorWindow = _ErrorWindow()
_errorWindow.showerror(text)
def initialise(root = None, size = None, fontScheme = None, useTkOptionDb = 0):
# Save flag specifying whether the Tk option database should be
# queried when setting megawidget option default values.
global _useTkOptionDb
_useTkOptionDb = useTkOptionDb
# If we haven't been given a root window, use the default or
# create one.
if root is None:
if Tkinter._default_root is None:
root = Tkinter.Tk()
else:
root = Tkinter._default_root
# Trap Tkinter Toplevel constructors so that a list of Toplevels
# can be maintained.
Tkinter.Toplevel.title = __TkinterToplevelTitle
# Trap Tkinter widget destruction so that megawidgets can be
# destroyed when their hull widget is destoyed and the list of
# Toplevels can be pruned.
Tkinter.Toplevel.destroy = __TkinterToplevelDestroy
Tkinter.Frame.destroy = __TkinterFrameDestroy
# Modify Tkinter's CallWrapper class to improve the display of
# errors which occur in callbacks.
Tkinter.CallWrapper = __TkinterCallWrapper
# Make sure we get to know when the window manager deletes the
# root window. Only do this if the protocol has not yet been set.
# This is required if there is a modal dialog displayed and the
# window manager deletes the root window. Otherwise the
# application will not exit, even though there are no windows.
if root.protocol('WM_DELETE_WINDOW') == '':
root.protocol('WM_DELETE_WINDOW', root.destroy)
# Set the base font size for the application and set the
# Tk option database font resources.
import PmwLogicalFont
PmwLogicalFont._font_initialise(root, size, fontScheme)
return root
def alignlabels(widgets, sticky = None):
if len(widgets) == 0:
return
widgets[0].update_idletasks()
# Determine the size of the maximum length label string.
maxLabelWidth = 0
for iwid in widgets:
labelWidth = iwid.grid_bbox(0, 1)[2]
if labelWidth > maxLabelWidth:
maxLabelWidth = labelWidth
# Adjust the margins for the labels such that the child sites and
# labels line up.
for iwid in widgets:
if sticky is not None:
iwid.component('label').grid(sticky=sticky)
iwid.grid_columnconfigure(0, minsize = maxLabelWidth)
#=============================================================================
# Private routines
#-----------------
class _TraceTk:
def __init__(self, tclInterp, file):
self.tclInterp = tclInterp
if file is None:
self.file = sys.stderr
else:
self.file = file
self.recursionCounter = 0
def getTclInterp(self):
return self.tclInterp
def call(self, *args, **kw):
file = self.file
file.write('=' * 60 + '\n')
file.write('tk call:' + str(args) + '\n')
self.recursionCounter = self.recursionCounter + 1
recursionStr = str(self.recursionCounter)
if self.recursionCounter > 1:
file.write('recursion: ' + recursionStr + '\n')
result = apply(self.tclInterp.call, args, kw)
if self.recursionCounter > 1:
file.write('end recursion: ' + recursionStr + '\n')
self.recursionCounter = self.recursionCounter - 1
if result:
file.write(' result:' + str(result) + '\n')
if _withStackTrace:
file.write('stack:\n')
traceback.print_stack()
return result
def __getattr__(self, key):
return getattr(self.tclInterp, key)
def _setTkInterps(window, tk):
window.tk = tk
for child in window.children.values():
_setTkInterps(child, tk)
#=============================================================================
# Functions to display a busy cursor. Keep a list of all toplevels
# and display the busy cursor over them. The list will contain the Tk
# root toplevel window as well as all other toplevel windows.
# Also keep a list of the widget which last had focus for each
# toplevel.
_toplevelBusyInfo = {}
# Pmw needs to know all toplevel windows, so that it can call blt busy
# on them. This is a hack so we get notified when a Tk topevel is
# created. Ideally, the __init__ 'method' should be overridden, but
# it is a 'read-only special attribute'. Luckily, title() is always
# called from the Tkinter Toplevel constructor.
def __TkinterToplevelTitle(self, *args):
# If this is being called from the constructor, include this
# Toplevel in the list of toplevels and set the initial
# WM_DELETE_WINDOW protocol to destroy() so that we get to know
# about it.
if not _toplevelBusyInfo.has_key(self):
_toplevelBusyInfo[self] = [0, None]
self.protocol('WM_DELETE_WINDOW', self.destroy)
return apply(Tkinter.Wm.title, (self,) + args)
_bltImported = 0
def _importBlt(window):
global _bltImported, _bltOK, _busy_hold, _busy_release
import PmwBlt
_bltOK = PmwBlt.haveblt(window)
_busy_hold = PmwBlt.busy_hold
_busy_release = PmwBlt.busy_release
_bltImported = 1
def _haveblt(window):
if not _bltImported:
_importBlt(window)
return _bltOK
def __addRootToToplevelBusyCount():
# Since we do not know when Tkinter will be initialised, we have
# to include the Tk root window in the list of toplevels at the
# last minute.
root = Tkinter._default_root
if root == None:
root = Tkinter.Tk()
if not _toplevelBusyInfo.has_key(root):
_toplevelBusyInfo[root] = [0, None]
class _BusyWrapper:
def __init__(self, command, updateFunction):
self._command = command
self._updateFunction = updateFunction
def callback(self, *args):
showbusycursor()
rtn = apply(self._command, args)
# Call update before hiding the busy windows to clear any
# events that may have occurred over the busy windows.
if callable(self._updateFunction):
self._updateFunction()
hidebusycursor()
return rtn
#=============================================================================
# Modify the Tkinter destroy methods so that it notifies us when a Tk
# toplevel or frame is destroyed.
# A map from the 'hull' component of a megawidget to the megawidget.
# This is used to clean up a megawidget when its hull is destroyed.
_hullToMegaWidget = {}
def __TkinterToplevelDestroy(tkWidget):
if _hullToMegaWidget.has_key(tkWidget):
mega = _hullToMegaWidget[tkWidget]
try:
mega.destroy()
except:
_reporterror(mega.destroy, ())
else:
del _toplevelBusyInfo[tkWidget]
Tkinter.Widget.destroy(tkWidget)
def __TkinterFrameDestroy(tkWidget):
if _hullToMegaWidget.has_key(tkWidget):
mega = _hullToMegaWidget[tkWidget]
try:
mega.destroy()
except:
_reporterror(mega.destroy, ())
else:
Tkinter.Widget.destroy(tkWidget)
def hulltomegawidget(tkWidget):
return _hullToMegaWidget[tkWidget]
#=============================================================================
# Add code to Tkinter to improve the display of errors which occur in
# callbacks.
class __TkinterCallWrapper:
def __init__(self, func, subst, widget):
self.func = func
self.subst = subst
self.widget = widget
def __call__(self, *args):
try:
if self.subst:
args = apply(self.subst, args)
return apply(self.func, args)
except SystemExit, msg:
raise SystemExit, msg
except:
_reporterror(self.func, args)
_eventTypeToName = {
2 : 'KeyPress',
3 : 'KeyRelease',
4 : 'ButtonPress',
5 : 'ButtonRelease',
6 : 'MotionNotify',
7 : 'EnterNotify',
8 : 'LeaveNotify',
9 : 'FocusIn',
10 : 'FocusOut',
11 : 'KeymapNotify',
12 : 'Expose',
13 : 'GraphicsExpose',
14 : 'NoExpose',
15 : 'VisibilityNotify',
16 : 'CreateNotify',
17 : 'DestroyNotify',
18 : 'UnmapNotify',
19 : 'MapNotify',
20 : 'MapRequest',
21 : 'ReparentNotify',
22 : 'ConfigureNotify',
23 : 'ConfigureRequest',
24 : 'GravityNotify',
25 : 'ResizeRequest',
26 : 'CirculateNotify',
27 : 'CirculateRequest',
28 : 'PropertyNotify',
29 : 'SelectionClear',
30 : 'SelectionRequest',
31 : 'SelectionNotify',
32 : 'ColormapNotify',
33 : 'ClientMessage',
34 : 'MappingNotify',
}
def _reporterror(func, args):
# Fetch current exception values.
exc_type, exc_value, exc_traceback = sys.exc_info()
# Give basic information about the callback exception.
if type(exc_type) == types.ClassType:
# Handle python 1.5 class exceptions.
exc_type = exc_type.__name__
msg = exc_type + ' Exception in Tk callback\n'
msg = msg + ' Function: %s (type: %s)\n' % (repr(func), type(func))
msg = msg + ' Args: %s\n' % str(args)
if type(args) == types.TupleType and len(args) > 0 and \
hasattr(args[0], 'type'):
eventArg = 1
else:
eventArg = 0
# If the argument to the callback is an event, add the event type.
if eventArg:
eventNum = string.atoi(args[0].type)
msg = msg + ' Event type: %s\n' % _eventTypeToName[eventNum]
# Add the traceback.
msg = msg + 'Traceback (innermost last):\n'
for tr in traceback.extract_tb(exc_traceback):
msg = msg + ' File "%s", line %s, in %s\n' % (tr[0], tr[1], tr[2])
msg = msg + ' %s\n' % tr[3]
msg = msg + '%s: %s\n' % (exc_type, exc_value)
# If the argument to the callback is an event, add the event contents.
if eventArg:
msg = msg + '\n================================================\n'
msg = msg + ' Event contents:\n'
keys = args[0].__dict__.keys()
keys.sort()
for key in keys:
msg = msg + ' %s: %s\n' % (key, args[0].__dict__[key])
clearbusycursor()
try:
displayerror(msg)
except:
print 'Failed to display error window.'
print 'Original error was:'
print msg
class _ErrorWindow:
def __init__(self):
self._errorQueue = []
self._errorCount = 0
self._open = 0
# Create the toplevel window
self._top = Tkinter.Toplevel()
self._top.protocol('WM_DELETE_WINDOW', self._hide)
self._top.title('Error in background function')
self._top.iconname('Background error')
# Create the text widget and scrollbar in a frame
upperframe = Tkinter.Frame(self._top)
scrollbar = Tkinter.Scrollbar(upperframe, orient='vertical')
scrollbar.pack(side = 'right', fill = 'y')
self._text = Tkinter.Text(upperframe, yscrollcommand=scrollbar.set)
self._text.pack(fill = 'both', expand = 1)
scrollbar.configure(command=self._text.yview)
# Create the buttons and label in a frame
lowerframe = Tkinter.Frame(self._top)
ignore = Tkinter.Button(lowerframe,
text = 'Ignore remaining errors', command = self._hide)
ignore.pack(side='left')
self._nextError = Tkinter.Button(lowerframe,
text = 'Show next error', command = self._next)
self._nextError.pack(side='left')
self._label = Tkinter.Label(lowerframe, relief='ridge')
self._label.pack(side='left', fill='x', expand=1)
# Pack the lower frame first so that it does not disappear
# when the window is resized.
lowerframe.pack(side = 'bottom', fill = 'x')
upperframe.pack(side = 'bottom', fill = 'both', expand = 1)
def showerror(self, text):
if self._open:
self._errorQueue.append(text)
else:
self._display(text)
self._open = 1
# Display the error window in the same place it was before.
if self._top.state() == 'normal':
# If update_idletasks is not called here, the window may
# be placed partially off the screen.
self._top.update_idletasks()
self._top.tkraise()
else:
geometry = self._top.geometry()
index = string.find(geometry, '+')
if index >= 0:
self._top.geometry(geometry[index:])
self._top.deiconify()
self._updateButtons()
# Release any grab, so that buttons in the error window work.
if len(MegaToplevel._grabStack) > 0:
widget = MegaToplevel._grabStack[-1][0]
widget.grab_release()
def _hide(self):
self._errorCount = self._errorCount + len(self._errorQueue)
self._errorQueue = []
self._top.withdraw()
self._open = 0
def _next(self):
# Display the next error in the queue.
text = self._errorQueue[0]
del self._errorQueue[0]
self._display(text)
self._updateButtons()
def _display(self, text):
self._errorCount = self._errorCount + 1
text = 'Error: %d\n%s' % (self._errorCount, text)
self._text.delete('1.0', 'end')
self._text.insert('end', text)
def _updateButtons(self):
numQueued = len(self._errorQueue)
if numQueued > 0:
self._label.configure(text='%d more errors' % numQueued)
self._nextError.configure(state='normal')
else:
self._label.configure(text='No more errors')
self._nextError.configure(state='disabled')