Created
January 4, 2022 10:08
-
-
Save ymoslem/c4b0cd287c7d5f2b7279dfce354d389b to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
### Loader functions: | |
_VERSION = '2.0.0' | |
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 | |
import collections | |
# 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 list(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. | |
dictio = {} | |
__methodDict(cls, dictio) | |
return list(dictio.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)') | |
'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)') | |
'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 | |
#Python3 strings are not type 'str' but class 'str'... | |
#if type(toPart) != bytes: | |
if not isinstance(toPart, str): | |
# check that it is something like a function | |
#for classes, only works in Python 2: if isinstance(toPart, collections.Callable):pyht | |
if hasattr(toPart, '__call__'): | |
# If a method is passed, use the function within it | |
if hasattr(toPart, 'im_func'): | |
toPart = toPart.__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 list(dict.keys()): | |
if ex[:1] == '_' or ex[-1:] == '_': | |
del dict[ex] | |
# discard dangerous methods supplied by the caller | |
for ex in exclude: | |
if ex in dict: | |
del dict[ex] | |
# discard methods already defined in fromClass | |
for ex in __methods(fromClass): | |
if ex in dict: | |
del dict[ex] | |
for method, func in list(dict.items()): | |
d = {'method': method, 'func': func} | |
if isinstance(toPart, str): | |
execString = \ | |
__stringBody % {'method' : method, 'attribute' : toPart} | |
else: | |
execString = \ | |
__funcBody % {'forwardFunc' : forwardName, 'method' : method} | |
exec(execString, d) | |
# this creates a method | |
#fromClass.__dict__[method] = d[method] | |
setattr(fromClass, 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 list(self._optionInfo.items()): | |
value = info[_VALUE] | |
if value is _DEFAULT_OPTION_VALUE: | |
resourceClass = str.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 list(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 name in optionInfo: | |
if name in keywords: | |
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 name in keywords: | |
keywords[name] = [default, 0] | |
def createcomponent(self, componentName, componentAliases, | |
componentGroup, widgetClass, *widgetArgs, **kw): | |
#print('inCreateComponent', componentName) | |
# Create a component (during construction or later). | |
if componentName in self.__componentInfo: | |
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 = str.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 list(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 list(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 = str.find(option, '_') | |
if index >= 0 and componentGroup == option[:index]: | |
rest = option[(index + 1):] | |
kw[rest] = keywords[option][0] | |
keywords[option][1] = 1 | |
if 'pyclass' in kw: | |
widgetClass = kw['pyclass'] | |
del kw['pyclass'] | |
if widgetClass is None: | |
return None | |
if len(widgetArgs) == 1 and type(widgetArgs[0]) == tuple: | |
# Arguments to the constructor can be specified as either | |
# multiple trailing arguments to createcomponent() or as a | |
# single tuple argument. | |
widgetArgs = widgetArgs[0] | |
widget = 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 list(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 = str.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 + str.join(unusedOptions, ', ') + \ | |
'" for ' + self.__class__.__name__) | |
# Call the configuration callback function for every option. | |
FUNCTION = _OPT_FUNCTION | |
for info in list(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 list(self._optionInfo.items()): | |
resourceClass = str.upper(option[0]) + option[1:] | |
rtn[option] = (option, option, resourceClass, | |
config[_OPT_DEFAULT], config[_OPT_VALUE]) | |
return rtn | |
else: | |
config = self._optionInfo[option] | |
resourceClass = str.upper(option[0]) + option[1:] | |
return (option, option, resourceClass, config[_OPT_DEFAULT], | |
config[_OPT_VALUE]) | |
# optimisations: | |
optionInfo = self._optionInfo | |
#Py2 optionInfo_has_key = optionInfo.has_key | |
componentInfo = self.__componentInfo | |
#Py2 componentInfo_has_key = componentInfo.has_key | |
componentAliases = self.__componentAliases | |
#Py2 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 = {} | |
#Py2 indirectOptions_has_key = indirectOptions.has_key | |
for option, value in list(kw.items()): | |
if option in optionInfo: | |
# 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 = str.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 component in componentAliases: | |
component, subComponent = componentAliases[component] | |
if subComponent is not None: | |
componentOption = subComponent + '_' \ | |
+ componentOption | |
# Expand option string to write on error | |
option = component + '_' + componentOption | |
if component in componentInfo: | |
# 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 list(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 componentConfigFunc in indirectOptions: | |
indirectOptions[componentConfigFunc] = {} | |
indirectOptions[componentConfigFunc][componentOption] \ | |
= value | |
else: | |
raise KeyError('Unknown option "' + option + \ | |
'" for ' + self.__class__.__name__) | |
# Call the configure methods for any components. | |
#list(map(apply, list(indirectOptions.keys()), | |
# ((),) * len(indirectOptions), list(indirectOptions.values()))) | |
for func in indirectOptions.keys(): | |
func( **indirectOptions[func]); | |
# 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): | |
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 = str.find(name, '_') | |
if index < 0: | |
component = name | |
remainingComponents = None | |
else: | |
component = name[:index] | |
remainingComponents = name[(index + 1):] | |
# Expand component alias | |
if component in self.__componentAliases: | |
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 self._hull not in _hullToMegaWidget | |
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 option in self._optionInfo: | |
return self._optionInfo[option][_OPT_VALUE] | |
else: | |
index = str.find(option, '_') | |
if index >= 0: | |
component = option[:index] | |
componentOption = option[(index + 1):] | |
# Expand component alias | |
if component in self.__componentAliases: | |
component, subComponent = self.__componentAliases[component] | |
if subComponent is not None: | |
componentOption = subComponent + '_' + componentOption | |
# Expand option string to write on error | |
option = component + '_' + componentOption | |
if component in self.__componentInfo: | |
# 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 list(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 list(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 = list(self.__componentInfo.keys()) | |
names.sort() | |
return names | |
def componentaliases(self): | |
# Return a list of all component aliases. | |
componentAliases = self.__componentAliases | |
names = list(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) | |
super().__init__(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) | |
#changed from self.register and self.protocol - python3 error... | |
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 self._hull in _hullToMegaWidget: | |
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) is str: | |
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) is str: | |
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 = str.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) is str: | |
parent = self._hull._nametowidget(parent) | |
master = parent.winfo_toplevel() | |
self.transient(master) | |
pushgrab(self._hull, globalMode, self.deactivate) | |
command = self['activatecommand'] | |
if isinstance(command, collections.Callable): | |
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 isinstance(command, collections.Callable): | |
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 == 1: | |
#this causes trace not to work - not enabled by default in tk anymore? | |
#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 list(_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 window in _toplevelBusyInfo: | |
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 list(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 root not in _toplevelBusyInfo: | |
_addToplevelBusyInfo(root) | |
def busycallback(command, updateFunction = None): | |
if not isinstance(command, collections.Callable): | |
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]) == tuple: | |
argStr = str(args[0]) | |
else: | |
argStr = str(args) | |
_traceTkFile.write('CALL TK> %d:%s%s' % | |
(_recursionCounter, ' ' * _recursionCounter, argStr)) | |
_recursionCounter = _recursionCounter + 1 | |
try: | |
result = self.tclInterp.call(*args, **kw) | |
except tkinter.TclError as 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 list(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 self not in _toplevelBusyInfo: | |
_addToplevelBusyInfo(self) | |
self._Pmw_WM_DELETE_name = self.register(self.destroy, None, 0) | |
self.protocol('WM_DELETE_WINDOW', self._Pmw_WM_DELETE_name) | |
return tkinter.Wm.title(*(self,) + args) | |
_haveBltBusy = None | |
def _havebltbusy(window): | |
global _busy_hold, _busy_release, _haveBltBusy | |
if _haveBltBusy is None: | |
from . 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 = self._command(*args) | |
# Call update before hiding the busy windows to clear any | |
# events that may have occurred over the busy windows. | |
if isinstance(self._updateFunction, collections.Callable): | |
self._updateFunction() | |
hidebusycursor() | |
return rtn | |
#============================================================================= | |
def drawarrow(canvas, color, direction, tag, baseOffset = 0.25, edgeOffset = 0.15): | |
canvas.delete(tag) | |
#Python 3 conversion | |
#bw = (str.atoi(canvas['borderwidth']) + | |
# str.atoi(canvas['highlightthickness'])) | |
#width = str.atoi(canvas['width']) | |
#height = str.atoi(canvas['height']) | |
bw = (int(canvas['borderwidth']) + | |
int(canvas['highlightthickness'])) | |
width = int(canvas['width']) | |
height = int(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} | |
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 tkWidget in _hullToMegaWidget: | |
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 tkWidget in _toplevelBusyInfo: | |
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 tkWidget in _hullToMegaWidget: | |
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 = self.subst(*args) | |
if _traceTk: | |
if not _callToTkReturned: | |
_traceTkFile.write('\n') | |
if hasattr(self.func, 'im_class'): | |
name = self.func.__self__.__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[int(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 self.func(*args) | |
except SystemExit as 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) == type: | |
# Handle python 1.5 class exceptions. | |
exc_type = exc_type.__name__ | |
msg = str(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) == tuple 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: | |
#Python 3 conversion | |
#eventNum = str.atoi(args[0].type) | |
eventNum = int(args[0].type) | |
if eventNum in list(_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 = list(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 = str.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 | |
import collections | |
# 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 isinstance(command, collections.Callable): | |
return command(name) | |
else: | |
if self.active(): | |
self.deactivate(name) | |
else: | |
self.withdraw() | |
def _buttons(self): | |
buttons = self['buttons'] | |
if type(buttons) != tuple and type(buttons) != list: | |
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 = ':'): | |
# to Py3 | |
#inputList = string.split(string.strip(text), separator) | |
inputList = text.strip().split(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:] | |
#Py3 if re.search('[^0-9]', string.join(inputList, '')) is not None: | |
if re.search('[^0-9]', ''.join(inputList)) is not None: | |
raise ValueError('invalid value: ' + text) | |
hour = int(inputList[0]) | |
minute = int(inputList[1]) | |
second = int(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, fmt = 'ymd', separator = '/'): | |
#Py3 inputList = string.split(string.strip(text), separator) | |
inputList = text.strip().split(separator) | |
if len(inputList) != 3: | |
raise ValueError('invalid value: ' + text) | |
#Py3 if re.search('[^0-9]', string.join(inputList, '')) is not None: | |
if re.search('[^0-9]', ''.join(inputList)) is not None: | |
raise ValueError('invalid value: ' + text) | |
formatList = list(fmt) | |
day = int(inputList[formatList.index('d')]) | |
month = int(inputList[formatList.index('m')]) | |
year = int(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 int(a / b) | |
else: | |
return -int(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 = 15821004 # last day to use Julian calendar | |
else: # British-American usage | |
lastJulianDate = 17520902 # last day to use Julian calendar | |
julian = ((year * 100) + month) * 100 + day <= lastJulianDate | |
if year < 0: | |
# Adjust BC year | |
year = year + 1 | |
if julian: | |
return 367 * year - _cdiv(7 * (year + 5001 + _cdiv((month - 9), 7)), 4) + \ | |
_cdiv(275 * month, 9) + day + 1729777 | |
else: | |
return (day - 32076) + \ | |
_cdiv(1461 * (year + 4800 + _cdiv((month - 14), 12)), 4) + \ | |
_cdiv(367 * (month - 2 - _cdiv((month - 14), 12) * 12), 12) - \ | |
_cdiv((3 * _cdiv((year + 4900 + _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 = 2299160 # last jdn to use Julian calendar | |
else: # British-American usage | |
lastJulianJdn = 2361221 # last jdn to use Julian calendar | |
julian = (jdn <= lastJulianJdn); | |
x = jdn + 68569 | |
if julian: | |
x = x + 38 | |
daysPer400Years = 146100 | |
fudgedDaysPer4000Years = 1461000 + 1 | |
else: | |
daysPer400Years = 146097 | |
fudgedDaysPer4000Years = 1460970 + 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 != '.': | |
#Py3 if string.find(text, '.') >= 0: | |
if text.find('.') >= 0: | |
raise ValueError('invalid value: ' + text) | |
#Py3 index = string.find(text, separator) | |
index = text.find(separator) | |
if index >= 0: | |
text = text[:index] + '.' + text[index + 1:] | |
return float(text) | |
###################################################################### | |
### File: PmwBalloon.py | |
import os | |
import string | |
import tkinter | |
import collections | |
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 'hull_background' not in kw: | |
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 None in widget._Pmw_BalloonBindIds: | |
(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 tagOrItem in widget._Pmw_BalloonBindIds: | |
(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 isinstance(cmd, collections.Callable): | |
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'] | |
#Python 3 conversion | |
#edges = (string.atoi(str(self.cget('hull_highlightthickness'))) + | |
# string.atoi(str(self.cget('hull_borderwidth')))) * 2 | |
edges = (int(str(self.cget('hull_highlightthickness'))) + | |
int(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) == int: | |
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 = [t[0] for t in 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 'text' not in kw: | |
kw['text'] = componentName | |
kw['default'] = 'normal' | |
button = 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 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 | |
import collections | |
# 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 str: | |
# 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 list(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 validator in extraValidators: | |
validator = extraValidators[validator][index] | |
elif validator in _standardValidators: | |
validator = _standardValidators[validator][index] | |
else: | |
return validator | |
if validator in traversedValidators: | |
return validator | |
def _validate(self): | |
dictio = { | |
'validator' : None, | |
'min' : None, | |
'max' : None, | |
'minstrict' : 1, | |
'maxstrict' : 1, | |
} | |
opt = self['validate'] | |
if type(opt) is dict: | |
dictio.update(opt) | |
else: | |
dictio['validator'] = opt | |
# Look up validator maps and replace 'validator' field with | |
# the corresponding function. | |
validator = dictio['validator'] | |
valFunction = self._getValidatorFunc(validator, 0) | |
self._checkValidateFunction(valFunction, 'validate', validator) | |
dictio['validator'] = valFunction | |
# Look up validator maps and replace 'stringtovalue' field | |
# with the corresponding function. | |
if 'stringtovalue' in dictio: | |
stringtovalue = dictio['stringtovalue'] | |
strFunction = self._getValidatorFunc(stringtovalue, 1) | |
self._checkValidateFunction( | |
strFunction, 'stringtovalue', stringtovalue) | |
else: | |
strFunction = self._getValidatorFunc(validator, 1) | |
if strFunction == validator: | |
strFunction = len | |
dictio['stringtovalue'] = strFunction | |
self._validationInfo = dictio | |
args = dictio.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(dictio['min']) is str and strFunction is not None: | |
dictio['min'] = strFunction(*(dictio['min'],), **args) | |
if type(dictio['max']) is str and strFunction is not None: | |
dictio['max'] = strFunction(*(dictio['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 isinstance(function, collections.Callable): | |
extraValidators = self['extravalidators'] | |
extra = list(extraValidators.keys()) | |
extra.sort() | |
extra = tuple(extra) | |
standard = list(_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 isinstance(cmd, collections.Callable): | |
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 isinstance(cmd, collections.Callable) 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() | |
dictio = self._validationInfo | |
args = self._validationArgs | |
if dictio['validator'] is not None: | |
status = dictio['validator'](*(text,), **args) | |
if status != OK: | |
return status | |
# Check for out of (min, max) range. | |
if dictio['stringtovalue'] is not None: | |
min = dictio['min'] | |
max = dictio['max'] | |
if min is None and max is None: | |
return OK | |
val = dictio['stringtovalue'](*(text,), **args) | |
if min is not None and val < min: | |
if dictio['minstrict']: | |
return ERROR | |
else: | |
return PARTIAL | |
if max is not None and val > max: | |
if dictio['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 isinstance(cmd, collections.Callable): | |
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: | |
int(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: | |
int(text, 16) | |
return OK | |
except ValueError: | |
return ERROR | |
def realvalidator(text, separator = '.'): | |
if separator != '.': | |
#Py3 if string.find(text, '.') >= 0: | |
if text.find('.') >= 0: | |
return ERROR | |
#Py3 index = string.find(text, separator) | |
index = text.find(separator) | |
if index >= 0: | |
text = text[:index] + '.' + text[index + 1:] | |
try: | |
float(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: | |
float(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, fmt=format, separator=separator) | |
return OK | |
except ValueError: | |
if re.search('[^0-9' + separator + ']', text) is not None: | |
return ERROR | |
return PARTIAL | |
_standardValidators = { | |
'numeric' : (numericvalidator, int), | |
'integer' : (integervalidator, int), | |
'hexadecimal' : (hexadecimalvalidator, lambda s: int(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 event.widget in _entryCache: | |
_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: | |
#Python 3 conversion | |
#height = (string.atoi(str(group._ring.cget('borderwidth'))) + | |
# string.atoi(str(group._ring.cget('highlightthickness')))) | |
height = (int(str(group._ring.cget('borderwidth'))) + | |
int(str(group._ring.cget('highlightthickness')))) | |
else: | |
height = group._tag.winfo_reqheight() | |
if maxTagHeight < height: | |
maxTagHeight = height | |
for group in groups: | |
#Python 3 conversion | |
#ringBorder = (string.atoi(str(group._ring.cget('borderwidth'))) + | |
# string.atoi(str(group._ring.cget('highlightthickness')))) | |
ringBorder = (int(str(group._ring.cget('borderwidth'))) + | |
int(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. | |
#TODO rename collapsedsize to collapsedheight | |
#after adding collapsedwitdh (Pmw 1.3.3) | |
#will both stay in place for compatibility... | |
optiondefs = ( | |
('collapsedsize', 6, INITOPT), | |
('collapsedheight', 6, INITOPT), | |
('collapsedwidth', 20, 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 = (int(str(self._ring.cget('borderwidth'))) + | |
int(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() | |
#Tracker item 1096289 | |
if self._tag is None: | |
tagHeight = 0 | |
else: | |
tagHeight = self._tag.winfo_reqheight() | |
tagWidth = self._tag.winfo_reqwidth() | |
self._ring.configure(height=(tagHeight / 2) + self['collapsedheight'], | |
width = tagWidth + self['collapsedwidth']) | |
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 'tearoff' in kw: | |
menukw['tearoff'] = kw['tearoff'] | |
del kw['tearoff'] | |
else: | |
menukw['tearoff'] = 0 | |
if 'name' in kw: | |
menukw['name'] = kw['name'] | |
del kw['name'] | |
if 'label' not in kw: | |
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) | |
parentMenu.add_cascade(*(), **kw) | |
menu = 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) | |
command(*(), **kw) | |
def _addHotkeyToOptions(self, menuName, kw, traverseSpec): | |
if (not self['hotkeys'] or 'underline' in kw or | |
'label' not in kw): | |
return | |
if type(traverseSpec) == int: | |
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'): | |
#Python 3 conversion | |
# underline = \ | |
# string.atoi(str(menu.entrycget(item, 'underline'))) | |
underline = \ | |
int(str(menu.entrycget(item, 'underline'))) | |
if underline != -1: | |
label = str(menu.entrycget(item, 'label')) | |
if underline < len(label): | |
hotkey = label[underline].lower() | |
if hotkey not in hotkeyList: | |
hotkeyList.append(hotkey) | |
name = kw['label'] | |
if type(traverseSpec) is str: | |
lowerLetter = traverseSpec.lower() | |
if traverseSpec in name and lowerLetter not in hotkeyList: | |
kw['underline'] = name.index(traverseSpec) | |
else: | |
targets = string.digits + string.ascii_letters | |
lowerName = name.lower() | |
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 list(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 list(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 'tearoff' in kw: | |
menukw['tearoff'] = kw['tearoff'] | |
del kw['tearoff'] | |
else: | |
menukw['tearoff'] = 0 | |
if textKey not in kw: | |
kw[textKey] = menuName | |
self._addHotkeyToOptions(parentMenuName, kw, textKey, traverseSpec) | |
if parentMenuName is None: | |
button = 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') | |
parentMenu.add_cascade(*(), **kw) | |
self._menuInfo[parentMenuName][1].append(statusHelp) | |
menu = 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) | |
command(*(), **kw) | |
def _addHotkeyToOptions(self, menuName, kw, textKey, traverseSpec): | |
if (not self['hotkeys'] or 'underline' in kw or | |
textKey not in kw): | |
return | |
if type(traverseSpec) == int: | |
kw['underline'] = traverseSpec | |
return | |
hotkeyList = [] | |
if menuName is None: | |
for menuName in list(self._menuInfo.keys()): | |
if self._menuInfo[menuName][0] is None: | |
menubutton = self.component(menuName + '-button') | |
#Python 3 conversion | |
#underline = string.atoi(str(menubutton.cget('underline'))) | |
underline = int(str(menubutton.cget('underline'))) | |
if underline != -1: | |
label = str(menubutton.cget(textKey)) | |
if underline < len(label): | |
hotkey = label[underline].lower() | |
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'): | |
#Python 3 conversion | |
#underline = string.atoi( | |
# str(menu.entrycget(item, 'underline'))) | |
underline = int( | |
str(menu.entrycget(item, 'underline'))) | |
if underline != -1: | |
label = str(menu.entrycget(item, textKey)) | |
if underline < len(label): | |
hotkey = label[underline].lower() | |
if hotkey not in hotkeyList: | |
hotkeyList.append(hotkey) | |
name = kw[textKey] | |
if type(traverseSpec) is str: | |
lowerLetter = traverseSpec.lower() | |
if traverseSpec in name and lowerLetter not in hotkeyList: | |
kw['underline'] = name.index(traverseSpec) | |
else: | |
targets = string.digits + string.ascii_letters | |
lowerName = name.lower() | |
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 list(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] = text.replace('\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 list(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 pageName in self._pageAttrs: | |
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 list(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 = 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 = 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 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') | |
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 'topPage' in self._pending: | |
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) == int: | |
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 list(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 | |
#Python 3 conversion | |
#canvasBorder = string.atoi(self._hull['borderwidth']) + \ | |
# string.atoi(self._hull['highlightthickness']) | |
canvasBorder = int(self._hull['borderwidth']) + \ | |
int(self._hull['highlightthickness']) | |
if not self._withTabs: | |
self.tabBottom = canvasBorder | |
oldTabBottom = self.tabBottom | |
if 'borderColor' in self._pending: | |
self._lightBorderColor, self._darkBorderColor = \ | |
Color.bordercolors(self, self['hull_background']) | |
# Draw all the tabs. | |
if self._withTabs and ('tabs' in self._pending or | |
'size' in self._pending): | |
# Find total requested width and maximum requested height | |
# of tabs. | |
sumTabReqWidth = 0 | |
maxTabHeight = 0 | |
for pageInfo in list(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 ('topPage' in self._pending or | |
'tabs' in self._pending or 'size' in self._pending): | |
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 'size' in self._pending 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 'borderColor' in self._pending: | |
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 \ | |
'size' in self._pending 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 | |
import sys | |
import collections | |
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): | |
#cleaning up old items only required for Python < 2.5.4 | |
if sys.version_info < (2, 5, 4): | |
# 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) == int: | |
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 isinstance(command, collections.Callable): | |
return command(text) | |
###################################################################### | |
### File: PmwPanedWidget.py | |
# PanedWidget | |
# a frame which may contain several resizable sub-frames | |
import string | |
import sys | |
import types | |
import tkinter | |
import collections | |
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 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. | |
#Python 3 conversion | |
#bw = string.atoi(str(self.cget('hull_borderwidth'))) | |
#hl = string.atoi(str(self.cget('hull_highlightthickness'))) | |
bw = int(str(self.cget('hull_borderwidth'))) | |
hl = int(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 list(args.items()): | |
if type(value) == float: | |
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 | |
#python 3 conversion | |
#bw = string.atoi(str(self.cget('hull_borderwidth'))) | |
#hl = string.atoi(str(self.cget('hull_highlightthickness'))) | |
bw = int(str(self.cget('hull_borderwidth'))) | |
hl = int(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] = 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 = 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 isinstance(cmd, collections.Callable): | |
cmd(list(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 'activatecommand' not in kw: | |
# 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 | |
import collections | |
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) == int: | |
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 'text' not in kw: | |
kw['text'] = componentName | |
if self['buttontype'] == 'radiobutton': | |
if 'anchor' not in kw: | |
kw['anchor'] = 'w' | |
if 'variable' not in kw: | |
kw['variable'] = self.var | |
if 'value' not in kw: | |
kw['value'] = kw['text'] | |
elif self['buttontype'] == 'checkbutton': | |
if 'anchor' not in kw: | |
kw['anchor'] = 'w' | |
button = 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 isinstance(command, collections.Callable): | |
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 isinstance(command, collections.Callable): | |
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 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) is str: | |
value = float(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) is str: | |
value = float(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 | |
import collections | |
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) != tuple: | |
items = tuple(items) | |
if len(items) > 0: | |
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) is str: | |
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) != tuple: | |
items = tuple(items) | |
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 isinstance(command, collections.Callable): | |
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 event.widget in _listboxCache: | |
_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 | |
import collections | |
_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 isinstance(historycommand, collections.Callable): | |
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 isinstance(historycommand, collections.Callable): | |
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 'activatecommand' not in kw: | |
# 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 | |
import collections | |
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 'hull_relief' not in kw: | |
frame.configure(relief = 'raised') | |
if 'hull_borderwidth' not in kw: | |
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 = (int(btn['borderwidth']) + | |
int(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 = text.split(':') | |
if len(list) != 3: | |
raise ValueError('invalid value: ' + text) | |
self._hour = int(list[0]) | |
self._minute = int(list[1]) | |
self._second = int(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 = (int(self._hourCounterEntry.get()) *3600) + \ | |
(int(self._minuteCounterEntry.get()) *60) + \ | |
int(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 isinstance(cmd, collections.Callable): | |
cmd() | |
def invoke(self): | |
cmd = self['command'] | |
if isinstance(cmd, collections.Callable): | |
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 'title' not in kw: | |
self.configure(title = 'About ' + applicationname) | |
if 'message_text' not in kw: | |
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 | |
import collections | |
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) is str: | |
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: | |
#Python 3 conversion | |
#index = string.atoi(cursels[0]) | |
index = int(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: | |
#Python 3 conversion | |
#index = string.atoi(cursels[0]) | |
index = int(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 isinstance(cmd, collections.Callable): | |
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): | |
#Python 3 conversion | |
#bw = (string.atoi(self._arrowBtn['borderwidth']) + | |
# string.atoi(self._arrowBtn['highlightthickness'])) | |
bw = (int(self._arrowBtn['borderwidth']) + | |
int(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 'activatecommand' not in kw: | |
# 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 | |
import collections | |
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 'hull_relief' not in kw: | |
frame.configure(relief = 'raised') | |
if 'hull_borderwidth' not in kw: | |
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 = (int(btn['borderwidth']) + | |
int(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 dict: | |
self._counterArgs = datatype.copy() | |
if 'counter' in self._counterArgs: | |
datatype = self._counterArgs['counter'] | |
del self._counterArgs['counter'] | |
else: | |
datatype = 'numeric' | |
else: | |
self._counterArgs = {} | |
if datatype in _counterCommands: | |
self._counterCommand = _counterCommands[datatype] | |
elif isinstance(datatype, collections.Callable): | |
self._counterCommand = datatype | |
else: | |
validValues = list(_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 = 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 = 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 = int(text) | |
if factor > 0: | |
value = (value // increment) * increment + increment | |
else: | |
value = ((value - 1) // increment) * increment | |
#removed "L" check since we're working with Python 3 | |
return str(int(value)) | |
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 = text.find('.') | |
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 'activatecommand' not in kw: | |
# 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 name not in _fontInfo: | |
raise ValueError('font %s does not exist' % name) | |
rtn = [] | |
for field in _fontFields: | |
if field in kw: | |
logicalValue = kw[field] | |
elif field in _fontInfo[name]: | |
logicalValue = _fontInfo[name][field] | |
else: | |
logicalValue = '*' | |
if (field, logicalValue) in _propertyAliases[name]: | |
realValue = _propertyAliases[name][(field, logicalValue)] | |
elif (field, None) in _propertyAliases[name]: | |
realValue = _propertyAliases[name][(field, None)] | |
elif (field, logicalValue) in _propertyAliases[None]: | |
realValue = _propertyAliases[None][(field, logicalValue)] | |
elif (field, None) in _propertyAliases[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 '-'.join(rtn) | |
#return str.join(rtn, '-') | |
def logicalfontnames(): | |
return list(_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'] |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment