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