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