# 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')