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