from Carbon import App, Evt, Qd, QuickDraw, Win
import string
from types import *
import sys

class WidgetsError(Exception): pass

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("<select>"):
			callback = self._bindings["<select>"]
			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
		App.DrawThemeFocusRect(self._bounds, onoff)
	
	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)."""
		Widget.__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.QDFlushPortBuffer(self._parentwindow.wid, None)
			Evt.WaitNextEvent(0, 3)
		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])