1# piddleQD.py -- a QuickDraw backend for PIDDLE
2# Copyright (C) 1999  Joseph J. Strout
3#
4# This library is free software; you can redistribute it and/or
5# modify it under the terms of the GNU Lesser General Public
6# License as published by the Free Software Foundation; either
7# version 2 of the License, or (at your option) any later version.
8#
9# This library is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12# Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public
15# License along with this library; if not, write to the Free Software
16# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
17
18"""QDCanvas
19
20This class implements a PIDDLE Canvas object that draws using QuickDraw
21(the MacOS drawing API) into an IDE window.
22
23Joe Strout (joe@strout.net), September 1999
24"""
25
26# Revisions:
27#
28#	9/20/99 JJS: added interactive methods and info line
29#
30#   6/14/99 JJS: added support for Copy command
31
32# Implementation notes:
33#	Each QDCanvas maintains a Picture, and uses this for refreshing.
34#	This means that drawing is not visible until it is flushed, but
35#	flushing is expensive, as the entire picture must be redrawn (to
36#	reopen it for further drawing).
37#
38#	The line color is stored in the QD fore color, and the fill color
39#	is stored in the back color -- fills are actually done by erasing.
40
41from piddle import *
42import Qd
43import QuickDraw
44import Scrap
45import W
46import Fonts
47import Events
48import Evt
49import string
50from types import *
51
52# utility functions
53def _setForeColor(c):
54	"Set the QD fore color from a piddle color."
55	Qd.RGBForeColor( (c.red*65535, c.green*65535, c.blue*65535) )
56
57def _setBackColor(c):
58	"Set the QD background color from a piddle color."
59	Qd.RGBBackColor( (c.red*65535, c.green*65535, c.blue*65535) )
60
61# global -- which QDCanvas has the current port
62_curCanvas = None
63
64# global dictionary mapping font names to QD font IDs
65_fontMap = {}
66for item in filter(lambda x:x[0]!='_',dir(Fonts)):
67	_fontMap[string.lower(item)] = Fonts.__dict__[item]
68_fontMap['system'] = Fonts.kFontIDGeneva
69_fontMap['monospaced'] = Fonts.kFontIDMonaco
70_fontMap['serif'] = Fonts.kFontIDNewYork
71_fontMap['sansserif'] = Fonts.kFontIDGeneva
72
73# utility classes
74class _PortSaver:
75
76	def __init__(self, qdcanvas):
77		self.port = Qd.GetPort()
78		Qd.SetPort(qdcanvas._window.wid)
79
80	def __del__(self):
81		Qd.SetPort(self.port)
82
83class _QDCanvasWindow(W.Window):
84	"This internally-used class implements the window in which QDCanvas draws."
85
86	def __init__(self, owner, size=(300,300), title="Graphics"):
87		self.owner = owner
88		size = (size[0], size[1]+16)			# leave room for info line!
89		W.Window.__init__(self, size, title)
90		self.infoline = ''
91		self.open()
92		self.lastMouse = (-1,-1)
93
94	def close(self):
95		try: self.owner._noteWinClosed(self)
96		except: pass
97		W.Window.close(self)
98
99	def domenu_copy(self, *args):
100		r = self._bounds
101		pict = Qd.OpenPicture(r)
102		self.owner._drawWindow()
103		Qd.ClosePicture()
104		Scrap.ZeroScrap()
105		Scrap.PutScrap('PICT',pict.data)
106
107	def do_update(self, window, event):
108		# draw the content
109		try: self.owner._drawWindow()
110		except: pass
111
112		# draw the info line
113		self.drawInfoLine()
114
115	def drawInfoLine(self):
116		Qd.ForeColor(QuickDraw.blackColor)
117		Qd.BackColor(QuickDraw.whiteColor)
118		bounds = self.getbounds()
119		width = bounds[2] - bounds[0]
120		height = bounds[3] - bounds[1]
121		Qd.MoveTo( 0, height-16 )
122		Qd.LineTo( width, height-16 )
123		r = (0,height-15,width,height)
124		Qd.EraseRect(r)
125		Qd.TextFont(Fonts.kFontIDGeneva)
126		Qd.TextSize(9)
127		Qd.TextFace(0)
128		Qd.MoveTo( 8, height-6 )
129		Qd.DrawString(self.infoline)
130
131	def do_activate(self, activate, event):
132		global _curCanvas
133		if _curCanvas == self.owner and not activate:
134			#print self.owner, "is no longer current"
135			_curCanvas = None
136
137	def do_contentclick(self, point, modifiers, event):
138		if self.owner: self.owner.onClick( self.owner, point[0], point[1] )
139
140	def do_char(self, char, event):
141		import Wkeys
142		(what, message, when, where, modifiers) = event
143		mods = []
144		if modifiers & Events.shiftKey: mods.append( modShift )
145		if modifiers & Events.controlKey: mods.append( modControl )
146		if self.owner: self.owner.onKey( self.owner, char, mods )
147		W.Window.do_char(self, char, event)
148
149	def idle(self, *args):
150		if self.owner:
151			bounds = self.getbounds()
152			x,y = mouse = Evt.GetMouse()
153			maxx, maxy = self.owner.size
154			if mouse != self.lastMouse and x >= 0 and x < maxx \
155					and y >= 0 and y < maxy:
156				self.owner.onOver( self.owner, x,y )
157				self.lastMouse = mouse
158		W.Window.idle(self,args)
159
160class QDCanvas( Canvas ):
161
162	def __init__(self, size=(300,300), name="Graphics"):
163		"Initialize QuickDraw canvas with window size and title."
164		self._window = _QDCanvasWindow(self, size, name)
165		self._port = Qd.GetPort()
166		Canvas.__init__(self, size, name)
167		self._penstate = Qd.GetPenState()
168		self.picture = Qd.OpenPicture(self._window._bounds)
169		self.picopen = 1
170
171		self.patch = 0		# PATCH just for testing!
172
173	#----------- custom QDCanvas methods -----------
174
175	def __setattr__(self, attribute, value):
176		self.__dict__[attribute] = value
177		if attribute == "defaultLineColor":
178			self._window.SetPort()
179			_setForeColor(self.defaultLineColor)
180		elif attribute == "defaultLineWidth":
181			self._window.SetPort()
182			Qd.PenSize(value,value)
183			self._penstate = Qd.GetPenState()
184		elif attribute == "defaultFont":
185			self._window.SetPort()
186			self._setFont(value)
187
188	def __del__(self):
189		#print "Deleting", self
190		try: self._window.close()
191		except: pass
192		self._window = None
193		Qd.KillPicture(self.picture)
194
195	def _noteWinClosed(self, win):
196		"Note that our window has been closed."
197		self._window = None
198
199	def _drawWindow(self):
200		"Update the drawing in the window."
201		if not hasattr(self,'picture'): return
202		if self.picopen:
203			self.flush()
204		else:
205			portsaver = _PortSaver(self)
206			Qd.DrawPicture(self.picture, self._window._bounds)
207
208	def _prepareToDraw(self):
209		global _curCanvas
210
211		# open the picture, if not already open
212		if not self.picopen:
213			portsaver = _PortSaver(self)
214			temp = Qd.OpenPicture(self._window._bounds)
215			Qd.DrawPicture( self.picture, self._window._bounds)
216			self.picture = temp
217			self.picopen = 1
218
219		# and set the default drawing parameters, if we weren't the default before
220		if Qd.GetPort() != self._port: # _curCanvas != self:
221			#print "setting port to", self
222			self._window.SetPort()
223			_setForeColor(self.defaultLineColor)
224			_setBackColor(self.defaultFillColor)
225			Qd.SetPenState(self._penstate)
226			self.patch = self.patch + 1
227			_curCanvas = self
228
229	def _setFont(self, font):
230		global _fontMap
231		if not font.face:
232			Qd.TextFont(Fonts.applFont)
233		elif hasattr(font,'_QDfontID'):
234			Qd.TextFont(font._QDfontID)
235		else:
236			if type(font.face) == StringType:
237				try: fontID = _fontMap[string.lower(font.face)]
238				except:
239					return 0	# font not found!
240			else:
241				for item in font.face:
242					fontID = None
243					try:
244						fontID = _fontMap[string.lower(item)]
245						break
246					except: pass
247				if fontID == None:
248					return 0	# font not found!
249
250			# cache the fontID for quicker reference next time!
251                        font.__dict__['_QDfontID'] = fontID
252			# font._QDfontID = fontID
253			Qd.TextFont(fontID)
254
255		# now, set the size and style as well!
256		Qd.TextSize(font.size)
257		stylecode = QuickDraw.bold * font.bold + \
258					QuickDraw.italic * font.italic + \
259					QuickDraw.underline * font.underline
260		Qd.TextFace( stylecode)
261		return 1
262
263	def close(self):
264		self._window.close()
265		self._window = None
266
267	#------------ canvas capabilities -------------
268	def isInteractive(self):
269		"Returns 1 if onClick, onOver, and onKey events are possible, 0 otherwise."
270		return 1
271
272	def canUpdate(self):
273		"Returns 1 if the drawing can be meaningfully updated over time \
274		(e.g., screen graphics), 0 otherwise (e.g., drawing to a file)."
275		# since we can update, but it gets progressively more expensive
276		# until you call clear(), we'll return 0.5 for canUpdate
277		return 0.5
278
279	#------------ general management -------------
280
281	def clear(self, andFlush=1):
282		"Call this to clear and reset the graphics context."
283		# in addition to clearing the screen, also reset our picture
284		portsaver = _PortSaver(self)
285		if self.picopen:
286			Qd.ClosePicture()
287		self.picture = Qd.OpenPicture(self._window._bounds)
288		self.picopen = 1
289		self._prepareToDraw()
290		_setBackColor(white)
291		Qd.EraseRect( self._window._bounds )
292		_setBackColor(self.defaultFillColor)
293		if andFlush: self.flush()		# by default, we flush upon clear
294
295	def flush(self):
296		"Call this when done with drawing, to indicate that the drawing \
297		should be printed/saved/blasted to screen etc."
298		if not self.picopen: return
299		portsaver = _PortSaver(self)
300		Qd.ClosePicture()
301		Qd.DrawPicture(self.picture, self._window._bounds)
302		self.picopen = 0
303
304	def setInfoLine(self, s):
305		"For interactive Canvases, displays the given string in the \
306		'info line' somewhere where the user can probably see it."
307		if self._window:
308			portsaver = _PortSaver(self)
309			self._window.infoline = str(s)
310			self._window.drawInfoLine()
311
312	#------------ string/font info ------------
313	def stringWidth(self, s, font=None):
314		"Return the logical width of the string if it were drawn \
315		in the current font (defaults to self.font)."
316		portsaver = _PortSaver(self)
317		self._prepareToDraw()
318		if font: self._setFont(font)
319		return Qd.StringWidth(s)
320
321	def fontHeight(self, font=None):
322		"Find the line height of the given font."
323		portsaver = _PortSaver(self)
324		self._prepareToDraw()
325		if font: self._setFont(font)
326		fontinfo = Qd.GetFontInfo()
327		return fontinfo[0] + fontinfo[1] + fontinfo[3]
328
329	def fontAscent(self, font=None):
330		"Find the ascent (height above base) of the given font."
331		portsaver = _PortSaver(self)
332		self._prepareToDraw()
333		if font: self._setFont(font)
334		return Qd.GetFontInfo()[0]
335
336	def fontDescent(self, font=None):
337		"Find the descent (extent below base) of the given font."
338		portsaver = _PortSaver(self)
339		self._prepareToDraw()
340		if font: self._setFont(font)
341		return Qd.GetFontInfo()[1]
342
343	#------------- drawing methods --------------
344
345	# Note default parameters "=None" means use the defaults
346	# set in the Canvas method: defaultLineColor, etc.
347
348	def drawLine(self, x1,y1, x2,y2, color=None, width=None):
349		"Draw a straight line between x1,y1 and x2,y2."
350		portsaver = _PortSaver(self)
351		self._prepareToDraw()
352		if color:
353			if color == transparent: return
354			_setForeColor(color)
355		elif self.defaultLineColor == transparent: return
356		if width!=None:
357			Qd.PenSize(width, width)
358			hw = (width-1)/2
359		else: hw = (self.defaultLineWidth-1)/2
360		if hw:
361			# adjust so that thick lines are centered on the given coordinates!
362			x1 = x1-hw
363			x2 = x2-hw
364			y1 = y1-hw
365			y2 = y2-hw
366		Qd.MoveTo( x1,y1 )	# later: handle scaling!
367		Qd.LineTo( x2,y2 )
368		if color:
369			_setForeColor(self.defaultLineColor)
370		if width: Qd.SetPenState(self._penstate)
371
372	def drawRect(self, x1,y1, x2,y2, edgeColor=None, edgeWidth=None, fillColor=None):
373		"Draw the rectangle between x1,y1, and x2,y2. \
374		These should have x1<x2 and y1<y2."
375		portsaver = _PortSaver(self)
376		self._prepareToDraw()
377		# first, draw the fill (if any)
378		if fillColor:
379			if fillColor != transparent:
380				_setBackColor(fillColor)
381				Qd.EraseRect( (x1,y1, x2,y2) )
382		else:
383			if self.defaultFillColor != transparent:
384				Qd.EraseRect( (x1,y1, x2,y2) )
385		# then, draw the frame
386		if edgeColor:
387			if edgeColor == transparent: return
388			_setForeColor(edgeColor)
389		elif self.defaultLineColor == transparent: return
390		if edgeWidth:
391			Qd.PenSize(edgeWidth, edgeWidth)
392			hw = (edgeWidth-1)/2
393		else: hw = (self.defaultLineWidth-1)/2
394		if hw:
395			# adjust so that thick lines are centered on the given coordinates!
396			x1 = x1-hw
397			x2 = x2+hw
398			y1 = y1-hw
399			y2 = y2+hw
400		Qd.FrameRect( (x1,y1, x2+1,y2+1) )
401		if edgeColor:
402			_setForeColor(self.defaultLineColor)
403		if edgeWidth: Qd.SetPenState(self._penstate)
404
405	def drawEllipse(self, x1,y1, x2,y2, edgeColor=None, edgeWidth=None, fillColor=None):
406		"Draw an orthogonal ellipse inscribed within the rectangle x1,y1,x2,y2. \
407		These should have x1<x2 and y1<y2."
408		portsaver = _PortSaver(self)
409		self._prepareToDraw()
410		# first, draw the fill (if any)
411		if fillColor:
412			if fillColor != transparent:
413				_setBackColor(fillColor)
414				Qd.EraseOval( (x1,y1, x2,y2) )
415		elif self.defaultFillColor != transparent:
416			Qd.EraseOval( (x1,y1, x2,y2) )
417		# then, draw the frame
418		if edgeColor:
419			if edgeColor == transparent: return
420			_setForeColor(edgeColor)
421		elif self.defaultLineColor == transparent: return
422		if edgeWidth:
423			Qd.PenSize(edgeWidth, edgeWidth)
424			hw = (edgeWidth-1)/2
425		else: hw = (self.defaultLineWidth-1)/2
426		if hw:
427			# adjust so that thick lines are centered on the given coordinates!
428			x1 = x1-hw
429			x2 = x2+hw
430			y1 = y1-hw
431			y2 = y2+hw
432		Qd.FrameOval( (x1,y1, x2+1,y2+1) )
433		if edgeColor:
434			_setForeColor(self.defaultLineColor)
435		if edgeWidth: Qd.SetPenState(self._penstate)
436
437	def drawArc(self, x1,y1, x2,y2, startAng=0, extent=360,
438			    edgeColor=None, edgeWidth=None, fillColor=None):
439		"Draw a partial oval inscribed within the rectangle x1,y1,x2,y2, \
440		starting at startAng degrees and covering extent degrees (counterclockwise). \
441		These should have x1<x2, y1<y2, and angle1 < angle2."
442		portsaver = _PortSaver(self)
443		self._prepareToDraw()
444		# first, draw the fill (if any)
445		if fillColor:
446			if fillColor != transparent:
447				_setBackColor(fillColor)
448				Qd.EraseArc( (x1,y1, x2,y2), 90-startAng, -extent )
449		elif self.defaultFillColor != transparent:
450			Qd.EraseOval( (x1,y1, x2,y2), 90-startAng, -extent )
451		# then, draw the frame
452		if edgeColor:
453			if edgeColor == transparent: return
454			_setForeColor(edgeColor)
455		elif self.defaultLineColor == transparent: return
456		if edgeWidth:
457			Qd.PenSize(edgeWidth, edgeWidth)
458			hw = (edgeWidth-1)/2
459		else: hw = (self.defaultLineWidth-1)/2
460		if hw:
461			# adjust so that thick lines are centered on the given coordinates!
462			x1 = x1-hw
463			x2 = x2+hw
464			y1 = y1-hw
465			y2 = y2+hw
466		Qd.FrameArc( (x1,y1, x2+1,y2+1), 90-startAng, -extent )
467		if edgeColor:
468			_setForeColor(self.defaultLineColor)
469		if edgeWidth: Qd.SetPenState(self._penstate)
470
471
472	def drawPolygon(self, pointlist,
473				edgeColor=None, edgeWidth=None, fillColor=None, closed=0):
474		"""drawPolygon(pointlist) -- draws a polygon
475		pointlist: a list of (x,y) tuples defining vertices
476		"""
477		portsaver = _PortSaver(self)
478		self._prepareToDraw()
479		polygon = Qd.OpenPoly()
480		filling = 0
481		if fillColor:
482			if fillColor != transparent:
483				_setBackColor(fillColor)
484				filling = 1
485		elif self.defaultFillColor != transparent:
486			filling = 1
487
488		Qd.MoveTo(pointlist[0][0], pointlist[0][1])
489		for p in pointlist[1:]:
490			Qd.LineTo(p[0],p[1])
491
492		Qd.ClosePoly()
493		if filling:
494			Qd.ErasePoly(polygon)
495			if fillColor:
496				_setBackColor(self.defaultFillColor)
497
498		if edgeColor:
499			if edgeColor == transparent: return
500			_setForeColor(edgeColor)
501		elif self.defaultLineColor == transparent: return
502		if edgeWidth: Qd.PenSize(edgeWidth, edgeWidth)
503		Qd.FramePoly(polygon)
504		if closed:
505			Qd.MoveTo( pointlist[0][0], pointlist[0][1] )
506			Qd.LineTo( pointlist[-1][0], pointlist[-1][1] )
507		if edgeColor:
508			_setForeColor(self.defaultLineColor)
509		if edgeWidth: Qd.SetPenState(self._penstate)
510
511
512
513	def drawString(self, s, x,y, font=None, color=None, angle=0):
514		"Draw a string starting at location x,y."
515		if '\n' in s or '\r' in s:
516			self.drawMultiLineString(s, x,y, font, color, angle)
517			return
518		portsaver = _PortSaver(self)
519		self._prepareToDraw()
520		if font: self._setFont(font)
521		if color:
522			if color == transparent: return
523			_setForeColor(color)
524		elif self.defaultLineColor == transparent: return
525		Qd.MoveTo( x,y )
526		if angle:
527			import QDRotate
528			QDRotate.DrawRotatedString(s,angle)
529		else:
530			Qd.DrawString(s)
531		if font: self._setFont(self.defaultFont)
532		if color:
533			_setForeColor(self.defaultLineColor)
534
535
536	def drawImage(self, image, x1,y1, x2=None,y2=None):
537		"""Draw a PIL Image into the specified rectangle.  If x2 and y2 are
538		omitted, they are calculated from the image size."""
539
540		from PixMapWrapper import PixMapWrapper
541		pm = PixMapWrapper()	# make a QD pixel map
542		pm.fromImage(image)		# load the image into it
543		if x2==None: x2 = x1 + pm.bounds[2]-pm.bounds[0]
544		if y2==None: y2 = y1 + pm.bounds[3]-pm.bounds[1]
545		self._prepareToDraw()
546		Qd.ForeColor(QuickDraw.blackColor)
547		Qd.BackColor(QuickDraw.whiteColor)
548		pm.blit(x1,y1,x2,y2, self._port)
549		_setForeColor(self.defaultLineColor)
550
551
552#-------------------------------------------------------------------------
553
554def test():
555	global canvas
556
557	# testing...
558	try:
559		canvas.close()
560	except: pass
561
562	canvas = QDCanvas()
563	canvas.defaultLineColor = Color(0.7,0.7,1.0)	# light blue
564
565	#import macfs
566	#fsspec, ok = macfs.PromptGetFile("Image File:")
567	#if not ok: return
568	#path = fsspec.as_pathname()
569	#import Image
570	#canvas.drawImage( Image.open(path), 0,0,300,300 );
571
572	def myOnClick(canvas,x,y): print "clicked %s,%s" % (x,y)
573	canvas.onClick = myOnClick
574
575	def myOnOver(canvas,x,y): canvas.setInfoLine( "mouse is over %s,%s" % (x,y) )
576
577	canvas.onOver = myOnOver
578
579	def myOnKey(canvas,key,mods): print "pressed %s with modifiers %s" % (key,mods)
580	canvas.onKey = myOnKey
581
582
583	canvas.drawLines( map(lambda i:(i*10,0,i*10,300), range(30)) )
584	canvas.drawLines( map(lambda i:(0,i*10,300,i*10), range(30)) )
585	canvas.defaultLineColor = black
586
587	canvas.drawLine(10,200, 20,190, color=red)
588	canvas.drawEllipse( 130,30, 200,100, fillColor=yellow, edgeWidth=4 )
589
590	canvas.drawArc( 130,30, 200,100, 45,50, fillColor=blue, edgeColor=navy, edgeWidth=4 )
591
592	canvas.defaultLineWidth = 4
593	canvas.drawRoundRect( 30,30, 100,100, fillColor=blue, edgeColor=maroon )
594	canvas.drawCurve( 20,20, 100,50, 50,100, 160,160 )
595
596	canvas.drawString("This is a test!", 30,130, Font(face="newyork",size=16,bold=1),
597			color=green, angle=-45)
598
599	polypoints = [ (160,120), (130,190), (210,145), (110,145), (190,190) ]
600	canvas.drawPolygon(polypoints, fillColor=lime, edgeColor=red, edgeWidth=3, closed=1)
601
602	canvas.drawRect( 200,200,260,260, edgeColor=yellow, edgeWidth=5 )
603	canvas.drawLine( 200,260,260,260, color=green, width=5 )
604	canvas.drawLine( 260,200,260,260, color=red, width=5 )
605
606
607	canvas.flush()
608
609
610#test()
611
612