from Carbon import Qd from Carbon import Win from Carbon import QuickDraw from Carbon import Evt import string from types import * import sys WidgetsError = "WidgetsError" DEBUG = 0 class Widget: """Base class for all widgets.""" _selectable = 0 def __init__(self, possize): self._widgets = [] self._widgetsdict = {} self._possize = possize self._bounds = None self._visible = 1 self._enabled = 0 self._selected = 0 self._activated = 0 self._callback = None self._parent = None self._parentwindow = None self._bindings = {} self._backcolor = None def show(self, onoff): self._visible = onoff for w in self._widgets: w.show(onoff) if self._parentwindow is not None and self._parentwindow.wid is not None: self.SetPort() if onoff: self.draw() else: Qd.EraseRect(self._bounds) def draw(self, visRgn = None): if self._visible: # draw your stuff here pass def getpossize(self): return self._possize def getbounds(self): return self._bounds def move(self, x, y = None): """absolute move""" if y == None: x, y = x if type(self._possize) <> TupleType: raise WidgetsError, "can't move widget with bounds function" l, t, r, b = self._possize self.resize(x, y, r, b) def rmove(self, x, y = None): """relative move""" if y == None: x, y = x if type(self._possize) <> TupleType: raise WidgetsError, "can't move widget with bounds function" l, t, r, b = self._possize self.resize(l + x, t + y, r, b) def resize(self, *args): if len(args) == 1: if type(args[0]) == FunctionType or type(args[0]) == MethodType: self._possize = args[0] else: apply(self.resize, args[0]) elif len(args) == 2: self._possize = (0, 0) + args elif len(args) == 4: self._possize = args else: raise TypeError, "wrong number of arguments" self._calcbounds() def open(self): self._calcbounds() def close(self): del self._callback del self._possize del self._bindings del self._parent del self._parentwindow def bind(self, key, callback): """bind a key or an 'event' to a callback""" if callback: self._bindings[key] = callback elif self._bindings.has_key(key): del self._bindings[key] def adjust(self, oldbounds): self.SetPort() self.GetWindow().InvalWindowRect(oldbounds) self.GetWindow().InvalWindowRect(self._bounds) def _calcbounds(self): # calculate absolute bounds relative to the window origin from our # abstract _possize attribute, which is either a 4-tuple or a callable object oldbounds = self._bounds pl, pt, pr, pb = self._parent._bounds if callable(self._possize): # _possize is callable, let it figure it out by itself: it should return # the bounds relative to our parent widget. width = pr - pl height = pb - pt self._bounds = Qd.OffsetRect(self._possize(width, height), pl, pt) else: # _possize must be a 4-tuple. This is where the algorithm by Peter Kriens and # Petr van Blokland kicks in. (*** Parts of this algorithm are applied for # patents by Ericsson, Sweden ***) l, t, r, b = self._possize # depending on the values of l(eft), t(op), r(right) and b(ottom), # they mean different things: if l < -1: # l is less than -1, this mean it measures from the *right* of it's parent l = pr + l else: # l is -1 or greater, this mean it measures from the *left* of it's parent l = pl + l if t < -1: # t is less than -1, this mean it measures from the *bottom* of it's parent t = pb + t else: # t is -1 or greater, this mean it measures from the *top* of it's parent t = pt + t if r > 1: # r is greater than 1, this means r is the *width* of the widget r = l + r else: # r is less than 1, this means it measures from the *right* of it's parent r = pr + r if b > 1: # b is greater than 1, this means b is the *height* of the widget b = t + b else: # b is less than 1, this means it measures from the *bottom* of it's parent b = pb + b self._bounds = (l, t, r, b) if oldbounds and oldbounds <> self._bounds: self.adjust(oldbounds) for w in self._widgets: w._calcbounds() def test(self, point): if Qd.PtInRect(point, self._bounds): return 1 def click(self, point, modifiers): pass def findwidget(self, point, onlyenabled = 1): if self.test(point): for w in self._widgets: widget = w.findwidget(point) if widget is not None: return widget if self._enabled or not onlyenabled: return self def forall(self, methodname, *args): for w in self._widgets: rv = apply(w.forall, (methodname,) + args) if rv: return rv if self._bindings.has_key("<" + methodname + ">"): callback = self._bindings["<" + methodname + ">"] rv = apply(callback, args) if rv: return rv if hasattr(self, methodname): method = getattr(self, methodname) return apply(method, args) def forall_butself(self, methodname, *args): for w in self._widgets: rv = apply(w.forall, (methodname,) + args) if rv: return rv def forall_frombottom(self, methodname, *args): if self._bindings.has_key("<" + methodname + ">"): callback = self._bindings["<" + methodname + ">"] rv = apply(callback, args) if rv: return rv if hasattr(self, methodname): method = getattr(self, methodname) rv = apply(method, args) if rv: return rv for w in self._widgets: rv = apply(w.forall_frombottom, (methodname,) + args) if rv: return rv def _addwidget(self, key, widget): if widget in self._widgets: raise ValueError, "duplicate widget" if self._widgetsdict.has_key(key): self._removewidget(key) self._widgets.append(widget) self._widgetsdict[key] = widget widget._parent = self self._setparentwindow(widget) if self._parentwindow and self._parentwindow.wid: widget.forall_frombottom("open") self.GetWindow().InvalWindowRect(widget._bounds) def _setparentwindow(self, widget): widget._parentwindow = self._parentwindow for w in widget._widgets: self._setparentwindow(w) def _removewidget(self, key): if not self._widgetsdict.has_key(key): raise KeyError, "no widget with key " + `key` widget = self._widgetsdict[key] for k in widget._widgetsdict.keys(): widget._removewidget(k) if self._parentwindow._currentwidget == widget: widget.select(0) self._parentwindow._currentwidget = None self.SetPort() self.GetWindow().InvalWindowRect(widget._bounds) widget.close() del self._widgetsdict[key] self._widgets.remove(widget) def __setattr__(self, attr, value): if type(value) == InstanceType and isinstance(value, Widget) and \ attr not in ("_currentwidget", "_lastrollover", "_parent", "_parentwindow", "_defaultbutton"): if hasattr(self, attr): raise ValueError, "Can't replace existing attribute: " + attr self._addwidget(attr, value) self.__dict__[attr] = value def __delattr__(self, attr): if attr == "_widgetsdict": raise AttributeError, "cannot delete attribute _widgetsdict" if self._widgetsdict.has_key(attr): self._removewidget(attr) if self.__dict__.has_key(attr): del self.__dict__[attr] elif self.__dict__.has_key(attr): del self.__dict__[attr] else: raise AttributeError, attr def __setitem__(self, key, value): self._addwidget(key, value) def __getitem__(self, key): if not self._widgetsdict.has_key(key): raise KeyError, key return self._widgetsdict[key] def __delitem__(self, key): self._removewidget(key) def SetPort(self): self._parentwindow.SetPort() def GetWindow(self): return self._parentwindow.GetWindow() def __del__(self): if DEBUG: print "%s instance deleted" % self.__class__.__name__ def _drawbounds(self): Qd.FrameRect(self._bounds) class ClickableWidget(Widget): """Base class for clickable widgets. (note: self._enabled must be true to receive click events.)""" def click(self, point, modifiers): pass def enable(self, onoff): self._enabled = onoff self.SetPort() self.draw() def callback(self): if self._callback: return CallbackCall(self._callback, 1) class SelectableWidget(ClickableWidget): """Base class for selectable widgets.""" _selectable = 1 def select(self, onoff, isclick = 0): if onoff == self._selected: return 1 if self._bindings.has_key(""] if callback(onoff): return 1 self._selected = onoff if onoff: if self._parentwindow._currentwidget is not None: self._parentwindow._currentwidget.select(0) self._parentwindow._currentwidget = self else: self._parentwindow._currentwidget = None def key(self, char, event): pass def drawselframe(self, onoff): if not self._parentwindow._hasselframes: return thickrect = Qd.InsetRect(self._bounds, -3, -3) state = Qd.GetPenState() Qd.PenSize(2, 2) if onoff: Qd.PenPat(Qd.qd.black) else: Qd.PenPat(Qd.qd.white) Qd.FrameRect(thickrect) Qd.SetPenState(state) def adjust(self, oldbounds): self.SetPort() if self._selected: self.GetWindow().InvalWindowRect(Qd.InsetRect(oldbounds, -3, -3)) self.GetWindow().InvalWindowRect(Qd.InsetRect(self._bounds, -3, -3)) else: self.GetWindow().InvalWindowRect(oldbounds) self.GetWindow().InvalWindowRect(self._bounds) class _Line(Widget): def __init__(self, possize, thickness = 1): Widget.__init__(self, possize) self._thickness = thickness def open(self): self._calcbounds() self.SetPort() self.draw() def draw(self, visRgn = None): if self._visible: Qd.PaintRect(self._bounds) def _drawbounds(self): pass class HorizontalLine(_Line): def _calcbounds(self): Widget._calcbounds(self) l, t, r, b = self._bounds self._bounds = l, t, r, t + self._thickness class VerticalLine(_Line): def _calcbounds(self): Widget._calcbounds(self) l, t, r, b = self._bounds self._bounds = l, t, l + self._thickness, b class Frame(Widget): def __init__(self, possize, pattern = Qd.qd.black, color = (0, 0, 0)): Widget.__init__(self, possize) self._framepattern = pattern self._framecolor = color def setcolor(self, color): self._framecolor = color self.SetPort() self.draw() def setpattern(self, pattern): self._framepattern = pattern self.SetPort() self.draw() def draw(self, visRgn = None): if self._visible: penstate = Qd.GetPenState() Qd.PenPat(self._framepattern) Qd.RGBForeColor(self._framecolor) Qd.FrameRect(self._bounds) Qd.RGBForeColor((0, 0, 0)) Qd.SetPenState(penstate) def _darkencolor((r, g, b)): return 0.75 * r, 0.75 * g, 0.75 * b class BevelBox(Widget): """'Platinum' beveled rectangle.""" def __init__(self, possize, color = (0xe000, 0xe000, 0xe000)): Widget.__init__(self, possize) self._color = color self._darkercolor = _darkencolor(color) def setcolor(self, color): self._color = color self.SetPort() self.draw() def draw(self, visRgn = None): if self._visible: l, t, r, b = Qd.InsetRect(self._bounds, 1, 1) Qd.RGBForeColor(self._color) Qd.PaintRect((l, t, r, b)) Qd.RGBForeColor(self._darkercolor) Qd.MoveTo(l, b) Qd.LineTo(r, b) Qd.LineTo(r, t) Qd.RGBForeColor((0, 0, 0)) class Group(Widget): """A container for subwidgets""" class HorizontalPanes(Widget): """Panes, a.k.a. frames. Works a bit like a group. Devides the widget area into "panes", which can be resized by the user by clicking and dragging between the subwidgets.""" _direction = 1 def __init__(self, possize, panesizes = None, gutter = 8): """panesizes should be a tuple of numbers. The length of the tuple is the number of panes, the items in the tuple are the relative sizes of these panes; these numbers should add up to 1 (the total size of all panes).""" ClickableWidget.__init__(self, possize) self._panesizes = panesizes self._gutter = gutter self._enabled = 1 self.setuppanes() #def open(self): # self.installbounds() # ClickableWidget.open(self) def _calcbounds(self): # hmmm. It should not neccesary be override _calcbounds :-( self.installbounds() Widget._calcbounds(self) def setuppanes(self): panesizes = self._panesizes total = 0 if panesizes is not None: #if len(self._widgets) <> len(panesizes): # raise TypeError, 'number of widgets does not match number of panes' for panesize in panesizes: if not 0 < panesize < 1: raise TypeError, 'pane sizes must be between 0 and 1, not including.' total = total + panesize if round(total, 4) <> 1.0: raise TypeError, 'pane sizes must add up to 1' else: # XXX does not work! step = 1.0 / len(self._widgets) panesizes = [] for i in range(len(self._widgets)): panesizes.append(step) current = 0 self._panesizes = [] self._gutters = [] for panesize in panesizes: if current: self._gutters.append(current) self._panesizes.append((current, current + panesize)) current = current + panesize self.makepanebounds() def getpanesizes(self): return map(lambda (fr, to): to-fr, self._panesizes) boundstemplate = "lambda width, height: (0, height * %s + %d, width, height * %s + %d)" def makepanebounds(self): halfgutter = self._gutter / 2 self._panebounds = [] for i in range(len(self._panesizes)): panestart, paneend = self._panesizes[i] boundsstring = self.boundstemplate % (`panestart`, panestart and halfgutter, `paneend`, (paneend <> 1.0) and -halfgutter) self._panebounds.append(eval(boundsstring)) def installbounds(self): #self.setuppanes() for i in range(len(self._widgets)): w = self._widgets[i] w._possize = self._panebounds[i] #if hasattr(w, "setuppanes"): # w.setuppanes() if hasattr(w, "installbounds"): w.installbounds() def rollover(self, point, onoff): if onoff: orgmouse = point[self._direction] halfgutter = self._gutter / 2 l, t, r, b = self._bounds if self._direction: begin, end = t, b else: begin, end = l, r i = self.findgutter(orgmouse, begin, end) if i is None: SetCursor("arrow") else: SetCursor(self._direction and 'vmover' or 'hmover') def findgutter(self, orgmouse, begin, end): tolerance = max(4, self._gutter) / 2 for i in range(len(self._gutters)): pos = begin + (end - begin) * self._gutters[i] if abs(orgmouse - pos) <= tolerance: break else: return return i def click(self, point, modifiers): # what a mess... orgmouse = point[self._direction] halfgutter = self._gutter / 2 l, t, r, b = self._bounds if self._direction: begin, end = t, b else: begin, end = l, r i = self.findgutter(orgmouse, begin, end) if i is None: return pos = orgpos = begin + (end - begin) * self._gutters[i] # init pos too, for fast click on border, bug done by Petr minpos = self._panesizes[i][0] maxpos = self._panesizes[i+1][1] minpos = begin + (end - begin) * minpos + 64 maxpos = begin + (end - begin) * maxpos - 64 if minpos > orgpos and maxpos < orgpos: return #SetCursor("fist") self.SetPort() if self._direction: rect = l, orgpos - 1, r, orgpos else: rect = orgpos - 1, t, orgpos, b # track mouse --- XXX move to separate method? Qd.PenMode(QuickDraw.srcXor) Qd.PenPat(Qd.qd.gray) Qd.PaintRect(rect) lastpos = None while Evt.Button(): pos = orgpos - orgmouse + Evt.GetMouse()[self._direction] pos = max(pos, minpos) pos = min(pos, maxpos) if pos == lastpos: continue Qd.PenPat(Qd.qd.gray) Qd.PaintRect(rect) if self._direction: rect = l, pos - 1, r, pos else: rect = pos - 1, t, pos, b Qd.PenPat(Qd.qd.gray) Qd.PaintRect(rect) lastpos = pos Qd.PaintRect(rect) Qd.PenNormal() SetCursor("watch") newpos = (pos - begin) / float(end - begin) self._gutters[i] = newpos self._panesizes[i] = self._panesizes[i][0], newpos self._panesizes[i+1] = newpos, self._panesizes[i+1][1] self.makepanebounds() self.installbounds() self._calcbounds() class VerticalPanes(HorizontalPanes): """see HorizontalPanes""" _direction = 0 boundstemplate = "lambda width, height: (width * %s + %d, 0, width * %s + %d, height)" class ColorPicker(ClickableWidget): """Color picker widget. Allows the user to choose a color.""" def __init__(self, possize, color = (0, 0, 0), callback = None): ClickableWidget.__init__(self, possize) self._color = color self._callback = callback self._enabled = 1 def click(self, point, modifiers): if not self._enabled: return import ColorPicker newcolor, ok = ColorPicker.GetColor("", self._color) if ok: self._color = newcolor self.SetPort() self.draw() if self._callback: return CallbackCall(self._callback, 0, self._color) def set(self, color): self._color = color self.SetPort() self.draw() def get(self): return self._color def draw(self, visRgn=None): if self._visible: if not visRgn: visRgn = self._parentwindow.wid.GetWindowPort().visRgn Qd.PenPat(Qd.qd.gray) rect = self._bounds Qd.FrameRect(rect) rect = Qd.InsetRect(rect, 3, 3) Qd.PenNormal() Qd.RGBForeColor(self._color) Qd.PaintRect(rect) Qd.RGBForeColor((0, 0, 0)) # misc utils def CallbackCall(callback, mustfit, *args): """internal helper routine for W""" # XXX this function should die. if type(callback) == FunctionType: func = callback maxargs = func.func_code.co_argcount elif type(callback) == MethodType: func = callback.im_func maxargs = func.func_code.co_argcount - 1 else: if callable(callback): return apply(callback, args) else: raise TypeError, "uncallable callback object" if func.func_defaults: minargs = maxargs - len(func.func_defaults) else: minargs = maxargs if minargs <= len(args) <= maxargs: return apply(callback, args) elif not mustfit and minargs == 0: return callback() else: if mustfit: raise TypeError, "callback accepts wrong number of arguments: " + `len(args)` else: raise TypeError, "callback accepts wrong number of arguments: 0 or " + `len(args)` def HasBaseClass(obj, class_): try: raise obj except class_: return 1 except: pass return 0 _cursors = { "watch" : Qd.GetCursor(QuickDraw.watchCursor).data, "arrow" : Qd.qd.arrow, "iBeam" : Qd.GetCursor(QuickDraw.iBeamCursor).data, "cross" : Qd.GetCursor(QuickDraw.crossCursor).data, "plus" : Qd.GetCursor(QuickDraw.plusCursor).data, "hand" : Qd.GetCursor(468).data, "fist" : Qd.GetCursor(469).data, "hmover" : Qd.GetCursor(470).data, "vmover" : Qd.GetCursor(471).data, "zoomin" : Qd.GetCursor(472).data, "zoomout" : Qd.GetCursor(473).data, "zoom" : Qd.GetCursor(474).data, } def SetCursor(what): """Set the cursorshape to any of these: 'arrow', 'cross', 'fist', 'hand', 'hmover', 'iBeam', 'plus', 'vmover', 'watch', 'zoom', 'zoomin', 'zoomout'.""" Qd.SetCursor(_cursors[what])