blob: e1e71b2d7cd111628937aab57e373b816bae5ff5 [file] [log] [blame]
Just van Rossum40f9b7b1999-01-30 22:39:17 +00001import Qd
2import Win
3import QuickDraw
4import Evt
5import string
6from types import *
7import sys
8
9WidgetsError = "WidgetsError"
10
11DEBUG = 0
12
13class Widget:
14
15 """Base class for all widgets."""
16
17 _selectable = 0
18
19 def __init__(self, possize):
20 self._widgets = []
21 self._widgetsdict = {}
22 self._possize = possize
23 self._bounds = None
24 self._visible = 1
25 self._enabled = 0
26 self._selected = 0
27 self._activated = 0
28 self._callback = None
29 self._parent = None
30 self._parentwindow = None
31 self._bindings = {}
32 self._backcolor = None
33
34 def show(self, onoff):
35 self._visible = onoff
36 for w in self._widgets:
37 w.show(onoff)
38 if self._parentwindow is not None and self._parentwindow.wid is not None:
39 self.SetPort()
40 if onoff:
41 self.draw()
42 else:
43 Qd.EraseRect(self._bounds)
44
45 def draw(self, visRgn = None):
46 if self._visible:
47 # draw your stuff here
48 pass
49
50 def getpossize(self):
51 return self._possize
52
53 def getbounds(self):
54 return self._bounds
55
56 def move(self, x, y = None):
57 """absolute move"""
58 if y == None:
59 x, y = x
60 if type(self._possize) <> TupleType:
61 raise WidgetsError, "can't move widget with bounds function"
62 l, t, r, b = self._possize
63 self.resize(x, y, r, b)
64
65 def rmove(self, x, y = None):
66 """relative move"""
67 if y == None:
68 x, y = x
69 if type(self._possize) <> TupleType:
70 raise WidgetsError, "can't move widget with bounds function"
71 l, t, r, b = self._possize
72 self.resize(l + x, t + y, r, b)
73
74 def resize(self, *args):
75 if len(args) == 1:
76 if type(args[0]) == FunctionType or type(args[0]) == MethodType:
77 self._possize = args[0]
78 else:
79 apply(self.resize, args[0])
80 elif len(args) == 2:
81 self._possize = (0, 0) + args
82 elif len(args) == 4:
83 self._possize = args
84 else:
85 raise TypeError, "wrong number of arguments"
86 self._calcbounds()
87
88 def open(self):
89 self._calcbounds()
90
91 def close(self):
92 del self._callback
93 del self._possize
94 del self._bindings
95 del self._parent
96 del self._parentwindow
97
98 def bind(self, key, callback):
99 """bind a key or an 'event' to a callback"""
100 if callback:
101 self._bindings[key] = callback
102 elif self._bindings.has_key(key):
103 del self._bindings[key]
104
105 def adjust(self, oldbounds):
106 self.SetPort()
107 Win.InvalRect(oldbounds)
108 Win.InvalRect(self._bounds)
109
110 def _calcbounds(self):
111 # calculate absolute bounds relative to the window origin from our
112 # abstract _possize attribute, which is either a 4-tuple or a callable object
113 oldbounds = self._bounds
114 pl, pt, pr, pb = self._parent._bounds
115 if callable(self._possize):
116 # _possize is callable, let it figure it out by itself: it should return
117 # the bounds relative to our parent widget.
118 width = pr - pl
119 height = pb - pt
120 self._bounds = Qd.OffsetRect(self._possize(width, height), pl, pt)
121 else:
122 # _possize must be a 4-tuple. This is where the algorithm by Peter Kriens and
123 # Petr van Blokland kicks in. (*** Parts of this algorithm are applied for
124 # patents by Ericsson, Sweden ***)
125 l, t, r, b = self._possize
126 # depending on the values of l(eft), t(op), r(right) and b(ottom),
127 # they mean different things:
128 if l < -1:
129 # l is less than -1, this mean it measures from the *right* of it's parent
130 l = pr + l
131 else:
132 # l is -1 or greater, this mean it measures from the *left* of it's parent
133 l = pl + l
134 if t < -1:
135 # t is less than -1, this mean it measures from the *bottom* of it's parent
136 t = pb + t
137 else:
138 # t is -1 or greater, this mean it measures from the *top* of it's parent
139 t = pt + t
140 if r > 1:
141 # r is greater than 1, this means r is the *width* of the widget
142 r = l + r
143 else:
144 # r is less than 1, this means it measures from the *right* of it's parent
145 r = pr + r
146 if b > 1:
147 # b is greater than 1, this means b is the *height* of the widget
148 b = t + b
149 else:
150 # b is less than 1, this means it measures from the *bottom* of it's parent
151 b = pb + b
152 self._bounds = (l, t, r, b)
153 if oldbounds and oldbounds <> self._bounds:
154 self.adjust(oldbounds)
155 for w in self._widgets:
156 w._calcbounds()
157
158 def test(self, point):
159 if Qd.PtInRect(point, self._bounds):
160 return 1
161
162 def click(self, point, modifiers):
163 pass
164
165 def findwidget(self, point, onlyenabled = 1):
166 if self.test(point):
167 for w in self._widgets:
168 widget = w.findwidget(point)
169 if widget is not None:
170 return widget
171 if self._enabled or not onlyenabled:
172 return self
173
174 def forall(self, methodname, *args):
175 for w in self._widgets:
176 rv = apply(w.forall, (methodname,) + args)
177 if rv:
178 return rv
179 if self._bindings.has_key("<" + methodname + ">"):
180 callback = self._bindings["<" + methodname + ">"]
181 rv = apply(callback, args)
182 if rv:
183 return rv
184 if hasattr(self, methodname):
185 method = getattr(self, methodname)
186 return apply(method, args)
187
188 def forall_butself(self, methodname, *args):
189 for w in self._widgets:
190 rv = apply(w.forall, (methodname,) + args)
191 if rv:
192 return rv
193
194 def forall_frombottom(self, methodname, *args):
195 if self._bindings.has_key("<" + methodname + ">"):
196 callback = self._bindings["<" + methodname + ">"]
197 rv = apply(callback, args)
198 if rv:
199 return rv
200 if hasattr(self, methodname):
201 method = getattr(self, methodname)
202 rv = apply(method, args)
203 if rv:
204 return rv
205 for w in self._widgets:
206 rv = apply(w.forall_frombottom, (methodname,) + args)
207 if rv:
208 return rv
209
210 def _addwidget(self, key, widget):
211 if widget in self._widgets:
212 raise ValueError, "duplicate widget"
213 if self._widgetsdict.has_key(key):
214 self._removewidget(key)
215 self._widgets.append(widget)
216 self._widgetsdict[key] = widget
217 widget._parent = self
218 self._setparentwindow(widget)
219 if self._parentwindow and self._parentwindow.wid:
220 widget.forall_frombottom("open")
221 Win.InvalRect(widget._bounds)
222
223 def _setparentwindow(self, widget):
224 widget._parentwindow = self._parentwindow
225 for w in widget._widgets:
226 self._setparentwindow(w)
227
228 def _removewidget(self, key):
229 if not self._widgetsdict.has_key(key):
230 raise KeyError, "no widget with key " + `key`
231 widget = self._widgetsdict[key]
232 for k in widget._widgetsdict.keys():
233 widget._removewidget(k)
234 if self._parentwindow._currentwidget == widget:
235 widget.select(0)
236 self._parentwindow._currentwidget = None
237 self.SetPort()
238 Win.InvalRect(widget._bounds)
239 widget.close()
240 del self._widgetsdict[key]
241 self._widgets.remove(widget)
242
243 def __setattr__(self, attr, value):
244 if type(value) == InstanceType and isinstance(value, Widget) and \
245 attr not in ("_currentwidget", "_lastrollover",
246 "_parent", "_parentwindow", "_defaultbutton"):
247 if hasattr(self, attr):
248 raise ValueError, "Can't replace existing attribute: " + attr
249 self._addwidget(attr, value)
250 self.__dict__[attr] = value
251
252 def __delattr__(self, attr):
253 if attr == "_widgetsdict":
254 raise AttributeError, "cannot delete attribute _widgetsdict"
255 if self._widgetsdict.has_key(attr):
256 self._removewidget(attr)
257 if self.__dict__.has_key(attr):
258 del self.__dict__[attr]
259 elif self.__dict__.has_key(attr):
260 del self.__dict__[attr]
261 else:
262 raise AttributeError, attr
263
264 def __setitem__(self, key, value):
265 self._addwidget(key, value)
266
267 def __getitem__(self, key):
268 if not self._widgetsdict.has_key(key):
269 raise KeyError, key
270 return self._widgetsdict[key]
271
272 def __delitem__(self, key):
273 self._removewidget(key)
274
275 def SetPort(self):
276 self._parentwindow.SetPort()
277
278 def __del__(self):
279 if DEBUG:
280 print "%s instance deleted" % self.__class__.__name__
281
282 def _drawbounds(self):
283 Qd.FrameRect(self._bounds)
284
285
286class ClickableWidget(Widget):
287
288 """Base class for clickable widgets. (note: self._enabled must be true to receive click events.)"""
289
290 def click(self, point, modifiers):
291 pass
292
293 def enable(self, onoff):
294 self._enabled = onoff
295 self.SetPort()
296 self.draw()
297
298 def callback(self):
299 if self._callback:
300 return CallbackCall(self._callback, 1)
301
302
303class SelectableWidget(ClickableWidget):
304
305 """Base class for selectable widgets."""
306
307 _selectable = 1
308
309 def select(self, onoff, isclick = 0):
310 if onoff == self._selected:
311 return 1
312 if self._bindings.has_key("<select>"):
313 callback = self._bindings["<select>"]
314 if callback(onoff):
315 return 1
316 self._selected = onoff
317 if onoff:
318 if self._parentwindow._currentwidget is not None:
319 self._parentwindow._currentwidget.select(0)
320 self._parentwindow._currentwidget = self
321 else:
322 self._parentwindow._currentwidget = None
323
324 def key(self, char, event):
325 pass
326
327 def drawselframe(self, onoff):
328 if not self._parentwindow._hasselframes:
329 return
330 thickrect = Qd.InsetRect(self._bounds, -3, -3)
331 state = Qd.GetPenState()
332 Qd.PenSize(2, 2)
333 if onoff:
334 Qd.PenPat(Qd.qd.black)
335 else:
336 Qd.PenPat(Qd.qd.white)
337 Qd.FrameRect(thickrect)
338 Qd.SetPenState(state)
339
340 def adjust(self, oldbounds):
341 self.SetPort()
342 if self._selected:
343 Win.InvalRect(Qd.InsetRect(oldbounds, -3, -3))
344 Win.InvalRect(Qd.InsetRect(self._bounds, -3, -3))
345 else:
346 Win.InvalRect(oldbounds)
347 Win.InvalRect(self._bounds)
348
349
350class _Line(Widget):
351
352 def __init__(self, possize, thickness = 1):
353 Widget.__init__(self, possize)
354 self._thickness = thickness
355
356 def open(self):
357 self._calcbounds()
358 self.SetPort()
359 self.draw()
360
361 def draw(self, visRgn = None):
362 if self._visible:
363 Qd.PaintRect(self._bounds)
364
365 def _drawbounds(self):
366 pass
367
368class HorizontalLine(_Line):
369
370 def _calcbounds(self):
371 Widget._calcbounds(self)
372 l, t, r, b = self._bounds
373 self._bounds = l, t, r, t + self._thickness
374
375class VerticalLine(_Line):
376
377 def _calcbounds(self):
378 Widget._calcbounds(self)
379 l, t, r, b = self._bounds
380 self._bounds = l, t, l + self._thickness, b
381
382
383class Frame(Widget):
384
385 def __init__(self, possize, pattern = Qd.qd.black, color = (0, 0, 0)):
386 Widget.__init__(self, possize)
387 self._framepattern = pattern
388 self._framecolor = color
389
390 def setcolor(self, color):
391 self._framecolor = color
392 self.SetPort()
393 self.draw()
394
395 def setpattern(self, pattern):
396 self._framepattern = pattern
397 self.SetPort()
398 self.draw()
399
400 def draw(self, visRgn = None):
401 if self._visible:
402 penstate = Qd.GetPenState()
403 Qd.PenPat(self._framepattern)
404 Qd.RGBForeColor(self._framecolor)
405 Qd.FrameRect(self._bounds)
406 Qd.RGBForeColor((0, 0, 0))
407 Qd.SetPenState(penstate)
408
409def _darkencolor((r, g, b)):
410 return 0.75 * r, 0.75 * g, 0.75 * b
411
412class BevelBox(Widget):
413
414 """'Platinum' beveled rectangle."""
415
416 def __init__(self, possize, color = (0xe000, 0xe000, 0xe000)):
417 Widget.__init__(self, possize)
418 self._color = color
419 self._darkercolor = _darkencolor(color)
420
421 def setcolor(self, color):
422 self._color = color
423 self.SetPort()
424 self.draw()
425
426 def draw(self, visRgn = None):
427 if self._visible:
428 l, t, r, b = Qd.InsetRect(self._bounds, 1, 1)
429 Qd.RGBForeColor(self._color)
430 Qd.PaintRect((l, t, r, b))
431 Qd.RGBForeColor(self._darkercolor)
432 Qd.MoveTo(l, b)
433 Qd.LineTo(r, b)
434 Qd.LineTo(r, t)
435 Qd.RGBForeColor((0, 0, 0))
436
437
438class Group(Widget):
439
440 """A container for subwidgets"""
441
442
443class HorizontalPanes(Widget):
444
445 """Panes, a.k.a. frames. Works a bit like a group. Devides the widget area into "panes",
446 which can be resized by the user by clicking and dragging between the subwidgets."""
447
448 _direction = 1
449
450 def __init__(self, possize, panesizes = None, gutter = 8):
451 """panesizes should be a tuple of numbers. The length of the tuple is the number of panes,
452 the items in the tuple are the relative sizes of these panes; these numbers should add up
453 to 1 (the total size of all panes)."""
454 ClickableWidget.__init__(self, possize)
455 self._panesizes = panesizes
456 self._gutter = gutter
457 self._enabled = 1
458 self.setuppanes()
459
460 #def open(self):
461 # self.installbounds()
462 # ClickableWidget.open(self)
463
464 def _calcbounds(self):
465 # hmmm. It should not neccesary be override _calcbounds :-(
466 self.installbounds()
467 Widget._calcbounds(self)
468
469 def setuppanes(self):
470 panesizes = self._panesizes
471 total = 0
472 if panesizes is not None:
473 #if len(self._widgets) <> len(panesizes):
474 # raise TypeError, 'number of widgets does not match number of panes'
475 for panesize in panesizes:
476 if not 0 < panesize < 1:
477 raise TypeError, 'pane sizes must be between 0 and 1, not including.'
478 total = total + panesize
479 if round(total, 4) <> 1.0:
480 raise TypeError, 'pane sizes must add up to 1'
481 else:
482 # XXX does not work!
483 step = 1.0 / len(self._widgets)
484 panesizes = []
485 for i in range(len(self._widgets)):
486 panesizes.append(step)
487 current = 0
488 self._panesizes = []
489 self._gutters = []
490 for panesize in panesizes:
491 if current:
492 self._gutters.append(current)
Jack Jansen34d11f02000-03-07 23:40:13 +0000493 self._panesizes.append((current, current + panesize))
Just van Rossum40f9b7b1999-01-30 22:39:17 +0000494 current = current + panesize
495 self.makepanebounds()
496
497 def getpanesizes(self):
498 return map(lambda (fr, to): to-fr, self._panesizes)
499
500 boundstemplate = "lambda width, height: (0, height * %s + %d, width, height * %s + %d)"
501
502 def makepanebounds(self):
503 halfgutter = self._gutter / 2
504 self._panebounds = []
505 for i in range(len(self._panesizes)):
506 panestart, paneend = self._panesizes[i]
507 boundsstring = self.boundstemplate % (`panestart`, panestart and halfgutter,
508 `paneend`, (paneend <> 1.0) and -halfgutter)
509 self._panebounds.append(eval(boundsstring))
510
511 def installbounds(self):
512 #self.setuppanes()
513 for i in range(len(self._widgets)):
514 w = self._widgets[i]
515 w._possize = self._panebounds[i]
516 #if hasattr(w, "setuppanes"):
517 # w.setuppanes()
518 if hasattr(w, "installbounds"):
519 w.installbounds()
520
521 def rollover(self, point, onoff):
522 if onoff:
523 orgmouse = point[self._direction]
524 halfgutter = self._gutter / 2
525 l, t, r, b = self._bounds
526 if self._direction:
527 begin, end = t, b
528 else:
529 begin, end = l, r
530
531 i = self.findgutter(orgmouse, begin, end)
532 if i is None:
533 SetCursor("arrow")
534 else:
535 SetCursor(self._direction and 'vmover' or 'hmover')
536
537 def findgutter(self, orgmouse, begin, end):
538 tolerance = max(4, self._gutter) / 2
539 for i in range(len(self._gutters)):
540 pos = begin + (end - begin) * self._gutters[i]
541 if abs(orgmouse - pos) <= tolerance:
542 break
543 else:
544 return
545 return i
546
547 def click(self, point, modifiers):
548 # what a mess...
549 orgmouse = point[self._direction]
550 halfgutter = self._gutter / 2
551 l, t, r, b = self._bounds
552 if self._direction:
553 begin, end = t, b
554 else:
555 begin, end = l, r
556
557 i = self.findgutter(orgmouse, begin, end)
558 if i is None:
559 return
560
561 pos = orgpos = begin + (end - begin) * self._gutters[i] # init pos too, for fast click on border, bug done by Petr
562
563 minpos = self._panesizes[i][0]
564 maxpos = self._panesizes[i+1][1]
565 minpos = begin + (end - begin) * minpos + 64
566 maxpos = begin + (end - begin) * maxpos - 64
567 if minpos > orgpos and maxpos < orgpos:
568 return
569
570 #SetCursor("fist")
571 self.SetPort()
572 if self._direction:
573 rect = l, orgpos - 1, r, orgpos
574 else:
575 rect = orgpos - 1, t, orgpos, b
576
577 # track mouse --- XXX move to separate method?
578 Qd.PenMode(QuickDraw.srcXor)
579 Qd.PenPat(Qd.qd.gray)
580 Qd.PaintRect(rect)
581 lastpos = None
582 while Evt.Button():
583 pos = orgpos - orgmouse + Evt.GetMouse()[self._direction]
584 pos = max(pos, minpos)
585 pos = min(pos, maxpos)
586 if pos == lastpos:
587 continue
588 Qd.PenPat(Qd.qd.gray)
589 Qd.PaintRect(rect)
590 if self._direction:
591 rect = l, pos - 1, r, pos
592 else:
593 rect = pos - 1, t, pos, b
594 Qd.PenPat(Qd.qd.gray)
595 Qd.PaintRect(rect)
596 lastpos = pos
597 Qd.PaintRect(rect)
598 Qd.PenNormal()
599 SetCursor("watch")
600
601 newpos = (pos - begin) / float(end - begin)
602 self._gutters[i] = newpos
603 self._panesizes[i] = self._panesizes[i][0], newpos
604 self._panesizes[i+1] = newpos, self._panesizes[i+1][1]
605 self.makepanebounds()
606 self.installbounds()
607 self._calcbounds()
608
609
610class VerticalPanes(HorizontalPanes):
611 """see HorizontalPanes"""
612 _direction = 0
613 boundstemplate = "lambda width, height: (width * %s + %d, 0, width * %s + %d, height)"
614
615
616class ColorPicker(ClickableWidget):
617
618 """Color picker widget. Allows the user to choose a color."""
619
620 def __init__(self, possize, color = (0, 0, 0), callback = None):
621 ClickableWidget.__init__(self, possize)
622 self._color = color
623 self._callback = callback
624 self._enabled = 1
625
626 def click(self, point, modifiers):
627 if not self._enabled:
628 return
629 import ColorPicker
630 newcolor, ok = ColorPicker.GetColor("", self._color)
631 if ok:
632 self._color = newcolor
633 self.SetPort()
634 self.draw()
635 if self._callback:
636 return CallbackCall(self._callback, 0, self._color)
637
638 def set(self, color):
639 self._color = color
640 self.SetPort()
641 self.draw()
642
643 def get(self):
644 return self._color
645
646 def draw(self, visRgn=None):
647 if self._visible:
648 if not visRgn:
649 visRgn = self._parentwindow.wid.GetWindowPort().visRgn
650 Qd.PenPat(Qd.qd.gray)
651 rect = self._bounds
652 Qd.FrameRect(rect)
653 rect = Qd.InsetRect(rect, 3, 3)
654 Qd.PenNormal()
655 Qd.RGBForeColor(self._color)
656 Qd.PaintRect(rect)
657 Qd.RGBForeColor((0, 0, 0))
658
659
660# misc utils
661
662def CallbackCall(callback, mustfit, *args):
663 """internal helper routine for W"""
664 # XXX this function should die.
665 if type(callback) == FunctionType:
666 func = callback
667 maxargs = func.func_code.co_argcount
668 elif type(callback) == MethodType:
669 func = callback.im_func
670 maxargs = func.func_code.co_argcount - 1
671 else:
672 if callable(callback):
673 return apply(callback, args)
674 else:
675 raise TypeError, "uncallable callback object"
676
677 if func.func_defaults:
678 minargs = maxargs - len(func.func_defaults)
679 else:
680 minargs = maxargs
681 if minargs <= len(args) <= maxargs:
682 return apply(callback, args)
683 elif not mustfit and minargs == 0:
684 return callback()
685 else:
686 if mustfit:
687 raise TypeError, "callback accepts wrong number of arguments: " + `len(args)`
688 else:
689 raise TypeError, "callback accepts wrong number of arguments: 0 or " + `len(args)`
690
691
692def HasBaseClass(obj, class_):
693 try:
694 raise obj
695 except class_:
696 return 1
697 except:
698 pass
699 return 0
700
701
702_cursors = {
703 "watch" : Qd.GetCursor(QuickDraw.watchCursor).data,
704 "arrow" : Qd.qd.arrow,
705 "iBeam" : Qd.GetCursor(QuickDraw.iBeamCursor).data,
706 "cross" : Qd.GetCursor(QuickDraw.crossCursor).data,
707 "plus" : Qd.GetCursor(QuickDraw.plusCursor).data,
708 "hand" : Qd.GetCursor(468).data,
709 "fist" : Qd.GetCursor(469).data,
710 "hmover" : Qd.GetCursor(470).data,
711 "vmover" : Qd.GetCursor(471).data,
712 "zoomin" : Qd.GetCursor(472).data,
713 "zoomout" : Qd.GetCursor(473).data,
714 "zoom" : Qd.GetCursor(474).data,
715}
716
717def SetCursor(what):
718 """Set the cursorshape to any of these: 'arrow', 'cross', 'fist', 'hand', 'hmover', 'iBeam',
719 'plus', 'vmover', 'watch', 'zoom', 'zoomin', 'zoomout'."""
720 Qd.SetCursor(_cursors[what])