import PmwColor Color = PmwColor del PmwColor import PmwBlt Blt = PmwBlt del PmwBlt ### Loader functions: _VERSION = '1.2' def setversion(version): if version != _VERSION: raise ValueError, 'Dynamic versioning not available' def setalphaversions(*alpha_versions): if alpha_versions != (): raise ValueError, 'Dynamic versioning not available' def version(alpha = 0): if alpha: return () else: return _VERSION def installedversions(alpha = 0): if alpha: return () else: return (_VERSION,) ###################################################################### ### File: PmwBase.py # Pmw megawidget base classes. # This module provides a foundation 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 os import string import sys import traceback import types import Tkinter # Special values used in index() methods of several megawidgets. END = ['end'] SELECT = ['select'] DEFAULT = ['default'] # Constant used to indicate that an option can only be set by a call # to the constructor. INITOPT = ['initopt'] _DEFAULT_OPTION_VALUE = ['default_option_value'] _useTkOptionDb = 0 # Symbolic constants for the indexes into an optionInfo list. _OPT_DEFAULT = 0 _OPT_VALUE = 1 _OPT_FUNCTION = 2 # Stacks _busyStack = [] # Stack which tracks nested calls to show/hidebusycursor (called # either directly or from activate()/deactivate()). Each element # is a dictionary containing: # 'newBusyWindows' : List of windows which had busy_hold called # on them during a call to showbusycursor(). # The corresponding call to hidebusycursor() # will call busy_release on these windows. # 'busyFocus' : The blt _Busy window which showbusycursor() # set the focus to. # 'previousFocus' : The focus as it was when showbusycursor() # was called. The corresponding call to # hidebusycursor() will restore this focus if # the focus has not been changed from busyFocus. _grabStack = [] # Stack of grabbed windows. It tracks calls to push/popgrab() # (called either directly or from activate()/deactivate()). The # window on the top of the stack is the window currently with the # grab. Each element is a dictionary containing: # 'grabWindow' : The window grabbed by pushgrab(). The # corresponding call to popgrab() will release # the grab on this window and restore the grab # on the next window in the stack (if there is one). # 'globalMode' : True if the grabWindow was grabbed with a # global grab, false if the grab was local # and 'nograb' if no grab was performed. # 'previousFocus' : The focus as it was when pushgrab() # was called. The corresponding call to # popgrab() will restore this focus. # 'deactivateFunction' : # The function to call (usually grabWindow.deactivate) if # popgrab() is called (usually from a deactivate() method) # on a window which is not at the top of the stack (that is, # does not have the grab or focus). For example, if a modal # dialog is deleted by the window manager or deactivated by # a timer. In this case, all dialogs above and including # this one are deactivated, starting at the top of the # stack. # Note that when dealing with focus windows, the name of the Tk # widget is used, since it may be the '_Busy' window, which has no # python instance associated with it. #============================================================================= # 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] #============================================================================= def setgeometryanddeiconify(window, geom): # To avoid flashes on X and to position the window correctly on NT # (caused by Tk bugs). if os.name == 'nt' or \ (os.name == 'posix' and sys.platform[:6] == 'cygwin'): # Require overrideredirect trick to stop window frame # appearing momentarily. redirect = window.overrideredirect() if not redirect: window.overrideredirect(1) window.deiconify() if geom is not None: window.geometry(geom) # Call update_idletasks to ensure NT moves the window to the # correct position it is raised. window.update_idletasks() window.tkraise() if not redirect: window.overrideredirect(0) else: if geom is not None: window.geometry(geom) # Problem!? Which way around should the following two calls # go? If deiconify() is called first then I get complaints # from people using the enlightenment or sawfish window # managers that when a dialog is activated it takes about 2 # seconds for the contents of the window to appear. But if # tkraise() is called first then I get complaints from people # using the twm window manager that when a dialog is activated # it appears in the top right corner of the screen and also # takes about 2 seconds to appear. #window.tkraise() # Call update_idletasks to ensure certain window managers (eg: # enlightenment and sawfish) do not cause Tk to delay for # about two seconds before displaying window. #window.update_idletasks() #window.deiconify() window.deiconify() if window.overrideredirect(): # The window is not under the control of the window manager # and so we need to raise it ourselves. window.tkraise() #============================================================================= 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. # # After megawidget construction, the dictionary contains # keywords which refer to a dynamic component group, so that # these components can be created after megawidget # construction and still use the group options given to the # constructor. # # self._constructorKeywords = {} # List of dynamic component groups. If a group is included in # this list, then it not an error if a keyword argument for # the group is given to the constructor or to configure(), but # no components with this group have been created. # self._dynamicGroups = () 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] def destroy(self): # Clean up optionInfo in case it contains circular references # in the function field, such as self._settitle in class # MegaToplevel. self._optionInfo = {} if self._hull is not None: del _hullToMegaWidget[self._hull] self._hull.destroy() #====================================================================== # Methods used (mainly) during the construction of the megawidget. def defineoptions(self, keywords, optionDefs, dynamicGroups = ()): # 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'): # First time defineoptions has been called. tmp = {} for option, value in keywords.items(): tmp[option] = [value, 0] self._constructorKeywords = tmp self._optionInfo = {} self._initialiseoptions_counter = 0 self._initialiseoptions_counter = self._initialiseoptions_counter + 1 if not hasattr(self, '_dynamicGroups'): self._dynamicGroups = () self._dynamicGroups = self._dynamicGroups + tuple(dynamicGroups) 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, componentName, componentAliases, componentGroup, widgetClass, *widgetArgs, **kw): # Create a component (during construction or later). if self.__componentInfo.has_key(componentName): raise ValueError, 'Component "%s" already exists' % componentName if '_' in componentName: raise ValueError, \ 'Component name "%s" must not contain "_"' % componentName if hasattr(self, '_constructorKeywords'): keywords = self._constructorKeywords else: keywords = {} for alias, component in componentAliases: # 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] componentPrefix = componentName + '_' nameLen = len(componentPrefix) for option in keywords.keys(): if len(option) > nameLen and option[:nameLen] == componentPrefix: # 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 componentGroup == 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 if len(widgetArgs) == 1 and type(widgetArgs[0]) == types.TupleType: # Arguments to the constructor can be specified as either # multiple trailing arguments to createcomponent() or as a # single tuple argument. widgetArgs = widgetArgs[0] widget = apply(widgetClass, widgetArgs, kw) componentClass = widget.__class__.__name__ self.__componentInfo[componentName] = (widget, widget.configure, componentClass, widget.cget, componentGroup) 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, dummy = None): self._initialiseoptions_counter = self._initialiseoptions_counter - 1 if self._initialiseoptions_counter == 0: unusedOptions = [] keywords = self._constructorKeywords for name in keywords.keys(): used = keywords[name][1] if not used: # This keyword argument has not been used. If it # does not refer to a dynamic group, mark it as # unused. index = string.find(name, '_') if index < 0 or name[:index] not in self._dynamicGroups: unusedOptions.append(name) if len(unusedOptions) > 0: if len(unusedOptions) == 1: text = 'Unknown option "' else: text = 'Unknown options "' raise KeyError, text + string.join(unusedOptions, ', ') + \ '" for ' + self.__class__.__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. # Make sure it is not an initialisation option. if optionInfo[option][FUNCTION] is INITOPT: raise KeyError, \ '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 and \ component not in self._dynamicGroups: raise KeyError, '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 KeyError, '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() def __setitem__(self, key, value): apply(self.configure, (), {key: value}) #====================================================================== # 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 KeyError, '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] #============================================================================= # The grab functions are mainly called by the activate() and # deactivate() methods. # # Use pushgrab() to add a new window to the grab stack. This # releases the grab by the window currently on top of the stack (if # there is one) and gives the grab and focus to the new widget. # # To remove the grab from the window on top of the grab stack, call # popgrab(). # # Use releasegrabs() to release the grab and clear the grab stack. def pushgrab(grabWindow, globalMode, deactivateFunction): prevFocus = grabWindow.tk.call('focus') grabInfo = { 'grabWindow' : grabWindow, 'globalMode' : globalMode, 'previousFocus' : prevFocus, 'deactivateFunction' : deactivateFunction, } _grabStack.append(grabInfo) _grabtop() grabWindow.focus_set() def popgrab(window): # Return the grab to the next window in the grab stack, if any. # If this window is not at the top of the grab stack, then it has # just been deleted by the window manager or deactivated by a # timer. Call the deactivate method for the modal dialog above # this one on the stack. if _grabStack[-1]['grabWindow'] != window: for index in range(len(_grabStack)): if _grabStack[index]['grabWindow'] == window: _grabStack[index + 1]['deactivateFunction']() break grabInfo = _grabStack[-1] del _grabStack[-1] topWidget = grabInfo['grabWindow'] prevFocus = grabInfo['previousFocus'] globalMode = grabInfo['globalMode'] if globalMode != 'nograb': topWidget.grab_release() if len(_grabStack) > 0: _grabtop() if prevFocus != '': try: topWidget.tk.call('focus', prevFocus) except Tkinter.TclError: # Previous focus widget has been deleted. Set focus # to root window. Tkinter._default_root.focus_set() else: # Make sure that focus does not remain on the released widget. if len(_grabStack) > 0: topWidget = _grabStack[-1]['grabWindow'] topWidget.focus_set() else: Tkinter._default_root.focus_set() def grabstacktopwindow(): if len(_grabStack) == 0: return None else: return _grabStack[-1]['grabWindow'] def releasegrabs(): # Release grab and clear the grab stack. current = Tkinter._default_root.grab_current() if current is not None: current.grab_release() _grabStack[:] = [] def _grabtop(): grabInfo = _grabStack[-1] topWidget = grabInfo['grabWindow'] globalMode = grabInfo['globalMode'] if globalMode == 'nograb': return while 1: try: if globalMode: topWidget.grab_set_global() else: topWidget.grab_set() break except Tkinter.TclError: # Another application has grab. Keep trying until # grab can succeed. topWidget.after(100) #============================================================================= class MegaToplevel(MegaArchetype): def __init__(self, parent = None, **kw): # Define the options for this megawidget. optiondefs = ( ('activatecommand', None, None), ('deactivatecommand', None, None), ('master', 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. # Set WM_DELETE_WINDOW protocol, deleting any old callback, so # memory does not leak. if hasattr(self._hull, '_Pmw_WM_DELETE_name'): self._hull.tk.deletecommand(self._hull._Pmw_WM_DELETE_name) self._hull._Pmw_WM_DELETE_name = \ self.register(self._userDeleteWindow, needcleanup = 0) self.protocol('WM_DELETE_WINDOW', self._hull._Pmw_WM_DELETE_name) # 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 self._active = 0 self._userDeleteFunc = self.destroy self._userModalDeleteFunc = self.deactivate # Check keywords and initialise options. self.initialiseoptions() 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): self.deactivate() # Remove circular references, so that object can get cleaned up. del self._userDeleteFunc del self._userModalDeleteFunc MegaArchetype.destroy(self) def show(self, master = None): if self.state() != 'normal': if self._firstShowing: # Just let the window manager determine the window # position for the first time. geom = None else: # Position the window at the same place it was last time. geom = self._sameposition() setgeometryanddeiconify(self, geom) if self._firstShowing: self._firstShowing = 0 else: if self.transient() == '': self.tkraise() # Do this last, otherwise get flashing on NT: if master is not None: if master == 'parent': parent = self.winfo_parent() # winfo_parent() should return the parent widget, but the # the current version of Tkinter returns a string. if type(parent) == types.StringType: parent = self._hull._nametowidget(parent) master = parent.winfo_toplevel() self.transient(master) self.focus() def _centreonscreen(self): # Centre the window on the screen. (Actually halfway across # and one third down.) parent = self.winfo_parent() if type(parent) == types.StringType: parent = self._hull._nametowidget(parent) # Find size of window. self.update_idletasks() width = self.winfo_width() height = self.winfo_height() if width == 1 and height == 1: # If the window has not yet been displayed, its size is # reported as 1x1, so use requested size. width = self.winfo_reqwidth() height = self.winfo_reqheight() # Place in centre of screen: x = (self.winfo_screenwidth() - width) / 2 - parent.winfo_vrootx() y = (self.winfo_screenheight() - height) / 3 - parent.winfo_vrooty() if x < 0: x = 0 if y < 0: y = 0 return '+%d+%d' % (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: return geometry[index:] else: return None def activate(self, globalMode = 0, geometry = 'centerscreenfirst'): if self._active: raise ValueError, 'Window is already active' if self.state() == 'normal': self.withdraw() self._active = 1 showbusycursor() if self._wait is None: self._wait = Tkinter.IntVar() self._wait.set(0) if geometry == 'centerscreenalways': geom = self._centreonscreen() elif geometry == 'centerscreenfirst': if self._firstShowing: # Centre the window the first time it is displayed. geom = self._centreonscreen() else: # Position the window at the same place it was last time. geom = self._sameposition() elif geometry[:5] == 'first': if self._firstShowing: geom = geometry[5:] else: # Position the window at the same place it was last time. geom = self._sameposition() else: geom = geometry self._firstShowing = 0 setgeometryanddeiconify(self, geom) # Do this last, otherwise get flashing on NT: master = self['master'] if master is not None: if master == 'parent': parent = self.winfo_parent() # winfo_parent() should return the parent widget, but the # the current version of Tkinter returns a string. if type(parent) == types.StringType: parent = self._hull._nametowidget(parent) master = parent.winfo_toplevel() self.transient(master) pushgrab(self._hull, globalMode, self.deactivate) command = self['activatecommand'] if callable(command): command() self.wait_variable(self._wait) return self._result def deactivate(self, result=None): if not self._active: return self._active = 0 # Restore the focus before withdrawing the window, since # otherwise the window manager may take the focus away so we # can't redirect it. Also, return the grab to the next active # window in the stack, if any. popgrab(self._hull) command = self['deactivatecommand'] if callable(command): command() self.withdraw() hidebusycursor(forceFocusRestore = 1) self._result = result self._wait.set(1) def active(self): return self._active 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) # Check keywords and initialise options. self.initialiseoptions() forwardmethods(MegaWidget, Tkinter.Frame, '_hull') #============================================================================= # Public functions #----------------- _traceTk = 0 def tracetk(root = None, on = 1, withStackTrace = 0, file=None): global _withStackTrace global _traceTkFile global _traceTk if root is None: root = Tkinter._default_root _withStackTrace = withStackTrace _traceTk = on if on: if hasattr(root.tk, '__class__'): # Tracing already on return if file is None: _traceTkFile = sys.stderr else: _traceTkFile = file tk = _TraceTk(root.tk) else: if not hasattr(root.tk, '__class__'): # Tracing already off return tk = root.tk.getTclInterp() _setTkInterps(root, tk) def showbusycursor(): _addRootToToplevelBusyInfo() root = Tkinter._default_root busyInfo = { 'newBusyWindows' : [], 'previousFocus' : None, 'busyFocus' : None, } _busyStack.append(busyInfo) if _disableKeyboardWhileBusy: # Remember the focus as it is now, before it is changed. busyInfo['previousFocus'] = root.tk.call('focus') if not _havebltbusy(root): # No busy command, so don't call busy hold on any windows. return for (window, winInfo) in _toplevelBusyInfo.items(): if (window.state() != 'withdrawn' and not winInfo['isBusy'] and not winInfo['excludeFromBusy']): busyInfo['newBusyWindows'].append(window) winInfo['isBusy'] = 1 _busy_hold(window, winInfo['busyCursorName']) # 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. window.tk.call('bindtags', winInfo['busyWindow'], 'Pmw_Dummy_Tag') if _disableKeyboardWhileBusy: # Remember previous focus widget for this toplevel window # and set focus to the busy window, which will ignore all # keyboard events. winInfo['windowFocus'] = \ window.tk.call('focus', '-lastfor', window._w) window.tk.call('focus', winInfo['busyWindow']) busyInfo['busyFocus'] = winInfo['busyWindow'] if len(busyInfo['newBusyWindows']) > 0: if os.name == 'nt': # NT needs an "update" before it will change the cursor. window.update() else: window.update_idletasks() def hidebusycursor(forceFocusRestore = 0): # Remember the focus as it is now, before it is changed. root = Tkinter._default_root if _disableKeyboardWhileBusy: currentFocus = root.tk.call('focus') # Pop the busy info off the stack. busyInfo = _busyStack[-1] del _busyStack[-1] for window in busyInfo['newBusyWindows']: # If this window has not been deleted, release the busy cursor. if _toplevelBusyInfo.has_key(window): winInfo = _toplevelBusyInfo[window] winInfo['isBusy'] = 0 _busy_release(window) if _disableKeyboardWhileBusy: # Restore previous focus window for this toplevel window, # but only if is still set to the busy window (it may have # been changed). windowFocusNow = window.tk.call('focus', '-lastfor', window._w) if windowFocusNow == winInfo['busyWindow']: try: window.tk.call('focus', winInfo['windowFocus']) 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 _disableKeyboardWhileBusy: # Restore the focus, depending on whether the focus had changed # between the calls to showbusycursor and hidebusycursor. if forceFocusRestore or busyInfo['busyFocus'] == currentFocus: # The focus had not changed, so restore it to as it was before # the call to showbusycursor, previousFocus = busyInfo['previousFocus'] if previousFocus is not None: try: root.tk.call('focus', previousFocus) except Tkinter.TclError: # Previous focus widget has been deleted; forget it. pass else: # The focus had changed, so restore it to what it had been # changed to before the call to hidebusycursor. root.tk.call('focus', currentFocus) def clearbusycursor(): while len(_busyStack) > 0: hidebusycursor() def setbusycursorattributes(window, **kw): _addRootToToplevelBusyInfo() for name, value in kw.items(): if name == 'exclude': _toplevelBusyInfo[window]['excludeFromBusy'] = value elif name == 'cursorName': _toplevelBusyInfo[window]['busyCursorName'] = value else: raise KeyError, 'Unknown busycursor attribute "' + name + '"' def _addRootToToplevelBusyInfo(): # Include the Tk root window in the list of toplevels. This must # not be called before Tkinter has had a chance to be initialised by # the application. root = Tkinter._default_root if root == None: root = Tkinter.Tk() if not _toplevelBusyInfo.has_key(root): _addToplevelBusyInfo(root) def busycallback(command, updateFunction = None): if not callable(command): raise ValueError, \ '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: # Print error on standard error as well as to error window. # Useful if error window fails to be displayed, for example # when exception is triggered in a <Destroy> binding for root # window. sys.stderr.write(text + '\n') if _errorWindow is None: # The error window has not yet been created. _errorWindow = _ErrorWindow() _errorWindow.showerror(text) _root = None _disableKeyboardWhileBusy = 1 def initialise( root = None, size = None, fontScheme = None, useTkOptionDb = 0, noBltBusy = 0, disableKeyboardWhileBusy = None, ): # Remember if show/hidebusycursor should ignore keyboard events. global _disableKeyboardWhileBusy if disableKeyboardWhileBusy is not None: _disableKeyboardWhileBusy = disableKeyboardWhileBusy # Do not use blt busy command if noBltBusy is set. Otherwise, # use blt busy if it is available. global _haveBltBusy if noBltBusy: _haveBltBusy = 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 # If this call is initialising a different Tk interpreter than the # last call, then re-initialise all global variables. Assume the # last interpreter has been destroyed - ie: Pmw does not (yet) # support multiple simultaneous interpreters. global _root if _root is not None and _root != root: global _busyStack global _errorWindow global _grabStack global _hullToMegaWidget global _toplevelBusyInfo _busyStack = [] _errorWindow = None _grabStack = [] _hullToMegaWidget = {} _toplevelBusyInfo = {} _root = 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.Widget.destroy = __TkinterWidgetDestroy # 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. _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 #----------------- _callToTkReturned = 1 _recursionCounter = 1 class _TraceTk: def __init__(self, tclInterp): self.tclInterp = tclInterp def getTclInterp(self): return self.tclInterp # Calling from python into Tk. def call(self, *args, **kw): global _callToTkReturned global _recursionCounter _callToTkReturned = 0 if len(args) == 1 and type(args[0]) == types.TupleType: argStr = str(args[0]) else: argStr = str(args) _traceTkFile.write('CALL TK> %d:%s%s' % (_recursionCounter, ' ' * _recursionCounter, argStr)) _recursionCounter = _recursionCounter + 1 try: result = apply(self.tclInterp.call, args, kw) except Tkinter.TclError, errorString: _callToTkReturned = 1 _recursionCounter = _recursionCounter - 1 _traceTkFile.write('\nTK ERROR> %d:%s-> %s\n' % (_recursionCounter, ' ' * _recursionCounter, repr(errorString))) if _withStackTrace: _traceTkFile.write('CALL TK> stack:\n') traceback.print_stack() raise Tkinter.TclError, errorString _recursionCounter = _recursionCounter - 1 if _callToTkReturned: _traceTkFile.write('CALL RTN> %d:%s-> %s' % (_recursionCounter, ' ' * _recursionCounter, repr(result))) else: _callToTkReturned = 1 if result: _traceTkFile.write(' -> %s' % repr(result)) _traceTkFile.write('\n') if _withStackTrace: _traceTkFile.write('CALL TK> stack:\n') traceback.print_stack() _traceTkFile.flush() 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. # Map from toplevel windows to # {'isBusy', 'windowFocus', 'busyWindow', # 'excludeFromBusy', 'busyCursorName'} _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 _addToplevelBusyInfo(window): if window._w == '.': busyWindow = '._Busy' else: busyWindow = window._w + '._Busy' _toplevelBusyInfo[window] = { 'isBusy' : 0, 'windowFocus' : None, 'busyWindow' : busyWindow, 'excludeFromBusy' : 0, 'busyCursorName' : None, } 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): _addToplevelBusyInfo(self) self._Pmw_WM_DELETE_name = self.register(self.destroy, None, 0) self.protocol('WM_DELETE_WINDOW', self._Pmw_WM_DELETE_name) return apply(Tkinter.Wm.title, (self,) + args) _haveBltBusy = None def _havebltbusy(window): global _busy_hold, _busy_release, _haveBltBusy if _haveBltBusy is None: import PmwBlt _haveBltBusy = PmwBlt.havebltbusy(window) _busy_hold = PmwBlt.busy_hold if os.name == 'nt': # There is a bug in Blt 2.4i on NT where the busy window # does not follow changes in the children of a window. # Using forget works around the problem. _busy_release = PmwBlt.busy_forget else: _busy_release = PmwBlt.busy_release return _haveBltBusy 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 #============================================================================= def drawarrow(canvas, color, direction, tag, baseOffset = 0.25, edgeOffset = 0.15): canvas.delete(tag) bw = (string.atoi(canvas['borderwidth']) + string.atoi(canvas['highlightthickness'])) width = string.atoi(canvas['width']) height = string.atoi(canvas['height']) if direction in ('up', 'down'): majorDimension = height minorDimension = width else: majorDimension = width minorDimension = height offset = round(baseOffset * majorDimension) if direction in ('down', 'right'): base = bw + offset apex = bw + majorDimension - offset else: base = bw + majorDimension - offset apex = bw + offset if minorDimension > 3 and minorDimension % 2 == 0: minorDimension = minorDimension - 1 half = int(minorDimension * (1 - 2 * edgeOffset)) / 2 low = round(bw + edgeOffset * minorDimension) middle = low + half high = low + 2 * half if direction in ('up', 'down'): coords = (low, base, high, base, middle, apex) else: coords = (base, low, base, high, apex, middle) kw = {'fill' : color, 'outline' : color, 'tag' : tag} apply(canvas.create_polygon, coords, kw) #============================================================================= # 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: # Delete the busy info structure for this toplevel (if the # window was created before initialise() was called, it # will not have any. if _toplevelBusyInfo.has_key(tkWidget): del _toplevelBusyInfo[tkWidget] if hasattr(tkWidget, '_Pmw_WM_DELETE_name'): tkWidget.tk.deletecommand(tkWidget._Pmw_WM_DELETE_name) del tkWidget._Pmw_WM_DELETE_name Tkinter.BaseWidget.destroy(tkWidget) def __TkinterWidgetDestroy(tkWidget): if _hullToMegaWidget.has_key(tkWidget): mega = _hullToMegaWidget[tkWidget] try: mega.destroy() except: _reporterror(mega.destroy, ()) else: Tkinter.BaseWidget.destroy(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 # Calling back from Tk into python. def __call__(self, *args): try: if self.subst: args = apply(self.subst, args) if _traceTk: if not _callToTkReturned: _traceTkFile.write('\n') if hasattr(self.func, 'im_class'): name = self.func.im_class.__name__ + '.' + \ self.func.__name__ else: name = self.func.__name__ if len(args) == 1 and hasattr(args[0], 'type'): # The argument to the callback is an event. eventName = _eventTypeToName[string.atoi(args[0].type)] if eventName in ('KeyPress', 'KeyRelease',): argStr = '(%s %s Event: %s)' % \ (eventName, args[0].keysym, args[0].widget) else: argStr = '(%s Event, %s)' % (eventName, args[0].widget) else: argStr = str(args) _traceTkFile.write('CALLBACK> %d:%s%s%s\n' % (_recursionCounter, ' ' * _recursionCounter, name, argStr)) _traceTkFile.flush() return apply(self.func, args) except SystemExit, msg: raise SystemExit, msg except: _reporterror(self.func, args) _eventTypeToName = { 2 : 'KeyPress', 15 : 'VisibilityNotify', 28 : 'PropertyNotify', 3 : 'KeyRelease', 16 : 'CreateNotify', 29 : 'SelectionClear', 4 : 'ButtonPress', 17 : 'DestroyNotify', 30 : 'SelectionRequest', 5 : 'ButtonRelease', 18 : 'UnmapNotify', 31 : 'SelectionNotify', 6 : 'MotionNotify', 19 : 'MapNotify', 32 : 'ColormapNotify', 7 : 'EnterNotify', 20 : 'MapRequest', 33 : 'ClientMessage', 8 : 'LeaveNotify', 21 : 'ReparentNotify', 34 : 'MappingNotify', 9 : 'FocusIn', 22 : 'ConfigureNotify', 35 : 'VirtualEvents', 10 : 'FocusOut', 23 : 'ConfigureRequest', 36 : 'ActivateNotify', 11 : 'KeymapNotify', 24 : 'GravityNotify', 37 : 'DeactivateNotify', 12 : 'Expose', 25 : 'ResizeRequest', 38 : 'MouseWheelEvent', 13 : 'GraphicsExpose', 26 : 'CirculateNotify', 14 : 'NoExpose', 27 : 'CirculateRequest', } 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) if eventNum in _eventTypeToName.keys(): msg = msg + ' Event type: %s (type num: %d)\n' % \ (_eventTypeToName[eventNum], eventNum) else: msg = msg + ' Unknown event type (type num: %d)\n' % 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: pass class _ErrorWindow: def __init__(self): self._errorQueue = [] self._errorCount = 0 self._open = 0 self._firstShowing = 1 # 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. Also, if it is not # called and many errors are generated quickly in # succession, the error window may not display errors # until the last one is generated and the interpreter # becomes idle. # XXX: remove this, since it causes omppython to go into an # infinite loop if an error occurs in an omp callback. # self._top.update_idletasks() pass else: if self._firstShowing: geom = None else: geometry = self._top.geometry() index = string.find(geometry, '+') if index >= 0: geom = geometry[index:] else: geom = None setgeometryanddeiconify(self._top, geom) if self._firstShowing: self._firstShowing = 0 else: self._top.tkraise() self._top.focus() self._updateButtons() # Release any grab, so that buttons in the error window work. releasegrabs() 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') ###################################################################### ### File: PmwDialog.py # Based on iwidgets2.2.0/dialog.itk and iwidgets2.2.0/dialogshell.itk code. # Convention: # Each dialog window should have one of these as the rightmost button: # Close Close a window which only displays information. # Cancel Close a window which may be used to change the state of # the application. import sys import types import Tkinter # A Toplevel with a ButtonBox and child site. class Dialog(MegaToplevel): def __init__(self, parent = None, **kw): # Define the megawidget options. optiondefs = ( ('buttonbox_hull_borderwidth', 1, None), ('buttonbox_hull_relief', 'raised', None), ('buttonboxpos', 's', INITOPT), ('buttons', ('OK',), self._buttons), ('command', None, None), ('dialogchildsite_borderwidth', 1, None), ('dialogchildsite_relief', 'raised', None), ('defaultbutton', None, self._defaultButton), ('master', 'parent', None), ('separatorwidth', 0, INITOPT), ) self.defineoptions(kw, optiondefs) # Initialise the base class (after defining the options). MegaToplevel.__init__(self, parent) # Create the components. oldInterior = MegaToplevel.interior(self) # Set up pack options according to the position of the button box. pos = self['buttonboxpos'] if pos not in 'nsew': raise ValueError, \ 'bad buttonboxpos option "%s": should be n, s, e, or w' \ % pos if pos in 'ns': orient = 'horizontal' fill = 'x' if pos == 'n': side = 'top' else: side = 'bottom' else: orient = 'vertical' fill = 'y' if pos == 'w': side = 'left' else: side = 'right' # Create the button box. self._buttonBox = self.createcomponent('buttonbox', (), None, ButtonBox, (oldInterior,), orient = orient) self._buttonBox.pack(side = side, fill = fill) # Create the separating line. width = self['separatorwidth'] if width > 0: self._separator = self.createcomponent('separator', (), None, Tkinter.Frame, (oldInterior,), relief = 'sunken', height = width, width = width, borderwidth = width / 2) self._separator.pack(side = side, fill = fill) # Create the child site. self.__dialogChildSite = self.createcomponent('dialogchildsite', (), None, Tkinter.Frame, (oldInterior,)) self.__dialogChildSite.pack(side=side, fill='both', expand=1) self.oldButtons = () self.oldDefault = None self.bind('<Return>', self._invokeDefault) self.userdeletefunc(self._doCommand) self.usermodaldeletefunc(self._doCommand) # Check keywords and initialise options. self.initialiseoptions() def interior(self): return self.__dialogChildSite def invoke(self, index = DEFAULT): return self._buttonBox.invoke(index) def _invokeDefault(self, event): try: self._buttonBox.index(DEFAULT) except ValueError: return self._buttonBox.invoke() def _doCommand(self, name = None): if name is not None and self.active() and \ grabstacktopwindow() != self.component('hull'): # This is a modal dialog but is not on the top of the grab # stack (ie: should not have the grab), so ignore this # event. This seems to be a bug in Tk and may occur in # nested modal dialogs. # # An example is the PromptDialog demonstration. To # trigger the problem, start the demo, then move the mouse # to the main window, hit <TAB> and then <TAB> again. The # highlight border of the "Show prompt dialog" button # should now be displayed. Now hit <SPACE>, <RETURN>, # <RETURN> rapidly several times. Eventually, hitting the # return key invokes the password dialog "OK" button even # though the confirm dialog is active (and therefore # should have the keyboard focus). Observed under Solaris # 2.5.1, python 1.5.2 and Tk8.0. # TODO: Give focus to the window on top of the grabstack. return command = self['command'] if callable(command): return command(name) else: if self.active(): self.deactivate(name) else: self.withdraw() def _buttons(self): buttons = self['buttons'] if type(buttons) != types.TupleType and type(buttons) != types.ListType: raise ValueError, \ 'bad buttons option "%s": should be a tuple' % str(buttons) if self.oldButtons == buttons: return self.oldButtons = buttons for index in range(self._buttonBox.numbuttons()): self._buttonBox.delete(0) for name in buttons: self._buttonBox.add(name, command=lambda self=self, name=name: self._doCommand(name)) if len(buttons) > 0: defaultbutton = self['defaultbutton'] if defaultbutton is None: self._buttonBox.setdefault(None) else: try: self._buttonBox.index(defaultbutton) except ValueError: pass else: self._buttonBox.setdefault(defaultbutton) self._buttonBox.alignbuttons() def _defaultButton(self): defaultbutton = self['defaultbutton'] if self.oldDefault == defaultbutton: return self.oldDefault = defaultbutton if len(self['buttons']) > 0: if defaultbutton is None: self._buttonBox.setdefault(None) else: try: self._buttonBox.index(defaultbutton) except ValueError: pass else: self._buttonBox.setdefault(defaultbutton) ###################################################################### ### File: PmwTimeFuncs.py # Functions for dealing with dates and times. import re import string def timestringtoseconds(text, separator = ':'): inputList = string.split(string.strip(text), separator) if len(inputList) != 3: raise ValueError, 'invalid value: ' + text sign = 1 if len(inputList[0]) > 0 and inputList[0][0] in ('+', '-'): if inputList[0][0] == '-': sign = -1 inputList[0] = inputList[0][1:] if re.search('[^0-9]', string.join(inputList, '')) is not None: raise ValueError, 'invalid value: ' + text hour = string.atoi(inputList[0]) minute = string.atoi(inputList[1]) second = string.atoi(inputList[2]) if minute >= 60 or second >= 60: raise ValueError, 'invalid value: ' + text return sign * (hour * 60 * 60 + minute * 60 + second) _year_pivot = 50 _century = 2000 def setyearpivot(pivot, century = None): global _year_pivot global _century oldvalues = (_year_pivot, _century) _year_pivot = pivot if century is not None: _century = century return oldvalues def datestringtojdn(text, format = 'ymd', separator = '/'): inputList = string.split(string.strip(text), separator) if len(inputList) != 3: raise ValueError, 'invalid value: ' + text if re.search('[^0-9]', string.join(inputList, '')) is not None: raise ValueError, 'invalid value: ' + text formatList = list(format) day = string.atoi(inputList[formatList.index('d')]) month = string.atoi(inputList[formatList.index('m')]) year = string.atoi(inputList[formatList.index('y')]) if _year_pivot is not None: if year >= 0 and year < 100: if year <= _year_pivot: year = year + _century else: year = year + _century - 100 jdn = ymdtojdn(year, month, day) if jdntoymd(jdn) != (year, month, day): raise ValueError, 'invalid value: ' + text return jdn def _cdiv(a, b): # Return a / b as calculated by most C language implementations, # assuming both a and b are integers. if a * b > 0: return a / b else: return -(abs(a) / abs(b)) def ymdtojdn(year, month, day, julian = -1, papal = 1): # set Julian flag if auto set if julian < 0: if papal: # Pope Gregory XIII's decree lastJulianDate = 15821004L # last day to use Julian calendar else: # British-American usage lastJulianDate = 17520902L # last day to use Julian calendar julian = ((year * 100L) + month) * 100 + day <= lastJulianDate if year < 0: # Adjust BC year year = year + 1 if julian: return 367L * year - _cdiv(7 * (year + 5001L + _cdiv((month - 9), 7)), 4) + \ _cdiv(275 * month, 9) + day + 1729777L else: return (day - 32076L) + \ _cdiv(1461L * (year + 4800L + _cdiv((month - 14), 12)), 4) + \ _cdiv(367 * (month - 2 - _cdiv((month - 14), 12) * 12), 12) - \ _cdiv((3 * _cdiv((year + 4900L + _cdiv((month - 14), 12)), 100)), 4) + \ 1 # correction by rdg def jdntoymd(jdn, julian = -1, papal = 1): # set Julian flag if auto set if julian < 0: if papal: # Pope Gregory XIII's decree lastJulianJdn = 2299160L # last jdn to use Julian calendar else: # British-American usage lastJulianJdn = 2361221L # last jdn to use Julian calendar julian = (jdn <= lastJulianJdn); x = jdn + 68569L if julian: x = x + 38 daysPer400Years = 146100L fudgedDaysPer4000Years = 1461000L + 1 else: daysPer400Years = 146097L fudgedDaysPer4000Years = 1460970L + 31 z = _cdiv(4 * x, daysPer400Years) x = x - _cdiv((daysPer400Years * z + 3), 4) y = _cdiv(4000 * (x + 1), fudgedDaysPer4000Years) x = x - _cdiv(1461 * y, 4) + 31 m = _cdiv(80 * x, 2447) d = x - _cdiv(2447 * m, 80) x = _cdiv(m, 11) m = m + 2 - 12 * x y = 100 * (z - 49) + y + x # Convert from longs to integers. yy = int(y) mm = int(m) dd = int(d) if yy <= 0: # Adjust BC years. yy = yy - 1 return (yy, mm, dd) def stringtoreal(text, separator = '.'): if separator != '.': if string.find(text, '.') >= 0: raise ValueError, 'invalid value: ' + text index = string.find(text, separator) if index >= 0: text = text[:index] + '.' + text[index + 1:] return string.atof(text) ###################################################################### ### File: PmwBalloon.py import os import string import Tkinter class Balloon(MegaToplevel): def __init__(self, parent = None, **kw): # Define the megawidget options. optiondefs = ( ('initwait', 500, None), # milliseconds ('label_background', 'lightyellow', None), ('label_foreground', 'black', None), ('label_justify', 'left', None), ('master', 'parent', None), ('relmouse', 'none', self._relmouse), ('state', 'both', self._state), ('statuscommand', None, None), ('xoffset', 20, None), # pixels ('yoffset', 1, None), # pixels ('hull_highlightthickness', 1, None), ('hull_highlightbackground', 'black', None), ) self.defineoptions(kw, optiondefs) # Initialise the base class (after defining the options). MegaToplevel.__init__(self, parent) self.withdraw() self.overrideredirect(1) # Create the components. interior = self.interior() self._label = self.createcomponent('label', (), None, Tkinter.Label, (interior,)) self._label.pack() # The default hull configuration options give a black border # around the balloon, but avoids a black 'flash' when the # balloon is deiconified, before the text appears. if not kw.has_key('hull_background'): self.configure(hull_background = \ str(self._label.cget('background'))) # Initialise instance variables. self._timer = None # The widget or item that is currently triggering the balloon. # It is None if the balloon is not being displayed. It is a # one-tuple if the balloon is being displayed in response to a # widget binding (value is the widget). It is a two-tuple if # the balloon is being displayed in response to a canvas or # text item binding (value is the widget and the item). self._currentTrigger = None # Check keywords and initialise options. self.initialiseoptions() def destroy(self): if self._timer is not None: self.after_cancel(self._timer) self._timer = None MegaToplevel.destroy(self) def bind(self, widget, balloonHelp, statusHelp = None): # If a previous bind for this widget exists, remove it. self.unbind(widget) if balloonHelp is None and statusHelp is None: return if statusHelp is None: statusHelp = balloonHelp enterId = widget.bind('<Enter>', lambda event, self = self, w = widget, sHelp = statusHelp, bHelp = balloonHelp: self._enter(event, w, sHelp, bHelp, 0)) # Set Motion binding so that if the pointer remains at rest # within the widget until the status line removes the help and # then the pointer moves again, then redisplay the help in the # status line. # Note: The Motion binding only works for basic widgets, and # the hull of megawidgets but not for other megawidget components. motionId = widget.bind('<Motion>', lambda event = None, self = self, statusHelp = statusHelp: self.showstatus(statusHelp)) leaveId = widget.bind('<Leave>', self._leave) buttonId = widget.bind('<ButtonPress>', self._buttonpress) # Set Destroy binding so that the balloon can be withdrawn and # the timer can be cancelled if the widget is destroyed. destroyId = widget.bind('<Destroy>', self._destroy) # Use the None item in the widget's private Pmw dictionary to # store the widget's bind callbacks, for later clean up. if not hasattr(widget, '_Pmw_BalloonBindIds'): widget._Pmw_BalloonBindIds = {} widget._Pmw_BalloonBindIds[None] = \ (enterId, motionId, leaveId, buttonId, destroyId) def unbind(self, widget): if hasattr(widget, '_Pmw_BalloonBindIds'): if widget._Pmw_BalloonBindIds.has_key(None): (enterId, motionId, leaveId, buttonId, destroyId) = \ widget._Pmw_BalloonBindIds[None] # Need to pass in old bindings, so that Tkinter can # delete the commands. Otherwise, memory is leaked. widget.unbind('<Enter>', enterId) widget.unbind('<Motion>', motionId) widget.unbind('<Leave>', leaveId) widget.unbind('<ButtonPress>', buttonId) widget.unbind('<Destroy>', destroyId) del widget._Pmw_BalloonBindIds[None] if self._currentTrigger is not None and len(self._currentTrigger) == 1: # The balloon is currently being displayed and the current # trigger is a widget. triggerWidget = self._currentTrigger[0] if triggerWidget == widget: if self._timer is not None: self.after_cancel(self._timer) self._timer = None self.withdraw() self.clearstatus() self._currentTrigger = None def tagbind(self, widget, tagOrItem, balloonHelp, statusHelp = None): # If a previous bind for this widget's tagOrItem exists, remove it. self.tagunbind(widget, tagOrItem) if balloonHelp is None and statusHelp is None: return if statusHelp is None: statusHelp = balloonHelp enterId = widget.tag_bind(tagOrItem, '<Enter>', lambda event, self = self, w = widget, sHelp = statusHelp, bHelp = balloonHelp: self._enter(event, w, sHelp, bHelp, 1)) motionId = widget.tag_bind(tagOrItem, '<Motion>', lambda event = None, self = self, statusHelp = statusHelp: self.showstatus(statusHelp)) leaveId = widget.tag_bind(tagOrItem, '<Leave>', self._leave) buttonId = widget.tag_bind(tagOrItem, '<ButtonPress>', self._buttonpress) # Use the tagOrItem item in the widget's private Pmw dictionary to # store the tagOrItem's bind callbacks, for later clean up. if not hasattr(widget, '_Pmw_BalloonBindIds'): widget._Pmw_BalloonBindIds = {} widget._Pmw_BalloonBindIds[tagOrItem] = \ (enterId, motionId, leaveId, buttonId) def tagunbind(self, widget, tagOrItem): if hasattr(widget, '_Pmw_BalloonBindIds'): if widget._Pmw_BalloonBindIds.has_key(tagOrItem): (enterId, motionId, leaveId, buttonId) = \ widget._Pmw_BalloonBindIds[tagOrItem] widget.tag_unbind(tagOrItem, '<Enter>', enterId) widget.tag_unbind(tagOrItem, '<Motion>', motionId) widget.tag_unbind(tagOrItem, '<Leave>', leaveId) widget.tag_unbind(tagOrItem, '<ButtonPress>', buttonId) del widget._Pmw_BalloonBindIds[tagOrItem] if self._currentTrigger is None: # The balloon is not currently being displayed. return if len(self._currentTrigger) == 1: # The current trigger is a widget. return if len(self._currentTrigger) == 2: # The current trigger is a canvas item. (triggerWidget, triggerItem) = self._currentTrigger if triggerWidget == widget and triggerItem == tagOrItem: if self._timer is not None: self.after_cancel(self._timer) self._timer = None self.withdraw() self.clearstatus() self._currentTrigger = None else: # The current trigger is a text item. (triggerWidget, x, y) = self._currentTrigger if triggerWidget == widget: currentPos = widget.index('@%d,%d' % (x, y)) currentTags = widget.tag_names(currentPos) if tagOrItem in currentTags: if self._timer is not None: self.after_cancel(self._timer) self._timer = None self.withdraw() self.clearstatus() self._currentTrigger = None def showstatus(self, statusHelp): if self['state'] in ('status', 'both'): cmd = self['statuscommand'] if callable(cmd): cmd(statusHelp) def clearstatus(self): self.showstatus(None) def _state(self): if self['state'] not in ('both', 'balloon', 'status', 'none'): raise ValueError, 'bad state option ' + repr(self['state']) + \ ': should be one of \'both\', \'balloon\', ' + \ '\'status\' or \'none\'' def _relmouse(self): if self['relmouse'] not in ('both', 'x', 'y', 'none'): raise ValueError, 'bad relmouse option ' + repr(self['relmouse'])+ \ ': should be one of \'both\', \'x\', ' + '\'y\' or \'none\'' def _enter(self, event, widget, statusHelp, balloonHelp, isItem): # Do not display balloon if mouse button is pressed. This # will only occur if the button was pressed inside a widget, # then the mouse moved out of and then back into the widget, # with the button still held down. The number 0x1f00 is the # button mask for the 5 possible buttons in X. buttonPressed = (event.state & 0x1f00) != 0 if not buttonPressed and balloonHelp is not None and \ self['state'] in ('balloon', 'both'): if self._timer is not None: self.after_cancel(self._timer) self._timer = None self._timer = self.after(self['initwait'], lambda self = self, widget = widget, help = balloonHelp, isItem = isItem: self._showBalloon(widget, help, isItem)) if isItem: if hasattr(widget, 'canvasx'): # The widget is a canvas. item = widget.find_withtag('current') if len(item) > 0: item = item[0] else: item = None self._currentTrigger = (widget, item) else: # The widget is a text widget. self._currentTrigger = (widget, event.x, event.y) else: self._currentTrigger = (widget,) self.showstatus(statusHelp) def _leave(self, event): if self._timer is not None: self.after_cancel(self._timer) self._timer = None self.withdraw() self.clearstatus() self._currentTrigger = None def _destroy(self, event): # Only withdraw the balloon and cancel the timer if the widget # being destroyed is the widget that triggered the balloon. # Note that in a Tkinter Destroy event, the widget field is a # string and not a widget as usual. if self._currentTrigger is None: # The balloon is not currently being displayed return if len(self._currentTrigger) == 1: # The current trigger is a widget (not an item) triggerWidget = self._currentTrigger[0] if str(triggerWidget) == event.widget: if self._timer is not None: self.after_cancel(self._timer) self._timer = None self.withdraw() self.clearstatus() self._currentTrigger = None def _buttonpress(self, event): if self._timer is not None: self.after_cancel(self._timer) self._timer = None self.withdraw() self._currentTrigger = None def _showBalloon(self, widget, balloonHelp, isItem): self._label.configure(text = balloonHelp) # First, display the balloon offscreen to get dimensions. screenWidth = self.winfo_screenwidth() screenHeight = self.winfo_screenheight() self.geometry('+%d+0' % (screenWidth + 1)) self.update_idletasks() if isItem: # Get the bounding box of the current item. bbox = widget.bbox('current') if bbox is None: # The item that triggered the balloon has disappeared, # perhaps by a user's timer event that occured between # the <Enter> event and the 'initwait' timer calling # this method. return # The widget is either a text or canvas. The meaning of # the values returned by the bbox method is different for # each, so use the existence of the 'canvasx' method to # distinguish between them. if hasattr(widget, 'canvasx'): # The widget is a canvas. Place balloon under canvas # item. The positions returned by bbox are relative # to the entire canvas, not just the visible part, so # need to convert to window coordinates. leftrel = bbox[0] - widget.canvasx(0) toprel = bbox[1] - widget.canvasy(0) bottomrel = bbox[3] - widget.canvasy(0) else: # The widget is a text widget. Place balloon under # the character closest to the mouse. The positions # returned by bbox are relative to the text widget # window (ie the visible part of the text only). leftrel = bbox[0] toprel = bbox[1] bottomrel = bbox[1] + bbox[3] else: leftrel = 0 toprel = 0 bottomrel = widget.winfo_height() xpointer, ypointer = widget.winfo_pointerxy() # -1 if off screen if xpointer >= 0 and self['relmouse'] in ('both', 'x'): x = xpointer else: x = leftrel + widget.winfo_rootx() x = x + self['xoffset'] if ypointer >= 0 and self['relmouse'] in ('both', 'y'): y = ypointer else: y = bottomrel + widget.winfo_rooty() y = y + self['yoffset'] edges = (string.atoi(str(self.cget('hull_highlightthickness'))) + string.atoi(str(self.cget('hull_borderwidth')))) * 2 if x + self._label.winfo_reqwidth() + edges > screenWidth: x = screenWidth - self._label.winfo_reqwidth() - edges if y + self._label.winfo_reqheight() + edges > screenHeight: if ypointer >= 0 and self['relmouse'] in ('both', 'y'): y = ypointer else: y = toprel + widget.winfo_rooty() y = y - self._label.winfo_reqheight() - self['yoffset'] - edges setgeometryanddeiconify(self, '+%d+%d' % (x, y)) ###################################################################### ### File: PmwButtonBox.py # Based on iwidgets2.2.0/buttonbox.itk code. import types import Tkinter class ButtonBox(MegaWidget): def __init__(self, parent = None, **kw): # Define the megawidget options. optiondefs = ( ('labelmargin', 0, INITOPT), ('labelpos', None, INITOPT), ('orient', 'horizontal', INITOPT), ('padx', 3, INITOPT), ('pady', 3, INITOPT), ) self.defineoptions(kw, optiondefs, dynamicGroups = ('Button',)) # Initialise the base class (after defining the options). MegaWidget.__init__(self, parent) # Create the components. interior = self.interior() if self['labelpos'] is None: self._buttonBoxFrame = self._hull columnOrRow = 0 else: self._buttonBoxFrame = self.createcomponent('frame', (), None, Tkinter.Frame, (interior,)) self._buttonBoxFrame.grid(column=2, row=2, sticky='nsew') columnOrRow = 2 self.createlabel(interior) orient = self['orient'] if orient == 'horizontal': interior.grid_columnconfigure(columnOrRow, weight = 1) elif orient == 'vertical': interior.grid_rowconfigure(columnOrRow, weight = 1) else: raise ValueError, 'bad orient option ' + repr(orient) + \ ': must be either \'horizontal\' or \'vertical\'' # Initialise instance variables. # List of tuples describing the buttons: # - name # - button widget self._buttonList = [] # The index of the default button. self._defaultButton = None self._timerId = None # Check keywords and initialise options. self.initialiseoptions() def destroy(self): if self._timerId: self.after_cancel(self._timerId) self._timerId = None MegaWidget.destroy(self) def numbuttons(self): return len(self._buttonList) def index(self, index, forInsert = 0): listLength = len(self._buttonList) if type(index) == types.IntType: if forInsert and index <= listLength: return index elif not forInsert and index < listLength: return index else: raise ValueError, 'index "%s" is out of range' % index elif index is END: if forInsert: return listLength elif listLength > 0: return listLength - 1 else: raise ValueError, 'ButtonBox has no buttons' elif index is DEFAULT: if self._defaultButton is not None: return self._defaultButton raise ValueError, 'ButtonBox has no default' else: names = map(lambda t: t[0], self._buttonList) if index in names: return names.index(index) validValues = 'a name, a number, END or DEFAULT' raise ValueError, \ 'bad index "%s": must be %s' % (index, validValues) def insert(self, componentName, beforeComponent = 0, **kw): if componentName in self.components(): raise ValueError, 'button "%s" already exists' % componentName if not kw.has_key('text'): kw['text'] = componentName kw['default'] = 'normal' button = apply(self.createcomponent, (componentName, (), 'Button', Tkinter.Button, (self._buttonBoxFrame,)), kw) index = self.index(beforeComponent, 1) horizontal = self['orient'] == 'horizontal' numButtons = len(self._buttonList) # Shift buttons up one position. for i in range(numButtons - 1, index - 1, -1): widget = self._buttonList[i][1] pos = i * 2 + 3 if horizontal: widget.grid(column = pos, row = 0) else: widget.grid(column = 0, row = pos) # Display the new button. if horizontal: button.grid(column = index * 2 + 1, row = 0, sticky = 'ew', padx = self['padx'], pady = self['pady']) self._buttonBoxFrame.grid_columnconfigure( numButtons * 2 + 2, weight = 1) else: button.grid(column = 0, row = index * 2 + 1, sticky = 'ew', padx = self['padx'], pady = self['pady']) self._buttonBoxFrame.grid_rowconfigure( numButtons * 2 + 2, weight = 1) self._buttonList.insert(index, (componentName, button)) return button def add(self, componentName, **kw): return apply(self.insert, (componentName, len(self._buttonList)), kw) def delete(self, index): index = self.index(index) (name, widget) = self._buttonList[index] widget.grid_forget() self.destroycomponent(name) numButtons = len(self._buttonList) # Shift buttons down one position. horizontal = self['orient'] == 'horizontal' for i in range(index + 1, numButtons): widget = self._buttonList[i][1] pos = i * 2 - 1 if horizontal: widget.grid(column = pos, row = 0) else: widget.grid(column = 0, row = pos) if horizontal: self._buttonBoxFrame.grid_columnconfigure(numButtons * 2 - 1, minsize = 0) self._buttonBoxFrame.grid_columnconfigure(numButtons * 2, weight = 0) else: self._buttonBoxFrame.grid_rowconfigure(numButtons * 2, weight = 0) del self._buttonList[index] def setdefault(self, index): # Turn off the default ring around the current default button. if self._defaultButton is not None: button = self._buttonList[self._defaultButton][1] button.configure(default = 'normal') self._defaultButton = None # Turn on the default ring around the new default button. if index is not None: index = self.index(index) self._defaultButton = index button = self._buttonList[index][1] button.configure(default = 'active') def invoke(self, index = DEFAULT, noFlash = 0): # Invoke the callback associated with the *index* button. If # *noFlash* is not set, flash the button to indicate to the # user that something happened. button = self._buttonList[self.index(index)][1] if not noFlash: state = button.cget('state') relief = button.cget('relief') button.configure(state = 'active', relief = 'sunken') self.update_idletasks() self.after(100) button.configure(state = state, relief = relief) return button.invoke() def button(self, buttonIndex): return self._buttonList[self.index(buttonIndex)][1] def alignbuttons(self, when = 'later'): if when == 'later': if not self._timerId: self._timerId = self.after_idle(self.alignbuttons, 'now') return self.update_idletasks() self._timerId = None # Determine the width of the maximum length button. max = 0 horizontal = (self['orient'] == 'horizontal') for index in range(len(self._buttonList)): gridIndex = index * 2 + 1 if horizontal: width = self._buttonBoxFrame.grid_bbox(gridIndex, 0)[2] else: width = self._buttonBoxFrame.grid_bbox(0, gridIndex)[2] if width > max: max = width # Set the width of all the buttons to be the same. if horizontal: for index in range(len(self._buttonList)): self._buttonBoxFrame.grid_columnconfigure(index * 2 + 1, minsize = max) else: self._buttonBoxFrame.grid_columnconfigure(0, minsize = max) ###################################################################### ### File: PmwEntryField.py # Based on iwidgets2.2.0/entryfield.itk code. import re import string import types import Tkinter # Possible return values of validation functions. OK = 1 ERROR = 0 PARTIAL = -1 class EntryField(MegaWidget): _classBindingsDefinedFor = 0 def __init__(self, parent = None, **kw): # Define the megawidget options. optiondefs = ( ('command', None, None), ('errorbackground', 'pink', None), ('invalidcommand', self.bell, None), ('labelmargin', 0, INITOPT), ('labelpos', None, INITOPT), ('modifiedcommand', None, None), ('sticky', 'ew', INITOPT), ('validate', None, self._validate), ('extravalidators', {}, None), ('value', '', INITOPT), ) self.defineoptions(kw, optiondefs) # Initialise the base class (after defining the options). MegaWidget.__init__(self, parent) # Create the components. interior = self.interior() self._entryFieldEntry = self.createcomponent('entry', (), None, Tkinter.Entry, (interior,)) self._entryFieldEntry.grid(column=2, row=2, sticky=self['sticky']) if self['value'] != '': self.__setEntry(self['value']) interior.grid_columnconfigure(2, weight=1) interior.grid_rowconfigure(2, weight=1) self.createlabel(interior) # Initialise instance variables. self.normalBackground = None self._previousText = None # Initialise instance. _registerEntryField(self._entryFieldEntry, self) # Establish the special class bindings if not already done. # Also create bindings if the Tkinter default interpreter has # changed. Use Tkinter._default_root to create class # bindings, so that a reference to root is created by # bind_class rather than a reference to self, which would # prevent object cleanup. if EntryField._classBindingsDefinedFor != Tkinter._default_root: tagList = self._entryFieldEntry.bindtags() root = Tkinter._default_root allSequences = {} for tag in tagList: sequences = root.bind_class(tag) if type(sequences) is types.StringType: # In old versions of Tkinter, bind_class returns a string sequences = root.tk.splitlist(sequences) for sequence in sequences: allSequences[sequence] = None for sequence in allSequences.keys(): root.bind_class('EntryFieldPre', sequence, _preProcess) root.bind_class('EntryFieldPost', sequence, _postProcess) EntryField._classBindingsDefinedFor = root self._entryFieldEntry.bindtags(('EntryFieldPre',) + self._entryFieldEntry.bindtags() + ('EntryFieldPost',)) self._entryFieldEntry.bind('<Return>', self._executeCommand) # Check keywords and initialise options. self.initialiseoptions() def destroy(self): _deregisterEntryField(self._entryFieldEntry) MegaWidget.destroy(self) def _getValidatorFunc(self, validator, index): # Search the extra and standard validator lists for the # given 'validator'. If 'validator' is an alias, then # continue the search using the alias. Make sure that # self-referencial aliases do not cause infinite loops. extraValidators = self['extravalidators'] traversedValidators = [] while 1: traversedValidators.append(validator) if extraValidators.has_key(validator): validator = extraValidators[validator][index] elif _standardValidators.has_key(validator): validator = _standardValidators[validator][index] else: return validator if validator in traversedValidators: return validator def _validate(self): dict = { 'validator' : None, 'min' : None, 'max' : None, 'minstrict' : 1, 'maxstrict' : 1, } opt = self['validate'] if type(opt) is types.DictionaryType: dict.update(opt) else: dict['validator'] = opt # Look up validator maps and replace 'validator' field with # the corresponding function. validator = dict['validator'] valFunction = self._getValidatorFunc(validator, 0) self._checkValidateFunction(valFunction, 'validate', validator) dict['validator'] = valFunction # Look up validator maps and replace 'stringtovalue' field # with the corresponding function. if dict.has_key('stringtovalue'): stringtovalue = dict['stringtovalue'] strFunction = self._getValidatorFunc(stringtovalue, 1) self._checkValidateFunction( strFunction, 'stringtovalue', stringtovalue) else: strFunction = self._getValidatorFunc(validator, 1) if strFunction == validator: strFunction = len dict['stringtovalue'] = strFunction self._validationInfo = dict args = dict.copy() del args['validator'] del args['min'] del args['max'] del args['minstrict'] del args['maxstrict'] del args['stringtovalue'] self._validationArgs = args self._previousText = None if type(dict['min']) == types.StringType and strFunction is not None: dict['min'] = apply(strFunction, (dict['min'],), args) if type(dict['max']) == types.StringType and strFunction is not None: dict['max'] = apply(strFunction, (dict['max'],), args) self._checkValidity() def _checkValidateFunction(self, function, option, validator): # Raise an error if 'function' is not a function or None. if function is not None and not callable(function): extraValidators = self['extravalidators'] extra = extraValidators.keys() extra.sort() extra = tuple(extra) standard = _standardValidators.keys() standard.sort() standard = tuple(standard) msg = 'bad %s value "%s": must be a function or one of ' \ 'the standard validators %s or extra validators %s' raise ValueError, msg % (option, validator, standard, extra) def _executeCommand(self, event = None): cmd = self['command'] if callable(cmd): if event is None: # Return result of command for invoke() method. return cmd() else: cmd() def _preProcess(self): self._previousText = self._entryFieldEntry.get() self._previousICursor = self._entryFieldEntry.index('insert') self._previousXview = self._entryFieldEntry.index('@0') if self._entryFieldEntry.selection_present(): self._previousSel= (self._entryFieldEntry.index('sel.first'), self._entryFieldEntry.index('sel.last')) else: self._previousSel = None def _postProcess(self): # No need to check if text has not changed. previousText = self._previousText if previousText == self._entryFieldEntry.get(): return self.valid() valid = self._checkValidity() if self.hulldestroyed(): # The invalidcommand called by _checkValidity() destroyed us. return valid cmd = self['modifiedcommand'] if callable(cmd) and previousText != self._entryFieldEntry.get(): cmd() return valid def checkentry(self): # If there is a variable specified by the entry_textvariable # option, checkentry() should be called after the set() method # of the variable is called. self._previousText = None return self._postProcess() def _getValidity(self): text = self._entryFieldEntry.get() dict = self._validationInfo args = self._validationArgs if dict['validator'] is not None: status = apply(dict['validator'], (text,), args) if status != OK: return status # Check for out of (min, max) range. if dict['stringtovalue'] is not None: min = dict['min'] max = dict['max'] if min is None and max is None: return OK val = apply(dict['stringtovalue'], (text,), args) if min is not None and val < min: if dict['minstrict']: return ERROR else: return PARTIAL if max is not None and val > max: if dict['maxstrict']: return ERROR else: return PARTIAL return OK def _checkValidity(self): valid = self._getValidity() oldValidity = valid if valid == ERROR: # The entry is invalid. cmd = self['invalidcommand'] if callable(cmd): cmd() if self.hulldestroyed(): # The invalidcommand destroyed us. return oldValidity # Restore the entry to its previous value. if self._previousText is not None: self.__setEntry(self._previousText) self._entryFieldEntry.icursor(self._previousICursor) self._entryFieldEntry.xview(self._previousXview) if self._previousSel is not None: self._entryFieldEntry.selection_range(self._previousSel[0], self._previousSel[1]) # Check if the saved text is valid as well. valid = self._getValidity() self._valid = valid if self.hulldestroyed(): # The validator or stringtovalue commands called by # _checkValidity() destroyed us. return oldValidity if valid == OK: if self.normalBackground is not None: self._entryFieldEntry.configure( background = self.normalBackground) self.normalBackground = None else: if self.normalBackground is None: self.normalBackground = self._entryFieldEntry.cget('background') self._entryFieldEntry.configure( background = self['errorbackground']) return oldValidity def invoke(self): return self._executeCommand() def valid(self): return self._valid == OK def clear(self): self.setentry('') def __setEntry(self, text): oldState = str(self._entryFieldEntry.cget('state')) if oldState != 'normal': self._entryFieldEntry.configure(state='normal') self._entryFieldEntry.delete(0, 'end') self._entryFieldEntry.insert(0, text) if oldState != 'normal': self._entryFieldEntry.configure(state=oldState) def setentry(self, text): self._preProcess() self.__setEntry(text) return self._postProcess() def getvalue(self): return self._entryFieldEntry.get() def setvalue(self, text): return self.setentry(text) forwardmethods(EntryField, Tkinter.Entry, '_entryFieldEntry') # ====================================================================== # Entry field validation functions _numericregex = re.compile('^[0-9]*$') _alphabeticregex = re.compile('^[a-z]*$', re.IGNORECASE) _alphanumericregex = re.compile('^[0-9a-z]*$', re.IGNORECASE) def numericvalidator(text): if text == '': return PARTIAL else: if _numericregex.match(text) is None: return ERROR else: return OK def integervalidator(text): if text in ('', '-', '+'): return PARTIAL try: string.atol(text) return OK except ValueError: return ERROR def alphabeticvalidator(text): if _alphabeticregex.match(text) is None: return ERROR else: return OK def alphanumericvalidator(text): if _alphanumericregex.match(text) is None: return ERROR else: return OK def hexadecimalvalidator(text): if text in ('', '0x', '0X', '+', '+0x', '+0X', '-', '-0x', '-0X'): return PARTIAL try: string.atol(text, 16) return OK except ValueError: return ERROR def realvalidator(text, separator = '.'): if separator != '.': if string.find(text, '.') >= 0: return ERROR index = string.find(text, separator) if index >= 0: text = text[:index] + '.' + text[index + 1:] try: string.atof(text) return OK except ValueError: # Check if the string could be made valid by appending a digit # eg ('-', '+', '.', '-.', '+.', '1.23e', '1E-'). if len(text) == 0: return PARTIAL if text[-1] in string.digits: return ERROR try: string.atof(text + '0') return PARTIAL except ValueError: return ERROR def timevalidator(text, separator = ':'): try: timestringtoseconds(text, separator) return OK except ValueError: if len(text) > 0 and text[0] in ('+', '-'): text = text[1:] if re.search('[^0-9' + separator + ']', text) is not None: return ERROR return PARTIAL def datevalidator(text, format = 'ymd', separator = '/'): try: datestringtojdn(text, format, separator) return OK except ValueError: if re.search('[^0-9' + separator + ']', text) is not None: return ERROR return PARTIAL _standardValidators = { 'numeric' : (numericvalidator, string.atol), 'integer' : (integervalidator, string.atol), 'hexadecimal' : (hexadecimalvalidator, lambda s: string.atol(s, 16)), 'real' : (realvalidator, stringtoreal), 'alphabetic' : (alphabeticvalidator, len), 'alphanumeric' : (alphanumericvalidator, len), 'time' : (timevalidator, timestringtoseconds), 'date' : (datevalidator, datestringtojdn), } _entryCache = {} def _registerEntryField(entry, entryField): # Register an EntryField widget for an Entry widget _entryCache[entry] = entryField def _deregisterEntryField(entry): # Deregister an Entry widget del _entryCache[entry] def _preProcess(event): # Forward preprocess events for an Entry to it's EntryField _entryCache[event.widget]._preProcess() def _postProcess(event): # Forward postprocess events for an Entry to it's EntryField # The function specified by the 'command' option may have destroyed # the megawidget in a binding earlier in bindtags, so need to check. if _entryCache.has_key(event.widget): _entryCache[event.widget]._postProcess() ###################################################################### ### File: PmwGroup.py import string import Tkinter def aligngrouptags(groups): # Adjust the y position of the tags in /groups/ so that they all # have the height of the highest tag. maxTagHeight = 0 for group in groups: if group._tag is None: height = (string.atoi(str(group._ring.cget('borderwidth'))) + string.atoi(str(group._ring.cget('highlightthickness')))) else: height = group._tag.winfo_reqheight() if maxTagHeight < height: maxTagHeight = height for group in groups: ringBorder = (string.atoi(str(group._ring.cget('borderwidth'))) + string.atoi(str(group._ring.cget('highlightthickness')))) topBorder = maxTagHeight / 2 - ringBorder / 2 group._hull.grid_rowconfigure(0, minsize = topBorder) group._ring.grid_rowconfigure(0, minsize = maxTagHeight - topBorder - ringBorder) if group._tag is not None: group._tag.place(y = maxTagHeight / 2) class Group( MegaWidget ): def __init__(self, parent = None, **kw): # Define the megawidget options. optiondefs = ( ('collapsedsize', 6, INITOPT), ('ring_borderwidth', 2, None), ('ring_relief', 'groove', None), ('tagindent', 10, INITOPT), ) self.defineoptions(kw, optiondefs) # Initialise the base class (after defining the options). MegaWidget.__init__(self, parent) # Create the components. interior = MegaWidget.interior(self) self._ring = self.createcomponent( 'ring', (), None, Tkinter.Frame, (interior,), ) self._groupChildSite = self.createcomponent( 'groupchildsite', (), None, Tkinter.Frame, (self._ring,) ) self._tag = self.createcomponent( 'tag', (), None, Tkinter.Label, (interior,), ) ringBorder = (string.atoi(str(self._ring.cget('borderwidth'))) + string.atoi(str(self._ring.cget('highlightthickness')))) if self._tag is None: tagHeight = ringBorder else: tagHeight = self._tag.winfo_reqheight() self._tag.place( x = ringBorder + self['tagindent'], y = tagHeight / 2, anchor = 'w') topBorder = tagHeight / 2 - ringBorder / 2 self._ring.grid(column = 0, row = 1, sticky = 'nsew') interior.grid_columnconfigure(0, weight = 1) interior.grid_rowconfigure(1, weight = 1) interior.grid_rowconfigure(0, minsize = topBorder) self._groupChildSite.grid(column = 0, row = 1, sticky = 'nsew') self._ring.grid_columnconfigure(0, weight = 1) self._ring.grid_rowconfigure(1, weight = 1) self._ring.grid_rowconfigure(0, minsize = tagHeight - topBorder - ringBorder) self.showing = 1 # Check keywords and initialise options. self.initialiseoptions() def toggle(self): if self.showing: self.collapse() else: self.expand() self.showing = not self.showing def expand(self): self._groupChildSite.grid(column = 0, row = 1, sticky = 'nsew') def collapse(self): self._groupChildSite.grid_forget() if self._tag is None: tagHeight = 0 else: tagHeight = self._tag.winfo_reqheight() self._ring.configure(height=(tagHeight / 2) + self['collapsedsize']) def interior(self): return self._groupChildSite ###################################################################### ### File: PmwLabeledWidget.py import Tkinter class LabeledWidget(MegaWidget): def __init__(self, parent = None, **kw): # Define the megawidget options. optiondefs = ( ('labelmargin', 0, INITOPT), ('labelpos', None, INITOPT), ('sticky', 'nsew', INITOPT), ) self.defineoptions(kw, optiondefs) # Initialise the base class (after defining the options). MegaWidget.__init__(self, parent) # Create the components. interior = MegaWidget.interior(self) self._labelChildSite = self.createcomponent('labelchildsite', (), None, Tkinter.Frame, (interior,)) self._labelChildSite.grid(column=2, row=2, sticky=self['sticky']) interior.grid_columnconfigure(2, weight=1) interior.grid_rowconfigure(2, weight=1) self.createlabel(interior) # Check keywords and initialise options. self.initialiseoptions() def interior(self): return self._labelChildSite ###################################################################### ### File: PmwMainMenuBar.py # Main menubar import string import types import Tkinter class MainMenuBar(MegaArchetype): def __init__(self, parent = None, **kw): # Define the megawidget options. optiondefs = ( ('balloon', None, None), ('hotkeys', 1, INITOPT), ('hull_tearoff', 0, None), ) self.defineoptions(kw, optiondefs, dynamicGroups = ('Menu',)) # Initialise the base class (after defining the options). MegaArchetype.__init__(self, parent, Tkinter.Menu) self._menuInfo = {} self._menuInfo[None] = (None, []) # Map from a menu name to a tuple of information about the menu. # The first item in the tuple is the name of the parent menu (for # toplevel menus this is None). The second item in the tuple is # a list of status help messages for each item in the menu. # The key for the information for the main menubar is None. self._menu = self.interior() self._menu.bind('<Leave>', self._resetHelpmessage) self._menu.bind('<Motion>', lambda event=None, self=self: self._menuHelp(event, None)) # Check keywords and initialise options. self.initialiseoptions() def deletemenuitems(self, menuName, start, end = None): self.component(menuName).delete(start, end) if end is None: del self._menuInfo[menuName][1][start] else: self._menuInfo[menuName][1][start:end+1] = [] def deletemenu(self, menuName): """Delete should be called for cascaded menus before main menus. """ parentName = self._menuInfo[menuName][0] del self._menuInfo[menuName] if parentName is None: parentMenu = self._menu else: parentMenu = self.component(parentName) menu = self.component(menuName) menuId = str(menu) for item in range(parentMenu.index('end') + 1): if parentMenu.type(item) == 'cascade': itemMenu = str(parentMenu.entrycget(item, 'menu')) if itemMenu == menuId: parentMenu.delete(item) del self._menuInfo[parentName][1][item] break self.destroycomponent(menuName) def disableall(self): for index in range(len(self._menuInfo[None][1])): self.entryconfigure(index, state = 'disabled') def enableall(self): for index in range(len(self._menuInfo[None][1])): self.entryconfigure(index, state = 'normal') def addmenu(self, menuName, balloonHelp, statusHelp = None, traverseSpec = None, **kw): if statusHelp is None: statusHelp = balloonHelp self._addmenu(None, menuName, balloonHelp, statusHelp, traverseSpec, kw) def addcascademenu(self, parentMenuName, menuName, statusHelp='', traverseSpec = None, **kw): self._addmenu(parentMenuName, menuName, None, statusHelp, traverseSpec, kw) def _addmenu(self, parentMenuName, menuName, balloonHelp, statusHelp, traverseSpec, kw): if (menuName) in self.components(): raise ValueError, 'menu "%s" already exists' % menuName menukw = {} if kw.has_key('tearoff'): menukw['tearoff'] = kw['tearoff'] del kw['tearoff'] else: menukw['tearoff'] = 0 if kw.has_key('name'): menukw['name'] = kw['name'] del kw['name'] if not kw.has_key('label'): kw['label'] = menuName self._addHotkeyToOptions(parentMenuName, kw, traverseSpec) if parentMenuName is None: parentMenu = self._menu balloon = self['balloon'] # Bug in Tk: balloon help not implemented # if balloon is not None: # balloon.mainmenubind(parentMenu, balloonHelp, statusHelp) else: parentMenu = self.component(parentMenuName) apply(parentMenu.add_cascade, (), kw) menu = apply(self.createcomponent, (menuName, (), 'Menu', Tkinter.Menu, (parentMenu,)), menukw) parentMenu.entryconfigure('end', menu = menu) self._menuInfo[parentMenuName][1].append(statusHelp) self._menuInfo[menuName] = (parentMenuName, []) menu.bind('<Leave>', self._resetHelpmessage) menu.bind('<Motion>', lambda event=None, self=self, menuName=menuName: self._menuHelp(event, menuName)) def addmenuitem(self, menuName, itemType, statusHelp = '', traverseSpec = None, **kw): menu = self.component(menuName) if itemType != 'separator': self._addHotkeyToOptions(menuName, kw, traverseSpec) if itemType == 'command': command = menu.add_command elif itemType == 'separator': command = menu.add_separator elif itemType == 'checkbutton': command = menu.add_checkbutton elif itemType == 'radiobutton': command = menu.add_radiobutton elif itemType == 'cascade': command = menu.add_cascade else: raise ValueError, 'unknown menuitem type "%s"' % itemType self._menuInfo[menuName][1].append(statusHelp) apply(command, (), kw) def _addHotkeyToOptions(self, menuName, kw, traverseSpec): if (not self['hotkeys'] or kw.has_key('underline') or not kw.has_key('label')): return if type(traverseSpec) == types.IntType: kw['underline'] = traverseSpec return if menuName is None: menu = self._menu else: menu = self.component(menuName) hotkeyList = [] end = menu.index('end') if end is not None: for item in range(end + 1): if menu.type(item) not in ('separator', 'tearoff'): underline = \ string.atoi(str(menu.entrycget(item, 'underline'))) if underline != -1: label = str(menu.entrycget(item, 'label')) if underline < len(label): hotkey = string.lower(label[underline]) if hotkey not in hotkeyList: hotkeyList.append(hotkey) name = kw['label'] if type(traverseSpec) == types.StringType: lowerLetter = string.lower(traverseSpec) if traverseSpec in name and lowerLetter not in hotkeyList: kw['underline'] = string.index(name, traverseSpec) else: targets = string.digits + string.letters lowerName = string.lower(name) for letter_index in range(len(name)): letter = lowerName[letter_index] if letter in targets and letter not in hotkeyList: kw['underline'] = letter_index break def _menuHelp(self, event, menuName): if menuName is None: menu = self._menu index = menu.index('@%d'% event.x) else: menu = self.component(menuName) index = menu.index('@%d'% event.y) balloon = self['balloon'] if balloon is not None: if index is None: balloon.showstatus('') else: if str(menu.cget('tearoff')) == '1': index = index - 1 if index >= 0: help = self._menuInfo[menuName][1][index] balloon.showstatus(help) def _resetHelpmessage(self, event=None): balloon = self['balloon'] if balloon is not None: balloon.clearstatus() forwardmethods(MainMenuBar, Tkinter.Menu, '_hull') ###################################################################### ### File: PmwMenuBar.py # Manager widget for menus. import string import types import Tkinter class MenuBar(MegaWidget): def __init__(self, parent = None, **kw): # Define the megawidget options. optiondefs = ( ('balloon', None, None), ('hotkeys', 1, INITOPT), ('padx', 0, INITOPT), ) self.defineoptions(kw, optiondefs, dynamicGroups = ('Menu', 'Button')) # Initialise the base class (after defining the options). MegaWidget.__init__(self, parent) self._menuInfo = {} # Map from a menu name to a tuple of information about the menu. # The first item in the tuple is the name of the parent menu (for # toplevel menus this is None). The second item in the tuple is # a list of status help messages for each item in the menu. # The third item in the tuple is the id of the binding used # to detect mouse motion to display status help. # Information for the toplevel menubuttons is not stored here. self._mydeletecommand = self.component('hull').tk.deletecommand # Cache this method for use later. # Check keywords and initialise options. self.initialiseoptions() def deletemenuitems(self, menuName, start, end = None): self.component(menuName + '-menu').delete(start, end) if end is None: del self._menuInfo[menuName][1][start] else: self._menuInfo[menuName][1][start:end+1] = [] def deletemenu(self, menuName): """Delete should be called for cascaded menus before main menus. """ # Clean up binding for this menu. parentName = self._menuInfo[menuName][0] bindId = self._menuInfo[menuName][2] _bindtag = 'PmwMenuBar' + str(self) + menuName self.unbind_class(_bindtag, '<Motion>') self._mydeletecommand(bindId) # unbind_class does not clean up del self._menuInfo[menuName] if parentName is None: self.destroycomponent(menuName + '-button') else: parentMenu = self.component(parentName + '-menu') menu = self.component(menuName + '-menu') menuId = str(menu) for item in range(parentMenu.index('end') + 1): if parentMenu.type(item) == 'cascade': itemMenu = str(parentMenu.entrycget(item, 'menu')) if itemMenu == menuId: parentMenu.delete(item) del self._menuInfo[parentName][1][item] break self.destroycomponent(menuName + '-menu') def disableall(self): for menuName in self._menuInfo.keys(): if self._menuInfo[menuName][0] is None: menubutton = self.component(menuName + '-button') menubutton.configure(state = 'disabled') def enableall(self): for menuName in self._menuInfo.keys(): if self._menuInfo[menuName][0] is None: menubutton = self.component(menuName + '-button') menubutton.configure(state = 'normal') def addmenu(self, menuName, balloonHelp, statusHelp = None, side = 'left', traverseSpec = None, **kw): self._addmenu(None, menuName, balloonHelp, statusHelp, traverseSpec, side, 'text', kw) def addcascademenu(self, parentMenuName, menuName, statusHelp = '', traverseSpec = None, **kw): self._addmenu(parentMenuName, menuName, None, statusHelp, traverseSpec, None, 'label', kw) def _addmenu(self, parentMenuName, menuName, balloonHelp, statusHelp, traverseSpec, side, textKey, kw): if (menuName + '-menu') in self.components(): raise ValueError, 'menu "%s" already exists' % menuName menukw = {} if kw.has_key('tearoff'): menukw['tearoff'] = kw['tearoff'] del kw['tearoff'] else: menukw['tearoff'] = 0 if not kw.has_key(textKey): kw[textKey] = menuName self._addHotkeyToOptions(parentMenuName, kw, textKey, traverseSpec) if parentMenuName is None: button = apply(self.createcomponent, (menuName + '-button', (), 'Button', Tkinter.Menubutton, (self.interior(),)), kw) button.pack(side=side, padx = self['padx']) balloon = self['balloon'] if balloon is not None: balloon.bind(button, balloonHelp, statusHelp) parentMenu = button else: parentMenu = self.component(parentMenuName + '-menu') apply(parentMenu.add_cascade, (), kw) self._menuInfo[parentMenuName][1].append(statusHelp) menu = apply(self.createcomponent, (menuName + '-menu', (), 'Menu', Tkinter.Menu, (parentMenu,)), menukw) if parentMenuName is None: button.configure(menu = menu) else: parentMenu.entryconfigure('end', menu = menu) # Need to put this binding after the class bindings so that # menu.index() does not lag behind. _bindtag = 'PmwMenuBar' + str(self) + menuName bindId = self.bind_class(_bindtag, '<Motion>', lambda event=None, self=self, menuName=menuName: self._menuHelp(menuName)) menu.bindtags(menu.bindtags() + (_bindtag,)) menu.bind('<Leave>', self._resetHelpmessage) self._menuInfo[menuName] = (parentMenuName, [], bindId) def addmenuitem(self, menuName, itemType, statusHelp = '', traverseSpec = None, **kw): menu = self.component(menuName + '-menu') if itemType != 'separator': self._addHotkeyToOptions(menuName, kw, 'label', traverseSpec) if itemType == 'command': command = menu.add_command elif itemType == 'separator': command = menu.add_separator elif itemType == 'checkbutton': command = menu.add_checkbutton elif itemType == 'radiobutton': command = menu.add_radiobutton elif itemType == 'cascade': command = menu.add_cascade else: raise ValueError, 'unknown menuitem type "%s"' % itemType self._menuInfo[menuName][1].append(statusHelp) apply(command, (), kw) def _addHotkeyToOptions(self, menuName, kw, textKey, traverseSpec): if (not self['hotkeys'] or kw.has_key('underline') or not kw.has_key(textKey)): return if type(traverseSpec) == types.IntType: kw['underline'] = traverseSpec return hotkeyList = [] if menuName is None: for menuName in self._menuInfo.keys(): if self._menuInfo[menuName][0] is None: menubutton = self.component(menuName + '-button') underline = string.atoi(str(menubutton.cget('underline'))) if underline != -1: label = str(menubutton.cget(textKey)) if underline < len(label): hotkey = string.lower(label[underline]) if hotkey not in hotkeyList: hotkeyList.append(hotkey) else: menu = self.component(menuName + '-menu') end = menu.index('end') if end is not None: for item in range(end + 1): if menu.type(item) not in ('separator', 'tearoff'): underline = string.atoi( str(menu.entrycget(item, 'underline'))) if underline != -1: label = str(menu.entrycget(item, textKey)) if underline < len(label): hotkey = string.lower(label[underline]) if hotkey not in hotkeyList: hotkeyList.append(hotkey) name = kw[textKey] if type(traverseSpec) == types.StringType: lowerLetter = string.lower(traverseSpec) if traverseSpec in name and lowerLetter not in hotkeyList: kw['underline'] = string.index(name, traverseSpec) else: targets = string.digits + string.letters lowerName = string.lower(name) for letter_index in range(len(name)): letter = lowerName[letter_index] if letter in targets and letter not in hotkeyList: kw['underline'] = letter_index break def _menuHelp(self, menuName): menu = self.component(menuName + '-menu') index = menu.index('active') balloon = self['balloon'] if balloon is not None: if index is None: balloon.showstatus('') else: if str(menu.cget('tearoff')) == '1': index = index - 1 if index >= 0: help = self._menuInfo[menuName][1][index] balloon.showstatus(help) def _resetHelpmessage(self, event=None): balloon = self['balloon'] if balloon is not None: balloon.clearstatus() ###################################################################### ### File: PmwMessageBar.py # Class to display messages in an information line. import string import Tkinter class MessageBar(MegaWidget): def __init__(self, parent = None, **kw): # Define the megawidget options. defaultMessageTypes = { # (priority, showtime, bells, logmessage) 'systemerror' : (5, 10, 2, 1), 'usererror' : (4, 5, 1, 0), 'busy' : (3, 0, 0, 0), 'systemevent' : (2, 5, 0, 0), 'userevent' : (2, 5, 0, 0), 'help' : (1, 5, 0, 0), 'state' : (0, 0, 0, 0), } optiondefs = ( ('labelmargin', 0, INITOPT), ('labelpos', None, INITOPT), ('messagetypes', defaultMessageTypes, INITOPT), ('silent', 0, None), ('sticky', 'ew', INITOPT), ) self.defineoptions(kw, optiondefs) # Initialise the base class (after defining the options). MegaWidget.__init__(self, parent) # Create the components. interior = self.interior() self._messageBarEntry = self.createcomponent('entry', (), None, Tkinter.Entry, (interior,)) # Can't always use 'disabled', since this greys out text in Tk 8.4.2 try: self._messageBarEntry.configure(state = 'readonly') except Tkinter.TclError: self._messageBarEntry.configure(state = 'disabled') self._messageBarEntry.grid(column=2, row=2, sticky=self['sticky']) interior.grid_columnconfigure(2, weight=1) interior.grid_rowconfigure(2, weight=1) self.createlabel(interior) # Initialise instance variables. self._numPriorities = 0 for info in self['messagetypes'].values(): if self._numPriorities < info[0]: self._numPriorities = info[0] self._numPriorities = self._numPriorities + 1 self._timer = [None] * self._numPriorities self._messagetext = [''] * self._numPriorities self._activemessage = [0] * self._numPriorities # Check keywords and initialise options. self.initialiseoptions() def destroy(self): for timerId in self._timer: if timerId is not None: self.after_cancel(timerId) self._timer = [None] * self._numPriorities MegaWidget.destroy(self) def message(self, type, text): # Display a message in the message bar. (priority, showtime, bells, logmessage) = self['messagetypes'][type] if not self['silent']: for i in range(bells): if i != 0: self.after(100) self.bell() self._activemessage[priority] = 1 if text is None: text = '' self._messagetext[priority] = string.replace(text, '\n', ' ') self._redisplayInfoMessage() if logmessage: # Should log this text to a text widget. pass if showtime > 0: if self._timer[priority] is not None: self.after_cancel(self._timer[priority]) # Define a callback to clear this message after a time. def _clearmessage(self=self, priority=priority): self._clearActivemessage(priority) mseconds = int(showtime * 1000) self._timer[priority] = self.after(mseconds, _clearmessage) def helpmessage(self, text): if text is None: self.resetmessages('help') else: self.message('help', text) def resetmessages(self, type): priority = self['messagetypes'][type][0] self._clearActivemessage(priority) for messagetype, info in self['messagetypes'].items(): thisPriority = info[0] showtime = info[1] if thisPriority < priority and showtime != 0: self._clearActivemessage(thisPriority) def _clearActivemessage(self, priority): self._activemessage[priority] = 0 if self._timer[priority] is not None: self.after_cancel(self._timer[priority]) self._timer[priority] = None self._redisplayInfoMessage() def _redisplayInfoMessage(self): text = '' for priority in range(self._numPriorities - 1, -1, -1): if self._activemessage[priority]: text = self._messagetext[priority] break self._messageBarEntry.configure(state = 'normal') self._messageBarEntry.delete(0, 'end') self._messageBarEntry.insert('end', text) # Can't always use 'disabled', since this greys out text in Tk 8.4.2 try: self._messageBarEntry.configure(state = 'readonly') except Tkinter.TclError: self._messageBarEntry.configure(state = 'disabled') forwardmethods(MessageBar, Tkinter.Entry, '_messageBarEntry') ###################################################################### ### File: PmwMessageDialog.py # Based on iwidgets2.2.0/messagedialog.itk code. import Tkinter class MessageDialog(Dialog): def __init__(self, parent = None, **kw): # Define the megawidget options. optiondefs = ( ('borderx', 20, INITOPT), ('bordery', 20, INITOPT), ('iconmargin', 20, INITOPT), ('iconpos', None, INITOPT), ) self.defineoptions(kw, optiondefs) # Initialise the base class (after defining the options). Dialog.__init__(self, parent) # Create the components. interior = self.interior() self._message = self.createcomponent('message', (), None, Tkinter.Label, (interior,)) iconpos = self['iconpos'] iconmargin = self['iconmargin'] borderx = self['borderx'] bordery = self['bordery'] border_right = 2 border_bottom = 2 if iconpos is None: self._message.grid(column = 1, row = 1) else: self._icon = self.createcomponent('icon', (), None, Tkinter.Label, (interior,)) if iconpos not in 'nsew': raise ValueError, \ 'bad iconpos option "%s": should be n, s, e, or w' \ % iconpos if iconpos in 'nw': icon = 1 message = 3 else: icon = 3 message = 1 if iconpos in 'ns': # vertical layout self._icon.grid(column = 1, row = icon) self._message.grid(column = 1, row = message) interior.grid_rowconfigure(2, minsize = iconmargin) border_bottom = 4 else: # horizontal layout self._icon.grid(column = icon, row = 1) self._message.grid(column = message, row = 1) interior.grid_columnconfigure(2, minsize = iconmargin) border_right = 4 interior.grid_columnconfigure(0, minsize = borderx) interior.grid_rowconfigure(0, minsize = bordery) interior.grid_columnconfigure(border_right, minsize = borderx) interior.grid_rowconfigure(border_bottom, minsize = bordery) # Check keywords and initialise options. self.initialiseoptions() ###################################################################### ### File: PmwNoteBook.py import string import types import Tkinter class NoteBook(MegaArchetype): def __init__(self, parent = None, **kw): # Define the megawidget options. optiondefs = ( ('hull_highlightthickness', 0, None), ('hull_borderwidth', 0, None), ('arrownavigation', 1, INITOPT), ('borderwidth', 2, INITOPT), ('createcommand', None, None), ('lowercommand', None, None), ('pagemargin', 4, INITOPT), ('raisecommand', None, None), ('tabpos', 'n', INITOPT), ) self.defineoptions(kw, optiondefs, dynamicGroups = ('Page', 'Tab')) # Initialise the base class (after defining the options). MegaArchetype.__init__(self, parent, Tkinter.Canvas) self.bind('<Map>', self._handleMap) self.bind('<Configure>', self._handleConfigure) tabpos = self['tabpos'] if tabpos is not None and tabpos != 'n': raise ValueError, \ 'bad tabpos option %s: should be n or None' % repr(tabpos) self._withTabs = (tabpos is not None) self._pageMargin = self['pagemargin'] self._borderWidth = self['borderwidth'] # Use a dictionary as a set of bits indicating what needs to # be redisplayed the next time _layout() is called. If # dictionary contains 'topPage' key, the value is the new top # page to be displayed. None indicates that all pages have # been deleted and that _layout() should draw a border under where # the tabs should be. self._pending = {} self._pending['size'] = 1 self._pending['borderColor'] = 1 self._pending['topPage'] = None if self._withTabs: self._pending['tabs'] = 1 self._canvasSize = None # This gets set by <Configure> events # Set initial height of space for tabs if self._withTabs: self.tabBottom = 35 else: self.tabBottom = 0 self._lightBorderColor, self._darkBorderColor = \ Color.bordercolors(self, self['hull_background']) self._pageNames = [] # List of page names # Map from page name to page info. Each item is itself a # dictionary containing the following items: # page the Tkinter.Frame widget for the page # created set to true the first time the page is raised # tabbutton the Tkinter.Button widget for the button (if any) # tabreqwidth requested width of the tab # tabreqheight requested height of the tab # tabitems the canvas items for the button: the button # window item, the lightshadow and the darkshadow # left the left and right canvas coordinates of the tab # right self._pageAttrs = {} # Name of page currently on top (actually displayed, using # create_window, not pending). Ignored if current top page # has been deleted or new top page is pending. None indicates # no pages in notebook. self._topPageName = None # Canvas items used: # Per tab: # top and left shadow # right shadow # button # Per notebook: # page # top page # left shadow # bottom and right shadow # top (one or two items) # Canvas tags used: # lighttag - top and left shadows of tabs and page # darktag - bottom and right shadows of tabs and page # (if no tabs then these are reversed) # (used to color the borders by recolorborders) # Create page border shadows. if self._withTabs: self._pageLeftBorder = self.create_polygon(0, 0, 0, 0, 0, 0, fill = self._lightBorderColor, tags = 'lighttag') self._pageBottomRightBorder = self.create_polygon(0, 0, 0, 0, 0, 0, fill = self._darkBorderColor, tags = 'darktag') self._pageTop1Border = self.create_polygon(0, 0, 0, 0, 0, 0, fill = self._darkBorderColor, tags = 'lighttag') self._pageTop2Border = self.create_polygon(0, 0, 0, 0, 0, 0, fill = self._darkBorderColor, tags = 'lighttag') else: self._pageLeftBorder = self.create_polygon(0, 0, 0, 0, 0, 0, fill = self._darkBorderColor, tags = 'darktag') self._pageBottomRightBorder = self.create_polygon(0, 0, 0, 0, 0, 0, fill = self._lightBorderColor, tags = 'lighttag') self._pageTopBorder = self.create_polygon(0, 0, 0, 0, 0, 0, fill = self._darkBorderColor, tags = 'darktag') # Check keywords and initialise options. self.initialiseoptions() def insert(self, pageName, before = 0, **kw): if self._pageAttrs.has_key(pageName): msg = 'Page "%s" already exists.' % pageName raise ValueError, msg # Do this early to catch bad <before> spec before creating any items. beforeIndex = self.index(before, 1) pageOptions = {} if self._withTabs: # Default tab button options. tabOptions = { 'text' : pageName, 'borderwidth' : 0, } # Divide the keyword options into the 'page_' and 'tab_' options. for key in kw.keys(): if key[:5] == 'page_': pageOptions[key[5:]] = kw[key] del kw[key] elif self._withTabs and key[:4] == 'tab_': tabOptions[key[4:]] = kw[key] del kw[key] else: raise KeyError, 'Unknown option "' + key + '"' # Create the frame to contain the page. page = apply(self.createcomponent, (pageName, (), 'Page', Tkinter.Frame, self._hull), pageOptions) attributes = {} attributes['page'] = page attributes['created'] = 0 if self._withTabs: # Create the button for the tab. def raiseThisPage(self = self, pageName = pageName): self.selectpage(pageName) tabOptions['command'] = raiseThisPage tab = apply(self.createcomponent, (pageName + '-tab', (), 'Tab', Tkinter.Button, self._hull), tabOptions) if self['arrownavigation']: # Allow the use of the arrow keys for Tab navigation: def next(event, self = self, pageName = pageName): self.nextpage(pageName) def prev(event, self = self, pageName = pageName): self.previouspage(pageName) tab.bind('<Left>', prev) tab.bind('<Right>', next) attributes['tabbutton'] = tab attributes['tabreqwidth'] = tab.winfo_reqwidth() attributes['tabreqheight'] = tab.winfo_reqheight() # Create the canvas item to manage the tab's button and the items # for the tab's shadow. windowitem = self.create_window(0, 0, window = tab, anchor = 'nw') lightshadow = self.create_polygon(0, 0, 0, 0, 0, 0, tags = 'lighttag', fill = self._lightBorderColor) darkshadow = self.create_polygon(0, 0, 0, 0, 0, 0, tags = 'darktag', fill = self._darkBorderColor) attributes['tabitems'] = (windowitem, lightshadow, darkshadow) self._pending['tabs'] = 1 self._pageAttrs[pageName] = attributes self._pageNames.insert(beforeIndex, pageName) # If this is the first page added, make it the new top page # and call the create and raise callbacks. if self.getcurselection() is None: self._pending['topPage'] = pageName self._raiseNewTop(pageName) self._layout() return page def add(self, pageName, **kw): return apply(self.insert, (pageName, len(self._pageNames)), kw) def delete(self, *pageNames): newTopPage = 0 for page in pageNames: pageIndex = self.index(page) pageName = self._pageNames[pageIndex] pageInfo = self._pageAttrs[pageName] if self.getcurselection() == pageName: if len(self._pageNames) == 1: newTopPage = 0 self._pending['topPage'] = None elif pageIndex == len(self._pageNames) - 1: newTopPage = 1 self._pending['topPage'] = self._pageNames[pageIndex - 1] else: newTopPage = 1 self._pending['topPage'] = self._pageNames[pageIndex + 1] if self._topPageName == pageName: self._hull.delete(self._topPageItem) self._topPageName = None if self._withTabs: self.destroycomponent(pageName + '-tab') apply(self._hull.delete, pageInfo['tabitems']) self.destroycomponent(pageName) del self._pageAttrs[pageName] del self._pageNames[pageIndex] # If the old top page was deleted and there are still pages # left in the notebook, call the create and raise callbacks. if newTopPage: pageName = self._pending['topPage'] self._raiseNewTop(pageName) if self._withTabs: self._pending['tabs'] = 1 self._layout() def page(self, pageIndex): pageName = self._pageNames[self.index(pageIndex)] return self._pageAttrs[pageName]['page'] def pagenames(self): return list(self._pageNames) def getcurselection(self): if self._pending.has_key('topPage'): return self._pending['topPage'] else: return self._topPageName def tab(self, pageIndex): if self._withTabs: pageName = self._pageNames[self.index(pageIndex)] return self._pageAttrs[pageName]['tabbutton'] else: return None def index(self, index, forInsert = 0): listLength = len(self._pageNames) if type(index) == types.IntType: if forInsert and index <= listLength: return index elif not forInsert and index < listLength: return index else: raise ValueError, 'index "%s" is out of range' % index elif index is END: if forInsert: return listLength elif listLength > 0: return listLength - 1 else: raise ValueError, 'NoteBook has no pages' elif index is SELECT: if listLength == 0: raise ValueError, 'NoteBook has no pages' return self._pageNames.index(self.getcurselection()) else: if index in self._pageNames: return self._pageNames.index(index) validValues = 'a name, a number, END or SELECT' raise ValueError, \ 'bad index "%s": must be %s' % (index, validValues) def selectpage(self, page): pageName = self._pageNames[self.index(page)] oldTopPage = self.getcurselection() if pageName != oldTopPage: self._pending['topPage'] = pageName if oldTopPage == self._topPageName: self._hull.delete(self._topPageItem) cmd = self['lowercommand'] if cmd is not None: cmd(oldTopPage) self._raiseNewTop(pageName) self._layout() # Set focus to the tab of new top page: if self._withTabs and self['arrownavigation']: self._pageAttrs[pageName]['tabbutton'].focus_set() def previouspage(self, pageIndex = None): if pageIndex is None: curpage = self.index(SELECT) else: curpage = self.index(pageIndex) if curpage > 0: self.selectpage(curpage - 1) def nextpage(self, pageIndex = None): if pageIndex is None: curpage = self.index(SELECT) else: curpage = self.index(pageIndex) if curpage < len(self._pageNames) - 1: self.selectpage(curpage + 1) def setnaturalsize(self, pageNames = None): self.update_idletasks() maxPageWidth = 1 maxPageHeight = 1 if pageNames is None: pageNames = self.pagenames() for pageName in pageNames: pageInfo = self._pageAttrs[pageName] page = pageInfo['page'] w = page.winfo_reqwidth() h = page.winfo_reqheight() if maxPageWidth < w: maxPageWidth = w if maxPageHeight < h: maxPageHeight = h pageBorder = self._borderWidth + self._pageMargin width = maxPageWidth + pageBorder * 2 height = maxPageHeight + pageBorder * 2 if self._withTabs: maxTabHeight = 0 for pageInfo in self._pageAttrs.values(): if maxTabHeight < pageInfo['tabreqheight']: maxTabHeight = pageInfo['tabreqheight'] height = height + maxTabHeight + self._borderWidth * 1.5 # Note that, since the hull is a canvas, the width and height # options specify the geometry *inside* the borderwidth and # highlightthickness. self.configure(hull_width = width, hull_height = height) def recolorborders(self): self._pending['borderColor'] = 1 self._layout() def _handleMap(self, event): self._layout() def _handleConfigure(self, event): self._canvasSize = (event.width, event.height) self._pending['size'] = 1 self._layout() def _raiseNewTop(self, pageName): if not self._pageAttrs[pageName]['created']: self._pageAttrs[pageName]['created'] = 1 cmd = self['createcommand'] if cmd is not None: cmd(pageName) cmd = self['raisecommand'] if cmd is not None: cmd(pageName) # This is the vertical layout of the notebook, from top (assuming # tabpos is 'n'): # hull highlightthickness (top) # hull borderwidth (top) # borderwidth (top border of tabs) # borderwidth * 0.5 (space for bevel) # tab button (maximum of requested height of all tab buttons) # borderwidth (border between tabs and page) # pagemargin (top) # the page itself # pagemargin (bottom) # borderwidth (border below page) # hull borderwidth (bottom) # hull highlightthickness (bottom) # # canvasBorder is sum of top two elements. # tabBottom is sum of top five elements. # # Horizontal layout (and also vertical layout when tabpos is None): # hull highlightthickness # hull borderwidth # borderwidth # pagemargin # the page itself # pagemargin # borderwidth # hull borderwidth # hull highlightthickness # def _layout(self): if not self.winfo_ismapped() or self._canvasSize is None: # Don't layout if the window is not displayed, or we # haven't yet received a <Configure> event. return hullWidth, hullHeight = self._canvasSize borderWidth = self._borderWidth canvasBorder = string.atoi(self._hull['borderwidth']) + \ string.atoi(self._hull['highlightthickness']) if not self._withTabs: self.tabBottom = canvasBorder oldTabBottom = self.tabBottom if self._pending.has_key('borderColor'): self._lightBorderColor, self._darkBorderColor = \ Color.bordercolors(self, self['hull_background']) # Draw all the tabs. if self._withTabs and (self._pending.has_key('tabs') or self._pending.has_key('size')): # Find total requested width and maximum requested height # of tabs. sumTabReqWidth = 0 maxTabHeight = 0 for pageInfo in self._pageAttrs.values(): sumTabReqWidth = sumTabReqWidth + pageInfo['tabreqwidth'] if maxTabHeight < pageInfo['tabreqheight']: maxTabHeight = pageInfo['tabreqheight'] if maxTabHeight != 0: # Add the top tab border plus a bit for the angled corners self.tabBottom = canvasBorder + maxTabHeight + borderWidth * 1.5 # Prepare for drawing the border around each tab button. tabTop = canvasBorder tabTop2 = tabTop + borderWidth tabTop3 = tabTop + borderWidth * 1.5 tabBottom2 = self.tabBottom tabBottom = self.tabBottom + borderWidth numTabs = len(self._pageNames) availableWidth = hullWidth - 2 * canvasBorder - \ numTabs * 2 * borderWidth x = canvasBorder cumTabReqWidth = 0 cumTabWidth = 0 # Position all the tabs. for pageName in self._pageNames: pageInfo = self._pageAttrs[pageName] (windowitem, lightshadow, darkshadow) = pageInfo['tabitems'] if sumTabReqWidth <= availableWidth: tabwidth = pageInfo['tabreqwidth'] else: # This ugly calculation ensures that, when the # notebook is not wide enough for the requested # widths of the tabs, the total width given to # the tabs exactly equals the available width, # without rounding errors. cumTabReqWidth = cumTabReqWidth + pageInfo['tabreqwidth'] tmp = (2*cumTabReqWidth*availableWidth + sumTabReqWidth) \ / (2 * sumTabReqWidth) tabwidth = tmp - cumTabWidth cumTabWidth = tmp # Position the tab's button canvas item. self.coords(windowitem, x + borderWidth, tabTop3) self.itemconfigure(windowitem, width = tabwidth, height = maxTabHeight) # Make a beautiful border around the tab. left = x left2 = left + borderWidth left3 = left + borderWidth * 1.5 right = left + tabwidth + 2 * borderWidth right2 = left + tabwidth + borderWidth right3 = left + tabwidth + borderWidth * 0.5 self.coords(lightshadow, left, tabBottom2, left, tabTop2, left2, tabTop, right2, tabTop, right3, tabTop2, left3, tabTop2, left2, tabTop3, left2, tabBottom, ) self.coords(darkshadow, right2, tabTop, right, tabTop2, right, tabBottom2, right2, tabBottom, right2, tabTop3, right3, tabTop2, ) pageInfo['left'] = left pageInfo['right'] = right x = x + tabwidth + 2 * borderWidth # Redraw shadow under tabs so that it appears that tab for old # top page is lowered and that tab for new top page is raised. if self._withTabs and (self._pending.has_key('topPage') or self._pending.has_key('tabs') or self._pending.has_key('size')): if self.getcurselection() is None: # No pages, so draw line across top of page area. self.coords(self._pageTop1Border, canvasBorder, self.tabBottom, hullWidth - canvasBorder, self.tabBottom, hullWidth - canvasBorder - borderWidth, self.tabBottom + borderWidth, borderWidth + canvasBorder, self.tabBottom + borderWidth, ) # Ignore second top border. self.coords(self._pageTop2Border, 0, 0, 0, 0, 0, 0) else: # Draw two lines, one on each side of the tab for the # top page, so that the tab appears to be raised. pageInfo = self._pageAttrs[self.getcurselection()] left = pageInfo['left'] right = pageInfo['right'] self.coords(self._pageTop1Border, canvasBorder, self.tabBottom, left, self.tabBottom, left + borderWidth, self.tabBottom + borderWidth, canvasBorder + borderWidth, self.tabBottom + borderWidth, ) self.coords(self._pageTop2Border, right, self.tabBottom, hullWidth - canvasBorder, self.tabBottom, hullWidth - canvasBorder - borderWidth, self.tabBottom + borderWidth, right - borderWidth, self.tabBottom + borderWidth, ) # Prevent bottom of dark border of tabs appearing over # page top border. self.tag_raise(self._pageTop1Border) self.tag_raise(self._pageTop2Border) # Position the page border shadows. if self._pending.has_key('size') or oldTabBottom != self.tabBottom: self.coords(self._pageLeftBorder, canvasBorder, self.tabBottom, borderWidth + canvasBorder, self.tabBottom + borderWidth, borderWidth + canvasBorder, hullHeight - canvasBorder - borderWidth, canvasBorder, hullHeight - canvasBorder, ) self.coords(self._pageBottomRightBorder, hullWidth - canvasBorder, self.tabBottom, hullWidth - canvasBorder, hullHeight - canvasBorder, canvasBorder, hullHeight - canvasBorder, borderWidth + canvasBorder, hullHeight - canvasBorder - borderWidth, hullWidth - canvasBorder - borderWidth, hullHeight - canvasBorder - borderWidth, hullWidth - canvasBorder - borderWidth, self.tabBottom + borderWidth, ) if not self._withTabs: self.coords(self._pageTopBorder, canvasBorder, self.tabBottom, hullWidth - canvasBorder, self.tabBottom, hullWidth - canvasBorder - borderWidth, self.tabBottom + borderWidth, borderWidth + canvasBorder, self.tabBottom + borderWidth, ) # Color borders. if self._pending.has_key('borderColor'): self.itemconfigure('lighttag', fill = self._lightBorderColor) self.itemconfigure('darktag', fill = self._darkBorderColor) newTopPage = self._pending.get('topPage') pageBorder = borderWidth + self._pageMargin # Raise new top page. if newTopPage is not None: self._topPageName = newTopPage self._topPageItem = self.create_window( pageBorder + canvasBorder, self.tabBottom + pageBorder, window = self._pageAttrs[newTopPage]['page'], anchor = 'nw', ) # Change position of top page if tab height has changed. if self._topPageName is not None and oldTabBottom != self.tabBottom: self.coords(self._topPageItem, pageBorder + canvasBorder, self.tabBottom + pageBorder) # Change size of top page if, # 1) there is a new top page. # 2) canvas size has changed, but not if there is no top # page (eg: initially or when all pages deleted). # 3) tab height has changed, due to difference in the height of a tab if (newTopPage is not None or \ self._pending.has_key('size') and self._topPageName is not None or oldTabBottom != self.tabBottom): self.itemconfigure(self._topPageItem, width = hullWidth - 2 * canvasBorder - pageBorder * 2, height = hullHeight - 2 * canvasBorder - pageBorder * 2 - (self.tabBottom - canvasBorder), ) self._pending = {} # Need to do forwarding to get the pack, grid, etc methods. # Unfortunately this means that all the other canvas methods are also # forwarded. forwardmethods(NoteBook, Tkinter.Canvas, '_hull') ###################################################################### ### File: PmwOptionMenu.py import types import Tkinter class OptionMenu(MegaWidget): def __init__(self, parent = None, **kw): # Define the megawidget options. optiondefs = ( ('command', None, None), ('items', (), INITOPT), ('initialitem', None, INITOPT), ('labelmargin', 0, INITOPT), ('labelpos', None, INITOPT), ('sticky', 'ew', INITOPT), ) self.defineoptions(kw, optiondefs) # Initialise the base class (after defining the options). MegaWidget.__init__(self, parent) # Create the components. interior = self.interior() self._menubutton = self.createcomponent('menubutton', (), None, Tkinter.Menubutton, (interior,), borderwidth = 2, indicatoron = 1, relief = 'raised', anchor = 'c', highlightthickness = 2, direction = 'flush', takefocus = 1, ) self._menubutton.grid(column = 2, row = 2, sticky = self['sticky']) self._menu = self.createcomponent('menu', (), None, Tkinter.Menu, (self._menubutton,), tearoff=0 ) self._menubutton.configure(menu = self._menu) interior.grid_columnconfigure(2, weight = 1) interior.grid_rowconfigure(2, weight = 1) # Create the label. self.createlabel(interior) # Add the items specified by the initialisation option. self._itemList = [] self.setitems(self['items'], self['initialitem']) # Check keywords and initialise options. self.initialiseoptions() def setitems(self, items, index = None): # Clean up old items and callback commands. for oldIndex in range(len(self._itemList)): tclCommandName = str(self._menu.entrycget(oldIndex, 'command')) if tclCommandName != '': self._menu.deletecommand(tclCommandName) self._menu.delete(0, 'end') self._itemList = list(items) # Set the items in the menu component. for item in items: self._menu.add_command(label = item, command = lambda self = self, item = item: self._invoke(item)) # Set the currently selected value. if index is None: var = str(self._menubutton.cget('textvariable')) if var != '': # None means do not change text variable. return if len(items) == 0: text = '' elif str(self._menubutton.cget('text')) in items: # Do not change selection if it is still valid return else: text = items[0] else: index = self.index(index) text = self._itemList[index] self.setvalue(text) def getcurselection(self): var = str(self._menubutton.cget('textvariable')) if var == '': return str(self._menubutton.cget('text')) else: return self._menu.tk.globalgetvar(var) def getvalue(self): return self.getcurselection() def setvalue(self, text): var = str(self._menubutton.cget('textvariable')) if var == '': self._menubutton.configure(text = text) else: self._menu.tk.globalsetvar(var, text) def index(self, index): listLength = len(self._itemList) if type(index) == types.IntType: if index < listLength: return index else: raise ValueError, 'index "%s" is out of range' % index elif index is END: if listLength > 0: return listLength - 1 else: raise ValueError, 'OptionMenu has no items' else: if index is SELECT: if listLength > 0: index = self.getcurselection() else: raise ValueError, 'OptionMenu has no items' if index in self._itemList: return self._itemList.index(index) raise ValueError, \ 'bad index "%s": must be a ' \ 'name, a number, END or SELECT' % (index,) def invoke(self, index = SELECT): index = self.index(index) text = self._itemList[index] return self._invoke(text) def _invoke(self, text): self.setvalue(text) command = self['command'] if callable(command): return command(text) ###################################################################### ### File: PmwPanedWidget.py # PanedWidget # a frame which may contain several resizable sub-frames import string import sys import types import Tkinter class PanedWidget(MegaWidget): def __init__(self, parent = None, **kw): # Define the megawidget options. optiondefs = ( ('command', None, None), ('orient', 'vertical', INITOPT), ('separatorrelief', 'sunken', INITOPT), ('separatorthickness', 2, INITOPT), ('handlesize', 8, INITOPT), ('hull_width', 400, None), ('hull_height', 400, None), ) self.defineoptions(kw, optiondefs, dynamicGroups = ('Frame', 'Separator', 'Handle')) # Initialise the base class (after defining the options). MegaWidget.__init__(self, parent) self.bind('<Configure>', self._handleConfigure) if self['orient'] not in ('horizontal', 'vertical'): raise ValueError, 'bad orient option ' + repr(self['orient']) + \ ': must be either \'horizontal\' or \'vertical\'' self._separatorThickness = self['separatorthickness'] self._handleSize = self['handlesize'] self._paneNames = [] # List of pane names self._paneAttrs = {} # Map from pane name to pane info self._timerId = None self._frame = {} self._separator = [] self._button = [] self._totalSize = 0 self._movePending = 0 self._relsize = {} self._relmin = {} self._relmax = {} self._size = {} self._min = {} self._max = {} self._rootp = None self._curSize = None self._beforeLimit = None self._afterLimit = None self._buttonIsDown = 0 self._majorSize = 100 self._minorSize = 100 # Check keywords and initialise options. self.initialiseoptions() def insert(self, name, before = 0, **kw): # Parse <kw> for options. self._initPaneOptions(name) self._parsePaneOptions(name, kw) insertPos = self._nameToIndex(before) atEnd = (insertPos == len(self._paneNames)) # Add the frame. self._paneNames[insertPos:insertPos] = [name] self._frame[name] = self.createcomponent(name, (), 'Frame', Tkinter.Frame, (self.interior(),)) # Add separator, if necessary. if len(self._paneNames) > 1: self._addSeparator() else: self._separator.append(None) self._button.append(None) # Add the new frame and adjust the PanedWidget if atEnd: size = self._size[name] if size > 0 or self._relsize[name] is not None: if self['orient'] == 'vertical': self._frame[name].place(x=0, relwidth=1, height=size, y=self._totalSize) else: self._frame[name].place(y=0, relheight=1, width=size, x=self._totalSize) else: if self['orient'] == 'vertical': self._frame[name].place(x=0, relwidth=1, y=self._totalSize) else: self._frame[name].place(y=0, relheight=1, x=self._totalSize) else: self._updateSizes() self._totalSize = self._totalSize + self._size[name] return self._frame[name] def add(self, name, **kw): return apply(self.insert, (name, len(self._paneNames)), kw) def delete(self, name): deletePos = self._nameToIndex(name) name = self._paneNames[deletePos] self.destroycomponent(name) del self._paneNames[deletePos] del self._frame[name] del self._size[name] del self._min[name] del self._max[name] del self._relsize[name] del self._relmin[name] del self._relmax[name] last = len(self._paneNames) del self._separator[last] del self._button[last] if last > 0: self.destroycomponent(self._sepName(last)) self.destroycomponent(self._buttonName(last)) self._plotHandles() def setnaturalsize(self): self.update_idletasks() totalWidth = 0 totalHeight = 0 maxWidth = 0 maxHeight = 0 for name in self._paneNames: frame = self._frame[name] w = frame.winfo_reqwidth() h = frame.winfo_reqheight() totalWidth = totalWidth + w totalHeight = totalHeight + h if maxWidth < w: maxWidth = w if maxHeight < h: maxHeight = h # Note that, since the hull is a frame, the width and height # options specify the geometry *outside* the borderwidth and # highlightthickness. bw = string.atoi(str(self.cget('hull_borderwidth'))) hl = string.atoi(str(self.cget('hull_highlightthickness'))) extra = (bw + hl) * 2 if str(self.cget('orient')) == 'horizontal': totalWidth = totalWidth + extra maxHeight = maxHeight + extra self.configure(hull_width = totalWidth, hull_height = maxHeight) else: totalHeight = (totalHeight + extra + (len(self._paneNames) - 1) * self._separatorThickness) maxWidth = maxWidth + extra self.configure(hull_width = maxWidth, hull_height = totalHeight) def move(self, name, newPos, newPosOffset = 0): # see if we can spare ourselves some work numPanes = len(self._paneNames) if numPanes < 2: return newPos = self._nameToIndex(newPos) + newPosOffset if newPos < 0 or newPos >=numPanes: return deletePos = self._nameToIndex(name) if deletePos == newPos: # inserting over ourself is a no-op return # delete name from old position in list name = self._paneNames[deletePos] del self._paneNames[deletePos] # place in new position self._paneNames[newPos:newPos] = [name] # force everything to redraw self._plotHandles() self._updateSizes() def _nameToIndex(self, nameOrIndex): try: pos = self._paneNames.index(nameOrIndex) except ValueError: pos = nameOrIndex return pos def _initPaneOptions(self, name): # Set defaults. self._size[name] = 0 self._relsize[name] = None self._min[name] = 0 self._relmin[name] = None self._max[name] = 100000 self._relmax[name] = None def _parsePaneOptions(self, name, args): # Parse <args> for options. for arg, value in args.items(): if type(value) == types.FloatType: relvalue = value value = self._absSize(relvalue) else: relvalue = None if arg == 'size': self._size[name], self._relsize[name] = value, relvalue elif arg == 'min': self._min[name], self._relmin[name] = value, relvalue elif arg == 'max': self._max[name], self._relmax[name] = value, relvalue else: raise ValueError, 'keyword must be "size", "min", or "max"' def _absSize(self, relvalue): return int(round(relvalue * self._majorSize)) def _sepName(self, n): return 'separator-%d' % n def _buttonName(self, n): return 'handle-%d' % n def _addSeparator(self): n = len(self._paneNames) - 1 downFunc = lambda event, s = self, num=n: s._btnDown(event, num) upFunc = lambda event, s = self, num=n: s._btnUp(event, num) moveFunc = lambda event, s = self, num=n: s._btnMove(event, num) # Create the line dividing the panes. sep = self.createcomponent(self._sepName(n), (), 'Separator', Tkinter.Frame, (self.interior(),), borderwidth = 1, relief = self['separatorrelief']) self._separator.append(sep) sep.bind('<ButtonPress-1>', downFunc) sep.bind('<Any-ButtonRelease-1>', upFunc) sep.bind('<B1-Motion>', moveFunc) if self['orient'] == 'vertical': cursor = 'sb_v_double_arrow' sep.configure(height = self._separatorThickness, width = 10000, cursor = cursor) else: cursor = 'sb_h_double_arrow' sep.configure(width = self._separatorThickness, height = 10000, cursor = cursor) self._totalSize = self._totalSize + self._separatorThickness # Create the handle on the dividing line. handle = self.createcomponent(self._buttonName(n), (), 'Handle', Tkinter.Frame, (self.interior(),), relief = 'raised', borderwidth = 1, width = self._handleSize, height = self._handleSize, cursor = cursor, ) self._button.append(handle) handle.bind('<ButtonPress-1>', downFunc) handle.bind('<Any-ButtonRelease-1>', upFunc) handle.bind('<B1-Motion>', moveFunc) self._plotHandles() for i in range(1, len(self._paneNames)): self._separator[i].tkraise() for i in range(1, len(self._paneNames)): self._button[i].tkraise() def _btnUp(self, event, item): self._buttonIsDown = 0 self._updateSizes() try: self._button[item].configure(relief='raised') except: pass def _btnDown(self, event, item): self._button[item].configure(relief='sunken') self._getMotionLimit(item) self._buttonIsDown = 1 self._movePending = 0 def _handleConfigure(self, event = None): self._getNaturalSizes() if self._totalSize == 0: return iterRange = list(self._paneNames) iterRange.reverse() if self._majorSize > self._totalSize: n = self._majorSize - self._totalSize self._iterate(iterRange, self._grow, n) elif self._majorSize < self._totalSize: n = self._totalSize - self._majorSize self._iterate(iterRange, self._shrink, n) self._plotHandles() self._updateSizes() def _getNaturalSizes(self): # Must call this in order to get correct winfo_width, winfo_height self.update_idletasks() self._totalSize = 0 if self['orient'] == 'vertical': self._majorSize = self.winfo_height() self._minorSize = self.winfo_width() majorspec = Tkinter.Frame.winfo_reqheight else: self._majorSize = self.winfo_width() self._minorSize = self.winfo_height() majorspec = Tkinter.Frame.winfo_reqwidth bw = string.atoi(str(self.cget('hull_borderwidth'))) hl = string.atoi(str(self.cget('hull_highlightthickness'))) extra = (bw + hl) * 2 self._majorSize = self._majorSize - extra self._minorSize = self._minorSize - extra if self._majorSize < 0: self._majorSize = 0 if self._minorSize < 0: self._minorSize = 0 for name in self._paneNames: # adjust the absolute sizes first... if self._relsize[name] is None: #special case if self._size[name] == 0: self._size[name] = apply(majorspec, (self._frame[name],)) self._setrel(name) else: self._size[name] = self._absSize(self._relsize[name]) if self._relmin[name] is not None: self._min[name] = self._absSize(self._relmin[name]) if self._relmax[name] is not None: self._max[name] = self._absSize(self._relmax[name]) # now adjust sizes if self._size[name] < self._min[name]: self._size[name] = self._min[name] self._setrel(name) if self._size[name] > self._max[name]: self._size[name] = self._max[name] self._setrel(name) self._totalSize = self._totalSize + self._size[name] # adjust for separators self._totalSize = (self._totalSize + (len(self._paneNames) - 1) * self._separatorThickness) def _setrel(self, name): if self._relsize[name] is not None: if self._majorSize != 0: self._relsize[name] = round(self._size[name]) / self._majorSize def _iterate(self, names, proc, n): for i in names: n = apply(proc, (i, n)) if n == 0: break def _grow(self, name, n): canGrow = self._max[name] - self._size[name] if canGrow > n: self._size[name] = self._size[name] + n self._setrel(name) return 0 elif canGrow > 0: self._size[name] = self._max[name] self._setrel(name) n = n - canGrow return n def _shrink(self, name, n): canShrink = self._size[name] - self._min[name] if canShrink > n: self._size[name] = self._size[name] - n self._setrel(name) return 0 elif canShrink > 0: self._size[name] = self._min[name] self._setrel(name) n = n - canShrink return n def _updateSizes(self): totalSize = 0 for name in self._paneNames: size = self._size[name] if self['orient'] == 'vertical': self._frame[name].place(x = 0, relwidth = 1, y = totalSize, height = size) else: self._frame[name].place(y = 0, relheight = 1, x = totalSize, width = size) totalSize = totalSize + size + self._separatorThickness # Invoke the callback command cmd = self['command'] if callable(cmd): cmd(map(lambda x, s = self: s._size[x], self._paneNames)) def _plotHandles(self): if len(self._paneNames) == 0: return if self['orient'] == 'vertical': btnp = self._minorSize - 13 else: h = self._minorSize if h > 18: btnp = 9 else: btnp = h - 9 firstPane = self._paneNames[0] totalSize = self._size[firstPane] first = 1 last = len(self._paneNames) - 1 # loop from first to last, inclusive for i in range(1, last + 1): handlepos = totalSize - 3 prevSize = self._size[self._paneNames[i - 1]] nextSize = self._size[self._paneNames[i]] offset1 = 0 if i == first: if prevSize < 4: offset1 = 4 - prevSize else: if prevSize < 8: offset1 = (8 - prevSize) / 2 offset2 = 0 if i == last: if nextSize < 4: offset2 = nextSize - 4 else: if nextSize < 8: offset2 = (nextSize - 8) / 2 handlepos = handlepos + offset1 if self['orient'] == 'vertical': height = 8 - offset1 + offset2 if height > 1: self._button[i].configure(height = height) self._button[i].place(x = btnp, y = handlepos) else: self._button[i].place_forget() self._separator[i].place(x = 0, y = totalSize, relwidth = 1) else: width = 8 - offset1 + offset2 if width > 1: self._button[i].configure(width = width) self._button[i].place(y = btnp, x = handlepos) else: self._button[i].place_forget() self._separator[i].place(y = 0, x = totalSize, relheight = 1) totalSize = totalSize + nextSize + self._separatorThickness def pane(self, name): return self._frame[self._paneNames[self._nameToIndex(name)]] # Return the name of all panes def panes(self): return list(self._paneNames) def configurepane(self, name, **kw): name = self._paneNames[self._nameToIndex(name)] self._parsePaneOptions(name, kw) self._handleConfigure() def updatelayout(self): self._handleConfigure() def _getMotionLimit(self, item): curBefore = (item - 1) * self._separatorThickness minBefore, maxBefore = curBefore, curBefore for name in self._paneNames[:item]: curBefore = curBefore + self._size[name] minBefore = minBefore + self._min[name] maxBefore = maxBefore + self._max[name] curAfter = (len(self._paneNames) - item) * self._separatorThickness minAfter, maxAfter = curAfter, curAfter for name in self._paneNames[item:]: curAfter = curAfter + self._size[name] minAfter = minAfter + self._min[name] maxAfter = maxAfter + self._max[name] beforeToGo = min(curBefore - minBefore, maxAfter - curAfter) afterToGo = min(curAfter - minAfter, maxBefore - curBefore) self._beforeLimit = curBefore - beforeToGo self._afterLimit = curBefore + afterToGo self._curSize = curBefore self._plotHandles() # Compress the motion so that update is quick even on slow machines # # theRootp = root position (either rootx or rooty) def _btnMove(self, event, item): self._rootp = event if self._movePending == 0: self._timerId = self.after_idle( lambda s = self, i = item: s._btnMoveCompressed(i)) self._movePending = 1 def destroy(self): if self._timerId is not None: self.after_cancel(self._timerId) self._timerId = None MegaWidget.destroy(self) def _btnMoveCompressed(self, item): if not self._buttonIsDown: return if self['orient'] == 'vertical': p = self._rootp.y_root - self.winfo_rooty() else: p = self._rootp.x_root - self.winfo_rootx() if p == self._curSize: self._movePending = 0 return if p < self._beforeLimit: p = self._beforeLimit if p >= self._afterLimit: p = self._afterLimit self._calculateChange(item, p) self.update_idletasks() self._movePending = 0 # Calculate the change in response to mouse motions def _calculateChange(self, item, p): if p < self._curSize: self._moveBefore(item, p) elif p > self._curSize: self._moveAfter(item, p) self._plotHandles() def _moveBefore(self, item, p): n = self._curSize - p # Shrink the frames before iterRange = list(self._paneNames[:item]) iterRange.reverse() self._iterate(iterRange, self._shrink, n) # Adjust the frames after iterRange = self._paneNames[item:] self._iterate(iterRange, self._grow, n) self._curSize = p def _moveAfter(self, item, p): n = p - self._curSize # Shrink the frames after iterRange = self._paneNames[item:] self._iterate(iterRange, self._shrink, n) # Adjust the frames before iterRange = list(self._paneNames[:item]) iterRange.reverse() self._iterate(iterRange, self._grow, n) self._curSize = p ###################################################################### ### File: PmwPromptDialog.py # Based on iwidgets2.2.0/promptdialog.itk code. # A Dialog with an entryfield class PromptDialog(Dialog): def __init__(self, parent = None, **kw): # Define the megawidget options. optiondefs = ( ('borderx', 20, INITOPT), ('bordery', 20, INITOPT), ) self.defineoptions(kw, optiondefs) # Initialise the base class (after defining the options). Dialog.__init__(self, parent) # Create the components. interior = self.interior() aliases = ( ('entry', 'entryfield_entry'), ('label', 'entryfield_label'), ) self._promptDialogEntry = self.createcomponent('entryfield', aliases, None, EntryField, (interior,)) self._promptDialogEntry.pack(fill='x', expand=1, padx = self['borderx'], pady = self['bordery']) if not kw.has_key('activatecommand'): # Whenever this dialog is activated, set the focus to the # EntryField's entry widget. tkentry = self.component('entry') self.configure(activatecommand = tkentry.focus_set) # Check keywords and initialise options. self.initialiseoptions() # Supply aliases to some of the entry component methods. def insertentry(self, index, text): self._promptDialogEntry.insert(index, text) def deleteentry(self, first, last=None): self._promptDialogEntry.delete(first, last) def indexentry(self, index): return self._promptDialogEntry.index(index) forwardmethods(PromptDialog, EntryField, '_promptDialogEntry') ###################################################################### ### File: PmwRadioSelect.py import types import Tkinter class RadioSelect(MegaWidget): # A collection of several buttons. In single mode, only one # button may be selected. In multiple mode, any number of buttons # may be selected. def __init__(self, parent = None, **kw): # Define the megawidget options. optiondefs = ( ('buttontype', 'button', INITOPT), ('command', None, None), ('labelmargin', 0, INITOPT), ('labelpos', None, INITOPT), ('orient', 'horizontal', INITOPT), ('padx', 5, INITOPT), ('pady', 5, INITOPT), ('selectmode', 'single', INITOPT), ) self.defineoptions(kw, optiondefs, dynamicGroups = ('Button',)) # Initialise the base class (after defining the options). MegaWidget.__init__(self, parent) # Create the components. interior = self.interior() if self['labelpos'] is None: self._radioSelectFrame = self._hull else: self._radioSelectFrame = self.createcomponent('frame', (), None, Tkinter.Frame, (interior,)) self._radioSelectFrame.grid(column=2, row=2, sticky='nsew') interior.grid_columnconfigure(2, weight=1) interior.grid_rowconfigure(2, weight=1) self.createlabel(interior) # Initialise instance variables. self._buttonList = [] if self['selectmode'] == 'single': self._singleSelect = 1 elif self['selectmode'] == 'multiple': self._singleSelect = 0 else: raise ValueError, 'bad selectmode option "' + \ self['selectmode'] + '": should be single or multiple' if self['buttontype'] == 'button': self.buttonClass = Tkinter.Button elif self['buttontype'] == 'radiobutton': self._singleSelect = 1 self.var = Tkinter.StringVar() self.buttonClass = Tkinter.Radiobutton elif self['buttontype'] == 'checkbutton': self._singleSelect = 0 self.buttonClass = Tkinter.Checkbutton else: raise ValueError, 'bad buttontype option "' + \ self['buttontype'] + \ '": should be button, radiobutton or checkbutton' if self._singleSelect: self.selection = None else: self.selection = [] if self['orient'] not in ('horizontal', 'vertical'): raise ValueError, 'bad orient option ' + repr(self['orient']) + \ ': must be either \'horizontal\' or \'vertical\'' # Check keywords and initialise options. self.initialiseoptions() def getcurselection(self): if self._singleSelect: return self.selection else: return tuple(self.selection) def getvalue(self): return self.getcurselection() def setvalue(self, textOrList): if self._singleSelect: self.__setSingleValue(textOrList) else: # Multiple selections oldselection = self.selection self.selection = textOrList for button in self._buttonList: if button in oldselection: if button not in self.selection: # button is currently selected but should not be widget = self.component(button) if self['buttontype'] == 'checkbutton': widget.deselect() else: # Button widget.configure(relief='raised') else: if button in self.selection: # button is not currently selected but should be widget = self.component(button) if self['buttontype'] == 'checkbutton': widget.select() else: # Button widget.configure(relief='sunken') def numbuttons(self): return len(self._buttonList) def index(self, index): # Return the integer index of the button with the given index. listLength = len(self._buttonList) if type(index) == types.IntType: if index < listLength: return index else: raise ValueError, 'index "%s" is out of range' % index elif index is END: if listLength > 0: return listLength - 1 else: raise ValueError, 'RadioSelect has no buttons' else: for count in range(listLength): name = self._buttonList[count] if index == name: return count validValues = 'a name, a number or END' raise ValueError, \ 'bad index "%s": must be %s' % (index, validValues) def button(self, buttonIndex): name = self._buttonList[self.index(buttonIndex)] return self.component(name) def add(self, componentName, **kw): if componentName in self._buttonList: raise ValueError, 'button "%s" already exists' % componentName kw['command'] = \ lambda self=self, name=componentName: self.invoke(name) if not kw.has_key('text'): kw['text'] = componentName if self['buttontype'] == 'radiobutton': if not kw.has_key('anchor'): kw['anchor'] = 'w' if not kw.has_key('variable'): kw['variable'] = self.var if not kw.has_key('value'): kw['value'] = kw['text'] elif self['buttontype'] == 'checkbutton': if not kw.has_key('anchor'): kw['anchor'] = 'w' button = apply(self.createcomponent, (componentName, (), 'Button', self.buttonClass, (self._radioSelectFrame,)), kw) if self['orient'] == 'horizontal': self._radioSelectFrame.grid_rowconfigure(0, weight=1) col = len(self._buttonList) button.grid(column=col, row=0, padx = self['padx'], pady = self['pady'], sticky='nsew') self._radioSelectFrame.grid_columnconfigure(col, weight=1) else: self._radioSelectFrame.grid_columnconfigure(0, weight=1) row = len(self._buttonList) button.grid(column=0, row=row, padx = self['padx'], pady = self['pady'], sticky='ew') self._radioSelectFrame.grid_rowconfigure(row, weight=1) self._buttonList.append(componentName) return button def deleteall(self): for name in self._buttonList: self.destroycomponent(name) self._buttonList = [] if self._singleSelect: self.selection = None else: self.selection = [] def __setSingleValue(self, value): self.selection = value if self['buttontype'] == 'radiobutton': widget = self.component(value) widget.select() else: # Button for button in self._buttonList: widget = self.component(button) if button == value: widget.configure(relief='sunken') else: widget.configure(relief='raised') def invoke(self, index): index = self.index(index) name = self._buttonList[index] if self._singleSelect: self.__setSingleValue(name) command = self['command'] if callable(command): return command(name) else: # Multiple selections widget = self.component(name) if name in self.selection: if self['buttontype'] == 'checkbutton': widget.deselect() else: widget.configure(relief='raised') self.selection.remove(name) state = 0 else: if self['buttontype'] == 'checkbutton': widget.select() else: widget.configure(relief='sunken') self.selection.append(name) state = 1 command = self['command'] if callable(command): return command(name, state) ###################################################################### ### File: PmwScrolledCanvas.py import Tkinter class ScrolledCanvas(MegaWidget): def __init__(self, parent = None, **kw): # Define the megawidget options. optiondefs = ( ('borderframe', 0, INITOPT), ('canvasmargin', 0, INITOPT), ('hscrollmode', 'dynamic', self._hscrollMode), ('labelmargin', 0, INITOPT), ('labelpos', None, INITOPT), ('scrollmargin', 2, INITOPT), ('usehullsize', 0, INITOPT), ('vscrollmode', 'dynamic', self._vscrollMode), ) self.defineoptions(kw, optiondefs) # Initialise the base class (after defining the options). MegaWidget.__init__(self, parent) # Create the components. self.origInterior = MegaWidget.interior(self) if self['usehullsize']: self.origInterior.grid_propagate(0) if self['borderframe']: # Create a frame widget to act as the border of the canvas. self._borderframe = self.createcomponent('borderframe', (), None, Tkinter.Frame, (self.origInterior,), relief = 'sunken', borderwidth = 2, ) self._borderframe.grid(row = 2, column = 2, sticky = 'news') # Create the canvas widget. self._canvas = self.createcomponent('canvas', (), None, Tkinter.Canvas, (self._borderframe,), highlightthickness = 0, borderwidth = 0, ) self._canvas.pack(fill = 'both', expand = 1) else: # Create the canvas widget. self._canvas = self.createcomponent('canvas', (), None, Tkinter.Canvas, (self.origInterior,), relief = 'sunken', borderwidth = 2, ) self._canvas.grid(row = 2, column = 2, sticky = 'news') self.origInterior.grid_rowconfigure(2, weight = 1, minsize = 0) self.origInterior.grid_columnconfigure(2, weight = 1, minsize = 0) # Create the horizontal scrollbar self._horizScrollbar = self.createcomponent('horizscrollbar', (), 'Scrollbar', Tkinter.Scrollbar, (self.origInterior,), orient='horizontal', command=self._canvas.xview ) # Create the vertical scrollbar self._vertScrollbar = self.createcomponent('vertscrollbar', (), 'Scrollbar', Tkinter.Scrollbar, (self.origInterior,), orient='vertical', command=self._canvas.yview ) self.createlabel(self.origInterior, childCols = 3, childRows = 3) # Initialise instance variables. self._horizScrollbarOn = 0 self._vertScrollbarOn = 0 self.scrollTimer = None self._scrollRecurse = 0 self._horizScrollbarNeeded = 0 self._vertScrollbarNeeded = 0 self.setregionTimer = None # Check keywords and initialise options. self.initialiseoptions() def destroy(self): if self.scrollTimer is not None: self.after_cancel(self.scrollTimer) self.scrollTimer = None if self.setregionTimer is not None: self.after_cancel(self.setregionTimer) self.setregionTimer = None MegaWidget.destroy(self) # ====================================================================== # Public methods. def interior(self): return self._canvas def resizescrollregion(self): if self.setregionTimer is None: self.setregionTimer = self.after_idle(self._setRegion) # ====================================================================== # Configuration methods. def _hscrollMode(self): # The horizontal scroll mode has been configured. mode = self['hscrollmode'] if mode == 'static': if not self._horizScrollbarOn: self._toggleHorizScrollbar() elif mode == 'dynamic': if self._horizScrollbarNeeded != self._horizScrollbarOn: self._toggleHorizScrollbar() elif mode == 'none': if self._horizScrollbarOn: self._toggleHorizScrollbar() else: message = 'bad hscrollmode option "%s": should be static, dynamic, or none' % mode raise ValueError, message self._configureScrollCommands() def _vscrollMode(self): # The vertical scroll mode has been configured. mode = self['vscrollmode'] if mode == 'static': if not self._vertScrollbarOn: self._toggleVertScrollbar() elif mode == 'dynamic': if self._vertScrollbarNeeded != self._vertScrollbarOn: self._toggleVertScrollbar() elif mode == 'none': if self._vertScrollbarOn: self._toggleVertScrollbar() else: message = 'bad vscrollmode option "%s": should be static, dynamic, or none' % mode raise ValueError, message self._configureScrollCommands() # ====================================================================== # Private methods. def _configureScrollCommands(self): # If both scrollmodes are not dynamic we can save a lot of # time by not having to create an idle job to handle the # scroll commands. # Clean up previous scroll commands to prevent memory leak. tclCommandName = str(self._canvas.cget('xscrollcommand')) if tclCommandName != '': self._canvas.deletecommand(tclCommandName) tclCommandName = str(self._canvas.cget('yscrollcommand')) if tclCommandName != '': self._canvas.deletecommand(tclCommandName) if self['hscrollmode'] == self['vscrollmode'] == 'dynamic': self._canvas.configure( xscrollcommand=self._scrollBothLater, yscrollcommand=self._scrollBothLater ) else: self._canvas.configure( xscrollcommand=self._scrollXNow, yscrollcommand=self._scrollYNow ) def _scrollXNow(self, first, last): self._horizScrollbar.set(first, last) self._horizScrollbarNeeded = ((first, last) != ('0', '1')) if self['hscrollmode'] == 'dynamic': if self._horizScrollbarNeeded != self._horizScrollbarOn: self._toggleHorizScrollbar() def _scrollYNow(self, first, last): self._vertScrollbar.set(first, last) self._vertScrollbarNeeded = ((first, last) != ('0', '1')) if self['vscrollmode'] == 'dynamic': if self._vertScrollbarNeeded != self._vertScrollbarOn: self._toggleVertScrollbar() def _scrollBothLater(self, first, last): # Called by the canvas to set the horizontal or vertical # scrollbar when it has scrolled or changed scrollregion. if self.scrollTimer is None: self.scrollTimer = self.after_idle(self._scrollBothNow) def _scrollBothNow(self): # This performs the function of _scrollXNow and _scrollYNow. # If one is changed, the other should be updated to match. self.scrollTimer = None # Call update_idletasks to make sure that the containing frame # has been resized before we attempt to set the scrollbars. # Otherwise the scrollbars may be mapped/unmapped continuously. self._scrollRecurse = self._scrollRecurse + 1 self.update_idletasks() self._scrollRecurse = self._scrollRecurse - 1 if self._scrollRecurse != 0: return xview = self._canvas.xview() yview = self._canvas.yview() self._horizScrollbar.set(xview[0], xview[1]) self._vertScrollbar.set(yview[0], yview[1]) self._horizScrollbarNeeded = (xview != (0.0, 1.0)) self._vertScrollbarNeeded = (yview != (0.0, 1.0)) # If both horizontal and vertical scrollmodes are dynamic and # currently only one scrollbar is mapped and both should be # toggled, then unmap the mapped scrollbar. This prevents a # continuous mapping and unmapping of the scrollbars. if (self['hscrollmode'] == self['vscrollmode'] == 'dynamic' and self._horizScrollbarNeeded != self._horizScrollbarOn and self._vertScrollbarNeeded != self._vertScrollbarOn and self._vertScrollbarOn != self._horizScrollbarOn): if self._horizScrollbarOn: self._toggleHorizScrollbar() else: self._toggleVertScrollbar() return if self['hscrollmode'] == 'dynamic': if self._horizScrollbarNeeded != self._horizScrollbarOn: self._toggleHorizScrollbar() if self['vscrollmode'] == 'dynamic': if self._vertScrollbarNeeded != self._vertScrollbarOn: self._toggleVertScrollbar() def _toggleHorizScrollbar(self): self._horizScrollbarOn = not self._horizScrollbarOn interior = self.origInterior if self._horizScrollbarOn: self._horizScrollbar.grid(row = 4, column = 2, sticky = 'news') interior.grid_rowconfigure(3, minsize = self['scrollmargin']) else: self._horizScrollbar.grid_forget() interior.grid_rowconfigure(3, minsize = 0) def _toggleVertScrollbar(self): self._vertScrollbarOn = not self._vertScrollbarOn interior = self.origInterior if self._vertScrollbarOn: self._vertScrollbar.grid(row = 2, column = 4, sticky = 'news') interior.grid_columnconfigure(3, minsize = self['scrollmargin']) else: self._vertScrollbar.grid_forget() interior.grid_columnconfigure(3, minsize = 0) def _setRegion(self): self.setregionTimer = None region = self._canvas.bbox('all') if region is not None: canvasmargin = self['canvasmargin'] region = (region[0] - canvasmargin, region[1] - canvasmargin, region[2] + canvasmargin, region[3] + canvasmargin) self._canvas.configure(scrollregion = region) # Need to explicitly forward this to override the stupid # (grid_)bbox method inherited from Tkinter.Frame.Grid. def bbox(self, *args): return apply(self._canvas.bbox, args) forwardmethods(ScrolledCanvas, Tkinter.Canvas, '_canvas') ###################################################################### ### File: PmwScrolledField.py import Tkinter class ScrolledField(MegaWidget): def __init__(self, parent = None, **kw): # Define the megawidget options. optiondefs = ( ('labelmargin', 0, INITOPT), ('labelpos', None, INITOPT), ('sticky', 'ew', INITOPT), ('text', '', self._text), ) self.defineoptions(kw, optiondefs) # Initialise the base class (after defining the options). MegaWidget.__init__(self, parent) # Create the components. interior = self.interior() self._scrolledFieldEntry = self.createcomponent('entry', (), None, Tkinter.Entry, (interior,)) # Can't always use 'disabled', since this greys out text in Tk 8.4.2 try: self._scrolledFieldEntry.configure(state = 'readonly') except Tkinter.TclError: self._scrolledFieldEntry.configure(state = 'disabled') self._scrolledFieldEntry.grid(column=2, row=2, sticky=self['sticky']) interior.grid_columnconfigure(2, weight=1) interior.grid_rowconfigure(2, weight=1) self.createlabel(interior) # Check keywords and initialise options. self.initialiseoptions() def _text(self): text = self['text'] self._scrolledFieldEntry.configure(state = 'normal') self._scrolledFieldEntry.delete(0, 'end') self._scrolledFieldEntry.insert('end', text) # Can't always use 'disabled', since this greys out text in Tk 8.4.2 try: self._scrolledFieldEntry.configure(state = 'readonly') except Tkinter.TclError: self._scrolledFieldEntry.configure(state = 'disabled') forwardmethods(ScrolledField, Tkinter.Entry, '_scrolledFieldEntry') ###################################################################### ### File: PmwScrolledFrame.py import string import types import Tkinter class ScrolledFrame(MegaWidget): def __init__(self, parent = None, **kw): # Define the megawidget options. optiondefs = ( ('borderframe', 1, INITOPT), ('horizflex', 'fixed', self._horizflex), ('horizfraction', 0.05, INITOPT), ('hscrollmode', 'dynamic', self._hscrollMode), ('labelmargin', 0, INITOPT), ('labelpos', None, INITOPT), ('scrollmargin', 2, INITOPT), ('usehullsize', 0, INITOPT), ('vertflex', 'fixed', self._vertflex), ('vertfraction', 0.05, INITOPT), ('vscrollmode', 'dynamic', self._vscrollMode), ) self.defineoptions(kw, optiondefs) # Initialise the base class (after defining the options). MegaWidget.__init__(self, parent) # Create the components. self.origInterior = MegaWidget.interior(self) if self['usehullsize']: self.origInterior.grid_propagate(0) if self['borderframe']: # Create a frame widget to act as the border of the clipper. self._borderframe = self.createcomponent('borderframe', (), None, Tkinter.Frame, (self.origInterior,), relief = 'sunken', borderwidth = 2, ) self._borderframe.grid(row = 2, column = 2, sticky = 'news') # Create the clipping window. self._clipper = self.createcomponent('clipper', (), None, Tkinter.Frame, (self._borderframe,), width = 400, height = 300, highlightthickness = 0, borderwidth = 0, ) self._clipper.pack(fill = 'both', expand = 1) else: # Create the clipping window. self._clipper = self.createcomponent('clipper', (), None, Tkinter.Frame, (self.origInterior,), width = 400, height = 300, relief = 'sunken', borderwidth = 2, ) self._clipper.grid(row = 2, column = 2, sticky = 'news') self.origInterior.grid_rowconfigure(2, weight = 1, minsize = 0) self.origInterior.grid_columnconfigure(2, weight = 1, minsize = 0) # Create the horizontal scrollbar self._horizScrollbar = self.createcomponent('horizscrollbar', (), 'Scrollbar', Tkinter.Scrollbar, (self.origInterior,), orient='horizontal', command=self.xview ) # Create the vertical scrollbar self._vertScrollbar = self.createcomponent('vertscrollbar', (), 'Scrollbar', Tkinter.Scrollbar, (self.origInterior,), orient='vertical', command=self.yview ) self.createlabel(self.origInterior, childCols = 3, childRows = 3) # Initialise instance variables. self._horizScrollbarOn = 0 self._vertScrollbarOn = 0 self.scrollTimer = None self._scrollRecurse = 0 self._horizScrollbarNeeded = 0 self._vertScrollbarNeeded = 0 self.startX = 0 self.startY = 0 self._flexoptions = ('fixed', 'expand', 'shrink', 'elastic') # Create a frame in the clipper to contain the widgets to be # scrolled. self._frame = self.createcomponent('frame', (), None, Tkinter.Frame, (self._clipper,) ) # Whenever the clipping window or scrolled frame change size, # update the scrollbars. self._frame.bind('<Configure>', self._reposition) self._clipper.bind('<Configure>', self._reposition) # Work around a bug in Tk where the value returned by the # scrollbar get() method is (0.0, 0.0, 0.0, 0.0) rather than # the expected 2-tuple. This occurs if xview() is called soon # after the ScrolledFrame has been created. self._horizScrollbar.set(0.0, 1.0) self._vertScrollbar.set(0.0, 1.0) # Check keywords and initialise options. self.initialiseoptions() def destroy(self): if self.scrollTimer is not None: self.after_cancel(self.scrollTimer) self.scrollTimer = None MegaWidget.destroy(self) # ====================================================================== # Public methods. def interior(self): return self._frame # Set timer to call real reposition method, so that it is not # called multiple times when many things are reconfigured at the # same time. def reposition(self): if self.scrollTimer is None: self.scrollTimer = self.after_idle(self._scrollBothNow) # Called when the user clicks in the horizontal scrollbar. # Calculates new position of frame then calls reposition() to # update the frame and the scrollbar. def xview(self, mode = None, value = None, units = None): if type(value) == types.StringType: value = string.atof(value) if mode is None: return self._horizScrollbar.get() elif mode == 'moveto': frameWidth = self._frame.winfo_reqwidth() self.startX = value * float(frameWidth) else: # mode == 'scroll' clipperWidth = self._clipper.winfo_width() if units == 'units': jump = int(clipperWidth * self['horizfraction']) else: jump = clipperWidth self.startX = self.startX + value * jump self.reposition() # Called when the user clicks in the vertical scrollbar. # Calculates new position of frame then calls reposition() to # update the frame and the scrollbar. def yview(self, mode = None, value = None, units = None): if type(value) == types.StringType: value = string.atof(value) if mode is None: return self._vertScrollbar.get() elif mode == 'moveto': frameHeight = self._frame.winfo_reqheight() self.startY = value * float(frameHeight) else: # mode == 'scroll' clipperHeight = self._clipper.winfo_height() if units == 'units': jump = int(clipperHeight * self['vertfraction']) else: jump = clipperHeight self.startY = self.startY + value * jump self.reposition() # ====================================================================== # Configuration methods. def _hscrollMode(self): # The horizontal scroll mode has been configured. mode = self['hscrollmode'] if mode == 'static': if not self._horizScrollbarOn: self._toggleHorizScrollbar() elif mode == 'dynamic': if self._horizScrollbarNeeded != self._horizScrollbarOn: self._toggleHorizScrollbar() elif mode == 'none': if self._horizScrollbarOn: self._toggleHorizScrollbar() else: message = 'bad hscrollmode option "%s": should be static, dynamic, or none' % mode raise ValueError, message def _vscrollMode(self): # The vertical scroll mode has been configured. mode = self['vscrollmode'] if mode == 'static': if not self._vertScrollbarOn: self._toggleVertScrollbar() elif mode == 'dynamic': if self._vertScrollbarNeeded != self._vertScrollbarOn: self._toggleVertScrollbar() elif mode == 'none': if self._vertScrollbarOn: self._toggleVertScrollbar() else: message = 'bad vscrollmode option "%s": should be static, dynamic, or none' % mode raise ValueError, message def _horizflex(self): # The horizontal flex mode has been configured. flex = self['horizflex'] if flex not in self._flexoptions: message = 'bad horizflex option "%s": should be one of %s' % \ (flex, str(self._flexoptions)) raise ValueError, message self.reposition() def _vertflex(self): # The vertical flex mode has been configured. flex = self['vertflex'] if flex not in self._flexoptions: message = 'bad vertflex option "%s": should be one of %s' % \ (flex, str(self._flexoptions)) raise ValueError, message self.reposition() # ====================================================================== # Private methods. def _reposition(self, event): self.reposition() def _getxview(self): # Horizontal dimension. clipperWidth = self._clipper.winfo_width() frameWidth = self._frame.winfo_reqwidth() if frameWidth <= clipperWidth: # The scrolled frame is smaller than the clipping window. self.startX = 0 endScrollX = 1.0 if self['horizflex'] in ('expand', 'elastic'): relwidth = 1 else: relwidth = '' else: # The scrolled frame is larger than the clipping window. if self['horizflex'] in ('shrink', 'elastic'): self.startX = 0 endScrollX = 1.0 relwidth = 1 else: if self.startX + clipperWidth > frameWidth: self.startX = frameWidth - clipperWidth endScrollX = 1.0 else: if self.startX < 0: self.startX = 0 endScrollX = (self.startX + clipperWidth) / float(frameWidth) relwidth = '' # Position frame relative to clipper. self._frame.place(x = -self.startX, relwidth = relwidth) return (self.startX / float(frameWidth), endScrollX) def _getyview(self): # Vertical dimension. clipperHeight = self._clipper.winfo_height() frameHeight = self._frame.winfo_reqheight() if frameHeight <= clipperHeight: # The scrolled frame is smaller than the clipping window. self.startY = 0 endScrollY = 1.0 if self['vertflex'] in ('expand', 'elastic'): relheight = 1 else: relheight = '' else: # The scrolled frame is larger than the clipping window. if self['vertflex'] in ('shrink', 'elastic'): self.startY = 0 endScrollY = 1.0 relheight = 1 else: if self.startY + clipperHeight > frameHeight: self.startY = frameHeight - clipperHeight endScrollY = 1.0 else: if self.startY < 0: self.startY = 0 endScrollY = (self.startY + clipperHeight) / float(frameHeight) relheight = '' # Position frame relative to clipper. self._frame.place(y = -self.startY, relheight = relheight) return (self.startY / float(frameHeight), endScrollY) # According to the relative geometries of the frame and the # clipper, reposition the frame within the clipper and reset the # scrollbars. def _scrollBothNow(self): self.scrollTimer = None # Call update_idletasks to make sure that the containing frame # has been resized before we attempt to set the scrollbars. # Otherwise the scrollbars may be mapped/unmapped continuously. self._scrollRecurse = self._scrollRecurse + 1 self.update_idletasks() self._scrollRecurse = self._scrollRecurse - 1 if self._scrollRecurse != 0: return xview = self._getxview() yview = self._getyview() self._horizScrollbar.set(xview[0], xview[1]) self._vertScrollbar.set(yview[0], yview[1]) self._horizScrollbarNeeded = (xview != (0.0, 1.0)) self._vertScrollbarNeeded = (yview != (0.0, 1.0)) # If both horizontal and vertical scrollmodes are dynamic and # currently only one scrollbar is mapped and both should be # toggled, then unmap the mapped scrollbar. This prevents a # continuous mapping and unmapping of the scrollbars. if (self['hscrollmode'] == self['vscrollmode'] == 'dynamic' and self._horizScrollbarNeeded != self._horizScrollbarOn and self._vertScrollbarNeeded != self._vertScrollbarOn and self._vertScrollbarOn != self._horizScrollbarOn): if self._horizScrollbarOn: self._toggleHorizScrollbar() else: self._toggleVertScrollbar() return if self['hscrollmode'] == 'dynamic': if self._horizScrollbarNeeded != self._horizScrollbarOn: self._toggleHorizScrollbar() if self['vscrollmode'] == 'dynamic': if self._vertScrollbarNeeded != self._vertScrollbarOn: self._toggleVertScrollbar() def _toggleHorizScrollbar(self): self._horizScrollbarOn = not self._horizScrollbarOn interior = self.origInterior if self._horizScrollbarOn: self._horizScrollbar.grid(row = 4, column = 2, sticky = 'news') interior.grid_rowconfigure(3, minsize = self['scrollmargin']) else: self._horizScrollbar.grid_forget() interior.grid_rowconfigure(3, minsize = 0) def _toggleVertScrollbar(self): self._vertScrollbarOn = not self._vertScrollbarOn interior = self.origInterior if self._vertScrollbarOn: self._vertScrollbar.grid(row = 2, column = 4, sticky = 'news') interior.grid_columnconfigure(3, minsize = self['scrollmargin']) else: self._vertScrollbar.grid_forget() interior.grid_columnconfigure(3, minsize = 0) ###################################################################### ### File: PmwScrolledListBox.py # Based on iwidgets2.2.0/scrolledlistbox.itk code. import types import Tkinter class ScrolledListBox(MegaWidget): _classBindingsDefinedFor = 0 def __init__(self, parent = None, **kw): # Define the megawidget options. optiondefs = ( ('dblclickcommand', None, None), ('hscrollmode', 'dynamic', self._hscrollMode), ('items', (), INITOPT), ('labelmargin', 0, INITOPT), ('labelpos', None, INITOPT), ('scrollmargin', 2, INITOPT), ('selectioncommand', None, None), ('usehullsize', 0, INITOPT), ('vscrollmode', 'dynamic', self._vscrollMode), ) self.defineoptions(kw, optiondefs) # Initialise the base class (after defining the options). MegaWidget.__init__(self, parent) # Create the components. interior = self.interior() if self['usehullsize']: interior.grid_propagate(0) # Create the listbox widget. self._listbox = self.createcomponent('listbox', (), None, Tkinter.Listbox, (interior,)) self._listbox.grid(row = 2, column = 2, sticky = 'news') interior.grid_rowconfigure(2, weight = 1, minsize = 0) interior.grid_columnconfigure(2, weight = 1, minsize = 0) # Create the horizontal scrollbar self._horizScrollbar = self.createcomponent('horizscrollbar', (), 'Scrollbar', Tkinter.Scrollbar, (interior,), orient='horizontal', command=self._listbox.xview ) # Create the vertical scrollbar self._vertScrollbar = self.createcomponent('vertscrollbar', (), 'Scrollbar', Tkinter.Scrollbar, (interior,), orient='vertical', command=self._listbox.yview ) self.createlabel(interior, childCols = 3, childRows = 3) # Add the items specified by the initialisation option. items = self['items'] if type(items) != types.TupleType: items = tuple(items) if len(items) > 0: apply(self._listbox.insert, ('end',) + items) _registerScrolledList(self._listbox, self) # Establish the special class bindings if not already done. # Also create bindings if the Tkinter default interpreter has # changed. Use Tkinter._default_root to create class # bindings, so that a reference to root is created by # bind_class rather than a reference to self, which would # prevent object cleanup. theTag = 'ScrolledListBoxTag' if ScrolledListBox._classBindingsDefinedFor != Tkinter._default_root: root = Tkinter._default_root def doubleEvent(event): _handleEvent(event, 'double') def keyEvent(event): _handleEvent(event, 'key') def releaseEvent(event): _handleEvent(event, 'release') # Bind space and return keys and button 1 to the selectioncommand. root.bind_class(theTag, '<Key-space>', keyEvent) root.bind_class(theTag, '<Key-Return>', keyEvent) root.bind_class(theTag, '<ButtonRelease-1>', releaseEvent) # Bind double button 1 click to the dblclickcommand. root.bind_class(theTag, '<Double-ButtonRelease-1>', doubleEvent) ScrolledListBox._classBindingsDefinedFor = root bindtags = self._listbox.bindtags() self._listbox.bindtags(bindtags + (theTag,)) # Initialise instance variables. self._horizScrollbarOn = 0 self._vertScrollbarOn = 0 self.scrollTimer = None self._scrollRecurse = 0 self._horizScrollbarNeeded = 0 self._vertScrollbarNeeded = 0 # Check keywords and initialise options. self.initialiseoptions() def destroy(self): if self.scrollTimer is not None: self.after_cancel(self.scrollTimer) self.scrollTimer = None _deregisterScrolledList(self._listbox) MegaWidget.destroy(self) # ====================================================================== # Public methods. def clear(self): self.setlist(()) def getcurselection(self): rtn = [] for sel in self.curselection(): rtn.append(self._listbox.get(sel)) return tuple(rtn) def getvalue(self): return self.getcurselection() def setvalue(self, textOrList): self._listbox.selection_clear(0, 'end') listitems = list(self._listbox.get(0, 'end')) if type(textOrList) == types.StringType: if textOrList in listitems: self._listbox.selection_set(listitems.index(textOrList)) else: raise ValueError, 'no such item "%s"' % textOrList else: for item in textOrList: if item in listitems: self._listbox.selection_set(listitems.index(item)) else: raise ValueError, 'no such item "%s"' % item def setlist(self, items): self._listbox.delete(0, 'end') if len(items) > 0: if type(items) != types.TupleType: items = tuple(items) apply(self._listbox.insert, (0,) + items) # Override Tkinter.Listbox get method, so that if it is called with # no arguments, return all list elements (consistent with other widgets). def get(self, first=None, last=None): if first is None: return self._listbox.get(0, 'end') else: return self._listbox.get(first, last) # ====================================================================== # Configuration methods. def _hscrollMode(self): # The horizontal scroll mode has been configured. mode = self['hscrollmode'] if mode == 'static': if not self._horizScrollbarOn: self._toggleHorizScrollbar() elif mode == 'dynamic': if self._horizScrollbarNeeded != self._horizScrollbarOn: self._toggleHorizScrollbar() elif mode == 'none': if self._horizScrollbarOn: self._toggleHorizScrollbar() else: message = 'bad hscrollmode option "%s": should be static, dynamic, or none' % mode raise ValueError, message self._configureScrollCommands() def _vscrollMode(self): # The vertical scroll mode has been configured. mode = self['vscrollmode'] if mode == 'static': if not self._vertScrollbarOn: self._toggleVertScrollbar() elif mode == 'dynamic': if self._vertScrollbarNeeded != self._vertScrollbarOn: self._toggleVertScrollbar() elif mode == 'none': if self._vertScrollbarOn: self._toggleVertScrollbar() else: message = 'bad vscrollmode option "%s": should be static, dynamic, or none' % mode raise ValueError, message self._configureScrollCommands() # ====================================================================== # Private methods. def _configureScrollCommands(self): # If both scrollmodes are not dynamic we can save a lot of # time by not having to create an idle job to handle the # scroll commands. # Clean up previous scroll commands to prevent memory leak. tclCommandName = str(self._listbox.cget('xscrollcommand')) if tclCommandName != '': self._listbox.deletecommand(tclCommandName) tclCommandName = str(self._listbox.cget('yscrollcommand')) if tclCommandName != '': self._listbox.deletecommand(tclCommandName) if self['hscrollmode'] == self['vscrollmode'] == 'dynamic': self._listbox.configure( xscrollcommand=self._scrollBothLater, yscrollcommand=self._scrollBothLater ) else: self._listbox.configure( xscrollcommand=self._scrollXNow, yscrollcommand=self._scrollYNow ) def _scrollXNow(self, first, last): self._horizScrollbar.set(first, last) self._horizScrollbarNeeded = ((first, last) != ('0', '1')) if self['hscrollmode'] == 'dynamic': if self._horizScrollbarNeeded != self._horizScrollbarOn: self._toggleHorizScrollbar() def _scrollYNow(self, first, last): self._vertScrollbar.set(first, last) self._vertScrollbarNeeded = ((first, last) != ('0', '1')) if self['vscrollmode'] == 'dynamic': if self._vertScrollbarNeeded != self._vertScrollbarOn: self._toggleVertScrollbar() def _scrollBothLater(self, first, last): # Called by the listbox to set the horizontal or vertical # scrollbar when it has scrolled or changed size or contents. if self.scrollTimer is None: self.scrollTimer = self.after_idle(self._scrollBothNow) def _scrollBothNow(self): # This performs the function of _scrollXNow and _scrollYNow. # If one is changed, the other should be updated to match. self.scrollTimer = None # Call update_idletasks to make sure that the containing frame # has been resized before we attempt to set the scrollbars. # Otherwise the scrollbars may be mapped/unmapped continuously. self._scrollRecurse = self._scrollRecurse + 1 self.update_idletasks() self._scrollRecurse = self._scrollRecurse - 1 if self._scrollRecurse != 0: return xview = self._listbox.xview() yview = self._listbox.yview() self._horizScrollbar.set(xview[0], xview[1]) self._vertScrollbar.set(yview[0], yview[1]) self._horizScrollbarNeeded = (xview != (0.0, 1.0)) self._vertScrollbarNeeded = (yview != (0.0, 1.0)) # If both horizontal and vertical scrollmodes are dynamic and # currently only one scrollbar is mapped and both should be # toggled, then unmap the mapped scrollbar. This prevents a # continuous mapping and unmapping of the scrollbars. if (self['hscrollmode'] == self['vscrollmode'] == 'dynamic' and self._horizScrollbarNeeded != self._horizScrollbarOn and self._vertScrollbarNeeded != self._vertScrollbarOn and self._vertScrollbarOn != self._horizScrollbarOn): if self._horizScrollbarOn: self._toggleHorizScrollbar() else: self._toggleVertScrollbar() return if self['hscrollmode'] == 'dynamic': if self._horizScrollbarNeeded != self._horizScrollbarOn: self._toggleHorizScrollbar() if self['vscrollmode'] == 'dynamic': if self._vertScrollbarNeeded != self._vertScrollbarOn: self._toggleVertScrollbar() def _toggleHorizScrollbar(self): self._horizScrollbarOn = not self._horizScrollbarOn interior = self.interior() if self._horizScrollbarOn: self._horizScrollbar.grid(row = 4, column = 2, sticky = 'news') interior.grid_rowconfigure(3, minsize = self['scrollmargin']) else: self._horizScrollbar.grid_forget() interior.grid_rowconfigure(3, minsize = 0) def _toggleVertScrollbar(self): self._vertScrollbarOn = not self._vertScrollbarOn interior = self.interior() if self._vertScrollbarOn: self._vertScrollbar.grid(row = 2, column = 4, sticky = 'news') interior.grid_columnconfigure(3, minsize = self['scrollmargin']) else: self._vertScrollbar.grid_forget() interior.grid_columnconfigure(3, minsize = 0) def _handleEvent(self, event, eventType): if eventType == 'double': command = self['dblclickcommand'] elif eventType == 'key': command = self['selectioncommand'] else: #eventType == 'release' # Do not execute the command if the mouse was released # outside the listbox. if (event.x < 0 or self._listbox.winfo_width() <= event.x or event.y < 0 or self._listbox.winfo_height() <= event.y): return command = self['selectioncommand'] if callable(command): command() # Need to explicitly forward this to override the stupid # (grid_)size method inherited from Tkinter.Frame.Grid. def size(self): return self._listbox.size() # Need to explicitly forward this to override the stupid # (grid_)bbox method inherited from Tkinter.Frame.Grid. def bbox(self, index): return self._listbox.bbox(index) forwardmethods(ScrolledListBox, Tkinter.Listbox, '_listbox') # ====================================================================== _listboxCache = {} def _registerScrolledList(listbox, scrolledList): # Register an ScrolledList widget for a Listbox widget _listboxCache[listbox] = scrolledList def _deregisterScrolledList(listbox): # Deregister a Listbox widget del _listboxCache[listbox] def _handleEvent(event, eventType): # Forward events for a Listbox to it's ScrolledListBox # A binding earlier in the bindtags list may have destroyed the # megawidget, so need to check. if _listboxCache.has_key(event.widget): _listboxCache[event.widget]._handleEvent(event, eventType) ###################################################################### ### File: PmwScrolledText.py # Based on iwidgets2.2.0/scrolledtext.itk code. import Tkinter class ScrolledText(MegaWidget): def __init__(self, parent = None, **kw): # Define the megawidget options. optiondefs = ( ('borderframe', 0, INITOPT), ('columnheader', 0, INITOPT), ('hscrollmode', 'dynamic', self._hscrollMode), ('labelmargin', 0, INITOPT), ('labelpos', None, INITOPT), ('rowcolumnheader',0, INITOPT), ('rowheader', 0, INITOPT), ('scrollmargin', 2, INITOPT), ('usehullsize', 0, INITOPT), ('vscrollmode', 'dynamic', self._vscrollMode), ) self.defineoptions(kw, optiondefs) # Initialise the base class (after defining the options). MegaWidget.__init__(self, parent) # Create the components. interior = self.interior() if self['usehullsize']: interior.grid_propagate(0) if self['borderframe']: # Create a frame widget to act as the border of the text # widget. Later, pack the text widget so that it fills # the frame. This avoids a problem in Tk, where window # items in a text widget may overlap the border of the # text widget. self._borderframe = self.createcomponent('borderframe', (), None, Tkinter.Frame, (interior,), relief = 'sunken', borderwidth = 2, ) self._borderframe.grid(row = 4, column = 4, sticky = 'news') # Create the text widget. self._textbox = self.createcomponent('text', (), None, Tkinter.Text, (self._borderframe,), highlightthickness = 0, borderwidth = 0, ) self._textbox.pack(fill = 'both', expand = 1) bw = self._borderframe.cget('borderwidth'), ht = self._borderframe.cget('highlightthickness'), else: # Create the text widget. self._textbox = self.createcomponent('text', (), None, Tkinter.Text, (interior,), ) self._textbox.grid(row = 4, column = 4, sticky = 'news') bw = self._textbox.cget('borderwidth'), ht = self._textbox.cget('highlightthickness'), # Create the header text widgets if self['columnheader']: self._columnheader = self.createcomponent('columnheader', (), 'Header', Tkinter.Text, (interior,), height=1, wrap='none', borderwidth = bw, highlightthickness = ht, ) self._columnheader.grid(row = 2, column = 4, sticky = 'ew') self._columnheader.configure( xscrollcommand = self._columnheaderscrolled) if self['rowheader']: self._rowheader = self.createcomponent('rowheader', (), 'Header', Tkinter.Text, (interior,), wrap='none', borderwidth = bw, highlightthickness = ht, ) self._rowheader.grid(row = 4, column = 2, sticky = 'ns') self._rowheader.configure( yscrollcommand = self._rowheaderscrolled) if self['rowcolumnheader']: self._rowcolumnheader = self.createcomponent('rowcolumnheader', (), 'Header', Tkinter.Text, (interior,), height=1, wrap='none', borderwidth = bw, highlightthickness = ht, ) self._rowcolumnheader.grid(row = 2, column = 2, sticky = 'nsew') interior.grid_rowconfigure(4, weight = 1, minsize = 0) interior.grid_columnconfigure(4, weight = 1, minsize = 0) # Create the horizontal scrollbar self._horizScrollbar = self.createcomponent('horizscrollbar', (), 'Scrollbar', Tkinter.Scrollbar, (interior,), orient='horizontal', command=self._textbox.xview ) # Create the vertical scrollbar self._vertScrollbar = self.createcomponent('vertscrollbar', (), 'Scrollbar', Tkinter.Scrollbar, (interior,), orient='vertical', command=self._textbox.yview ) self.createlabel(interior, childCols = 5, childRows = 5) # Initialise instance variables. self._horizScrollbarOn = 0 self._vertScrollbarOn = 0 self.scrollTimer = None self._scrollRecurse = 0 self._horizScrollbarNeeded = 0 self._vertScrollbarNeeded = 0 self._textWidth = None # These four variables avoid an infinite loop caused by the # row or column header's scrollcommand causing the main text # widget's scrollcommand to be called and vice versa. self._textboxLastX = None self._textboxLastY = None self._columnheaderLastX = None self._rowheaderLastY = None # Check keywords and initialise options. self.initialiseoptions() def destroy(self): if self.scrollTimer is not None: self.after_cancel(self.scrollTimer) self.scrollTimer = None MegaWidget.destroy(self) # ====================================================================== # Public methods. def clear(self): self.settext('') def importfile(self, fileName, where = 'end'): file = open(fileName, 'r') self._textbox.insert(where, file.read()) file.close() def exportfile(self, fileName): file = open(fileName, 'w') file.write(self._textbox.get('1.0', 'end')) file.close() def settext(self, text): disabled = (str(self._textbox.cget('state')) == 'disabled') if disabled: self._textbox.configure(state='normal') self._textbox.delete('0.0', 'end') self._textbox.insert('end', text) if disabled: self._textbox.configure(state='disabled') # Override Tkinter.Text get method, so that if it is called with # no arguments, return all text (consistent with other widgets). def get(self, first=None, last=None): if first is None: return self._textbox.get('1.0', 'end') else: return self._textbox.get(first, last) def getvalue(self): return self.get() def setvalue(self, text): return self.settext(text) def appendtext(self, text): oldTop, oldBottom = self._textbox.yview() disabled = (str(self._textbox.cget('state')) == 'disabled') if disabled: self._textbox.configure(state='normal') self._textbox.insert('end', text) if disabled: self._textbox.configure(state='disabled') if oldBottom == 1.0: self._textbox.yview('moveto', 1.0) # ====================================================================== # Configuration methods. def _hscrollMode(self): # The horizontal scroll mode has been configured. mode = self['hscrollmode'] if mode == 'static': if not self._horizScrollbarOn: self._toggleHorizScrollbar() elif mode == 'dynamic': if self._horizScrollbarNeeded != self._horizScrollbarOn: self._toggleHorizScrollbar() elif mode == 'none': if self._horizScrollbarOn: self._toggleHorizScrollbar() else: message = 'bad hscrollmode option "%s": should be static, dynamic, or none' % mode raise ValueError, message self._configureScrollCommands() def _vscrollMode(self): # The vertical scroll mode has been configured. mode = self['vscrollmode'] if mode == 'static': if not self._vertScrollbarOn: self._toggleVertScrollbar() elif mode == 'dynamic': if self._vertScrollbarNeeded != self._vertScrollbarOn: self._toggleVertScrollbar() elif mode == 'none': if self._vertScrollbarOn: self._toggleVertScrollbar() else: message = 'bad vscrollmode option "%s": should be static, dynamic, or none' % mode raise ValueError, message self._configureScrollCommands() # ====================================================================== # Private methods. def _configureScrollCommands(self): # If both scrollmodes are not dynamic we can save a lot of # time by not having to create an idle job to handle the # scroll commands. # Clean up previous scroll commands to prevent memory leak. tclCommandName = str(self._textbox.cget('xscrollcommand')) if tclCommandName != '': self._textbox.deletecommand(tclCommandName) tclCommandName = str(self._textbox.cget('yscrollcommand')) if tclCommandName != '': self._textbox.deletecommand(tclCommandName) if self['hscrollmode'] == self['vscrollmode'] == 'dynamic': self._textbox.configure( xscrollcommand=self._scrollBothLater, yscrollcommand=self._scrollBothLater ) else: self._textbox.configure( xscrollcommand=self._scrollXNow, yscrollcommand=self._scrollYNow ) def _scrollXNow(self, first, last): self._horizScrollbar.set(first, last) self._horizScrollbarNeeded = ((first, last) != ('0', '1')) # This code is the same as in _scrollBothNow. Keep it that way. if self['hscrollmode'] == 'dynamic': currentWidth = self._textbox.winfo_width() if self._horizScrollbarNeeded != self._horizScrollbarOn: if self._horizScrollbarNeeded or \ self._textWidth != currentWidth: self._toggleHorizScrollbar() self._textWidth = currentWidth if self['columnheader']: if self._columnheaderLastX != first: self._columnheaderLastX = first self._columnheader.xview('moveto', first) def _scrollYNow(self, first, last): if first == '0' and last == '0': return self._vertScrollbar.set(first, last) self._vertScrollbarNeeded = ((first, last) != ('0', '1')) if self['vscrollmode'] == 'dynamic': if self._vertScrollbarNeeded != self._vertScrollbarOn: self._toggleVertScrollbar() if self['rowheader']: if self._rowheaderLastY != first: self._rowheaderLastY = first self._rowheader.yview('moveto', first) def _scrollBothLater(self, first, last): # Called by the text widget to set the horizontal or vertical # scrollbar when it has scrolled or changed size or contents. if self.scrollTimer is None: self.scrollTimer = self.after_idle(self._scrollBothNow) def _scrollBothNow(self): # This performs the function of _scrollXNow and _scrollYNow. # If one is changed, the other should be updated to match. self.scrollTimer = None # Call update_idletasks to make sure that the containing frame # has been resized before we attempt to set the scrollbars. # Otherwise the scrollbars may be mapped/unmapped continuously. self._scrollRecurse = self._scrollRecurse + 1 self.update_idletasks() self._scrollRecurse = self._scrollRecurse - 1 if self._scrollRecurse != 0: return xview = self._textbox.xview() yview = self._textbox.yview() # The text widget returns a yview of (0.0, 0.0) just after it # has been created. Ignore this. if yview == (0.0, 0.0): return if self['columnheader']: if self._columnheaderLastX != xview[0]: self._columnheaderLastX = xview[0] self._columnheader.xview('moveto', xview[0]) if self['rowheader']: if self._rowheaderLastY != yview[0]: self._rowheaderLastY = yview[0] self._rowheader.yview('moveto', yview[0]) self._horizScrollbar.set(xview[0], xview[1]) self._vertScrollbar.set(yview[0], yview[1]) self._horizScrollbarNeeded = (xview != (0.0, 1.0)) self._vertScrollbarNeeded = (yview != (0.0, 1.0)) # If both horizontal and vertical scrollmodes are dynamic and # currently only one scrollbar is mapped and both should be # toggled, then unmap the mapped scrollbar. This prevents a # continuous mapping and unmapping of the scrollbars. if (self['hscrollmode'] == self['vscrollmode'] == 'dynamic' and self._horizScrollbarNeeded != self._horizScrollbarOn and self._vertScrollbarNeeded != self._vertScrollbarOn and self._vertScrollbarOn != self._horizScrollbarOn): if self._horizScrollbarOn: self._toggleHorizScrollbar() else: self._toggleVertScrollbar() return if self['hscrollmode'] == 'dynamic': # The following test is done to prevent continuous # mapping and unmapping of the horizontal scrollbar. # This may occur when some event (scrolling, resizing # or text changes) modifies the displayed text such # that the bottom line in the window is the longest # line displayed. If this causes the horizontal # scrollbar to be mapped, the scrollbar may "cover up" # the bottom line, which would mean that the scrollbar # is no longer required. If the scrollbar is then # unmapped, the bottom line will then become visible # again, which would cause the scrollbar to be mapped # again, and so on... # # The idea is that, if the width of the text widget # has not changed and the scrollbar is currently # mapped, then do not unmap the scrollbar even if it # is no longer required. This means that, during # normal scrolling of the text, once the horizontal # scrollbar has been mapped it will not be unmapped # (until the width of the text widget changes). currentWidth = self._textbox.winfo_width() if self._horizScrollbarNeeded != self._horizScrollbarOn: if self._horizScrollbarNeeded or \ self._textWidth != currentWidth: self._toggleHorizScrollbar() self._textWidth = currentWidth if self['vscrollmode'] == 'dynamic': if self._vertScrollbarNeeded != self._vertScrollbarOn: self._toggleVertScrollbar() def _columnheaderscrolled(self, first, last): if self._textboxLastX != first: self._textboxLastX = first self._textbox.xview('moveto', first) def _rowheaderscrolled(self, first, last): if self._textboxLastY != first: self._textboxLastY = first self._textbox.yview('moveto', first) def _toggleHorizScrollbar(self): self._horizScrollbarOn = not self._horizScrollbarOn interior = self.interior() if self._horizScrollbarOn: self._horizScrollbar.grid(row = 6, column = 4, sticky = 'news') interior.grid_rowconfigure(5, minsize = self['scrollmargin']) else: self._horizScrollbar.grid_forget() interior.grid_rowconfigure(5, minsize = 0) def _toggleVertScrollbar(self): self._vertScrollbarOn = not self._vertScrollbarOn interior = self.interior() if self._vertScrollbarOn: self._vertScrollbar.grid(row = 4, column = 6, sticky = 'news') interior.grid_columnconfigure(5, minsize = self['scrollmargin']) else: self._vertScrollbar.grid_forget() interior.grid_columnconfigure(5, minsize = 0) # Need to explicitly forward this to override the stupid # (grid_)bbox method inherited from Tkinter.Frame.Grid. def bbox(self, index): return self._textbox.bbox(index) forwardmethods(ScrolledText, Tkinter.Text, '_textbox') ###################################################################### ### File: PmwHistoryText.py _ORIGINAL = 0 _MODIFIED = 1 _DISPLAY = 2 class HistoryText(ScrolledText): def __init__(self, parent = None, **kw): # Define the megawidget options. optiondefs = ( ('compressany', 1, None), ('compresstail', 1, None), ('historycommand', None, None), ) self.defineoptions(kw, optiondefs) # Initialise the base class (after defining the options). ScrolledText.__init__(self, parent) # Initialise instance variables. self._list = [] self._currIndex = 0 self._pastIndex = None self._lastIndex = 0 # pointer to end of history list # Check keywords and initialise options. self.initialiseoptions() def addhistory(self): text = self.get() if text[-1] == '\n': text = text[:-1] if len(self._list) == 0: # This is the first history entry. Add it. self._list.append([text, text, _MODIFIED]) return currentEntry = self._list[self._currIndex] if text == currentEntry[_ORIGINAL]: # The current history entry has not been modified. Check if # we need to add it again. if self['compresstail'] and self._currIndex == self._lastIndex: return if self['compressany']: return # Undo any changes for the current history entry, since they # will now be available in the new entry. currentEntry[_MODIFIED] = currentEntry[_ORIGINAL] historycommand = self['historycommand'] if self._currIndex == self._lastIndex: # The last history entry is currently being displayed, # so disable the special meaning of the 'Next' button. self._pastIndex = None nextState = 'disabled' else: # A previous history entry is currently being displayed, # so allow the 'Next' button to go to the entry after this one. self._pastIndex = self._currIndex nextState = 'normal' if callable(historycommand): historycommand('normal', nextState) # Create the new history entry. self._list.append([text, text, _MODIFIED]) # Move the pointer into the history entry list to the end. self._lastIndex = self._lastIndex + 1 self._currIndex = self._lastIndex def next(self): if self._currIndex == self._lastIndex and self._pastIndex is None: self.bell() else: self._modifyDisplay('next') def prev(self): self._pastIndex = None if self._currIndex == 0: self.bell() else: self._modifyDisplay('prev') def undo(self): if len(self._list) != 0: self._modifyDisplay('undo') def redo(self): if len(self._list) != 0: self._modifyDisplay('redo') def gethistory(self): return self._list def _modifyDisplay(self, command): # Modify the display to show either the next or previous # history entry (next, prev) or the original or modified # version of the current history entry (undo, redo). # Save the currently displayed text. currentText = self.get() if currentText[-1] == '\n': currentText = currentText[:-1] currentEntry = self._list[self._currIndex] if currentEntry[_DISPLAY] == _MODIFIED: currentEntry[_MODIFIED] = currentText elif currentEntry[_ORIGINAL] != currentText: currentEntry[_MODIFIED] = currentText if command in ('next', 'prev'): currentEntry[_DISPLAY] = _MODIFIED if command in ('next', 'prev'): prevstate = 'normal' nextstate = 'normal' if command == 'next': if self._pastIndex is not None: self._currIndex = self._pastIndex self._pastIndex = None self._currIndex = self._currIndex + 1 if self._currIndex == self._lastIndex: nextstate = 'disabled' elif command == 'prev': self._currIndex = self._currIndex - 1 if self._currIndex == 0: prevstate = 'disabled' historycommand = self['historycommand'] if callable(historycommand): historycommand(prevstate, nextstate) currentEntry = self._list[self._currIndex] else: if command == 'undo': currentEntry[_DISPLAY] = _ORIGINAL elif command == 'redo': currentEntry[_DISPLAY] = _MODIFIED # Display the new text. self.delete('1.0', 'end') self.insert('end', currentEntry[currentEntry[_DISPLAY]]) ###################################################################### ### File: PmwSelectionDialog.py # Not Based on iwidgets version. class SelectionDialog(Dialog): # Dialog window with selection list. # Dialog window displaying a list and requesting the user to # select one. def __init__(self, parent = None, **kw): # Define the megawidget options. optiondefs = ( ('borderx', 10, INITOPT), ('bordery', 10, INITOPT), ) self.defineoptions(kw, optiondefs) # Initialise the base class (after defining the options). Dialog.__init__(self, parent) # Create the components. interior = self.interior() aliases = ( ('listbox', 'scrolledlist_listbox'), ('label', 'scrolledlist_label'), ) self._list = self.createcomponent('scrolledlist', aliases, None, ScrolledListBox, (interior,), dblclickcommand = self.invoke) self._list.pack(side='top', expand='true', fill='both', padx = self['borderx'], pady = self['bordery']) if not kw.has_key('activatecommand'): # Whenever this dialog is activated, set the focus to the # ScrolledListBox's listbox widget. listbox = self.component('listbox') self.configure(activatecommand = listbox.focus_set) # Check keywords and initialise options. self.initialiseoptions() # Need to explicitly forward this to override the stupid # (grid_)size method inherited from Tkinter.Toplevel.Grid. def size(self): return self.component('listbox').size() # Need to explicitly forward this to override the stupid # (grid_)bbox method inherited from Tkinter.Toplevel.Grid. def bbox(self, index): return self.component('listbox').size(index) forwardmethods(SelectionDialog, ScrolledListBox, '_list') ###################################################################### ### File: PmwTextDialog.py # A Dialog with a ScrolledText widget. class TextDialog(Dialog): def __init__(self, parent = None, **kw): # Define the megawidget options. optiondefs = ( ('borderx', 10, INITOPT), ('bordery', 10, INITOPT), ) self.defineoptions(kw, optiondefs) # Initialise the base class (after defining the options). Dialog.__init__(self, parent) # Create the components. interior = self.interior() aliases = ( ('text', 'scrolledtext_text'), ('label', 'scrolledtext_label'), ) self._text = self.createcomponent('scrolledtext', aliases, None, ScrolledText, (interior,)) self._text.pack(side='top', expand=1, fill='both', padx = self['borderx'], pady = self['bordery']) # Check keywords and initialise options. self.initialiseoptions() # Need to explicitly forward this to override the stupid # (grid_)bbox method inherited from Tkinter.Toplevel.Grid. def bbox(self, index): return self._text.bbox(index) forwardmethods(TextDialog, ScrolledText, '_text') ###################################################################### ### File: PmwTimeCounter.py # Authors: Joe VanAndel and Greg McFarlane import string import sys import time import Tkinter class TimeCounter(MegaWidget): """Up-down counter A TimeCounter is a single-line entry widget with Up and Down arrows which increment and decrement the Time value in the entry. """ def __init__(self, parent = None, **kw): # Define the megawidget options. optiondefs = ( ('autorepeat', 1, None), ('buttonaspect', 1.0, INITOPT), ('command', None, None), ('initwait', 300, None), ('labelmargin', 0, INITOPT), ('labelpos', None, INITOPT), ('max', None, self._max), ('min', None, self._min), ('padx', 0, INITOPT), ('pady', 0, INITOPT), ('repeatrate', 50, None), ('value', None, INITOPT), ) self.defineoptions(kw, optiondefs) # Initialise the base class (after defining the options). MegaWidget.__init__(self, parent) self.arrowDirection = {} self._flag = 'stopped' self._timerId = None self._createComponents(kw) value = self['value'] if value is None: now = time.time() value = time.strftime('%H:%M:%S', time.localtime(now)) self.setvalue(value) # Check keywords and initialise options. self.initialiseoptions() def _createComponents(self, kw): # Create the components. interior = self.interior() # If there is no label, put the arrows and the entry directly # into the interior, otherwise create a frame for them. In # either case the border around the arrows and the entry will # be raised (but not around the label). if self['labelpos'] is None: frame = interior if not kw.has_key('hull_relief'): frame.configure(relief = 'raised') if not kw.has_key('hull_borderwidth'): frame.configure(borderwidth = 1) else: frame = self.createcomponent('frame', (), None, Tkinter.Frame, (interior,), relief = 'raised', borderwidth = 1) frame.grid(column=2, row=2, sticky='nsew') interior.grid_columnconfigure(2, weight=1) interior.grid_rowconfigure(2, weight=1) # Create the down arrow buttons. # Create the hour down arrow. self._downHourArrowBtn = self.createcomponent('downhourarrow', (), 'Arrow', Tkinter.Canvas, (frame,), width = 16, height = 16, relief = 'raised', borderwidth = 2) self.arrowDirection[self._downHourArrowBtn] = 'down' self._downHourArrowBtn.grid(column = 0, row = 2) # Create the minute down arrow. self._downMinuteArrowBtn = self.createcomponent('downminutearrow', (), 'Arrow', Tkinter.Canvas, (frame,), width = 16, height = 16, relief = 'raised', borderwidth = 2) self.arrowDirection[self._downMinuteArrowBtn] = 'down' self._downMinuteArrowBtn.grid(column = 1, row = 2) # Create the second down arrow. self._downSecondArrowBtn = self.createcomponent('downsecondarrow', (), 'Arrow', Tkinter.Canvas, (frame,), width = 16, height = 16, relief = 'raised', borderwidth = 2) self.arrowDirection[self._downSecondArrowBtn] = 'down' self._downSecondArrowBtn.grid(column = 2, row = 2) # Create the entry fields. # Create the hour entry field. self._hourCounterEntry = self.createcomponent('hourentryfield', (('hourentry', 'hourentryfield_entry'),), None, EntryField, (frame,), validate='integer', entry_width = 2) self._hourCounterEntry.grid(column = 0, row = 1, sticky = 'news') # Create the minute entry field. self._minuteCounterEntry = self.createcomponent('minuteentryfield', (('minuteentry', 'minuteentryfield_entry'),), None, EntryField, (frame,), validate='integer', entry_width = 2) self._minuteCounterEntry.grid(column = 1, row = 1, sticky = 'news') # Create the second entry field. self._secondCounterEntry = self.createcomponent('secondentryfield', (('secondentry', 'secondentryfield_entry'),), None, EntryField, (frame,), validate='integer', entry_width = 2) self._secondCounterEntry.grid(column = 2, row = 1, sticky = 'news') # Create the up arrow buttons. # Create the hour up arrow. self._upHourArrowBtn = self.createcomponent('uphourarrow', (), 'Arrow', Tkinter.Canvas, (frame,), width = 16, height = 16, relief = 'raised', borderwidth = 2) self.arrowDirection[self._upHourArrowBtn] = 'up' self._upHourArrowBtn.grid(column = 0, row = 0) # Create the minute up arrow. self._upMinuteArrowBtn = self.createcomponent('upminutearrow', (), 'Arrow', Tkinter.Canvas, (frame,), width = 16, height = 16, relief = 'raised', borderwidth = 2) self.arrowDirection[self._upMinuteArrowBtn] = 'up' self._upMinuteArrowBtn.grid(column = 1, row = 0) # Create the second up arrow. self._upSecondArrowBtn = self.createcomponent('upsecondarrow', (), 'Arrow', Tkinter.Canvas, (frame,), width = 16, height = 16, relief = 'raised', borderwidth = 2) self.arrowDirection[self._upSecondArrowBtn] = 'up' self._upSecondArrowBtn.grid(column = 2, row = 0) # Make it resize nicely. padx = self['padx'] pady = self['pady'] for col in range(3): frame.grid_columnconfigure(col, weight = 1, pad = padx) frame.grid_rowconfigure(0, pad = pady) frame.grid_rowconfigure(2, pad = pady) frame.grid_rowconfigure(1, weight = 1) # Create the label. self.createlabel(interior) # Set bindings. # Up hour self._upHourArrowBtn.bind('<Configure>', lambda event, s=self,button=self._upHourArrowBtn: s._drawArrow(button, 'up')) self._upHourArrowBtn.bind('<1>', lambda event, s=self,button=self._upHourArrowBtn: s._countUp(button, 3600)) self._upHourArrowBtn.bind('<Any-ButtonRelease-1>', lambda event, s=self, button=self._upHourArrowBtn: s._stopUpDown(button)) # Up minute self._upMinuteArrowBtn.bind('<Configure>', lambda event, s=self,button=self._upMinuteArrowBtn: s._drawArrow(button, 'up')) self._upMinuteArrowBtn.bind('<1>', lambda event, s=self,button=self._upMinuteArrowBtn: s._countUp(button, 60)) self._upMinuteArrowBtn.bind('<Any-ButtonRelease-1>', lambda event, s=self, button=self._upMinuteArrowBtn: s._stopUpDown(button)) # Up second self._upSecondArrowBtn.bind('<Configure>', lambda event, s=self,button=self._upSecondArrowBtn: s._drawArrow(button, 'up')) self._upSecondArrowBtn.bind('<1>', lambda event, s=self,button=self._upSecondArrowBtn: s._countUp(button, 1)) self._upSecondArrowBtn.bind('<Any-ButtonRelease-1>', lambda event, s=self, button=self._upSecondArrowBtn: s._stopUpDown(button)) # Down hour self._downHourArrowBtn.bind('<Configure>', lambda event, s=self,button=self._downHourArrowBtn: s._drawArrow(button, 'down')) self._downHourArrowBtn.bind('<1>', lambda event, s=self,button=self._downHourArrowBtn: s._countDown(button, 3600)) self._downHourArrowBtn.bind('<Any-ButtonRelease-1>', lambda event, s=self, button=self._downHourArrowBtn: s._stopUpDown(button)) # Down minute self._downMinuteArrowBtn.bind('<Configure>', lambda event, s=self,button=self._downMinuteArrowBtn: s._drawArrow(button, 'down')) self._downMinuteArrowBtn.bind('<1>', lambda event, s=self,button=self._downMinuteArrowBtn: s._countDown(button, 60)) self._downMinuteArrowBtn.bind('<Any-ButtonRelease-1>', lambda event, s=self, button=self._downMinuteArrowBtn: s._stopUpDown(button)) # Down second self._downSecondArrowBtn.bind('<Configure>', lambda event, s=self,button=self._downSecondArrowBtn: s._drawArrow(button, 'down')) self._downSecondArrowBtn.bind('<1>', lambda event, s=self, button=self._downSecondArrowBtn: s._countDown(button,1)) self._downSecondArrowBtn.bind('<Any-ButtonRelease-1>', lambda event, s=self, button=self._downSecondArrowBtn: s._stopUpDown(button)) self._hourCounterEntry.component('entry').bind( '<Return>', self._invoke) self._minuteCounterEntry.component('entry').bind( '<Return>', self._invoke) self._secondCounterEntry.component('entry').bind( '<Return>', self._invoke) self._hourCounterEntry.bind('<Configure>', self._resizeArrow) self._minuteCounterEntry.bind('<Configure>', self._resizeArrow) self._secondCounterEntry.bind('<Configure>', self._resizeArrow) def _drawArrow(self, arrow, direction): drawarrow(arrow, self['hourentry_foreground'], direction, 'arrow') def _resizeArrow(self, event = None): for btn in (self._upHourArrowBtn, self._upMinuteArrowBtn, self._upSecondArrowBtn, self._downHourArrowBtn, self._downMinuteArrowBtn, self._downSecondArrowBtn): bw = (string.atoi(btn['borderwidth']) + string.atoi(btn['highlightthickness'])) newHeight = self._hourCounterEntry.winfo_reqheight() - 2 * bw newWidth = int(newHeight * self['buttonaspect']) btn.configure(width=newWidth, height=newHeight) self._drawArrow(btn, self.arrowDirection[btn]) def _min(self): min = self['min'] if min is None: self._minVal = 0 else: self._minVal = timestringtoseconds(min) def _max(self): max = self['max'] if max is None: self._maxVal = None else: self._maxVal = timestringtoseconds(max) def getvalue(self): return self.getstring() def setvalue(self, text): list = string.split(text, ':') if len(list) != 3: raise ValueError, 'invalid value: ' + text self._hour = string.atoi(list[0]) self._minute = string.atoi(list[1]) self._second = string.atoi(list[2]) self._setHMS() def getstring(self): return '%02d:%02d:%02d' % (self._hour, self._minute, self._second) def getint(self): return self._hour * 3600 + self._minute * 60 + self._second def _countUp(self, button, increment): self._relief = self._upHourArrowBtn.cget('relief') button.configure(relief='sunken') self._count(1, 'start', increment) def _countDown(self, button, increment): self._relief = self._downHourArrowBtn.cget('relief') button.configure(relief='sunken') self._count(-1, 'start', increment) def increment(self, seconds = 1): self._count(1, 'force', seconds) def decrement(self, seconds = 1): self._count(-1, 'force', seconds) def _count(self, factor, newFlag = None, increment = 1): if newFlag != 'force': if newFlag is not None: self._flag = newFlag if self._flag == 'stopped': return value = (string.atoi(self._hourCounterEntry.get()) *3600) + \ (string.atoi(self._minuteCounterEntry.get()) *60) + \ string.atoi(self._secondCounterEntry.get()) + \ factor * increment min = self._minVal max = self._maxVal if value < min: value = min if max is not None and value > max: value = max self._hour = value /3600 self._minute = (value - (self._hour*3600)) / 60 self._second = value - (self._hour*3600) - (self._minute*60) self._setHMS() if newFlag != 'force': if self['autorepeat']: if self._flag == 'start': delay = self['initwait'] self._flag = 'running' else: delay = self['repeatrate'] self._timerId = self.after( delay, lambda self=self, factor=factor,increment=increment: self._count(factor,'running', increment)) def _setHMS(self): self._hourCounterEntry.setentry('%02d' % self._hour) self._minuteCounterEntry.setentry('%02d' % self._minute) self._secondCounterEntry.setentry('%02d' % self._second) def _stopUpDown(self, button): if self._timerId is not None: self.after_cancel(self._timerId) self._timerId = None button.configure(relief=self._relief) self._flag = 'stopped' def _invoke(self, event): cmd = self['command'] if callable(cmd): cmd() def invoke(self): cmd = self['command'] if callable(cmd): return cmd() def destroy(self): if self._timerId is not None: self.after_cancel(self._timerId) self._timerId = None MegaWidget.destroy(self) ###################################################################### ### File: PmwAboutDialog.py class AboutDialog(MessageDialog): # Window to display version and contact information. # Class members containing resettable 'default' values: _version = '' _copyright = '' _contact = '' def __init__(self, parent = None, **kw): # Define the megawidget options. optiondefs = ( ('applicationname', '', INITOPT), ('iconpos', 'w', None), ('icon_bitmap', 'info', None), ('buttons', ('Close',), None), ('defaultbutton', 0, None), ) self.defineoptions(kw, optiondefs) # Initialise the base class (after defining the options). MessageDialog.__init__(self, parent) applicationname = self['applicationname'] if not kw.has_key('title'): self.configure(title = 'About ' + applicationname) if not kw.has_key('message_text'): text = applicationname + '\n\n' if AboutDialog._version != '': text = text + 'Version ' + AboutDialog._version + '\n' if AboutDialog._copyright != '': text = text + AboutDialog._copyright + '\n\n' if AboutDialog._contact != '': text = text + AboutDialog._contact self.configure(message_text=text) # Check keywords and initialise options. self.initialiseoptions() def aboutversion(value): AboutDialog._version = value def aboutcopyright(value): AboutDialog._copyright = value def aboutcontact(value): AboutDialog._contact = value ###################################################################### ### File: PmwComboBox.py # Based on iwidgets2.2.0/combobox.itk code. import os import string import types import Tkinter class ComboBox(MegaWidget): def __init__(self, parent = None, **kw): # Define the megawidget options. optiondefs = ( ('autoclear', 0, INITOPT), ('buttonaspect', 1.0, INITOPT), ('dropdown', 1, INITOPT), ('fliparrow', 0, INITOPT), ('history', 1, INITOPT), ('labelmargin', 0, INITOPT), ('labelpos', None, INITOPT), ('listheight', 200, INITOPT), ('selectioncommand', None, None), ('sticky', 'ew', INITOPT), ('unique', 1, INITOPT), ) self.defineoptions(kw, optiondefs) # Initialise the base class (after defining the options). MegaWidget.__init__(self, parent) # Create the components. interior = self.interior() self._entryfield = self.createcomponent('entryfield', (('entry', 'entryfield_entry'),), None, EntryField, (interior,)) self._entryfield.grid(column=2, row=2, sticky=self['sticky']) interior.grid_columnconfigure(2, weight = 1) self._entryWidget = self._entryfield.component('entry') if self['dropdown']: self._isPosted = 0 interior.grid_rowconfigure(2, weight = 1) # Create the arrow button. self._arrowBtn = self.createcomponent('arrowbutton', (), None, Tkinter.Canvas, (interior,), borderwidth = 2, relief = 'raised', width = 16, height = 16) if 'n' in self['sticky']: sticky = 'n' else: sticky = '' if 's' in self['sticky']: sticky = sticky + 's' self._arrowBtn.grid(column=3, row=2, sticky = sticky) self._arrowRelief = self._arrowBtn.cget('relief') # Create the label. self.createlabel(interior, childCols=2) # Create the dropdown window. self._popup = self.createcomponent('popup', (), None, Tkinter.Toplevel, (interior,)) self._popup.withdraw() self._popup.overrideredirect(1) # Create the scrolled listbox inside the dropdown window. self._list = self.createcomponent('scrolledlist', (('listbox', 'scrolledlist_listbox'),), None, ScrolledListBox, (self._popup,), hull_borderwidth = 2, hull_relief = 'raised', hull_height = self['listheight'], usehullsize = 1, listbox_exportselection = 0) self._list.pack(expand=1, fill='both') self.__listbox = self._list.component('listbox') # Bind events to the arrow button. self._arrowBtn.bind('<1>', self._postList) self._arrowBtn.bind('<Configure>', self._drawArrow) self._arrowBtn.bind('<3>', self._next) self._arrowBtn.bind('<Shift-3>', self._previous) self._arrowBtn.bind('<Down>', self._next) self._arrowBtn.bind('<Up>', self._previous) self._arrowBtn.bind('<Control-n>', self._next) self._arrowBtn.bind('<Control-p>', self._previous) self._arrowBtn.bind('<Shift-Down>', self._postList) self._arrowBtn.bind('<Shift-Up>', self._postList) self._arrowBtn.bind('<F34>', self._postList) self._arrowBtn.bind('<F28>', self._postList) self._arrowBtn.bind('<space>', self._postList) # Bind events to the dropdown window. self._popup.bind('<Escape>', self._unpostList) self._popup.bind('<space>', self._selectUnpost) self._popup.bind('<Return>', self._selectUnpost) self._popup.bind('<ButtonRelease-1>', self._dropdownBtnRelease) self._popup.bind('<ButtonPress-1>', self._unpostOnNextRelease) # Bind events to the Tk listbox. self.__listbox.bind('<Enter>', self._unpostOnNextRelease) # Bind events to the Tk entry widget. self._entryWidget.bind('<Configure>', self._resizeArrow) self._entryWidget.bind('<Shift-Down>', self._postList) self._entryWidget.bind('<Shift-Up>', self._postList) self._entryWidget.bind('<F34>', self._postList) self._entryWidget.bind('<F28>', self._postList) # Need to unpost the popup if the entryfield is unmapped (eg: # its toplevel window is withdrawn) while the popup list is # displayed. self._entryWidget.bind('<Unmap>', self._unpostList) else: # Create the scrolled listbox below the entry field. self._list = self.createcomponent('scrolledlist', (('listbox', 'scrolledlist_listbox'),), None, ScrolledListBox, (interior,), selectioncommand = self._selectCmd) self._list.grid(column=2, row=3, sticky='nsew') self.__listbox = self._list.component('listbox') # The scrolled listbox should expand vertically. interior.grid_rowconfigure(3, weight = 1) # Create the label. self.createlabel(interior, childRows=2) self._entryWidget.bind('<Down>', self._next) self._entryWidget.bind('<Up>', self._previous) self._entryWidget.bind('<Control-n>', self._next) self._entryWidget.bind('<Control-p>', self._previous) self.__listbox.bind('<Control-n>', self._next) self.__listbox.bind('<Control-p>', self._previous) if self['history']: self._entryfield.configure(command=self._addHistory) # Check keywords and initialise options. self.initialiseoptions() def destroy(self): if self['dropdown'] and self._isPosted: popgrab(self._popup) MegaWidget.destroy(self) #====================================================================== # Public methods def get(self, first = None, last=None): if first is None: return self._entryWidget.get() else: return self._list.get(first, last) def invoke(self): if self['dropdown']: self._postList() else: return self._selectCmd() def selectitem(self, index, setentry=1): if type(index) == types.StringType: text = index items = self._list.get(0, 'end') if text in items: index = list(items).index(text) else: raise IndexError, 'index "%s" not found' % text elif setentry: text = self._list.get(0, 'end')[index] self._list.select_clear(0, 'end') self._list.select_set(index, index) self._list.activate(index) self.see(index) if setentry: self._entryfield.setentry(text) # Need to explicitly forward this to override the stupid # (grid_)size method inherited from Tkinter.Frame.Grid. def size(self): return self._list.size() # Need to explicitly forward this to override the stupid # (grid_)bbox method inherited from Tkinter.Frame.Grid. def bbox(self, index): return self._list.bbox(index) def clear(self): self._entryfield.clear() self._list.clear() #====================================================================== # Private methods for both dropdown and simple comboboxes. def _addHistory(self): input = self._entryWidget.get() if input != '': index = None if self['unique']: # If item is already in list, select it and return. items = self._list.get(0, 'end') if input in items: index = list(items).index(input) if index is None: index = self._list.index('end') self._list.insert('end', input) self.selectitem(index) if self['autoclear']: self._entryWidget.delete(0, 'end') # Execute the selectioncommand on the new entry. self._selectCmd() def _next(self, event): size = self.size() if size <= 1: return cursels = self.curselection() if len(cursels) == 0: index = 0 else: index = string.atoi(cursels[0]) if index == size - 1: index = 0 else: index = index + 1 self.selectitem(index) def _previous(self, event): size = self.size() if size <= 1: return cursels = self.curselection() if len(cursels) == 0: index = size - 1 else: index = string.atoi(cursels[0]) if index == 0: index = size - 1 else: index = index - 1 self.selectitem(index) def _selectCmd(self, event=None): sels = self.getcurselection() if len(sels) == 0: item = None else: item = sels[0] self._entryfield.setentry(item) cmd = self['selectioncommand'] if callable(cmd): if event is None: # Return result of selectioncommand for invoke() method. return cmd(item) else: cmd(item) #====================================================================== # Private methods for dropdown combobox. def _drawArrow(self, event=None, sunken=0): arrow = self._arrowBtn if sunken: self._arrowRelief = arrow.cget('relief') arrow.configure(relief = 'sunken') else: arrow.configure(relief = self._arrowRelief) if self._isPosted and self['fliparrow']: direction = 'up' else: direction = 'down' drawarrow(arrow, self['entry_foreground'], direction, 'arrow') def _postList(self, event = None): self._isPosted = 1 self._drawArrow(sunken=1) # Make sure that the arrow is displayed sunken. self.update_idletasks() x = self._entryfield.winfo_rootx() y = self._entryfield.winfo_rooty() + \ self._entryfield.winfo_height() w = self._entryfield.winfo_width() + self._arrowBtn.winfo_width() h = self.__listbox.winfo_height() sh = self.winfo_screenheight() if y + h > sh and y > sh / 2: y = self._entryfield.winfo_rooty() - h self._list.configure(hull_width=w) setgeometryanddeiconify(self._popup, '+%d+%d' % (x, y)) # Grab the popup, so that all events are delivered to it, and # set focus to the listbox, to make keyboard navigation # easier. pushgrab(self._popup, 1, self._unpostList) self.__listbox.focus_set() self._drawArrow() # Ignore the first release of the mouse button after posting the # dropdown list, unless the mouse enters the dropdown list. self._ignoreRelease = 1 def _dropdownBtnRelease(self, event): if (event.widget == self._list.component('vertscrollbar') or event.widget == self._list.component('horizscrollbar')): return if self._ignoreRelease: self._unpostOnNextRelease() return self._unpostList() if (event.x >= 0 and event.x < self.__listbox.winfo_width() and event.y >= 0 and event.y < self.__listbox.winfo_height()): self._selectCmd() def _unpostOnNextRelease(self, event = None): self._ignoreRelease = 0 def _resizeArrow(self, event): bw = (string.atoi(self._arrowBtn['borderwidth']) + string.atoi(self._arrowBtn['highlightthickness'])) newHeight = self._entryfield.winfo_reqheight() - 2 * bw newWidth = int(newHeight * self['buttonaspect']) self._arrowBtn.configure(width=newWidth, height=newHeight) self._drawArrow() def _unpostList(self, event=None): if not self._isPosted: # It is possible to get events on an unposted popup. For # example, by repeatedly pressing the space key to post # and unpost the popup. The <space> event may be # delivered to the popup window even though # popgrab() has set the focus away from the # popup window. (Bug in Tk?) return # Restore the focus before withdrawing the window, since # otherwise the window manager may take the focus away so we # can't redirect it. Also, return the grab to the next active # window in the stack, if any. popgrab(self._popup) self._popup.withdraw() self._isPosted = 0 self._drawArrow() def _selectUnpost(self, event): self._unpostList() self._selectCmd() forwardmethods(ComboBox, ScrolledListBox, '_list') forwardmethods(ComboBox, EntryField, '_entryfield') ###################################################################### ### File: PmwComboBoxDialog.py # Not Based on iwidgets version. class ComboBoxDialog(Dialog): # Dialog window with simple combobox. # Dialog window displaying a list and entry field and requesting # the user to make a selection or enter a value def __init__(self, parent = None, **kw): # Define the megawidget options. optiondefs = ( ('borderx', 10, INITOPT), ('bordery', 10, INITOPT), ) self.defineoptions(kw, optiondefs) # Initialise the base class (after defining the options). Dialog.__init__(self, parent) # Create the components. interior = self.interior() aliases = ( ('listbox', 'combobox_listbox'), ('scrolledlist', 'combobox_scrolledlist'), ('entry', 'combobox_entry'), ('label', 'combobox_label'), ) self._combobox = self.createcomponent('combobox', aliases, None, ComboBox, (interior,), scrolledlist_dblclickcommand = self.invoke, dropdown = 0, ) self._combobox.pack(side='top', expand='true', fill='both', padx = self['borderx'], pady = self['bordery']) if not kw.has_key('activatecommand'): # Whenever this dialog is activated, set the focus to the # ComboBox's listbox widget. listbox = self.component('listbox') self.configure(activatecommand = listbox.focus_set) # Check keywords and initialise options. self.initialiseoptions() # Need to explicitly forward this to override the stupid # (grid_)size method inherited from Tkinter.Toplevel.Grid. def size(self): return self._combobox.size() # Need to explicitly forward this to override the stupid # (grid_)bbox method inherited from Tkinter.Toplevel.Grid. def bbox(self, index): return self._combobox.bbox(index) forwardmethods(ComboBoxDialog, ComboBox, '_combobox') ###################################################################### ### File: PmwCounter.py import string import sys import types import Tkinter class Counter(MegaWidget): def __init__(self, parent = None, **kw): # Define the megawidget options. optiondefs = ( ('autorepeat', 1, None), ('buttonaspect', 1.0, INITOPT), ('datatype', 'numeric', self._datatype), ('increment', 1, None), ('initwait', 300, None), ('labelmargin', 0, INITOPT), ('labelpos', None, INITOPT), ('orient', 'horizontal', INITOPT), ('padx', 0, INITOPT), ('pady', 0, INITOPT), ('repeatrate', 50, None), ('sticky', 'ew', INITOPT), ) self.defineoptions(kw, optiondefs) # Initialise the base class (after defining the options). MegaWidget.__init__(self, parent) # Initialise instance variables. self._timerId = None self._normalRelief = None # Create the components. interior = self.interior() # If there is no label, put the arrows and the entry directly # into the interior, otherwise create a frame for them. In # either case the border around the arrows and the entry will # be raised (but not around the label). if self['labelpos'] is None: frame = interior if not kw.has_key('hull_relief'): frame.configure(relief = 'raised') if not kw.has_key('hull_borderwidth'): frame.configure(borderwidth = 1) else: frame = self.createcomponent('frame', (), None, Tkinter.Frame, (interior,), relief = 'raised', borderwidth = 1) frame.grid(column=2, row=2, sticky=self['sticky']) interior.grid_columnconfigure(2, weight=1) interior.grid_rowconfigure(2, weight=1) # Create the down arrow. self._downArrowBtn = self.createcomponent('downarrow', (), 'Arrow', Tkinter.Canvas, (frame,), width = 16, height = 16, relief = 'raised', borderwidth = 2) # Create the entry field. self._counterEntry = self.createcomponent('entryfield', (('entry', 'entryfield_entry'),), None, EntryField, (frame,)) # Create the up arrow. self._upArrowBtn = self.createcomponent('uparrow', (), 'Arrow', Tkinter.Canvas, (frame,), width = 16, height = 16, relief = 'raised', borderwidth = 2) padx = self['padx'] pady = self['pady'] orient = self['orient'] if orient == 'horizontal': self._downArrowBtn.grid(column = 0, row = 0) self._counterEntry.grid(column = 1, row = 0, sticky = self['sticky']) self._upArrowBtn.grid(column = 2, row = 0) frame.grid_columnconfigure(1, weight = 1) frame.grid_rowconfigure(0, weight = 1) if Tkinter.TkVersion >= 4.2: frame.grid_columnconfigure(0, pad = padx) frame.grid_columnconfigure(2, pad = padx) frame.grid_rowconfigure(0, pad = pady) elif orient == 'vertical': self._upArrowBtn.grid(column = 0, row = 0, sticky = 's') self._counterEntry.grid(column = 0, row = 1, sticky = self['sticky']) self._downArrowBtn.grid(column = 0, row = 2, sticky = 'n') frame.grid_columnconfigure(0, weight = 1) frame.grid_rowconfigure(0, weight = 1) frame.grid_rowconfigure(2, weight = 1) if Tkinter.TkVersion >= 4.2: frame.grid_rowconfigure(0, pad = pady) frame.grid_rowconfigure(2, pad = pady) frame.grid_columnconfigure(0, pad = padx) else: raise ValueError, 'bad orient option ' + repr(orient) + \ ': must be either \'horizontal\' or \'vertical\'' self.createlabel(interior) self._upArrowBtn.bind('<Configure>', self._drawUpArrow) self._upArrowBtn.bind('<1>', self._countUp) self._upArrowBtn.bind('<Any-ButtonRelease-1>', self._stopCounting) self._downArrowBtn.bind('<Configure>', self._drawDownArrow) self._downArrowBtn.bind('<1>', self._countDown) self._downArrowBtn.bind('<Any-ButtonRelease-1>', self._stopCounting) self._counterEntry.bind('<Configure>', self._resizeArrow) entry = self._counterEntry.component('entry') entry.bind('<Down>', lambda event, s = self: s._key_decrement(event)) entry.bind('<Up>', lambda event, s = self: s._key_increment(event)) # Need to cancel the timer if an arrow button is unmapped (eg: # its toplevel window is withdrawn) while the mouse button is # held down. The canvas will not get the ButtonRelease event # if it is not mapped, since the implicit grab is cancelled. self._upArrowBtn.bind('<Unmap>', self._stopCounting) self._downArrowBtn.bind('<Unmap>', self._stopCounting) # Check keywords and initialise options. self.initialiseoptions() def _resizeArrow(self, event): for btn in (self._upArrowBtn, self._downArrowBtn): bw = (string.atoi(btn['borderwidth']) + string.atoi(btn['highlightthickness'])) newHeight = self._counterEntry.winfo_reqheight() - 2 * bw newWidth = int(newHeight * self['buttonaspect']) btn.configure(width=newWidth, height=newHeight) self._drawArrow(btn) def _drawUpArrow(self, event): self._drawArrow(self._upArrowBtn) def _drawDownArrow(self, event): self._drawArrow(self._downArrowBtn) def _drawArrow(self, arrow): if self['orient'] == 'vertical': if arrow == self._upArrowBtn: direction = 'up' else: direction = 'down' else: if arrow == self._upArrowBtn: direction = 'right' else: direction = 'left' drawarrow(arrow, self['entry_foreground'], direction, 'arrow') def _stopCounting(self, event = None): if self._timerId is not None: self.after_cancel(self._timerId) self._timerId = None if self._normalRelief is not None: button, relief = self._normalRelief button.configure(relief=relief) self._normalRelief = None def _countUp(self, event): self._normalRelief = (self._upArrowBtn, self._upArrowBtn.cget('relief')) self._upArrowBtn.configure(relief='sunken') # Force arrow down (it may come up immediately, if increment fails). self._upArrowBtn.update_idletasks() self._count(1, 1) def _countDown(self, event): self._normalRelief = (self._downArrowBtn, self._downArrowBtn.cget('relief')) self._downArrowBtn.configure(relief='sunken') # Force arrow down (it may come up immediately, if increment fails). self._downArrowBtn.update_idletasks() self._count(-1, 1) def increment(self): self._forceCount(1) def decrement(self): self._forceCount(-1) def _key_increment(self, event): self._forceCount(1) self.update_idletasks() def _key_decrement(self, event): self._forceCount(-1) self.update_idletasks() def _datatype(self): datatype = self['datatype'] if type(datatype) is types.DictionaryType: self._counterArgs = datatype.copy() if self._counterArgs.has_key('counter'): datatype = self._counterArgs['counter'] del self._counterArgs['counter'] else: datatype = 'numeric' else: self._counterArgs = {} if _counterCommands.has_key(datatype): self._counterCommand = _counterCommands[datatype] elif callable(datatype): self._counterCommand = datatype else: validValues = _counterCommands.keys() validValues.sort() raise ValueError, ('bad datatype value "%s": must be a' + ' function or one of %s') % (datatype, validValues) def _forceCount(self, factor): if not self.valid(): self.bell() return text = self._counterEntry.get() try: value = apply(self._counterCommand, (text, factor, self['increment']), self._counterArgs) except ValueError: self.bell() return previousICursor = self._counterEntry.index('insert') if self._counterEntry.setentry(value) == OK: self._counterEntry.xview('end') self._counterEntry.icursor(previousICursor) def _count(self, factor, first): if not self.valid(): self.bell() return self._timerId = None origtext = self._counterEntry.get() try: value = apply(self._counterCommand, (origtext, factor, self['increment']), self._counterArgs) except ValueError: # If text is invalid, stop counting. self._stopCounting() self.bell() return # If incrementing produces an invalid value, restore previous # text and stop counting. previousICursor = self._counterEntry.index('insert') valid = self._counterEntry.setentry(value) if valid != OK: self._stopCounting() self._counterEntry.setentry(origtext) if valid == PARTIAL: self.bell() return self._counterEntry.xview('end') self._counterEntry.icursor(previousICursor) if self['autorepeat']: if first: delay = self['initwait'] else: delay = self['repeatrate'] self._timerId = self.after(delay, lambda self=self, factor=factor: self._count(factor, 0)) def destroy(self): self._stopCounting() MegaWidget.destroy(self) forwardmethods(Counter, EntryField, '_counterEntry') def _changeNumber(text, factor, increment): value = string.atol(text) if factor > 0: value = (value / increment) * increment + increment else: value = ((value - 1) / increment) * increment # Get rid of the 'L' at the end of longs (in python up to 1.5.2). rtn = str(value) if rtn[-1] == 'L': return rtn[:-1] else: return rtn def _changeReal(text, factor, increment, separator = '.'): value = stringtoreal(text, separator) div = value / increment # Compare reals using str() to avoid problems caused by binary # numbers being only approximations to decimal numbers. # For example, if value is -0.3 and increment is 0.1, then # int(value/increment) = -2, not -3 as one would expect. if str(div)[-2:] == '.0': # value is an even multiple of increment. div = round(div) + factor else: # value is not an even multiple of increment. div = int(div) * 1.0 if value < 0: div = div - 1 if factor > 0: div = (div + 1) value = div * increment text = str(value) if separator != '.': index = string.find(text, '.') if index >= 0: text = text[:index] + separator + text[index + 1:] return text def _changeDate(value, factor, increment, format = 'ymd', separator = '/', yyyy = 0): jdn = datestringtojdn(value, format, separator) + factor * increment y, m, d = jdntoymd(jdn) result = '' for index in range(3): if index > 0: result = result + separator f = format[index] if f == 'y': if yyyy: result = result + '%02d' % y else: result = result + '%02d' % (y % 100) elif f == 'm': result = result + '%02d' % m elif f == 'd': result = result + '%02d' % d return result _SECSPERDAY = 24 * 60 * 60 def _changeTime(value, factor, increment, separator = ':', time24 = 0): unixTime = timestringtoseconds(value, separator) if factor > 0: chunks = unixTime / increment + 1 else: chunks = (unixTime - 1) / increment unixTime = chunks * increment if time24: while unixTime < 0: unixTime = unixTime + _SECSPERDAY while unixTime >= _SECSPERDAY: unixTime = unixTime - _SECSPERDAY if unixTime < 0: unixTime = -unixTime sign = '-' else: sign = '' secs = unixTime % 60 unixTime = unixTime / 60 mins = unixTime % 60 hours = unixTime / 60 return '%s%02d%s%02d%s%02d' % (sign, hours, separator, mins, separator, secs) # hexadecimal, alphabetic, alphanumeric not implemented _counterCommands = { 'numeric' : _changeNumber, # } integer 'integer' : _changeNumber, # } these two use the same function 'real' : _changeReal, # real number 'time' : _changeTime, 'date' : _changeDate, } ###################################################################### ### File: PmwCounterDialog.py # A Dialog with a counter class CounterDialog(Dialog): def __init__(self, parent = None, **kw): # Define the megawidget options. optiondefs = ( ('borderx', 20, INITOPT), ('bordery', 20, INITOPT), ) self.defineoptions(kw, optiondefs) # Initialise the base class (after defining the options). Dialog.__init__(self, parent) # Create the components. interior = self.interior() # Create the counter. aliases = ( ('entryfield', 'counter_entryfield'), ('entry', 'counter_entryfield_entry'), ('label', 'counter_label') ) self._cdCounter = self.createcomponent('counter', aliases, None, Counter, (interior,)) self._cdCounter.pack(fill='x', expand=1, padx = self['borderx'], pady = self['bordery']) if not kw.has_key('activatecommand'): # Whenever this dialog is activated, set the focus to the # Counter's entry widget. tkentry = self.component('entry') self.configure(activatecommand = tkentry.focus_set) # Check keywords and initialise options. self.initialiseoptions() # Supply aliases to some of the entry component methods. def insertentry(self, index, text): self._cdCounter.insert(index, text) def deleteentry(self, first, last=None): self._cdCounter.delete(first, last) def indexentry(self, index): return self._cdCounter.index(index) forwardmethods(CounterDialog, Counter, '_cdCounter') ###################################################################### ### File: PmwLogicalFont.py import os import string def _font_initialise(root, size=None, fontScheme = None): global _fontSize if size is not None: _fontSize = size if fontScheme in ('pmw1', 'pmw2'): if os.name == 'posix': defaultFont = logicalfont('Helvetica') menuFont = logicalfont('Helvetica', weight='bold', slant='italic') scaleFont = logicalfont('Helvetica', slant='italic') root.option_add('*Font', defaultFont, 'userDefault') root.option_add('*Menu*Font', menuFont, 'userDefault') root.option_add('*Menubutton*Font', menuFont, 'userDefault') root.option_add('*Scale.*Font', scaleFont, 'userDefault') if fontScheme == 'pmw1': balloonFont = logicalfont('Helvetica', -6, pixel = '12') else: # fontScheme == 'pmw2' balloonFont = logicalfont('Helvetica', -2) root.option_add('*Balloon.*Font', balloonFont, 'userDefault') else: defaultFont = logicalfont('Helvetica') root.option_add('*Font', defaultFont, 'userDefault') elif fontScheme == 'default': defaultFont = ('Helvetica', '-%d' % (_fontSize,), 'bold') entryFont = ('Helvetica', '-%d' % (_fontSize,)) textFont = ('Courier', '-%d' % (_fontSize,)) root.option_add('*Font', defaultFont, 'userDefault') root.option_add('*Entry*Font', entryFont, 'userDefault') root.option_add('*Text*Font', textFont, 'userDefault') def logicalfont(name='Helvetica', sizeIncr = 0, **kw): if not _fontInfo.has_key(name): raise ValueError, 'font %s does not exist' % name rtn = [] for field in _fontFields: if kw.has_key(field): logicalValue = kw[field] elif _fontInfo[name].has_key(field): logicalValue = _fontInfo[name][field] else: logicalValue = '*' if _propertyAliases[name].has_key((field, logicalValue)): realValue = _propertyAliases[name][(field, logicalValue)] elif _propertyAliases[name].has_key((field, None)): realValue = _propertyAliases[name][(field, None)] elif _propertyAliases[None].has_key((field, logicalValue)): realValue = _propertyAliases[None][(field, logicalValue)] elif _propertyAliases[None].has_key((field, None)): realValue = _propertyAliases[None][(field, None)] else: realValue = logicalValue if field == 'size': if realValue == '*': realValue = _fontSize realValue = str((realValue + sizeIncr) * 10) rtn.append(realValue) return string.join(rtn, '-') def logicalfontnames(): return _fontInfo.keys() if os.name == 'nt': _fontSize = 16 else: _fontSize = 14 _fontFields = ( 'registry', 'foundry', 'family', 'weight', 'slant', 'width', 'style', 'pixel', 'size', 'xres', 'yres', 'spacing', 'avgwidth', 'charset', 'encoding') # <_propertyAliases> defines other names for which property values may # be known by. This is required because italics in adobe-helvetica # are specified by 'o', while other fonts use 'i'. _propertyAliases = {} _propertyAliases[None] = { ('slant', 'italic') : 'i', ('slant', 'normal') : 'r', ('weight', 'light') : 'normal', ('width', 'wide') : 'normal', ('width', 'condensed') : 'normal', } # <_fontInfo> describes a 'logical' font, giving the default values of # some of its properties. _fontInfo = {} _fontInfo['Helvetica'] = { 'foundry' : 'adobe', 'family' : 'helvetica', 'registry' : '', 'charset' : 'iso8859', 'encoding' : '1', 'spacing' : 'p', 'slant' : 'normal', 'width' : 'normal', 'weight' : 'normal', } _propertyAliases['Helvetica'] = { ('slant', 'italic') : 'o', ('weight', 'normal') : 'medium', ('weight', 'light') : 'medium', } _fontInfo['Times'] = { 'foundry' : 'adobe', 'family' : 'times', 'registry' : '', 'charset' : 'iso8859', 'encoding' : '1', 'spacing' : 'p', 'slant' : 'normal', 'width' : 'normal', 'weight' : 'normal', } _propertyAliases['Times'] = { ('weight', 'normal') : 'medium', ('weight', 'light') : 'medium', } _fontInfo['Fixed'] = { 'foundry' : 'misc', 'family' : 'fixed', 'registry' : '', 'charset' : 'iso8859', 'encoding' : '1', 'spacing' : 'c', 'slant' : 'normal', 'width' : 'normal', 'weight' : 'normal', } _propertyAliases['Fixed'] = { ('weight', 'normal') : 'medium', ('weight', 'light') : 'medium', ('style', None) : '', ('width', 'condensed') : 'semicondensed', } _fontInfo['Courier'] = { 'foundry' : 'adobe', 'family' : 'courier', 'registry' : '', 'charset' : 'iso8859', 'encoding' : '1', 'spacing' : 'm', 'slant' : 'normal', 'width' : 'normal', 'weight' : 'normal', } _propertyAliases['Courier'] = { ('weight', 'normal') : 'medium', ('weight', 'light') : 'medium', ('style', None) : '', } _fontInfo['Typewriter'] = { 'foundry' : 'b&h', 'family' : 'lucidatypewriter', 'registry' : '', 'charset' : 'iso8859', 'encoding' : '1', 'spacing' : 'm', 'slant' : 'normal', 'width' : 'normal', 'weight' : 'normal', } _propertyAliases['Typewriter'] = { ('weight', 'normal') : 'medium', ('weight', 'light') : 'medium', } if os.name == 'nt': # For some reason 'fixed' fonts on NT aren't. _fontInfo['Fixed'] = _fontInfo['Courier'] _propertyAliases['Fixed'] = _propertyAliases['Courier']