mirror of
https://github.com/saitohirga/WSJT-X.git
synced 2024-08-21 14:06:32 -04:00
git-svn-id: svn+ssh://svn.code.sf.net/p/wsjt/wsjt/WSJT/trunk@1 ab8295b8-cf94-4d9e-aec4-7959e3be5d79
9234 lines
294 KiB
Python
9234 lines
294 KiB
Python
|
|
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']
|