# 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 from PmwUtils import forwardmethods # 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 #============================================================================= 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 # - 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 = {} # 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: index = string.find(name, '_') if index < 0: # 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 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 the widget framing the remaining interior space. # By default it returns the hull. # Return the widget framing the interior space of the # megawidget. For the MegaWidget and MegaToplevel classes, # this is the same as the 'hull' component. When a subclass # is creating components they should usually use interior() as # the widget in which components should be contained. # Megawidgets (such as a toplevel window with a menu bar and # status bar) which can be further subclassed should redefine # interior() to return the widget in which subclasses should # create their components. The overall containing widget is # always available as 'hull' (and should not be redefined). # This method is for use by MegaWidget or MegaToplevel # subclasses. Other classes should still access all # megawidget components by name e.g. a widget redefining # interior() should create it as a component of the # megawidget. return 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): # Toplevel megawidget base class. # # The MegaToplevel class inherits everything from the # MegaArchetype class, and adds a Tkinter Toplevel called the # 'hull' component to represent the body of the megawidget. The # window class name for the hull is set to the most-specific class # name for the megawidget. The class acts as the base class of # megawidgets that are contained in their own toplevel window. # Derived classes specialise this widget by creating other widget # components as children of the hull. # # The MegaToplevel class forwards all Tkinter Toplevel methods on # to the hull component. For example, methods such as show and # activate and all the wm methods can be used with a MegaToplevel # megawidget. # # Components # hull exterior of the megawidget (a Tkinter Toplevel) # # Options # activatecommand called whenever the toplevel is activated # title window manager title of the toplevel window # # Use show/withdraw for normal use and activate/deactivate for # modal dialog use. If the window is deleted by the window # manager while being shown normally, the default behaviour is to # destroy the window. If the window is deleted by the window # manager while the window is active (ie: when used as a modal # dialog), the window is deactivated. Use the userdeletefunc() # and usermodaldeletefunc() methods to override these behaviours. # Do not call protocol('WM_DELETE_WINDOW', func) directly if you # want to use this toplevel as a modal dialog. # The currently active windows form a stack with the most recently # activated window at the top of the stack. All mouse and # keyboard events are sent to this top window. When it # deactivates, the next window in the stack starts to receive # events. <_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), ('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: self._firstShowing = 0 else: geometry = self.geometry() index = string.find(geometry, '+') if index >= 0: self.geometry(geometry[index:]) self.deiconify() def activate(self, globalMode=0, master=None): 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' # 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)) self.deiconify() self.wait_visibility() self._active = 'yes' while 1: try: if globalMode: self.grab_set_global() else: self.grab_set() break except Tkinter.TclError: sys.exc_traceback = None # Clean up object references # 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 not self.active(): 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] if globalMode: widget.grab_set_global() else: widget.grab_set() self._result = result self._wait.set(1) def active(self): return self._active != 'no' forwardmethods(MegaToplevel, Tkinter.Toplevel, '_hull') #============================================================================= class MegaWidget(MegaArchetype): # Frame megawidget base class. # # The MegaWidget class inherits everything from the MegaArchetype # class, and adds a Tkinter Frame called the 'hull' component to # represent the body of the megawidget. The window class name for # the hull is set to the most-specific class name for the # megawidget. The class acts as the base class of megawidgets # that are not contained in their own toplevel window. Derived # classes specialize this widget by creating other widget # components as children of the hull. # # The MegaWidget class forwards all Tkinter Frame methods on to # the hull component. For example, methods such as pack and # configure and all the winfo_ methods can be used with a # MegaWidget. # # Components # hull exterior of the megawidget (a Tkinter Frame) # Options # background forwarded to hull component # cursor forwarded to hull component 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). sys.exc_traceback = None # Clean up object references 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). sys.exc_traceback = None # Clean up object references 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 def reporterrorstofile(file = None): global _errorReportFile _errorReportFile = file def displayerror(text): global _errorWindow 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.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 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, ()) sys.exc_traceback = None # Clean up object references 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, ()) sys.exc_traceback = None # Clean up object references 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) sys.exc_traceback = None # Clean up object references _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): # Save current exception values, just in case a new one occurs. exc_type, exc_value, exc_traceback = \ sys.exc_type, sys.exc_value, sys.exc_traceback # 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() if _errorReportFile is not None: _errorReportFile.write(msg + '\n') else: try: displayerror(msg) except: exc_type, exc_value, exc_traceback = \ sys.exc_type, sys.exc_value, sys.exc_traceback print 'Failed to display error window.' print 'Original error was:' print msg _errorReportFile = None _errorWindow = None 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')