Commit | Line | Data |
---|---|---|
920dae64 AT |
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') |