| 1 | # Pmw megawidget base classes. |
| 2 | |
| 3 | # This module provides a foundation for building megawidgets. It |
| 4 | # contains the MegaArchetype class which manages component widgets and |
| 5 | # configuration options. Also provided are the MegaToplevel and |
| 6 | # MegaWidget classes, derived from the MegaArchetype class. The |
| 7 | # MegaToplevel class contains a Tkinter Toplevel widget to act as the |
| 8 | # container of the megawidget. This is used as the base class of all |
| 9 | # megawidgets that are contained in their own top level window, such |
| 10 | # as a Dialog window. The MegaWidget class contains a Tkinter Frame |
| 11 | # to act as the container of the megawidget. This is used as the base |
| 12 | # class of all other megawidgets, such as a ComboBox or ButtonBox. |
| 13 | # |
| 14 | # Megawidgets are built by creating a class that inherits from either |
| 15 | # the MegaToplevel or MegaWidget class. |
| 16 | |
| 17 | import os |
| 18 | import string |
| 19 | import sys |
| 20 | import traceback |
| 21 | import types |
| 22 | import Tkinter |
| 23 | |
| 24 | # Special values used in index() methods of several megawidgets. |
| 25 | END = ['end'] |
| 26 | SELECT = ['select'] |
| 27 | DEFAULT = ['default'] |
| 28 | |
| 29 | # Constant used to indicate that an option can only be set by a call |
| 30 | # to the constructor. |
| 31 | INITOPT = ['initopt'] |
| 32 | _DEFAULT_OPTION_VALUE = ['default_option_value'] |
| 33 | _useTkOptionDb = 0 |
| 34 | |
| 35 | # Symbolic constants for the indexes into an optionInfo list. |
| 36 | _OPT_DEFAULT = 0 |
| 37 | _OPT_VALUE = 1 |
| 38 | _OPT_FUNCTION = 2 |
| 39 | |
| 40 | # Stacks |
| 41 | |
| 42 | _busyStack = [] |
| 43 | # Stack which tracks nested calls to show/hidebusycursor (called |
| 44 | # either directly or from activate()/deactivate()). Each element |
| 45 | # is a dictionary containing: |
| 46 | # 'newBusyWindows' : List of windows which had busy_hold called |
| 47 | # on them during a call to showbusycursor(). |
| 48 | # The corresponding call to hidebusycursor() |
| 49 | # will call busy_release on these windows. |
| 50 | # 'busyFocus' : The blt _Busy window which showbusycursor() |
| 51 | # set the focus to. |
| 52 | # 'previousFocus' : The focus as it was when showbusycursor() |
| 53 | # was called. The corresponding call to |
| 54 | # hidebusycursor() will restore this focus if |
| 55 | # the focus has not been changed from busyFocus. |
| 56 | |
| 57 | _grabStack = [] |
| 58 | # Stack of grabbed windows. It tracks calls to push/popgrab() |
| 59 | # (called either directly or from activate()/deactivate()). The |
| 60 | # window on the top of the stack is the window currently with the |
| 61 | # grab. Each element is a dictionary containing: |
| 62 | # 'grabWindow' : The window grabbed by pushgrab(). The |
| 63 | # corresponding call to popgrab() will release |
| 64 | # the grab on this window and restore the grab |
| 65 | # on the next window in the stack (if there is one). |
| 66 | # 'globalMode' : True if the grabWindow was grabbed with a |
| 67 | # global grab, false if the grab was local |
| 68 | # and 'nograb' if no grab was performed. |
| 69 | # 'previousFocus' : The focus as it was when pushgrab() |
| 70 | # was called. The corresponding call to |
| 71 | # popgrab() will restore this focus. |
| 72 | # 'deactivateFunction' : |
| 73 | # The function to call (usually grabWindow.deactivate) if |
| 74 | # popgrab() is called (usually from a deactivate() method) |
| 75 | # on a window which is not at the top of the stack (that is, |
| 76 | # does not have the grab or focus). For example, if a modal |
| 77 | # dialog is deleted by the window manager or deactivated by |
| 78 | # a timer. In this case, all dialogs above and including |
| 79 | # this one are deactivated, starting at the top of the |
| 80 | # stack. |
| 81 | |
| 82 | # Note that when dealing with focus windows, the name of the Tk |
| 83 | # widget is used, since it may be the '_Busy' window, which has no |
| 84 | # python instance associated with it. |
| 85 | |
| 86 | #============================================================================= |
| 87 | |
| 88 | # Functions used to forward methods from a class to a component. |
| 89 | |
| 90 | # Fill in a flattened method resolution dictionary for a class (attributes are |
| 91 | # filtered out). Flattening honours the MI method resolution rules |
| 92 | # (depth-first search of bases in order). The dictionary has method names |
| 93 | # for keys and functions for values. |
| 94 | def __methodDict(cls, dict): |
| 95 | |
| 96 | # the strategy is to traverse the class in the _reverse_ of the normal |
| 97 | # order, and overwrite any duplicates. |
| 98 | baseList = list(cls.__bases__) |
| 99 | baseList.reverse() |
| 100 | |
| 101 | # do bases in reverse order, so first base overrides last base |
| 102 | for super in baseList: |
| 103 | __methodDict(super, dict) |
| 104 | |
| 105 | # do my methods last to override base classes |
| 106 | for key, value in cls.__dict__.items(): |
| 107 | # ignore class attributes |
| 108 | if type(value) == types.FunctionType: |
| 109 | dict[key] = value |
| 110 | |
| 111 | def __methods(cls): |
| 112 | # Return all method names for a class. |
| 113 | |
| 114 | # Return all method names for a class (attributes are filtered |
| 115 | # out). Base classes are searched recursively. |
| 116 | |
| 117 | dict = {} |
| 118 | __methodDict(cls, dict) |
| 119 | return dict.keys() |
| 120 | |
| 121 | # Function body to resolve a forwarding given the target method name and the |
| 122 | # attribute name. The resulting lambda requires only self, but will forward |
| 123 | # any other parameters. |
| 124 | __stringBody = ( |
| 125 | 'def %(method)s(this, *args, **kw): return ' + |
| 126 | 'apply(this.%(attribute)s.%(method)s, args, kw)') |
| 127 | |
| 128 | # Get a unique id |
| 129 | __counter = 0 |
| 130 | def __unique(): |
| 131 | global __counter |
| 132 | __counter = __counter + 1 |
| 133 | return str(__counter) |
| 134 | |
| 135 | # Function body to resolve a forwarding given the target method name and the |
| 136 | # index of the resolution function. The resulting lambda requires only self, |
| 137 | # but will forward any other parameters. The target instance is identified |
| 138 | # by invoking the resolution function. |
| 139 | __funcBody = ( |
| 140 | 'def %(method)s(this, *args, **kw): return ' + |
| 141 | 'apply(this.%(forwardFunc)s().%(method)s, args, kw)') |
| 142 | |
| 143 | def forwardmethods(fromClass, toClass, toPart, exclude = ()): |
| 144 | # Forward all methods from one class to another. |
| 145 | |
| 146 | # Forwarders will be created in fromClass to forward method |
| 147 | # invocations to toClass. The methods to be forwarded are |
| 148 | # identified by flattening the interface of toClass, and excluding |
| 149 | # methods identified in the exclude list. Methods already defined |
| 150 | # in fromClass, or special methods with one or more leading or |
| 151 | # trailing underscores will not be forwarded. |
| 152 | |
| 153 | # For a given object of class fromClass, the corresponding toClass |
| 154 | # object is identified using toPart. This can either be a String |
| 155 | # denoting an attribute of fromClass objects, or a function taking |
| 156 | # a fromClass object and returning a toClass object. |
| 157 | |
| 158 | # Example: |
| 159 | # class MyClass: |
| 160 | # ... |
| 161 | # def __init__(self): |
| 162 | # ... |
| 163 | # self.__target = TargetClass() |
| 164 | # ... |
| 165 | # def findtarget(self): |
| 166 | # return self.__target |
| 167 | # forwardmethods(MyClass, TargetClass, '__target', ['dangerous1', 'dangerous2']) |
| 168 | # # ...or... |
| 169 | # forwardmethods(MyClass, TargetClass, MyClass.findtarget, |
| 170 | # ['dangerous1', 'dangerous2']) |
| 171 | |
| 172 | # In both cases, all TargetClass methods will be forwarded from |
| 173 | # MyClass except for dangerous1, dangerous2, special methods like |
| 174 | # __str__, and pre-existing methods like findtarget. |
| 175 | |
| 176 | |
| 177 | # Allow an attribute name (String) or a function to determine the instance |
| 178 | if type(toPart) != types.StringType: |
| 179 | |
| 180 | # check that it is something like a function |
| 181 | if callable(toPart): |
| 182 | |
| 183 | # If a method is passed, use the function within it |
| 184 | if hasattr(toPart, 'im_func'): |
| 185 | toPart = toPart.im_func |
| 186 | |
| 187 | # After this is set up, forwarders in this class will use |
| 188 | # the forwarding function. The forwarding function name is |
| 189 | # guaranteed to be unique, so that it can't be hidden by subclasses |
| 190 | forwardName = '__fwdfunc__' + __unique() |
| 191 | fromClass.__dict__[forwardName] = toPart |
| 192 | |
| 193 | # It's not a valid type |
| 194 | else: |
| 195 | raise TypeError, 'toPart must be attribute name, function or method' |
| 196 | |
| 197 | # get the full set of candidate methods |
| 198 | dict = {} |
| 199 | __methodDict(toClass, dict) |
| 200 | |
| 201 | # discard special methods |
| 202 | for ex in dict.keys(): |
| 203 | if ex[:1] == '_' or ex[-1:] == '_': |
| 204 | del dict[ex] |
| 205 | # discard dangerous methods supplied by the caller |
| 206 | for ex in exclude: |
| 207 | if dict.has_key(ex): |
| 208 | del dict[ex] |
| 209 | # discard methods already defined in fromClass |
| 210 | for ex in __methods(fromClass): |
| 211 | if dict.has_key(ex): |
| 212 | del dict[ex] |
| 213 | |
| 214 | for method, func in dict.items(): |
| 215 | d = {'method': method, 'func': func} |
| 216 | if type(toPart) == types.StringType: |
| 217 | execString = \ |
| 218 | __stringBody % {'method' : method, 'attribute' : toPart} |
| 219 | else: |
| 220 | execString = \ |
| 221 | __funcBody % {'forwardFunc' : forwardName, 'method' : method} |
| 222 | |
| 223 | exec execString in d |
| 224 | |
| 225 | # this creates a method |
| 226 | fromClass.__dict__[method] = d[method] |
| 227 | |
| 228 | #============================================================================= |
| 229 | |
| 230 | def setgeometryanddeiconify(window, geom): |
| 231 | # To avoid flashes on X and to position the window correctly on NT |
| 232 | # (caused by Tk bugs). |
| 233 | |
| 234 | if os.name == 'nt' or \ |
| 235 | (os.name == 'posix' and sys.platform[:6] == 'cygwin'): |
| 236 | # Require overrideredirect trick to stop window frame |
| 237 | # appearing momentarily. |
| 238 | redirect = window.overrideredirect() |
| 239 | if not redirect: |
| 240 | window.overrideredirect(1) |
| 241 | window.deiconify() |
| 242 | if geom is not None: |
| 243 | window.geometry(geom) |
| 244 | # Call update_idletasks to ensure NT moves the window to the |
| 245 | # correct position it is raised. |
| 246 | window.update_idletasks() |
| 247 | window.tkraise() |
| 248 | if not redirect: |
| 249 | window.overrideredirect(0) |
| 250 | else: |
| 251 | if geom is not None: |
| 252 | window.geometry(geom) |
| 253 | |
| 254 | # Problem!? Which way around should the following two calls |
| 255 | # go? If deiconify() is called first then I get complaints |
| 256 | # from people using the enlightenment or sawfish window |
| 257 | # managers that when a dialog is activated it takes about 2 |
| 258 | # seconds for the contents of the window to appear. But if |
| 259 | # tkraise() is called first then I get complaints from people |
| 260 | # using the twm window manager that when a dialog is activated |
| 261 | # it appears in the top right corner of the screen and also |
| 262 | # takes about 2 seconds to appear. |
| 263 | |
| 264 | #window.tkraise() |
| 265 | # Call update_idletasks to ensure certain window managers (eg: |
| 266 | # enlightenment and sawfish) do not cause Tk to delay for |
| 267 | # about two seconds before displaying window. |
| 268 | #window.update_idletasks() |
| 269 | #window.deiconify() |
| 270 | |
| 271 | window.deiconify() |
| 272 | if window.overrideredirect(): |
| 273 | # The window is not under the control of the window manager |
| 274 | # and so we need to raise it ourselves. |
| 275 | window.tkraise() |
| 276 | |
| 277 | #============================================================================= |
| 278 | |
| 279 | class MegaArchetype: |
| 280 | # Megawidget abstract root class. |
| 281 | |
| 282 | # This class provides methods which are inherited by classes |
| 283 | # implementing useful bases (this class doesn't provide a |
| 284 | # container widget inside which the megawidget can be built). |
| 285 | |
| 286 | def __init__(self, parent = None, hullClass = None): |
| 287 | |
| 288 | # Mapping from each megawidget option to a list of information |
| 289 | # about the option |
| 290 | # - default value |
| 291 | # - current value |
| 292 | # - function to call when the option is initialised in the |
| 293 | # call to initialiseoptions() in the constructor or |
| 294 | # modified via configure(). If this is INITOPT, the |
| 295 | # option is an initialisation option (an option that can |
| 296 | # be set by the call to the constructor but can not be |
| 297 | # used with configure). |
| 298 | # This mapping is not initialised here, but in the call to |
| 299 | # defineoptions() which precedes construction of this base class. |
| 300 | # |
| 301 | # self._optionInfo = {} |
| 302 | |
| 303 | # Mapping from each component name to a tuple of information |
| 304 | # about the component. |
| 305 | # - component widget instance |
| 306 | # - configure function of widget instance |
| 307 | # - the class of the widget (Frame, EntryField, etc) |
| 308 | # - cget function of widget instance |
| 309 | # - the name of the component group of this component, if any |
| 310 | self.__componentInfo = {} |
| 311 | |
| 312 | # Mapping from alias names to the names of components or |
| 313 | # sub-components. |
| 314 | self.__componentAliases = {} |
| 315 | |
| 316 | # Contains information about the keywords provided to the |
| 317 | # constructor. It is a mapping from the keyword to a tuple |
| 318 | # containing: |
| 319 | # - value of keyword |
| 320 | # - a boolean indicating if the keyword has been used. |
| 321 | # A keyword is used if, during the construction of a megawidget, |
| 322 | # - it is defined in a call to defineoptions() or addoptions(), or |
| 323 | # - it references, by name, a component of the megawidget, or |
| 324 | # - it references, by group, at least one component |
| 325 | # At the end of megawidget construction, a call is made to |
| 326 | # initialiseoptions() which reports an error if there are |
| 327 | # unused options given to the constructor. |
| 328 | # |
| 329 | # After megawidget construction, the dictionary contains |
| 330 | # keywords which refer to a dynamic component group, so that |
| 331 | # these components can be created after megawidget |
| 332 | # construction and still use the group options given to the |
| 333 | # constructor. |
| 334 | # |
| 335 | # self._constructorKeywords = {} |
| 336 | |
| 337 | # List of dynamic component groups. If a group is included in |
| 338 | # this list, then it not an error if a keyword argument for |
| 339 | # the group is given to the constructor or to configure(), but |
| 340 | # no components with this group have been created. |
| 341 | # self._dynamicGroups = () |
| 342 | |
| 343 | if hullClass is None: |
| 344 | self._hull = None |
| 345 | else: |
| 346 | if parent is None: |
| 347 | parent = Tkinter._default_root |
| 348 | |
| 349 | # Create the hull. |
| 350 | self._hull = self.createcomponent('hull', |
| 351 | (), None, |
| 352 | hullClass, (parent,)) |
| 353 | _hullToMegaWidget[self._hull] = self |
| 354 | |
| 355 | if _useTkOptionDb: |
| 356 | # Now that a widget has been created, query the Tk |
| 357 | # option database to get the default values for the |
| 358 | # options which have not been set in the call to the |
| 359 | # constructor. This assumes that defineoptions() is |
| 360 | # called before the __init__(). |
| 361 | option_get = self.option_get |
| 362 | _VALUE = _OPT_VALUE |
| 363 | _DEFAULT = _OPT_DEFAULT |
| 364 | for name, info in self._optionInfo.items(): |
| 365 | value = info[_VALUE] |
| 366 | if value is _DEFAULT_OPTION_VALUE: |
| 367 | resourceClass = string.upper(name[0]) + name[1:] |
| 368 | value = option_get(name, resourceClass) |
| 369 | if value != '': |
| 370 | try: |
| 371 | # Convert the string to int/float/tuple, etc |
| 372 | value = eval(value, {'__builtins__': {}}) |
| 373 | except: |
| 374 | pass |
| 375 | info[_VALUE] = value |
| 376 | else: |
| 377 | info[_VALUE] = info[_DEFAULT] |
| 378 | |
| 379 | def destroy(self): |
| 380 | # Clean up optionInfo in case it contains circular references |
| 381 | # in the function field, such as self._settitle in class |
| 382 | # MegaToplevel. |
| 383 | |
| 384 | self._optionInfo = {} |
| 385 | if self._hull is not None: |
| 386 | del _hullToMegaWidget[self._hull] |
| 387 | self._hull.destroy() |
| 388 | |
| 389 | #====================================================================== |
| 390 | # Methods used (mainly) during the construction of the megawidget. |
| 391 | |
| 392 | def defineoptions(self, keywords, optionDefs, dynamicGroups = ()): |
| 393 | # Create options, providing the default value and the method |
| 394 | # to call when the value is changed. If any option created by |
| 395 | # base classes has the same name as one in <optionDefs>, the |
| 396 | # base class's value and function will be overriden. |
| 397 | |
| 398 | # This should be called before the constructor of the base |
| 399 | # class, so that default values defined in the derived class |
| 400 | # override those in the base class. |
| 401 | |
| 402 | if not hasattr(self, '_constructorKeywords'): |
| 403 | # First time defineoptions has been called. |
| 404 | tmp = {} |
| 405 | for option, value in keywords.items(): |
| 406 | tmp[option] = [value, 0] |
| 407 | self._constructorKeywords = tmp |
| 408 | self._optionInfo = {} |
| 409 | self._initialiseoptions_counter = 0 |
| 410 | self._initialiseoptions_counter = self._initialiseoptions_counter + 1 |
| 411 | |
| 412 | if not hasattr(self, '_dynamicGroups'): |
| 413 | self._dynamicGroups = () |
| 414 | self._dynamicGroups = self._dynamicGroups + tuple(dynamicGroups) |
| 415 | self.addoptions(optionDefs) |
| 416 | |
| 417 | def addoptions(self, optionDefs): |
| 418 | # Add additional options, providing the default value and the |
| 419 | # method to call when the value is changed. See |
| 420 | # "defineoptions" for more details |
| 421 | |
| 422 | # optimisations: |
| 423 | optionInfo = self._optionInfo |
| 424 | optionInfo_has_key = optionInfo.has_key |
| 425 | keywords = self._constructorKeywords |
| 426 | keywords_has_key = keywords.has_key |
| 427 | FUNCTION = _OPT_FUNCTION |
| 428 | |
| 429 | for name, default, function in optionDefs: |
| 430 | if '_' not in name: |
| 431 | # The option will already exist if it has been defined |
| 432 | # in a derived class. In this case, do not override the |
| 433 | # default value of the option or the callback function |
| 434 | # if it is not None. |
| 435 | if not optionInfo_has_key(name): |
| 436 | if keywords_has_key(name): |
| 437 | value = keywords[name][0] |
| 438 | optionInfo[name] = [default, value, function] |
| 439 | del keywords[name] |
| 440 | else: |
| 441 | if _useTkOptionDb: |
| 442 | optionInfo[name] = \ |
| 443 | [default, _DEFAULT_OPTION_VALUE, function] |
| 444 | else: |
| 445 | optionInfo[name] = [default, default, function] |
| 446 | elif optionInfo[name][FUNCTION] is None: |
| 447 | optionInfo[name][FUNCTION] = function |
| 448 | else: |
| 449 | # This option is of the form "component_option". If this is |
| 450 | # not already defined in self._constructorKeywords add it. |
| 451 | # This allows a derived class to override the default value |
| 452 | # of an option of a component of a base class. |
| 453 | if not keywords_has_key(name): |
| 454 | keywords[name] = [default, 0] |
| 455 | |
| 456 | def createcomponent(self, componentName, componentAliases, |
| 457 | componentGroup, widgetClass, *widgetArgs, **kw): |
| 458 | # Create a component (during construction or later). |
| 459 | |
| 460 | if self.__componentInfo.has_key(componentName): |
| 461 | raise ValueError, 'Component "%s" already exists' % componentName |
| 462 | |
| 463 | if '_' in componentName: |
| 464 | raise ValueError, \ |
| 465 | 'Component name "%s" must not contain "_"' % componentName |
| 466 | |
| 467 | if hasattr(self, '_constructorKeywords'): |
| 468 | keywords = self._constructorKeywords |
| 469 | else: |
| 470 | keywords = {} |
| 471 | for alias, component in componentAliases: |
| 472 | # Create aliases to the component and its sub-components. |
| 473 | index = string.find(component, '_') |
| 474 | if index < 0: |
| 475 | self.__componentAliases[alias] = (component, None) |
| 476 | else: |
| 477 | mainComponent = component[:index] |
| 478 | subComponent = component[(index + 1):] |
| 479 | self.__componentAliases[alias] = (mainComponent, subComponent) |
| 480 | |
| 481 | # Remove aliases from the constructor keyword arguments by |
| 482 | # replacing any keyword arguments that begin with *alias* |
| 483 | # with corresponding keys beginning with *component*. |
| 484 | |
| 485 | alias = alias + '_' |
| 486 | aliasLen = len(alias) |
| 487 | for option in keywords.keys(): |
| 488 | if len(option) > aliasLen and option[:aliasLen] == alias: |
| 489 | newkey = component + '_' + option[aliasLen:] |
| 490 | keywords[newkey] = keywords[option] |
| 491 | del keywords[option] |
| 492 | |
| 493 | componentPrefix = componentName + '_' |
| 494 | nameLen = len(componentPrefix) |
| 495 | for option in keywords.keys(): |
| 496 | if len(option) > nameLen and option[:nameLen] == componentPrefix: |
| 497 | # The keyword argument refers to this component, so add |
| 498 | # this to the options to use when constructing the widget. |
| 499 | kw[option[nameLen:]] = keywords[option][0] |
| 500 | del keywords[option] |
| 501 | else: |
| 502 | # Check if this keyword argument refers to the group |
| 503 | # of this component. If so, add this to the options |
| 504 | # to use when constructing the widget. Mark the |
| 505 | # keyword argument as being used, but do not remove it |
| 506 | # since it may be required when creating another |
| 507 | # component. |
| 508 | index = string.find(option, '_') |
| 509 | if index >= 0 and componentGroup == option[:index]: |
| 510 | rest = option[(index + 1):] |
| 511 | kw[rest] = keywords[option][0] |
| 512 | keywords[option][1] = 1 |
| 513 | |
| 514 | if kw.has_key('pyclass'): |
| 515 | widgetClass = kw['pyclass'] |
| 516 | del kw['pyclass'] |
| 517 | if widgetClass is None: |
| 518 | return None |
| 519 | if len(widgetArgs) == 1 and type(widgetArgs[0]) == types.TupleType: |
| 520 | # Arguments to the constructor can be specified as either |
| 521 | # multiple trailing arguments to createcomponent() or as a |
| 522 | # single tuple argument. |
| 523 | widgetArgs = widgetArgs[0] |
| 524 | widget = apply(widgetClass, widgetArgs, kw) |
| 525 | componentClass = widget.__class__.__name__ |
| 526 | self.__componentInfo[componentName] = (widget, widget.configure, |
| 527 | componentClass, widget.cget, componentGroup) |
| 528 | |
| 529 | return widget |
| 530 | |
| 531 | def destroycomponent(self, name): |
| 532 | # Remove a megawidget component. |
| 533 | |
| 534 | # This command is for use by megawidget designers to destroy a |
| 535 | # megawidget component. |
| 536 | |
| 537 | self.__componentInfo[name][0].destroy() |
| 538 | del self.__componentInfo[name] |
| 539 | |
| 540 | def createlabel(self, parent, childCols = 1, childRows = 1): |
| 541 | |
| 542 | labelpos = self['labelpos'] |
| 543 | labelmargin = self['labelmargin'] |
| 544 | if labelpos is None: |
| 545 | return |
| 546 | |
| 547 | label = self.createcomponent('label', |
| 548 | (), None, |
| 549 | Tkinter.Label, (parent,)) |
| 550 | |
| 551 | if labelpos[0] in 'ns': |
| 552 | # vertical layout |
| 553 | if labelpos[0] == 'n': |
| 554 | row = 0 |
| 555 | margin = 1 |
| 556 | else: |
| 557 | row = childRows + 3 |
| 558 | margin = row - 1 |
| 559 | label.grid(column=2, row=row, columnspan=childCols, sticky=labelpos) |
| 560 | parent.grid_rowconfigure(margin, minsize=labelmargin) |
| 561 | else: |
| 562 | # horizontal layout |
| 563 | if labelpos[0] == 'w': |
| 564 | col = 0 |
| 565 | margin = 1 |
| 566 | else: |
| 567 | col = childCols + 3 |
| 568 | margin = col - 1 |
| 569 | label.grid(column=col, row=2, rowspan=childRows, sticky=labelpos) |
| 570 | parent.grid_columnconfigure(margin, minsize=labelmargin) |
| 571 | |
| 572 | def initialiseoptions(self, dummy = None): |
| 573 | self._initialiseoptions_counter = self._initialiseoptions_counter - 1 |
| 574 | if self._initialiseoptions_counter == 0: |
| 575 | unusedOptions = [] |
| 576 | keywords = self._constructorKeywords |
| 577 | for name in keywords.keys(): |
| 578 | used = keywords[name][1] |
| 579 | if not used: |
| 580 | # This keyword argument has not been used. If it |
| 581 | # does not refer to a dynamic group, mark it as |
| 582 | # unused. |
| 583 | index = string.find(name, '_') |
| 584 | if index < 0 or name[:index] not in self._dynamicGroups: |
| 585 | unusedOptions.append(name) |
| 586 | if len(unusedOptions) > 0: |
| 587 | if len(unusedOptions) == 1: |
| 588 | text = 'Unknown option "' |
| 589 | else: |
| 590 | text = 'Unknown options "' |
| 591 | raise KeyError, text + string.join(unusedOptions, ', ') + \ |
| 592 | '" for ' + self.__class__.__name__ |
| 593 | |
| 594 | # Call the configuration callback function for every option. |
| 595 | FUNCTION = _OPT_FUNCTION |
| 596 | for info in self._optionInfo.values(): |
| 597 | func = info[FUNCTION] |
| 598 | if func is not None and func is not INITOPT: |
| 599 | func() |
| 600 | |
| 601 | #====================================================================== |
| 602 | # Method used to configure the megawidget. |
| 603 | |
| 604 | def configure(self, option=None, **kw): |
| 605 | # Query or configure the megawidget options. |
| 606 | # |
| 607 | # If not empty, *kw* is a dictionary giving new |
| 608 | # values for some of the options of this megawidget or its |
| 609 | # components. For options defined for this megawidget, set |
| 610 | # the value of the option to the new value and call the |
| 611 | # configuration callback function, if any. For options of the |
| 612 | # form <component>_<option>, where <component> is a component |
| 613 | # of this megawidget, call the configure method of the |
| 614 | # component giving it the new value of the option. The |
| 615 | # <component> part may be an alias or a component group name. |
| 616 | # |
| 617 | # If *option* is None, return all megawidget configuration |
| 618 | # options and settings. Options are returned as standard 5 |
| 619 | # element tuples |
| 620 | # |
| 621 | # If *option* is a string, return the 5 element tuple for the |
| 622 | # given configuration option. |
| 623 | |
| 624 | # First, deal with the option queries. |
| 625 | if len(kw) == 0: |
| 626 | # This configure call is querying the values of one or all options. |
| 627 | # Return 5-tuples: |
| 628 | # (optionName, resourceName, resourceClass, default, value) |
| 629 | if option is None: |
| 630 | rtn = {} |
| 631 | for option, config in self._optionInfo.items(): |
| 632 | resourceClass = string.upper(option[0]) + option[1:] |
| 633 | rtn[option] = (option, option, resourceClass, |
| 634 | config[_OPT_DEFAULT], config[_OPT_VALUE]) |
| 635 | return rtn |
| 636 | else: |
| 637 | config = self._optionInfo[option] |
| 638 | resourceClass = string.upper(option[0]) + option[1:] |
| 639 | return (option, option, resourceClass, config[_OPT_DEFAULT], |
| 640 | config[_OPT_VALUE]) |
| 641 | |
| 642 | # optimisations: |
| 643 | optionInfo = self._optionInfo |
| 644 | optionInfo_has_key = optionInfo.has_key |
| 645 | componentInfo = self.__componentInfo |
| 646 | componentInfo_has_key = componentInfo.has_key |
| 647 | componentAliases = self.__componentAliases |
| 648 | componentAliases_has_key = componentAliases.has_key |
| 649 | VALUE = _OPT_VALUE |
| 650 | FUNCTION = _OPT_FUNCTION |
| 651 | |
| 652 | # This will contain a list of options in *kw* which |
| 653 | # are known to this megawidget. |
| 654 | directOptions = [] |
| 655 | |
| 656 | # This will contain information about the options in |
| 657 | # *kw* of the form <component>_<option>, where |
| 658 | # <component> is a component of this megawidget. It is a |
| 659 | # dictionary whose keys are the configure method of each |
| 660 | # component and whose values are a dictionary of options and |
| 661 | # values for the component. |
| 662 | indirectOptions = {} |
| 663 | indirectOptions_has_key = indirectOptions.has_key |
| 664 | |
| 665 | for option, value in kw.items(): |
| 666 | if optionInfo_has_key(option): |
| 667 | # This is one of the options of this megawidget. |
| 668 | # Make sure it is not an initialisation option. |
| 669 | if optionInfo[option][FUNCTION] is INITOPT: |
| 670 | raise KeyError, \ |
| 671 | 'Cannot configure initialisation option "' \ |
| 672 | + option + '" for ' + self.__class__.__name__ |
| 673 | optionInfo[option][VALUE] = value |
| 674 | directOptions.append(option) |
| 675 | else: |
| 676 | index = string.find(option, '_') |
| 677 | if index >= 0: |
| 678 | # This option may be of the form <component>_<option>. |
| 679 | component = option[:index] |
| 680 | componentOption = option[(index + 1):] |
| 681 | |
| 682 | # Expand component alias |
| 683 | if componentAliases_has_key(component): |
| 684 | component, subComponent = componentAliases[component] |
| 685 | if subComponent is not None: |
| 686 | componentOption = subComponent + '_' \ |
| 687 | + componentOption |
| 688 | |
| 689 | # Expand option string to write on error |
| 690 | option = component + '_' + componentOption |
| 691 | |
| 692 | if componentInfo_has_key(component): |
| 693 | # Configure the named component |
| 694 | componentConfigFuncs = [componentInfo[component][1]] |
| 695 | else: |
| 696 | # Check if this is a group name and configure all |
| 697 | # components in the group. |
| 698 | componentConfigFuncs = [] |
| 699 | for info in componentInfo.values(): |
| 700 | if info[4] == component: |
| 701 | componentConfigFuncs.append(info[1]) |
| 702 | |
| 703 | if len(componentConfigFuncs) == 0 and \ |
| 704 | component not in self._dynamicGroups: |
| 705 | raise KeyError, 'Unknown option "' + option + \ |
| 706 | '" for ' + self.__class__.__name__ |
| 707 | |
| 708 | # Add the configure method(s) (may be more than |
| 709 | # one if this is configuring a component group) |
| 710 | # and option/value to dictionary. |
| 711 | for componentConfigFunc in componentConfigFuncs: |
| 712 | if not indirectOptions_has_key(componentConfigFunc): |
| 713 | indirectOptions[componentConfigFunc] = {} |
| 714 | indirectOptions[componentConfigFunc][componentOption] \ |
| 715 | = value |
| 716 | else: |
| 717 | raise KeyError, 'Unknown option "' + option + \ |
| 718 | '" for ' + self.__class__.__name__ |
| 719 | |
| 720 | # Call the configure methods for any components. |
| 721 | map(apply, indirectOptions.keys(), |
| 722 | ((),) * len(indirectOptions), indirectOptions.values()) |
| 723 | |
| 724 | # Call the configuration callback function for each option. |
| 725 | for option in directOptions: |
| 726 | info = optionInfo[option] |
| 727 | func = info[_OPT_FUNCTION] |
| 728 | if func is not None: |
| 729 | func() |
| 730 | |
| 731 | def __setitem__(self, key, value): |
| 732 | apply(self.configure, (), {key: value}) |
| 733 | |
| 734 | #====================================================================== |
| 735 | # Methods used to query the megawidget. |
| 736 | |
| 737 | def component(self, name): |
| 738 | # Return a component widget of the megawidget given the |
| 739 | # component's name |
| 740 | # This allows the user of a megawidget to access and configure |
| 741 | # widget components directly. |
| 742 | |
| 743 | # Find the main component and any subcomponents |
| 744 | index = string.find(name, '_') |
| 745 | if index < 0: |
| 746 | component = name |
| 747 | remainingComponents = None |
| 748 | else: |
| 749 | component = name[:index] |
| 750 | remainingComponents = name[(index + 1):] |
| 751 | |
| 752 | # Expand component alias |
| 753 | if self.__componentAliases.has_key(component): |
| 754 | component, subComponent = self.__componentAliases[component] |
| 755 | if subComponent is not None: |
| 756 | if remainingComponents is None: |
| 757 | remainingComponents = subComponent |
| 758 | else: |
| 759 | remainingComponents = subComponent + '_' \ |
| 760 | + remainingComponents |
| 761 | |
| 762 | widget = self.__componentInfo[component][0] |
| 763 | if remainingComponents is None: |
| 764 | return widget |
| 765 | else: |
| 766 | return widget.component(remainingComponents) |
| 767 | |
| 768 | def interior(self): |
| 769 | return self._hull |
| 770 | |
| 771 | def hulldestroyed(self): |
| 772 | return not _hullToMegaWidget.has_key(self._hull) |
| 773 | |
| 774 | def __str__(self): |
| 775 | return str(self._hull) |
| 776 | |
| 777 | def cget(self, option): |
| 778 | # Get current configuration setting. |
| 779 | |
| 780 | # Return the value of an option, for example myWidget['font']. |
| 781 | |
| 782 | if self._optionInfo.has_key(option): |
| 783 | return self._optionInfo[option][_OPT_VALUE] |
| 784 | else: |
| 785 | index = string.find(option, '_') |
| 786 | if index >= 0: |
| 787 | component = option[:index] |
| 788 | componentOption = option[(index + 1):] |
| 789 | |
| 790 | # Expand component alias |
| 791 | if self.__componentAliases.has_key(component): |
| 792 | component, subComponent = self.__componentAliases[component] |
| 793 | if subComponent is not None: |
| 794 | componentOption = subComponent + '_' + componentOption |
| 795 | |
| 796 | # Expand option string to write on error |
| 797 | option = component + '_' + componentOption |
| 798 | |
| 799 | if self.__componentInfo.has_key(component): |
| 800 | # Call cget on the component. |
| 801 | componentCget = self.__componentInfo[component][3] |
| 802 | return componentCget(componentOption) |
| 803 | else: |
| 804 | # If this is a group name, call cget for one of |
| 805 | # the components in the group. |
| 806 | for info in self.__componentInfo.values(): |
| 807 | if info[4] == component: |
| 808 | componentCget = info[3] |
| 809 | return componentCget(componentOption) |
| 810 | |
| 811 | raise KeyError, 'Unknown option "' + option + \ |
| 812 | '" for ' + self.__class__.__name__ |
| 813 | |
| 814 | __getitem__ = cget |
| 815 | |
| 816 | def isinitoption(self, option): |
| 817 | return self._optionInfo[option][_OPT_FUNCTION] is INITOPT |
| 818 | |
| 819 | def options(self): |
| 820 | options = [] |
| 821 | if hasattr(self, '_optionInfo'): |
| 822 | for option, info in self._optionInfo.items(): |
| 823 | isinit = info[_OPT_FUNCTION] is INITOPT |
| 824 | default = info[_OPT_DEFAULT] |
| 825 | options.append((option, default, isinit)) |
| 826 | options.sort() |
| 827 | return options |
| 828 | |
| 829 | def components(self): |
| 830 | # Return a list of all components. |
| 831 | |
| 832 | # This list includes the 'hull' component and all widget subcomponents |
| 833 | |
| 834 | names = self.__componentInfo.keys() |
| 835 | names.sort() |
| 836 | return names |
| 837 | |
| 838 | def componentaliases(self): |
| 839 | # Return a list of all component aliases. |
| 840 | |
| 841 | componentAliases = self.__componentAliases |
| 842 | |
| 843 | names = componentAliases.keys() |
| 844 | names.sort() |
| 845 | rtn = [] |
| 846 | for alias in names: |
| 847 | (mainComponent, subComponent) = componentAliases[alias] |
| 848 | if subComponent is None: |
| 849 | rtn.append((alias, mainComponent)) |
| 850 | else: |
| 851 | rtn.append((alias, mainComponent + '_' + subComponent)) |
| 852 | |
| 853 | return rtn |
| 854 | |
| 855 | def componentgroup(self, name): |
| 856 | return self.__componentInfo[name][4] |
| 857 | |
| 858 | #============================================================================= |
| 859 | |
| 860 | # The grab functions are mainly called by the activate() and |
| 861 | # deactivate() methods. |
| 862 | # |
| 863 | # Use pushgrab() to add a new window to the grab stack. This |
| 864 | # releases the grab by the window currently on top of the stack (if |
| 865 | # there is one) and gives the grab and focus to the new widget. |
| 866 | # |
| 867 | # To remove the grab from the window on top of the grab stack, call |
| 868 | # popgrab(). |
| 869 | # |
| 870 | # Use releasegrabs() to release the grab and clear the grab stack. |
| 871 | |
| 872 | def pushgrab(grabWindow, globalMode, deactivateFunction): |
| 873 | prevFocus = grabWindow.tk.call('focus') |
| 874 | grabInfo = { |
| 875 | 'grabWindow' : grabWindow, |
| 876 | 'globalMode' : globalMode, |
| 877 | 'previousFocus' : prevFocus, |
| 878 | 'deactivateFunction' : deactivateFunction, |
| 879 | } |
| 880 | _grabStack.append(grabInfo) |
| 881 | _grabtop() |
| 882 | grabWindow.focus_set() |
| 883 | |
| 884 | def popgrab(window): |
| 885 | # Return the grab to the next window in the grab stack, if any. |
| 886 | |
| 887 | # If this window is not at the top of the grab stack, then it has |
| 888 | # just been deleted by the window manager or deactivated by a |
| 889 | # timer. Call the deactivate method for the modal dialog above |
| 890 | # this one on the stack. |
| 891 | if _grabStack[-1]['grabWindow'] != window: |
| 892 | for index in range(len(_grabStack)): |
| 893 | if _grabStack[index]['grabWindow'] == window: |
| 894 | _grabStack[index + 1]['deactivateFunction']() |
| 895 | break |
| 896 | |
| 897 | grabInfo = _grabStack[-1] |
| 898 | del _grabStack[-1] |
| 899 | |
| 900 | topWidget = grabInfo['grabWindow'] |
| 901 | prevFocus = grabInfo['previousFocus'] |
| 902 | globalMode = grabInfo['globalMode'] |
| 903 | |
| 904 | if globalMode != 'nograb': |
| 905 | topWidget.grab_release() |
| 906 | |
| 907 | if len(_grabStack) > 0: |
| 908 | _grabtop() |
| 909 | if prevFocus != '': |
| 910 | try: |
| 911 | topWidget.tk.call('focus', prevFocus) |
| 912 | except Tkinter.TclError: |
| 913 | # Previous focus widget has been deleted. Set focus |
| 914 | # to root window. |
| 915 | Tkinter._default_root.focus_set() |
| 916 | else: |
| 917 | # Make sure that focus does not remain on the released widget. |
| 918 | if len(_grabStack) > 0: |
| 919 | topWidget = _grabStack[-1]['grabWindow'] |
| 920 | topWidget.focus_set() |
| 921 | else: |
| 922 | Tkinter._default_root.focus_set() |
| 923 | |
| 924 | def grabstacktopwindow(): |
| 925 | if len(_grabStack) == 0: |
| 926 | return None |
| 927 | else: |
| 928 | return _grabStack[-1]['grabWindow'] |
| 929 | |
| 930 | def releasegrabs(): |
| 931 | # Release grab and clear the grab stack. |
| 932 | |
| 933 | current = Tkinter._default_root.grab_current() |
| 934 | if current is not None: |
| 935 | current.grab_release() |
| 936 | _grabStack[:] = [] |
| 937 | |
| 938 | def _grabtop(): |
| 939 | grabInfo = _grabStack[-1] |
| 940 | topWidget = grabInfo['grabWindow'] |
| 941 | globalMode = grabInfo['globalMode'] |
| 942 | |
| 943 | if globalMode == 'nograb': |
| 944 | return |
| 945 | |
| 946 | while 1: |
| 947 | try: |
| 948 | if globalMode: |
| 949 | topWidget.grab_set_global() |
| 950 | else: |
| 951 | topWidget.grab_set() |
| 952 | break |
| 953 | except Tkinter.TclError: |
| 954 | # Another application has grab. Keep trying until |
| 955 | # grab can succeed. |
| 956 | topWidget.after(100) |
| 957 | |
| 958 | #============================================================================= |
| 959 | |
| 960 | class MegaToplevel(MegaArchetype): |
| 961 | |
| 962 | def __init__(self, parent = None, **kw): |
| 963 | # Define the options for this megawidget. |
| 964 | optiondefs = ( |
| 965 | ('activatecommand', None, None), |
| 966 | ('deactivatecommand', None, None), |
| 967 | ('master', None, None), |
| 968 | ('title', None, self._settitle), |
| 969 | ('hull_class', self.__class__.__name__, None), |
| 970 | ) |
| 971 | self.defineoptions(kw, optiondefs) |
| 972 | |
| 973 | # Initialise the base class (after defining the options). |
| 974 | MegaArchetype.__init__(self, parent, Tkinter.Toplevel) |
| 975 | |
| 976 | # Initialise instance. |
| 977 | |
| 978 | # Set WM_DELETE_WINDOW protocol, deleting any old callback, so |
| 979 | # memory does not leak. |
| 980 | if hasattr(self._hull, '_Pmw_WM_DELETE_name'): |
| 981 | self._hull.tk.deletecommand(self._hull._Pmw_WM_DELETE_name) |
| 982 | self._hull._Pmw_WM_DELETE_name = \ |
| 983 | self.register(self._userDeleteWindow, needcleanup = 0) |
| 984 | self.protocol('WM_DELETE_WINDOW', self._hull._Pmw_WM_DELETE_name) |
| 985 | |
| 986 | # Initialise instance variables. |
| 987 | |
| 988 | self._firstShowing = 1 |
| 989 | # Used by show() to ensure window retains previous position on screen. |
| 990 | |
| 991 | # The IntVar() variable to wait on during a modal dialog. |
| 992 | self._wait = None |
| 993 | |
| 994 | self._active = 0 |
| 995 | self._userDeleteFunc = self.destroy |
| 996 | self._userModalDeleteFunc = self.deactivate |
| 997 | |
| 998 | # Check keywords and initialise options. |
| 999 | self.initialiseoptions() |
| 1000 | |
| 1001 | def _settitle(self): |
| 1002 | title = self['title'] |
| 1003 | if title is not None: |
| 1004 | self.title(title) |
| 1005 | |
| 1006 | def userdeletefunc(self, func=None): |
| 1007 | if func: |
| 1008 | self._userDeleteFunc = func |
| 1009 | else: |
| 1010 | return self._userDeleteFunc |
| 1011 | |
| 1012 | def usermodaldeletefunc(self, func=None): |
| 1013 | if func: |
| 1014 | self._userModalDeleteFunc = func |
| 1015 | else: |
| 1016 | return self._userModalDeleteFunc |
| 1017 | |
| 1018 | def _userDeleteWindow(self): |
| 1019 | if self.active(): |
| 1020 | self._userModalDeleteFunc() |
| 1021 | else: |
| 1022 | self._userDeleteFunc() |
| 1023 | |
| 1024 | def destroy(self): |
| 1025 | # Allow this to be called more than once. |
| 1026 | if _hullToMegaWidget.has_key(self._hull): |
| 1027 | self.deactivate() |
| 1028 | |
| 1029 | # Remove circular references, so that object can get cleaned up. |
| 1030 | del self._userDeleteFunc |
| 1031 | del self._userModalDeleteFunc |
| 1032 | |
| 1033 | MegaArchetype.destroy(self) |
| 1034 | |
| 1035 | def show(self, master = None): |
| 1036 | if self.state() != 'normal': |
| 1037 | if self._firstShowing: |
| 1038 | # Just let the window manager determine the window |
| 1039 | # position for the first time. |
| 1040 | geom = None |
| 1041 | else: |
| 1042 | # Position the window at the same place it was last time. |
| 1043 | geom = self._sameposition() |
| 1044 | setgeometryanddeiconify(self, geom) |
| 1045 | |
| 1046 | if self._firstShowing: |
| 1047 | self._firstShowing = 0 |
| 1048 | else: |
| 1049 | if self.transient() == '': |
| 1050 | self.tkraise() |
| 1051 | |
| 1052 | # Do this last, otherwise get flashing on NT: |
| 1053 | if master is not None: |
| 1054 | if master == 'parent': |
| 1055 | parent = self.winfo_parent() |
| 1056 | # winfo_parent() should return the parent widget, but the |
| 1057 | # the current version of Tkinter returns a string. |
| 1058 | if type(parent) == types.StringType: |
| 1059 | parent = self._hull._nametowidget(parent) |
| 1060 | master = parent.winfo_toplevel() |
| 1061 | self.transient(master) |
| 1062 | |
| 1063 | self.focus() |
| 1064 | |
| 1065 | def _centreonscreen(self): |
| 1066 | # Centre the window on the screen. (Actually halfway across |
| 1067 | # and one third down.) |
| 1068 | |
| 1069 | parent = self.winfo_parent() |
| 1070 | if type(parent) == types.StringType: |
| 1071 | parent = self._hull._nametowidget(parent) |
| 1072 | |
| 1073 | # Find size of window. |
| 1074 | self.update_idletasks() |
| 1075 | width = self.winfo_width() |
| 1076 | height = self.winfo_height() |
| 1077 | if width == 1 and height == 1: |
| 1078 | # If the window has not yet been displayed, its size is |
| 1079 | # reported as 1x1, so use requested size. |
| 1080 | width = self.winfo_reqwidth() |
| 1081 | height = self.winfo_reqheight() |
| 1082 | |
| 1083 | # Place in centre of screen: |
| 1084 | x = (self.winfo_screenwidth() - width) / 2 - parent.winfo_vrootx() |
| 1085 | y = (self.winfo_screenheight() - height) / 3 - parent.winfo_vrooty() |
| 1086 | if x < 0: |
| 1087 | x = 0 |
| 1088 | if y < 0: |
| 1089 | y = 0 |
| 1090 | return '+%d+%d' % (x, y) |
| 1091 | |
| 1092 | def _sameposition(self): |
| 1093 | # Position the window at the same place it was last time. |
| 1094 | |
| 1095 | geometry = self.geometry() |
| 1096 | index = string.find(geometry, '+') |
| 1097 | if index >= 0: |
| 1098 | return geometry[index:] |
| 1099 | else: |
| 1100 | return None |
| 1101 | |
| 1102 | def activate(self, globalMode = 0, geometry = 'centerscreenfirst'): |
| 1103 | if self._active: |
| 1104 | raise ValueError, 'Window is already active' |
| 1105 | if self.state() == 'normal': |
| 1106 | self.withdraw() |
| 1107 | |
| 1108 | self._active = 1 |
| 1109 | |
| 1110 | showbusycursor() |
| 1111 | |
| 1112 | if self._wait is None: |
| 1113 | self._wait = Tkinter.IntVar() |
| 1114 | self._wait.set(0) |
| 1115 | |
| 1116 | if geometry == 'centerscreenalways': |
| 1117 | geom = self._centreonscreen() |
| 1118 | elif geometry == 'centerscreenfirst': |
| 1119 | if self._firstShowing: |
| 1120 | # Centre the window the first time it is displayed. |
| 1121 | geom = self._centreonscreen() |
| 1122 | else: |
| 1123 | # Position the window at the same place it was last time. |
| 1124 | geom = self._sameposition() |
| 1125 | elif geometry[:5] == 'first': |
| 1126 | if self._firstShowing: |
| 1127 | geom = geometry[5:] |
| 1128 | else: |
| 1129 | # Position the window at the same place it was last time. |
| 1130 | geom = self._sameposition() |
| 1131 | else: |
| 1132 | geom = geometry |
| 1133 | |
| 1134 | self._firstShowing = 0 |
| 1135 | |
| 1136 | setgeometryanddeiconify(self, geom) |
| 1137 | |
| 1138 | # Do this last, otherwise get flashing on NT: |
| 1139 | master = self['master'] |
| 1140 | if master is not None: |
| 1141 | if master == 'parent': |
| 1142 | parent = self.winfo_parent() |
| 1143 | # winfo_parent() should return the parent widget, but the |
| 1144 | # the current version of Tkinter returns a string. |
| 1145 | if type(parent) == types.StringType: |
| 1146 | parent = self._hull._nametowidget(parent) |
| 1147 | master = parent.winfo_toplevel() |
| 1148 | self.transient(master) |
| 1149 | |
| 1150 | pushgrab(self._hull, globalMode, self.deactivate) |
| 1151 | command = self['activatecommand'] |
| 1152 | if callable(command): |
| 1153 | command() |
| 1154 | self.wait_variable(self._wait) |
| 1155 | |
| 1156 | return self._result |
| 1157 | |
| 1158 | def deactivate(self, result=None): |
| 1159 | if not self._active: |
| 1160 | return |
| 1161 | self._active = 0 |
| 1162 | |
| 1163 | # Restore the focus before withdrawing the window, since |
| 1164 | # otherwise the window manager may take the focus away so we |
| 1165 | # can't redirect it. Also, return the grab to the next active |
| 1166 | # window in the stack, if any. |
| 1167 | popgrab(self._hull) |
| 1168 | |
| 1169 | command = self['deactivatecommand'] |
| 1170 | if callable(command): |
| 1171 | command() |
| 1172 | |
| 1173 | self.withdraw() |
| 1174 | hidebusycursor(forceFocusRestore = 1) |
| 1175 | |
| 1176 | self._result = result |
| 1177 | self._wait.set(1) |
| 1178 | |
| 1179 | def active(self): |
| 1180 | return self._active |
| 1181 | |
| 1182 | forwardmethods(MegaToplevel, Tkinter.Toplevel, '_hull') |
| 1183 | |
| 1184 | #============================================================================= |
| 1185 | |
| 1186 | class MegaWidget(MegaArchetype): |
| 1187 | def __init__(self, parent = None, **kw): |
| 1188 | # Define the options for this megawidget. |
| 1189 | optiondefs = ( |
| 1190 | ('hull_class', self.__class__.__name__, None), |
| 1191 | ) |
| 1192 | self.defineoptions(kw, optiondefs) |
| 1193 | |
| 1194 | # Initialise the base class (after defining the options). |
| 1195 | MegaArchetype.__init__(self, parent, Tkinter.Frame) |
| 1196 | |
| 1197 | # Check keywords and initialise options. |
| 1198 | self.initialiseoptions() |
| 1199 | |
| 1200 | forwardmethods(MegaWidget, Tkinter.Frame, '_hull') |
| 1201 | |
| 1202 | #============================================================================= |
| 1203 | |
| 1204 | # Public functions |
| 1205 | #----------------- |
| 1206 | |
| 1207 | _traceTk = 0 |
| 1208 | def tracetk(root = None, on = 1, withStackTrace = 0, file=None): |
| 1209 | global _withStackTrace |
| 1210 | global _traceTkFile |
| 1211 | global _traceTk |
| 1212 | |
| 1213 | if root is None: |
| 1214 | root = Tkinter._default_root |
| 1215 | |
| 1216 | _withStackTrace = withStackTrace |
| 1217 | _traceTk = on |
| 1218 | if on: |
| 1219 | if hasattr(root.tk, '__class__'): |
| 1220 | # Tracing already on |
| 1221 | return |
| 1222 | if file is None: |
| 1223 | _traceTkFile = sys.stderr |
| 1224 | else: |
| 1225 | _traceTkFile = file |
| 1226 | tk = _TraceTk(root.tk) |
| 1227 | else: |
| 1228 | if not hasattr(root.tk, '__class__'): |
| 1229 | # Tracing already off |
| 1230 | return |
| 1231 | tk = root.tk.getTclInterp() |
| 1232 | _setTkInterps(root, tk) |
| 1233 | |
| 1234 | def showbusycursor(): |
| 1235 | |
| 1236 | _addRootToToplevelBusyInfo() |
| 1237 | root = Tkinter._default_root |
| 1238 | |
| 1239 | busyInfo = { |
| 1240 | 'newBusyWindows' : [], |
| 1241 | 'previousFocus' : None, |
| 1242 | 'busyFocus' : None, |
| 1243 | } |
| 1244 | _busyStack.append(busyInfo) |
| 1245 | |
| 1246 | if _disableKeyboardWhileBusy: |
| 1247 | # Remember the focus as it is now, before it is changed. |
| 1248 | busyInfo['previousFocus'] = root.tk.call('focus') |
| 1249 | |
| 1250 | if not _havebltbusy(root): |
| 1251 | # No busy command, so don't call busy hold on any windows. |
| 1252 | return |
| 1253 | |
| 1254 | for (window, winInfo) in _toplevelBusyInfo.items(): |
| 1255 | if (window.state() != 'withdrawn' and not winInfo['isBusy'] |
| 1256 | and not winInfo['excludeFromBusy']): |
| 1257 | busyInfo['newBusyWindows'].append(window) |
| 1258 | winInfo['isBusy'] = 1 |
| 1259 | _busy_hold(window, winInfo['busyCursorName']) |
| 1260 | |
| 1261 | # Make sure that no events for the busy window get |
| 1262 | # through to Tkinter, otherwise it will crash in |
| 1263 | # _nametowidget with a 'KeyError: _Busy' if there is |
| 1264 | # a binding on the toplevel window. |
| 1265 | window.tk.call('bindtags', winInfo['busyWindow'], 'Pmw_Dummy_Tag') |
| 1266 | |
| 1267 | if _disableKeyboardWhileBusy: |
| 1268 | # Remember previous focus widget for this toplevel window |
| 1269 | # and set focus to the busy window, which will ignore all |
| 1270 | # keyboard events. |
| 1271 | winInfo['windowFocus'] = \ |
| 1272 | window.tk.call('focus', '-lastfor', window._w) |
| 1273 | window.tk.call('focus', winInfo['busyWindow']) |
| 1274 | busyInfo['busyFocus'] = winInfo['busyWindow'] |
| 1275 | |
| 1276 | if len(busyInfo['newBusyWindows']) > 0: |
| 1277 | if os.name == 'nt': |
| 1278 | # NT needs an "update" before it will change the cursor. |
| 1279 | window.update() |
| 1280 | else: |
| 1281 | window.update_idletasks() |
| 1282 | |
| 1283 | def hidebusycursor(forceFocusRestore = 0): |
| 1284 | |
| 1285 | # Remember the focus as it is now, before it is changed. |
| 1286 | root = Tkinter._default_root |
| 1287 | if _disableKeyboardWhileBusy: |
| 1288 | currentFocus = root.tk.call('focus') |
| 1289 | |
| 1290 | # Pop the busy info off the stack. |
| 1291 | busyInfo = _busyStack[-1] |
| 1292 | del _busyStack[-1] |
| 1293 | |
| 1294 | for window in busyInfo['newBusyWindows']: |
| 1295 | # If this window has not been deleted, release the busy cursor. |
| 1296 | if _toplevelBusyInfo.has_key(window): |
| 1297 | winInfo = _toplevelBusyInfo[window] |
| 1298 | winInfo['isBusy'] = 0 |
| 1299 | _busy_release(window) |
| 1300 | |
| 1301 | if _disableKeyboardWhileBusy: |
| 1302 | # Restore previous focus window for this toplevel window, |
| 1303 | # but only if is still set to the busy window (it may have |
| 1304 | # been changed). |
| 1305 | windowFocusNow = window.tk.call('focus', '-lastfor', window._w) |
| 1306 | if windowFocusNow == winInfo['busyWindow']: |
| 1307 | try: |
| 1308 | window.tk.call('focus', winInfo['windowFocus']) |
| 1309 | except Tkinter.TclError: |
| 1310 | # Previous focus widget has been deleted. Set focus |
| 1311 | # to toplevel window instead (can't leave focus on |
| 1312 | # busy window). |
| 1313 | window.focus_set() |
| 1314 | |
| 1315 | if _disableKeyboardWhileBusy: |
| 1316 | # Restore the focus, depending on whether the focus had changed |
| 1317 | # between the calls to showbusycursor and hidebusycursor. |
| 1318 | if forceFocusRestore or busyInfo['busyFocus'] == currentFocus: |
| 1319 | # The focus had not changed, so restore it to as it was before |
| 1320 | # the call to showbusycursor, |
| 1321 | previousFocus = busyInfo['previousFocus'] |
| 1322 | if previousFocus is not None: |
| 1323 | try: |
| 1324 | root.tk.call('focus', previousFocus) |
| 1325 | except Tkinter.TclError: |
| 1326 | # Previous focus widget has been deleted; forget it. |
| 1327 | pass |
| 1328 | else: |
| 1329 | # The focus had changed, so restore it to what it had been |
| 1330 | # changed to before the call to hidebusycursor. |
| 1331 | root.tk.call('focus', currentFocus) |
| 1332 | |
| 1333 | def clearbusycursor(): |
| 1334 | while len(_busyStack) > 0: |
| 1335 | hidebusycursor() |
| 1336 | |
| 1337 | def setbusycursorattributes(window, **kw): |
| 1338 | _addRootToToplevelBusyInfo() |
| 1339 | for name, value in kw.items(): |
| 1340 | if name == 'exclude': |
| 1341 | _toplevelBusyInfo[window]['excludeFromBusy'] = value |
| 1342 | elif name == 'cursorName': |
| 1343 | _toplevelBusyInfo[window]['busyCursorName'] = value |
| 1344 | else: |
| 1345 | raise KeyError, 'Unknown busycursor attribute "' + name + '"' |
| 1346 | |
| 1347 | def _addRootToToplevelBusyInfo(): |
| 1348 | # Include the Tk root window in the list of toplevels. This must |
| 1349 | # not be called before Tkinter has had a chance to be initialised by |
| 1350 | # the application. |
| 1351 | |
| 1352 | root = Tkinter._default_root |
| 1353 | if root == None: |
| 1354 | root = Tkinter.Tk() |
| 1355 | if not _toplevelBusyInfo.has_key(root): |
| 1356 | _addToplevelBusyInfo(root) |
| 1357 | |
| 1358 | def busycallback(command, updateFunction = None): |
| 1359 | if not callable(command): |
| 1360 | raise ValueError, \ |
| 1361 | 'cannot register non-command busy callback %s %s' % \ |
| 1362 | (repr(command), type(command)) |
| 1363 | wrapper = _BusyWrapper(command, updateFunction) |
| 1364 | return wrapper.callback |
| 1365 | |
| 1366 | _errorReportFile = None |
| 1367 | _errorWindow = None |
| 1368 | |
| 1369 | def reporterrorstofile(file = None): |
| 1370 | global _errorReportFile |
| 1371 | _errorReportFile = file |
| 1372 | |
| 1373 | def displayerror(text): |
| 1374 | global _errorWindow |
| 1375 | |
| 1376 | if _errorReportFile is not None: |
| 1377 | _errorReportFile.write(text + '\n') |
| 1378 | else: |
| 1379 | # Print error on standard error as well as to error window. |
| 1380 | # Useful if error window fails to be displayed, for example |
| 1381 | # when exception is triggered in a <Destroy> binding for root |
| 1382 | # window. |
| 1383 | sys.stderr.write(text + '\n') |
| 1384 | |
| 1385 | if _errorWindow is None: |
| 1386 | # The error window has not yet been created. |
| 1387 | _errorWindow = _ErrorWindow() |
| 1388 | |
| 1389 | _errorWindow.showerror(text) |
| 1390 | |
| 1391 | _root = None |
| 1392 | _disableKeyboardWhileBusy = 1 |
| 1393 | |
| 1394 | def initialise( |
| 1395 | root = None, |
| 1396 | size = None, |
| 1397 | fontScheme = None, |
| 1398 | useTkOptionDb = 0, |
| 1399 | noBltBusy = 0, |
| 1400 | disableKeyboardWhileBusy = None, |
| 1401 | ): |
| 1402 | # Remember if show/hidebusycursor should ignore keyboard events. |
| 1403 | global _disableKeyboardWhileBusy |
| 1404 | if disableKeyboardWhileBusy is not None: |
| 1405 | _disableKeyboardWhileBusy = disableKeyboardWhileBusy |
| 1406 | |
| 1407 | # Do not use blt busy command if noBltBusy is set. Otherwise, |
| 1408 | # use blt busy if it is available. |
| 1409 | global _haveBltBusy |
| 1410 | if noBltBusy: |
| 1411 | _haveBltBusy = 0 |
| 1412 | |
| 1413 | # Save flag specifying whether the Tk option database should be |
| 1414 | # queried when setting megawidget option default values. |
| 1415 | global _useTkOptionDb |
| 1416 | _useTkOptionDb = useTkOptionDb |
| 1417 | |
| 1418 | # If we haven't been given a root window, use the default or |
| 1419 | # create one. |
| 1420 | if root is None: |
| 1421 | if Tkinter._default_root is None: |
| 1422 | root = Tkinter.Tk() |
| 1423 | else: |
| 1424 | root = Tkinter._default_root |
| 1425 | |
| 1426 | # If this call is initialising a different Tk interpreter than the |
| 1427 | # last call, then re-initialise all global variables. Assume the |
| 1428 | # last interpreter has been destroyed - ie: Pmw does not (yet) |
| 1429 | # support multiple simultaneous interpreters. |
| 1430 | global _root |
| 1431 | if _root is not None and _root != root: |
| 1432 | global _busyStack |
| 1433 | global _errorWindow |
| 1434 | global _grabStack |
| 1435 | global _hullToMegaWidget |
| 1436 | global _toplevelBusyInfo |
| 1437 | _busyStack = [] |
| 1438 | _errorWindow = None |
| 1439 | _grabStack = [] |
| 1440 | _hullToMegaWidget = {} |
| 1441 | _toplevelBusyInfo = {} |
| 1442 | _root = root |
| 1443 | |
| 1444 | # Trap Tkinter Toplevel constructors so that a list of Toplevels |
| 1445 | # can be maintained. |
| 1446 | Tkinter.Toplevel.title = __TkinterToplevelTitle |
| 1447 | |
| 1448 | # Trap Tkinter widget destruction so that megawidgets can be |
| 1449 | # destroyed when their hull widget is destoyed and the list of |
| 1450 | # Toplevels can be pruned. |
| 1451 | Tkinter.Toplevel.destroy = __TkinterToplevelDestroy |
| 1452 | Tkinter.Widget.destroy = __TkinterWidgetDestroy |
| 1453 | |
| 1454 | # Modify Tkinter's CallWrapper class to improve the display of |
| 1455 | # errors which occur in callbacks. |
| 1456 | Tkinter.CallWrapper = __TkinterCallWrapper |
| 1457 | |
| 1458 | # Make sure we get to know when the window manager deletes the |
| 1459 | # root window. Only do this if the protocol has not yet been set. |
| 1460 | # This is required if there is a modal dialog displayed and the |
| 1461 | # window manager deletes the root window. Otherwise the |
| 1462 | # application will not exit, even though there are no windows. |
| 1463 | if root.protocol('WM_DELETE_WINDOW') == '': |
| 1464 | root.protocol('WM_DELETE_WINDOW', root.destroy) |
| 1465 | |
| 1466 | # Set the base font size for the application and set the |
| 1467 | # Tk option database font resources. |
| 1468 | import PmwLogicalFont |
| 1469 | PmwLogicalFont._font_initialise(root, size, fontScheme) |
| 1470 | |
| 1471 | return root |
| 1472 | |
| 1473 | def alignlabels(widgets, sticky = None): |
| 1474 | if len(widgets) == 0: |
| 1475 | return |
| 1476 | |
| 1477 | widgets[0].update_idletasks() |
| 1478 | |
| 1479 | # Determine the size of the maximum length label string. |
| 1480 | maxLabelWidth = 0 |
| 1481 | for iwid in widgets: |
| 1482 | labelWidth = iwid.grid_bbox(0, 1)[2] |
| 1483 | if labelWidth > maxLabelWidth: |
| 1484 | maxLabelWidth = labelWidth |
| 1485 | |
| 1486 | # Adjust the margins for the labels such that the child sites and |
| 1487 | # labels line up. |
| 1488 | for iwid in widgets: |
| 1489 | if sticky is not None: |
| 1490 | iwid.component('label').grid(sticky=sticky) |
| 1491 | iwid.grid_columnconfigure(0, minsize = maxLabelWidth) |
| 1492 | #============================================================================= |
| 1493 | |
| 1494 | # Private routines |
| 1495 | #----------------- |
| 1496 | _callToTkReturned = 1 |
| 1497 | _recursionCounter = 1 |
| 1498 | |
| 1499 | class _TraceTk: |
| 1500 | def __init__(self, tclInterp): |
| 1501 | self.tclInterp = tclInterp |
| 1502 | |
| 1503 | def getTclInterp(self): |
| 1504 | return self.tclInterp |
| 1505 | |
| 1506 | # Calling from python into Tk. |
| 1507 | def call(self, *args, **kw): |
| 1508 | global _callToTkReturned |
| 1509 | global _recursionCounter |
| 1510 | |
| 1511 | _callToTkReturned = 0 |
| 1512 | if len(args) == 1 and type(args[0]) == types.TupleType: |
| 1513 | argStr = str(args[0]) |
| 1514 | else: |
| 1515 | argStr = str(args) |
| 1516 | _traceTkFile.write('CALL TK> %d:%s%s' % |
| 1517 | (_recursionCounter, ' ' * _recursionCounter, argStr)) |
| 1518 | _recursionCounter = _recursionCounter + 1 |
| 1519 | try: |
| 1520 | result = apply(self.tclInterp.call, args, kw) |
| 1521 | except Tkinter.TclError, errorString: |
| 1522 | _callToTkReturned = 1 |
| 1523 | _recursionCounter = _recursionCounter - 1 |
| 1524 | _traceTkFile.write('\nTK ERROR> %d:%s-> %s\n' % |
| 1525 | (_recursionCounter, ' ' * _recursionCounter, |
| 1526 | repr(errorString))) |
| 1527 | if _withStackTrace: |
| 1528 | _traceTkFile.write('CALL TK> stack:\n') |
| 1529 | traceback.print_stack() |
| 1530 | raise Tkinter.TclError, errorString |
| 1531 | |
| 1532 | _recursionCounter = _recursionCounter - 1 |
| 1533 | if _callToTkReturned: |
| 1534 | _traceTkFile.write('CALL RTN> %d:%s-> %s' % |
| 1535 | (_recursionCounter, ' ' * _recursionCounter, repr(result))) |
| 1536 | else: |
| 1537 | _callToTkReturned = 1 |
| 1538 | if result: |
| 1539 | _traceTkFile.write(' -> %s' % repr(result)) |
| 1540 | _traceTkFile.write('\n') |
| 1541 | if _withStackTrace: |
| 1542 | _traceTkFile.write('CALL TK> stack:\n') |
| 1543 | traceback.print_stack() |
| 1544 | |
| 1545 | _traceTkFile.flush() |
| 1546 | return result |
| 1547 | |
| 1548 | def __getattr__(self, key): |
| 1549 | return getattr(self.tclInterp, key) |
| 1550 | |
| 1551 | def _setTkInterps(window, tk): |
| 1552 | window.tk = tk |
| 1553 | for child in window.children.values(): |
| 1554 | _setTkInterps(child, tk) |
| 1555 | |
| 1556 | #============================================================================= |
| 1557 | |
| 1558 | # Functions to display a busy cursor. Keep a list of all toplevels |
| 1559 | # and display the busy cursor over them. The list will contain the Tk |
| 1560 | # root toplevel window as well as all other toplevel windows. |
| 1561 | # Also keep a list of the widget which last had focus for each |
| 1562 | # toplevel. |
| 1563 | |
| 1564 | # Map from toplevel windows to |
| 1565 | # {'isBusy', 'windowFocus', 'busyWindow', |
| 1566 | # 'excludeFromBusy', 'busyCursorName'} |
| 1567 | _toplevelBusyInfo = {} |
| 1568 | |
| 1569 | # Pmw needs to know all toplevel windows, so that it can call blt busy |
| 1570 | # on them. This is a hack so we get notified when a Tk topevel is |
| 1571 | # created. Ideally, the __init__ 'method' should be overridden, but |
| 1572 | # it is a 'read-only special attribute'. Luckily, title() is always |
| 1573 | # called from the Tkinter Toplevel constructor. |
| 1574 | |
| 1575 | def _addToplevelBusyInfo(window): |
| 1576 | if window._w == '.': |
| 1577 | busyWindow = '._Busy' |
| 1578 | else: |
| 1579 | busyWindow = window._w + '._Busy' |
| 1580 | |
| 1581 | _toplevelBusyInfo[window] = { |
| 1582 | 'isBusy' : 0, |
| 1583 | 'windowFocus' : None, |
| 1584 | 'busyWindow' : busyWindow, |
| 1585 | 'excludeFromBusy' : 0, |
| 1586 | 'busyCursorName' : None, |
| 1587 | } |
| 1588 | |
| 1589 | def __TkinterToplevelTitle(self, *args): |
| 1590 | # If this is being called from the constructor, include this |
| 1591 | # Toplevel in the list of toplevels and set the initial |
| 1592 | # WM_DELETE_WINDOW protocol to destroy() so that we get to know |
| 1593 | # about it. |
| 1594 | if not _toplevelBusyInfo.has_key(self): |
| 1595 | _addToplevelBusyInfo(self) |
| 1596 | self._Pmw_WM_DELETE_name = self.register(self.destroy, None, 0) |
| 1597 | self.protocol('WM_DELETE_WINDOW', self._Pmw_WM_DELETE_name) |
| 1598 | |
| 1599 | return apply(Tkinter.Wm.title, (self,) + args) |
| 1600 | |
| 1601 | _haveBltBusy = None |
| 1602 | def _havebltbusy(window): |
| 1603 | global _busy_hold, _busy_release, _haveBltBusy |
| 1604 | if _haveBltBusy is None: |
| 1605 | import PmwBlt |
| 1606 | _haveBltBusy = PmwBlt.havebltbusy(window) |
| 1607 | _busy_hold = PmwBlt.busy_hold |
| 1608 | if os.name == 'nt': |
| 1609 | # There is a bug in Blt 2.4i on NT where the busy window |
| 1610 | # does not follow changes in the children of a window. |
| 1611 | # Using forget works around the problem. |
| 1612 | _busy_release = PmwBlt.busy_forget |
| 1613 | else: |
| 1614 | _busy_release = PmwBlt.busy_release |
| 1615 | return _haveBltBusy |
| 1616 | |
| 1617 | class _BusyWrapper: |
| 1618 | def __init__(self, command, updateFunction): |
| 1619 | self._command = command |
| 1620 | self._updateFunction = updateFunction |
| 1621 | |
| 1622 | def callback(self, *args): |
| 1623 | showbusycursor() |
| 1624 | rtn = apply(self._command, args) |
| 1625 | |
| 1626 | # Call update before hiding the busy windows to clear any |
| 1627 | # events that may have occurred over the busy windows. |
| 1628 | if callable(self._updateFunction): |
| 1629 | self._updateFunction() |
| 1630 | |
| 1631 | hidebusycursor() |
| 1632 | return rtn |
| 1633 | |
| 1634 | #============================================================================= |
| 1635 | |
| 1636 | def drawarrow(canvas, color, direction, tag, baseOffset = 0.25, edgeOffset = 0.15): |
| 1637 | canvas.delete(tag) |
| 1638 | |
| 1639 | bw = (string.atoi(canvas['borderwidth']) + |
| 1640 | string.atoi(canvas['highlightthickness'])) |
| 1641 | width = string.atoi(canvas['width']) |
| 1642 | height = string.atoi(canvas['height']) |
| 1643 | |
| 1644 | if direction in ('up', 'down'): |
| 1645 | majorDimension = height |
| 1646 | minorDimension = width |
| 1647 | else: |
| 1648 | majorDimension = width |
| 1649 | minorDimension = height |
| 1650 | |
| 1651 | offset = round(baseOffset * majorDimension) |
| 1652 | if direction in ('down', 'right'): |
| 1653 | base = bw + offset |
| 1654 | apex = bw + majorDimension - offset |
| 1655 | else: |
| 1656 | base = bw + majorDimension - offset |
| 1657 | apex = bw + offset |
| 1658 | |
| 1659 | if minorDimension > 3 and minorDimension % 2 == 0: |
| 1660 | minorDimension = minorDimension - 1 |
| 1661 | half = int(minorDimension * (1 - 2 * edgeOffset)) / 2 |
| 1662 | low = round(bw + edgeOffset * minorDimension) |
| 1663 | middle = low + half |
| 1664 | high = low + 2 * half |
| 1665 | |
| 1666 | if direction in ('up', 'down'): |
| 1667 | coords = (low, base, high, base, middle, apex) |
| 1668 | else: |
| 1669 | coords = (base, low, base, high, apex, middle) |
| 1670 | kw = {'fill' : color, 'outline' : color, 'tag' : tag} |
| 1671 | apply(canvas.create_polygon, coords, kw) |
| 1672 | |
| 1673 | #============================================================================= |
| 1674 | |
| 1675 | # Modify the Tkinter destroy methods so that it notifies us when a Tk |
| 1676 | # toplevel or frame is destroyed. |
| 1677 | |
| 1678 | # A map from the 'hull' component of a megawidget to the megawidget. |
| 1679 | # This is used to clean up a megawidget when its hull is destroyed. |
| 1680 | _hullToMegaWidget = {} |
| 1681 | |
| 1682 | def __TkinterToplevelDestroy(tkWidget): |
| 1683 | if _hullToMegaWidget.has_key(tkWidget): |
| 1684 | mega = _hullToMegaWidget[tkWidget] |
| 1685 | try: |
| 1686 | mega.destroy() |
| 1687 | except: |
| 1688 | _reporterror(mega.destroy, ()) |
| 1689 | else: |
| 1690 | # Delete the busy info structure for this toplevel (if the |
| 1691 | # window was created before Pmw.initialise() was called, it |
| 1692 | # will not have any. |
| 1693 | if _toplevelBusyInfo.has_key(tkWidget): |
| 1694 | del _toplevelBusyInfo[tkWidget] |
| 1695 | if hasattr(tkWidget, '_Pmw_WM_DELETE_name'): |
| 1696 | tkWidget.tk.deletecommand(tkWidget._Pmw_WM_DELETE_name) |
| 1697 | del tkWidget._Pmw_WM_DELETE_name |
| 1698 | Tkinter.BaseWidget.destroy(tkWidget) |
| 1699 | |
| 1700 | def __TkinterWidgetDestroy(tkWidget): |
| 1701 | if _hullToMegaWidget.has_key(tkWidget): |
| 1702 | mega = _hullToMegaWidget[tkWidget] |
| 1703 | try: |
| 1704 | mega.destroy() |
| 1705 | except: |
| 1706 | _reporterror(mega.destroy, ()) |
| 1707 | else: |
| 1708 | Tkinter.BaseWidget.destroy(tkWidget) |
| 1709 | |
| 1710 | #============================================================================= |
| 1711 | |
| 1712 | # Add code to Tkinter to improve the display of errors which occur in |
| 1713 | # callbacks. |
| 1714 | |
| 1715 | class __TkinterCallWrapper: |
| 1716 | def __init__(self, func, subst, widget): |
| 1717 | self.func = func |
| 1718 | self.subst = subst |
| 1719 | self.widget = widget |
| 1720 | |
| 1721 | # Calling back from Tk into python. |
| 1722 | def __call__(self, *args): |
| 1723 | try: |
| 1724 | if self.subst: |
| 1725 | args = apply(self.subst, args) |
| 1726 | if _traceTk: |
| 1727 | if not _callToTkReturned: |
| 1728 | _traceTkFile.write('\n') |
| 1729 | if hasattr(self.func, 'im_class'): |
| 1730 | name = self.func.im_class.__name__ + '.' + \ |
| 1731 | self.func.__name__ |
| 1732 | else: |
| 1733 | name = self.func.__name__ |
| 1734 | if len(args) == 1 and hasattr(args[0], 'type'): |
| 1735 | # The argument to the callback is an event. |
| 1736 | eventName = _eventTypeToName[string.atoi(args[0].type)] |
| 1737 | if eventName in ('KeyPress', 'KeyRelease',): |
| 1738 | argStr = '(%s %s Event: %s)' % \ |
| 1739 | (eventName, args[0].keysym, args[0].widget) |
| 1740 | else: |
| 1741 | argStr = '(%s Event, %s)' % (eventName, args[0].widget) |
| 1742 | else: |
| 1743 | argStr = str(args) |
| 1744 | _traceTkFile.write('CALLBACK> %d:%s%s%s\n' % |
| 1745 | (_recursionCounter, ' ' * _recursionCounter, name, argStr)) |
| 1746 | _traceTkFile.flush() |
| 1747 | return apply(self.func, args) |
| 1748 | except SystemExit, msg: |
| 1749 | raise SystemExit, msg |
| 1750 | except: |
| 1751 | _reporterror(self.func, args) |
| 1752 | |
| 1753 | _eventTypeToName = { |
| 1754 | 2 : 'KeyPress', 15 : 'VisibilityNotify', 28 : 'PropertyNotify', |
| 1755 | 3 : 'KeyRelease', 16 : 'CreateNotify', 29 : 'SelectionClear', |
| 1756 | 4 : 'ButtonPress', 17 : 'DestroyNotify', 30 : 'SelectionRequest', |
| 1757 | 5 : 'ButtonRelease', 18 : 'UnmapNotify', 31 : 'SelectionNotify', |
| 1758 | 6 : 'MotionNotify', 19 : 'MapNotify', 32 : 'ColormapNotify', |
| 1759 | 7 : 'EnterNotify', 20 : 'MapRequest', 33 : 'ClientMessage', |
| 1760 | 8 : 'LeaveNotify', 21 : 'ReparentNotify', 34 : 'MappingNotify', |
| 1761 | 9 : 'FocusIn', 22 : 'ConfigureNotify', 35 : 'VirtualEvents', |
| 1762 | 10 : 'FocusOut', 23 : 'ConfigureRequest', 36 : 'ActivateNotify', |
| 1763 | 11 : 'KeymapNotify', 24 : 'GravityNotify', 37 : 'DeactivateNotify', |
| 1764 | 12 : 'Expose', 25 : 'ResizeRequest', 38 : 'MouseWheelEvent', |
| 1765 | 13 : 'GraphicsExpose', 26 : 'CirculateNotify', |
| 1766 | 14 : 'NoExpose', 27 : 'CirculateRequest', |
| 1767 | } |
| 1768 | |
| 1769 | def _reporterror(func, args): |
| 1770 | # Fetch current exception values. |
| 1771 | exc_type, exc_value, exc_traceback = sys.exc_info() |
| 1772 | |
| 1773 | # Give basic information about the callback exception. |
| 1774 | if type(exc_type) == types.ClassType: |
| 1775 | # Handle python 1.5 class exceptions. |
| 1776 | exc_type = exc_type.__name__ |
| 1777 | msg = exc_type + ' Exception in Tk callback\n' |
| 1778 | msg = msg + ' Function: %s (type: %s)\n' % (repr(func), type(func)) |
| 1779 | msg = msg + ' Args: %s\n' % str(args) |
| 1780 | |
| 1781 | if type(args) == types.TupleType and len(args) > 0 and \ |
| 1782 | hasattr(args[0], 'type'): |
| 1783 | eventArg = 1 |
| 1784 | else: |
| 1785 | eventArg = 0 |
| 1786 | |
| 1787 | # If the argument to the callback is an event, add the event type. |
| 1788 | if eventArg: |
| 1789 | eventNum = string.atoi(args[0].type) |
| 1790 | if eventNum in _eventTypeToName.keys(): |
| 1791 | msg = msg + ' Event type: %s (type num: %d)\n' % \ |
| 1792 | (_eventTypeToName[eventNum], eventNum) |
| 1793 | else: |
| 1794 | msg = msg + ' Unknown event type (type num: %d)\n' % eventNum |
| 1795 | |
| 1796 | # Add the traceback. |
| 1797 | msg = msg + 'Traceback (innermost last):\n' |
| 1798 | for tr in traceback.extract_tb(exc_traceback): |
| 1799 | msg = msg + ' File "%s", line %s, in %s\n' % (tr[0], tr[1], tr[2]) |
| 1800 | msg = msg + ' %s\n' % tr[3] |
| 1801 | msg = msg + '%s: %s\n' % (exc_type, exc_value) |
| 1802 | |
| 1803 | # If the argument to the callback is an event, add the event contents. |
| 1804 | if eventArg: |
| 1805 | msg = msg + '\n================================================\n' |
| 1806 | msg = msg + ' Event contents:\n' |
| 1807 | keys = args[0].__dict__.keys() |
| 1808 | keys.sort() |
| 1809 | for key in keys: |
| 1810 | msg = msg + ' %s: %s\n' % (key, args[0].__dict__[key]) |
| 1811 | |
| 1812 | clearbusycursor() |
| 1813 | try: |
| 1814 | displayerror(msg) |
| 1815 | except: |
| 1816 | pass |
| 1817 | |
| 1818 | class _ErrorWindow: |
| 1819 | def __init__(self): |
| 1820 | |
| 1821 | self._errorQueue = [] |
| 1822 | self._errorCount = 0 |
| 1823 | self._open = 0 |
| 1824 | self._firstShowing = 1 |
| 1825 | |
| 1826 | # Create the toplevel window |
| 1827 | self._top = Tkinter.Toplevel() |
| 1828 | self._top.protocol('WM_DELETE_WINDOW', self._hide) |
| 1829 | self._top.title('Error in background function') |
| 1830 | self._top.iconname('Background error') |
| 1831 | |
| 1832 | # Create the text widget and scrollbar in a frame |
| 1833 | upperframe = Tkinter.Frame(self._top) |
| 1834 | |
| 1835 | scrollbar = Tkinter.Scrollbar(upperframe, orient='vertical') |
| 1836 | scrollbar.pack(side = 'right', fill = 'y') |
| 1837 | |
| 1838 | self._text = Tkinter.Text(upperframe, yscrollcommand=scrollbar.set) |
| 1839 | self._text.pack(fill = 'both', expand = 1) |
| 1840 | scrollbar.configure(command=self._text.yview) |
| 1841 | |
| 1842 | # Create the buttons and label in a frame |
| 1843 | lowerframe = Tkinter.Frame(self._top) |
| 1844 | |
| 1845 | ignore = Tkinter.Button(lowerframe, |
| 1846 | text = 'Ignore remaining errors', command = self._hide) |
| 1847 | ignore.pack(side='left') |
| 1848 | |
| 1849 | self._nextError = Tkinter.Button(lowerframe, |
| 1850 | text = 'Show next error', command = self._next) |
| 1851 | self._nextError.pack(side='left') |
| 1852 | |
| 1853 | self._label = Tkinter.Label(lowerframe, relief='ridge') |
| 1854 | self._label.pack(side='left', fill='x', expand=1) |
| 1855 | |
| 1856 | # Pack the lower frame first so that it does not disappear |
| 1857 | # when the window is resized. |
| 1858 | lowerframe.pack(side = 'bottom', fill = 'x') |
| 1859 | upperframe.pack(side = 'bottom', fill = 'both', expand = 1) |
| 1860 | |
| 1861 | def showerror(self, text): |
| 1862 | if self._open: |
| 1863 | self._errorQueue.append(text) |
| 1864 | else: |
| 1865 | self._display(text) |
| 1866 | self._open = 1 |
| 1867 | |
| 1868 | # Display the error window in the same place it was before. |
| 1869 | if self._top.state() == 'normal': |
| 1870 | # If update_idletasks is not called here, the window may |
| 1871 | # be placed partially off the screen. Also, if it is not |
| 1872 | # called and many errors are generated quickly in |
| 1873 | # succession, the error window may not display errors |
| 1874 | # until the last one is generated and the interpreter |
| 1875 | # becomes idle. |
| 1876 | # XXX: remove this, since it causes omppython to go into an |
| 1877 | # infinite loop if an error occurs in an omp callback. |
| 1878 | # self._top.update_idletasks() |
| 1879 | |
| 1880 | pass |
| 1881 | else: |
| 1882 | if self._firstShowing: |
| 1883 | geom = None |
| 1884 | else: |
| 1885 | geometry = self._top.geometry() |
| 1886 | index = string.find(geometry, '+') |
| 1887 | if index >= 0: |
| 1888 | geom = geometry[index:] |
| 1889 | else: |
| 1890 | geom = None |
| 1891 | setgeometryanddeiconify(self._top, geom) |
| 1892 | |
| 1893 | if self._firstShowing: |
| 1894 | self._firstShowing = 0 |
| 1895 | else: |
| 1896 | self._top.tkraise() |
| 1897 | |
| 1898 | self._top.focus() |
| 1899 | |
| 1900 | self._updateButtons() |
| 1901 | |
| 1902 | # Release any grab, so that buttons in the error window work. |
| 1903 | releasegrabs() |
| 1904 | |
| 1905 | def _hide(self): |
| 1906 | self._errorCount = self._errorCount + len(self._errorQueue) |
| 1907 | self._errorQueue = [] |
| 1908 | self._top.withdraw() |
| 1909 | self._open = 0 |
| 1910 | |
| 1911 | def _next(self): |
| 1912 | # Display the next error in the queue. |
| 1913 | |
| 1914 | text = self._errorQueue[0] |
| 1915 | del self._errorQueue[0] |
| 1916 | |
| 1917 | self._display(text) |
| 1918 | self._updateButtons() |
| 1919 | |
| 1920 | def _display(self, text): |
| 1921 | self._errorCount = self._errorCount + 1 |
| 1922 | text = 'Error: %d\n%s' % (self._errorCount, text) |
| 1923 | self._text.delete('1.0', 'end') |
| 1924 | self._text.insert('end', text) |
| 1925 | |
| 1926 | def _updateButtons(self): |
| 1927 | numQueued = len(self._errorQueue) |
| 1928 | if numQueued > 0: |
| 1929 | self._label.configure(text='%d more errors' % numQueued) |
| 1930 | self._nextError.configure(state='normal') |
| 1931 | else: |
| 1932 | self._label.configure(text='No more errors') |
| 1933 | self._nextError.configure(state='disabled') |