"""cs1graphics.py Copyright 2007, David Letscher and Michael H. Goldwasser Go to www.cs1graphics.org for more information. Version 1.0: 12/19/2007 Version 1.0.1: 03/30/2010 (bug fix in Text.getFontSize, Path.addPoint) """ # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # Configuration options _debug = 0 # can be 0, 1, 2 or 3 import copy as _copy import math as _math import random as _random import Queue as _Queue import time as _time import threading as _threading import thread as _thread import atexit as _atexit try: set # Python 2.4 or later except: try: from sets import Set as set # Python 2.3 compatibility except: raise RuntimeError('Cannot find set class; Use Python 2.3 or later') _ourRandom = _random.Random() _ourRandom.seed(1234) # initialize the random seed so that treatment of equal depths is reproducible # Beginning of library _underlyingLibrary = None class _GraphicsManager: def __init__(self): self._running = True # Synchornization mechanisms self._commandQueue = _Queue.Queue() self._releaseQueue = _Queue.Queue() # For blocking commands on the queue self._eventQueue = _Queue.Queue() self._refreshLock = _threading.Lock() # Underlying objects self._underlyingObject = dict() # Key is chain, value is a renderedDrawable self._currentChain = tuple() # Current chain of objects, each item in tuple is pair: (id, callingFunction) self._currentCanvas = None self._transformChain = tuple() self._transform = dict() # Key = chain, value = transforms self._renderOrder = dict() self._canvases = dict() # Key = drawble, value = set of canvases self._chains = dict() # Key = (drawable, canvas), value = set of chains self._openCanvases = set() # Event handling self._handlers = dict() # Key = drawable or canvas, value = callback object # Mouse self._mousePrevPosition = None self._mouseButtonDown = False self._forceUpdates = True def addCommandToQueue(self, command, blocking=False): if self._running: self._commandQueue.put( (command, blocking) ) if blocking: return self._releaseQueue.get() def processCommands(self): global _graphicsManager try: while self._running and self._commandQueue.qsize() > 0: (command, blocking) = self._commandQueue.get(0) self.processCommand(command, blocking) except _Queue.Empty: if _debug >= 1: print 'Queue empty exception' def processCommand(self, command, blocking): global _graphicsManager, _tkroot if _debug >= 2: print print "Manager executing:", command, blocking if _debug >= 3: print "Current chain:", self._currentChain try: print "Update info", self._needsUpdatingInfo except: pass # Store a possible return value result = None # Canvases if command[0] == 'create canvas': command[1]._canvas = _RenderedCanvas( command[1], command[2], command[3], command[4], command[5], command[6]) self._transform[((command[1], None), )] = _Transformation() elif command[0] == 'update canvas': if command[1]._canvas: command[1]._canvas.setHeight(command[2]) command[1]._canvas.setWidth(command[3]) command[1]._canvas.setBackgroundColor(command[4]) command[1]._canvas.setTitle(command[5]) elif command[0] == 'close canvas': command[1]._canvas._tkWin.withdraw() #command[1]._canvas = None keys = _graphicsManager._underlyingObject.keys() for k in keys: if k[0][0] == command[1]: _graphicsManager._underlyingObject.pop(k) elif command[0] == 'open canvas': command[1]._canvas._tkWin.deiconify() elif command[0] == 'begin refresh': self._currentChain = ((command[1], None), ) self._transformationChain = (command[1]._base_Transformation, ) self._currentCanvas = command[1] self._force = command[2] self._forceUpdates = False self._renderOrder[self._currentCanvas] = list() self._chainCount = dict() elif command[0] == 'complete refresh': # Remove any deleted objects for item in self._underlyingObject.keys(): if item[0][0] == self._currentCanvas and item not in self._renderOrder[self._currentCanvas]: self.removeUnderlying(item) self._currentChain = tuple() self._tranformationChain = tuple() self._currentCanvas = None self._forceUpdates = True _tkroot.update() _tkroot.update() elif command[0] == 'set chain': self._currentChain = command[1] self._transformationChain = (self._transform[self._currentChain], ) for x in self._prevRenderOrder: print x if self._currentChain in self._prevRenderOrder: i = self._prevRenderOrder.index(self._currentChain) if i > 0: self._renderOrder[self._currentCanvas] = self._prevRenderOrder[:i] elif command[0] == 'save to file': command[1]._canvas.saveToFile(command[2]) # Drawables elif command[0] == 'begin draw': self._chainCount[ self._currentChain + ( (command[1], )) ] = self._chainCount.get(self._currentChain + ( (command[1], )), 0) + 1 self._currentChain += ( (command[1], self._chainCount[ self._currentChain + ( (command[1], )) ]), ) if self._canvases.has_key(command[1]): self._canvases[command[1]].add(self._currentCanvas) else: self._canvases[command[1]] = set([self._currentCanvas]) if len(self._currentChain) > 2 and self._currentChain[-1][0] == self._currentChain[-2][0]: self._transformationChain += (self._transformationChain[-1], ) else: self._transformationChain += (self._transformationChain[-1] * command[1]._transform, ) self._transform[self._currentChain] = self._transformationChain[-1] if self._chains.has_key((command[1], self._currentCanvas)): self._chains[(command[1], self._currentCanvas)].add(self._currentChain) else: self._chains[(command[1], self._currentCanvas)] = set([self._currentChain]) self._renderOrder[self._currentCanvas].append(self._currentChain) elif command[0] == 'complete draw': self._currentChain = self._currentChain[:-1] self._transformationChain = self._transformationChain[:-1] elif command[0] == 'draw circle': chain = self._currentChain if self._underlyingObject.has_key(chain): self._underlyingObject[chain].update(self._transformationChain[-1], self._force) else: # Create object self._underlyingObject[chain] = _RenderedCircle( command[1], self._currentCanvas, self._transformationChain[-1] ) elif command[0] == 'draw rectangle': chain = self._currentChain if self._underlyingObject.has_key(chain): self._underlyingObject[chain].update(self._transformationChain[-1], self._force) else: # Create object self._underlyingObject[chain] = _RenderedRectangle( command[1], self._currentCanvas, self._transformationChain[-1] ) elif command[0] == 'draw path': chain = self._currentChain if self._underlyingObject.has_key(chain): self._underlyingObject[chain].update(self._transformationChain[-1], self._force) else: # Create path in the correct position and add it to the canvas self._underlyingObject[self._currentChain] = _RenderedPath( command[1], self._currentCanvas, self._transformationChain[-1], command[1].getPoints() ) elif command[0] == 'draw polygon': chain = self._currentChain if self._underlyingObject.has_key(chain): self._underlyingObject[chain].update(self._transformationChain[-1], self._force) else: # Create polygon in the correct position and add it to the canvas self._underlyingObject[self._currentChain] = _RenderedPolygon( command[1], self._currentCanvas, self._transformationChain[-1], command[1].getPoints() ) elif command[0] == 'draw text': chain = self._currentChain if self._underlyingObject.has_key(chain): self._underlyingObject[chain].update(self._transformationChain[-1], self._force) else: self._underlyingObject[self._currentChain] = _RenderedText( command[1], self._currentCanvas, self._transformationChain[-1] ) elif command[0] == 'get text size': text = command[1] size = command[2] tkWin = _Tkinter.Toplevel() canvas = _Tkinter.Canvas(tkWin) i = canvas.create_text(0, 0, text=text, font=('Helvetica', size, 'normal') ) bbox = canvas.bbox(i) canvas.delete(i) tkWin.withdraw() result = (bbox[2]-bbox[0],bbox[3]-bbox[1]) # Release blocking if blocking: self._releaseQueue.put(result) def objectChanged(self, drawable, position, properties, depth): if _debug >= 3: print "Object changed:", drawable, self._canvases.get(drawable, list()), drawable, position, properties, depth for can in self._canvases.get(drawable, list()): if can._autoRefresh: if _debug >= 3: print "Chains for update:", self._chains[(drawable,can)] if not depth and False: can._trueRefresh(False, set(self._chains[(drawable, can)]), position, properties, depth) else: can._trueRefresh(False, None, True, True, True) def removeUnderlying(self, chain): pass def addHandler(self, shape, callback): if self._handlers.has_key(shape): if callback not in self._handlers[shape]: self._handlers[shape].append(callback) else: raise ValueError else: self._handlers[shape] = [callback] def removeHandler(self, shape, callback): self._handlers[shape].remove(callback) def triggerHandler(self, shape, event): if self._handlers.has_key(shape) and len(self._handlers[shape]) > 0: event._trigger = shape for handler in self._handlers[shape]: eventThread = _EventThread(handler, event) eventThread.start() return True return False class Point(object): """Stores a two-dimensional point using cartesian coordinates.""" def __init__(self, initialX=0, initialY=0): """Create a new point instance. initialX x-coordinate of the point (default 0) initialY y-coordinate of the point (default 0) """ if not isinstance(initialX, (int,float)): raise TypeError('numeric value expected for x-coodinate') if not isinstance(initialY, (int,float)): raise TypeError('numeric value expected for y-coodinate') self._x = initialX self._y = initialY def getX(self): """Return the x-coordinate.""" return self._x def setX(self, val): """Set the x-coordinate to val.""" if not isinstance(val, (int,float)): raise TypeError('numeric value expected for x-coodinate') self._x = val def getY(self): """Return the y-coordinate.""" return self._y def setY(self, val): """Set the y-coordinate to val.""" if not isinstance(val, (int,float)): raise TypeError('numeric value expected for y-coodinate') self._y = val def get(self): """Return an (x,y) tuple.""" return self._x, self._y def scale(self, factor): """Scale the coordinates by the given factor.""" if not isinstance(factor, (int,float)): raise TypeError('numeric value expected for factor') self._x *= factor self._y *= factor def distance(self, other): """Return the distance between this point and the other.""" if not isinstance(other, Point): raise TypeError('other must be a Point instance') dx = self._x - other._x dy = self._y - other._y return _math.sqrt(dx * dx + dy * dy) def normalize(self): """Mutate the point, scaling it to have distance one from the origin. If the point currently represents the origin, it is unchanged. """ mag = self.distance( Point() ) if mag > 0: self.scale(1./mag) def __str__(self): """Return a string representation of the point (e.g., '<0,0>').""" return '<' + str(self._x) + ',' + str(self._y) + '>' def __add__(self, other): """Return a new point that is the sum of this Point and the other.""" if not isinstance(other, Point): raise TypeError('both operands must be Point instances') return Point(self._x + other._x, self._y + other._y) def __mul__(self, operand): """Return the result when multiplying the Point by an operand. When the operand is a scalar (i.e., an int or float), return a Point that has coordinates equal to the original times the factor. When operand is another Point, return a scalar that is the dot product of the two points. """ if isinstance(operand, (int,float)): # multiply by constant return Point(self._x * operand, self._y * operand) elif isinstance(operand, Point): # dot-product return self._x * operand._x + self._y * operand._y else: raise TypeError('unexpected operand for multiplication') def __rmul__(self, operand): """Return the result when multiplying the Point by an operand. See __mul__ for details. """ return self * operand def __xor__(self, angle): """Return a new point instance representing the original, rotated about the origin. angle number of degrees of rotation (clockwise) """ if not isinstance(angle, (int,float)): raise TypeError('numeric value expected for angle') angle = _math.pi*angle/180. mag = _math.sqrt(self._x * self._x + self._y * self._y) return Point(self._x * _math.cos(angle) - self._y * _math.sin(angle), self._x * _math.sin(angle) + self._y * _math.cos(angle)) class _Transformation(object): def __init__(self, value=None): if value: self._matrix = value[:4] self._translation = value[4:] else: self._matrix = (1.,0.,0.,1.) self._translation = (0.,0.) def __repr__(self): return '\n _Transformation '+str(hex(id(self)))+':\n matrix = %s\n translation = %s\n'%(repr(self._matrix), repr(self._translation)) def image(self, point): return Point( self._matrix[0]*point._x + self._matrix[1]*point._y + self._translation[0], self._matrix[2]*point._x + self._matrix[3]*point._y + self._translation[1]) def inv(self): detinv = 1. / self.det() m = ( self._matrix[3] * detinv, -self._matrix[1] * detinv, -self._matrix[2] * detinv, self._matrix[0] * detinv ) t = ( -m[0]*self._translation[0] - m[1]*self._translation[1], -m[2]*self._translation[0] - m[3]*self._translation[1]) return _Transformation(m+t) def __mul__(self, other): m = ( self._matrix[0] * other._matrix[0] + self._matrix[1] * other._matrix[2], self._matrix[0] * other._matrix[1] + self._matrix[1] * other._matrix[3], self._matrix[2] * other._matrix[0] + self._matrix[3] * other._matrix[2], self._matrix[2] * other._matrix[1] + self._matrix[3] * other._matrix[3]) p = self.image( Point(other._translation[0], other._translation[1]) ) return _Transformation(m + (p.getX(), p.getY())) def det(self): return (self._matrix[0] * self._matrix[3] - self._matrix[1] * self._matrix[2]) class Color(object): """A color representation. A color can be specified by name or RGB value, and can be made transparent. Available colors are: aliceblue, antiquewhite, antiquewhite1, antiquewhite2, antiquewhite3, antiquewhite4, aquamarine, aquamarine1, aquamarine2, aquamarine3, aquamarine4, azure, azure1, azure2, azure3, azure4, beige, bisque, bisque1, bisque2, bisque3, bisque4, black, blanchedalmond, blue, blue1, blue2, blue3, blue4, blueviolet, brown, brown1, brown2, brown3, brown4, burlywood, burlywood1, burlywood2, burlywood3, burlywood4, cadetblue, cadetblue1, cadetblue2, cadetblue3, cadetblue4, chartreuse, chartreuse1, chartreuse2, chartreuse3, chartreuse4, chocolate, chocolate1, chocolate2, chocolate3, chocolate4, coral, coral1, coral2, coral3, coral4, cornflowerblue, cornsilk, cornsilk1, cornsilk2, cornsilk3, cornsilk4, cyan, cyan1, cyan2, cyan3, cyan4, darkblue, darkcyan, darkgoldenrod, darkgoldenrod1, darkgoldenrod2, darkgoldenrod3, darkgoldenrod4, darkgray, darkgreen, darkgrey, darkkhaki, darkmagenta, darkolivegreen, darkolivegreen1, darkolivegreen2, darkolivegreen3, darkolivegreen4, darkorange, darkorange1, darkorange2, darkorange3, darkorange4, darkorchid, darkorchid1, darkorchid2, darkorchid3, darkorchid4, darkred, darksalmon, darkseagreen, darkseagreen1, darkseagreen2, darkseagreen3, darkseagreen4, darkslateblue, darkslategray, darkslategray1, darkslategray2, darkslategray3, darkslategray4, darkslategrey, darkturquoise, darkviolet, deeppink, deeppink1, deeppink2, deeppink3, deeppink4, deepskyblue, deepskyblue1, deepskyblue2, deepskyblue3, deepskyblue4, dimgray, dimgrey, dodgerblue, dodgerblue1, dodgerblue2, dodgerblue3, dodgerblue4, firebrick, firebrick1, firebrick2, firebrick3, firebrick4, floralwhite, forestgreen, gainsboro, ghostwhite, gold, gold1, gold2, gold3, gold4, goldenrod, goldenrod1, goldenrod2, goldenrod3, goldenrod4, gray, gray0, gray1, gray10, gray100, gray11, gray12, gray13, gray14, gray15, gray16, gray17, gray18, gray19, gray2, gray20, gray21, gray22, gray23, gray24, gray25, gray26, gray27, gray28, gray29, gray3, gray30, gray31, gray32, gray33, gray34, gray35, gray36, gray37, gray38, gray39, gray4, gray40, gray41, gray42, gray43, gray44, gray45, gray46, gray47, gray48, gray49, gray5, gray50, gray51, gray52, gray53, gray54, gray55, gray56, gray57, gray58, gray59, gray6, gray60, gray61, gray62, gray63, gray64, gray65, gray66, gray67, gray68, gray69, gray7, gray70, gray71, gray72, gray73, gray74, gray75, gray76, gray77, gray78, gray79, gray8, gray80, gray81, gray82, gray83, gray84, gray85, gray86, gray87, gray88, gray89, gray9, gray90, gray91, gray92, gray93, gray94, gray95, gray96, gray97, gray98, gray99, green, green1, green2, green3, green4, greenyellow, grey, grey0, grey1, grey10, grey100, grey11, grey12, grey13, grey14, grey15, grey16, grey17, grey18, grey19, grey2, grey20, grey21, grey22, grey23, grey24, grey25, grey26, grey27, grey28, grey29, grey3, grey30, grey31, grey32, grey33, grey34, grey35, grey36, grey37, grey38, grey39, grey4, grey40, grey41, grey42, grey43, grey44, grey45, grey46, grey47, grey48, grey49, grey5, grey50, grey51, grey52, grey53, grey54, grey55, grey56, grey57, grey58, grey59, grey6, grey60, grey61, grey62, grey63, grey64, grey65, grey66, grey67, grey68, grey69, grey7, grey70, grey71, grey72, grey73, grey74, grey75, grey76, grey77, grey78, grey79, grey8, grey80, grey81, grey82, grey83, grey84, grey85, grey86, grey87, grey88, grey89, grey9, grey90, grey91, grey92, grey93, grey94, grey95, grey96, grey97, grey98, grey99, honeydew, honeydew1, honeydew2, honeydew3, honeydew4, hotpink, hotpink1, hotpink2, hotpink3, hotpink4, indianred, indianred1, indianred2, indianred3, indianred4, ivory, ivory1, ivory2, ivory3, ivory4, khaki, khaki1, khaki2, khaki3, khaki4, lavender, lavenderblush, lavenderblush1, lavenderblush2, lavenderblush3, lavenderblush4, lawngreen, lemonchiffon, lemonchiffon1, lemonchiffon2, lemonchiffon3, lemonchiffon4, lightblue, lightblue1, lightblue2, lightblue3, lightblue4, lightcoral, lightcyan, lightcyan1, lightcyan2, lightcyan3, lightcyan4, lightgoldenrod, lightgoldenrod1, lightgoldenrod2, lightgoldenrod3, lightgoldenrod4, lightgoldenrodyellow, lightgray, lightgreen, lightgrey, lightpink, lightpink1, lightpink2, lightpink3, lightpink4, lightsalmon, lightsalmon1, lightsalmon2, lightsalmon3, lightsalmon4, lightseagreen, lightskyblue, lightskyblue1, lightskyblue2, lightskyblue3, lightskyblue4, lightslateblue, lightslategray, lightslategrey, lightsteelblue, lightsteelblue1, lightsteelblue2, lightsteelblue3, lightsteelblue4, lightyellow, lightyellow1, lightyellow2, lightyellow3, lightyellow4, limegreen, linen, magenta, magenta1, magenta2, magenta3, magenta4, maroon, maroon1, maroon2, maroon3, maroon4, mediumaquamarine, mediumblue, mediumorchid, mediumorchid1, mediumorchid2, mediumorchid3, mediumorchid4, mediumpurple, mediumpurple1, mediumpurple2, mediumpurple3, mediumpurple4, mediumseagreen, mediumslateblue, mediumspringgreen, mediumturquoise, mediumvioletred, midnightblue, mintcream, mistyrose, mistyrose1, mistyrose2, mistyrose3, mistyrose4, moccasin, navajowhite, navajowhite1, navajowhite2, navajowhite3, navajowhite4, navy, navyblue, oldlace, olivedrab, olivedrab1, olivedrab2, olivedrab3, olivedrab4, orange, orange1, orange2, orange3, orange4, orangered, orangered1, orangered2, orangered3, orangered4, orchid, orchid1, orchid2, orchid3, orchid4, palegoldenrod, palegreen, palegreen1, palegreen2, palegreen3, palegreen4, paleturquoise, paleturquoise1, paleturquoise2, paleturquoise3, paleturquoise4, palevioletred, palevioletred1, palevioletred2, palevioletred3, palevioletred4, papayawhip, peachpuff, peachpuff1, peachpuff2, peachpuff3, peachpuff4, peru, pink, pink1, pink2, pink3, pink4, plum, plum1, plum2, plum3, plum4, powderblue, purple, purple1, purple2, purple3, purple4, red, red1, red2, red3, red4, rosybrown, rosybrown1, rosybrown2, rosybrown3, rosybrown4, royalblue, royalblue1, royalblue2, royalblue3, royalblue4, saddlebrown, salmon, salmon1, salmon2, salmon3, salmon4, sandybrown, seagreen, seagreen1, seagreen2, seagreen3, seagreen4, seashell, seashell1, seashell2, seashell3, seashell4, sienna, sienna1, sienna2, sienna3, sienna4, skyblue, skyblue1, skyblue2, skyblue3, skyblue4, slateblue, slateblue1, slateblue2, slateblue3, slateblue4, slategray, slategray1, slategray2, slategray3, slategray4, slategrey, snow, snow1, snow2, snow3, snow4, springgreen, springgreen1, springgreen2, springgreen3, springgreen4, steelblue, steelblue1, steelblue2, steelblue3, steelblue4, tan, tan1, tan2, tan3, tan4, thistle, thistle1, thistle2, thistle3, thistle4, tomato, tomato1, tomato2, tomato3, tomato4, turquoise, turquoise1, turquoise2, turquoise3, turquoise4, violet, violetred, violetred1, violetred2, violetred3, violetred4, wheat, wheat1, wheat2, wheat3, wheat4, white, whitesmoke, yellow, yellow1, yellow2, yellow3, yellow4, yellowgreen """ _colorValues = { 'aliceblue' : (240,248,255), 'antiquewhite' : (250,235,215), 'antiquewhite1' : (255,239,219), 'antiquewhite2' : (238,223,204), 'antiquewhite3' : (205,192,176), 'antiquewhite4' : (139,131,120), 'aquamarine' : (127,255,212), 'aquamarine1' : (127,255,212), 'aquamarine2' : (118,238,198), 'aquamarine3' : (102,205,170), 'aquamarine4' : ( 69,139,116), 'azure' : (240,255,255), 'azure1' : (240,255,255), 'azure2' : (224,238,238), 'azure3' : (193,205,205), 'azure4' : (131,139,139), 'beige' : (245,245,220), 'bisque' : (255,228,196), 'bisque1' : (255,228,196), 'bisque2' : (238,213,183), 'bisque3' : (205,183,158), 'bisque4' : (139,125,107), 'black' : ( 0, 0, 0), 'blanchedalmond' : (255,235,205), 'blue' : ( 0, 0,255), 'blue1' : ( 0, 0,255), 'blue2' : ( 0, 0,238), 'blue3' : ( 0, 0,205), 'blue4' : ( 0, 0,139), 'blueviolet' : (138, 43,226), 'brown' : (165, 42, 42), 'brown1' : (255, 64, 64), 'brown2' : (238, 59, 59), 'brown3' : (205, 51, 51), 'brown4' : (139, 35, 35), 'burlywood' : (222,184,135), 'burlywood1' : (255,211,155), 'burlywood2' : (238,197,145), 'burlywood3' : (205,170,125), 'burlywood4' : (139,115, 85), 'cadetblue' : ( 95,158,160), 'cadetblue1' : (152,245,255), 'cadetblue2' : (142,229,238), 'cadetblue3' : (122,197,205), 'cadetblue4' : ( 83,134,139), 'chartreuse' : (127,255, 0), 'chartreuse1' : (127,255, 0), 'chartreuse2' : (118,238, 0), 'chartreuse3' : (102,205, 0), 'chartreuse4' : ( 69,139, 0), 'chocolate' : (210,105, 30), 'chocolate1' : (255,127, 36), 'chocolate2' : (238,118, 33), 'chocolate3' : (205,102, 29), 'chocolate4' : (139, 69, 19), 'coral' : (255,127, 80), 'coral1' : (255,114, 86), 'coral2' : (238,106, 80), 'coral3' : (205, 91, 69), 'coral4' : (139, 62, 47), 'cornflowerblue' : (100,149,237), 'cornsilk' : (255,248,220), 'cornsilk1' : (255,248,220), 'cornsilk2' : (238,232,205), 'cornsilk3' : (205,200,177), 'cornsilk4' : (139,136,120), 'cyan' : ( 0,255,255), 'cyan1' : ( 0,255,255), 'cyan2' : ( 0,238,238), 'cyan3' : ( 0,205,205), 'cyan4' : ( 0,139,139), 'darkblue' : ( 0, 0,139), 'darkcyan' : ( 0,139,139), 'darkgoldenrod' : (184,134, 11), 'darkgoldenrod1' : (255,185, 15), 'darkgoldenrod2' : (238,173, 14), 'darkgoldenrod3' : (205,149, 12), 'darkgoldenrod4' : (139,101, 8), 'darkgray' : (169,169,169), 'darkgreen' : ( 0,100, 0), 'darkgrey' : (169,169,169), 'darkkhaki' : (189,183,107), 'darkmagenta' : (139, 0,139), 'darkolivegreen' : ( 85,107, 47), 'darkolivegreen1' : (202,255,112), 'darkolivegreen2' : (188,238,104), 'darkolivegreen3' : (162,205, 90), 'darkolivegreen4' : (110,139, 61), 'darkorange' : (255,140, 0), 'darkorange1' : (255,127, 0), 'darkorange2' : (238,118, 0), 'darkorange3' : (205,102, 0), 'darkorange4' : (139, 69, 0), 'darkorchid' : (153, 50,204), 'darkorchid1' : (191, 62,255), 'darkorchid2' : (178, 58,238), 'darkorchid3' : (154, 50,205), 'darkorchid4' : (104, 34,139), 'darkred' : (139, 0, 0), 'darksalmon' : (233,150,122), 'darkseagreen' : (143,188,143), 'darkseagreen1' : (193,255,193), 'darkseagreen2' : (180,238,180), 'darkseagreen3' : (155,205,155), 'darkseagreen4' : (105,139,105), 'darkslateblue' : ( 72, 61,139), 'darkslategray' : ( 47, 79, 79), 'darkslategray1' : (151,255,255), 'darkslategray2' : (141,238,238), 'darkslategray3' : (121,205,205), 'darkslategray4' : ( 82,139,139), 'darkslategrey' : ( 47, 79, 79), 'darkturquoise' : ( 0,206,209), 'darkviolet' : (148, 0,211), 'deeppink' : (255, 20,147), 'deeppink1' : (255, 20,147), 'deeppink2' : (238, 18,137), 'deeppink3' : (205, 16,118), 'deeppink4' : (139, 10, 80), 'deepskyblue' : ( 0,191,255), 'deepskyblue1' : ( 0,191,255), 'deepskyblue2' : ( 0,178,238), 'deepskyblue3' : ( 0,154,205), 'deepskyblue4' : ( 0,104,139), 'dimgray' : (105,105,105), 'dimgrey' : (105,105,105), 'dodgerblue' : ( 30,144,255), 'dodgerblue1' : ( 30,144,255), 'dodgerblue2' : ( 28,134,238), 'dodgerblue3' : ( 24,116,205), 'dodgerblue4' : ( 16, 78,139), 'firebrick' : (178, 34, 34), 'firebrick1' : (255, 48, 48), 'firebrick2' : (238, 44, 44), 'firebrick3' : (205, 38, 38), 'firebrick4' : (139, 26, 26), 'floralwhite' : (255,250,240), 'forestgreen' : ( 34,139, 34), 'gainsboro' : (220,220,220), 'ghostwhite' : (248,248,255), 'gold' : (255,215, 0), 'gold1' : (255,215, 0), 'gold2' : (238,201, 0), 'gold3' : (205,173, 0), 'gold4' : (139,117, 0), 'goldenrod' : (218,165, 32), 'goldenrod1' : (255,193, 37), 'goldenrod2' : (238,180, 34), 'goldenrod3' : (205,155, 29), 'goldenrod4' : (139,105, 20), 'gray' : (190,190,190), 'gray0' : ( 0, 0, 0), 'gray1' : ( 3, 3, 3), 'gray10' : ( 26, 26, 26), 'gray100' : (255,255,255), 'gray11' : ( 28, 28, 28), 'gray12' : ( 31, 31, 31), 'gray13' : ( 33, 33, 33), 'gray14' : ( 36, 36, 36), 'gray15' : ( 38, 38, 38), 'gray16' : ( 41, 41, 41), 'gray17' : ( 43, 43, 43), 'gray18' : ( 46, 46, 46), 'gray19' : ( 48, 48, 48), 'gray2' : ( 5, 5, 5), 'gray20' : ( 51, 51, 51), 'gray21' : ( 54, 54, 54), 'gray22' : ( 56, 56, 56), 'gray23' : ( 59, 59, 59), 'gray24' : ( 61, 61, 61), 'gray25' : ( 64, 64, 64), 'gray26' : ( 66, 66, 66), 'gray27' : ( 69, 69, 69), 'gray28' : ( 71, 71, 71), 'gray29' : ( 74, 74, 74), 'gray3' : ( 8, 8, 8), 'gray30' : ( 77, 77, 77), 'gray31' : ( 79, 79, 79), 'gray32' : ( 82, 82, 82), 'gray33' : ( 84, 84, 84), 'gray34' : ( 87, 87, 87), 'gray35' : ( 89, 89, 89), 'gray36' : ( 92, 92, 92), 'gray37' : ( 94, 94, 94), 'gray38' : ( 97, 97, 97), 'gray39' : ( 99, 99, 99), 'gray4' : ( 10, 10, 10), 'gray40' : (102,102,102), 'gray41' : (105,105,105), 'gray42' : (107,107,107), 'gray43' : (110,110,110), 'gray44' : (112,112,112), 'gray45' : (115,115,115), 'gray46' : (117,117,117), 'gray47' : (120,120,120), 'gray48' : (122,122,122), 'gray49' : (125,125,125), 'gray5' : ( 13, 13, 13), 'gray50' : (127,127,127), 'gray51' : (130,130,130), 'gray52' : (133,133,133), 'gray53' : (135,135,135), 'gray54' : (138,138,138), 'gray55' : (140,140,140), 'gray56' : (143,143,143), 'gray57' : (145,145,145), 'gray58' : (148,148,148), 'gray59' : (150,150,150), 'gray6' : ( 15, 15, 15), 'gray60' : (153,153,153), 'gray61' : (156,156,156), 'gray62' : (158,158,158), 'gray63' : (161,161,161), 'gray64' : (163,163,163), 'gray65' : (166,166,166), 'gray66' : (168,168,168), 'gray67' : (171,171,171), 'gray68' : (173,173,173), 'gray69' : (176,176,176), 'gray7' : ( 18, 18, 18), 'gray70' : (179,179,179), 'gray71' : (181,181,181), 'gray72' : (184,184,184), 'gray73' : (186,186,186), 'gray74' : (189,189,189), 'gray75' : (191,191,191), 'gray76' : (194,194,194), 'gray77' : (196,196,196), 'gray78' : (199,199,199), 'gray79' : (201,201,201), 'gray8' : ( 20, 20, 20), 'gray80' : (204,204,204), 'gray81' : (207,207,207), 'gray82' : (209,209,209), 'gray83' : (212,212,212), 'gray84' : (214,214,214), 'gray85' : (217,217,217), 'gray86' : (219,219,219), 'gray87' : (222,222,222), 'gray88' : (224,224,224), 'gray89' : (227,227,227), 'gray9' : ( 23, 23, 23), 'gray90' : (229,229,229), 'gray91' : (232,232,232), 'gray92' : (235,235,235), 'gray93' : (237,237,237), 'gray94' : (240,240,240), 'gray95' : (242,242,242), 'gray96' : (245,245,245), 'gray97' : (247,247,247), 'gray98' : (250,250,250), 'gray99' : (252,252,252), 'green' : ( 0,255, 0), 'green1' : ( 0,255, 0), 'green2' : ( 0,238, 0), 'green3' : ( 0,205, 0), 'green4' : ( 0,139, 0), 'greenyellow' : (173,255, 47), 'grey' : (190,190,190), 'grey0' : ( 0, 0, 0), 'grey1' : ( 3, 3, 3), 'grey10' : ( 26, 26, 26), 'grey100' : (255,255,255), 'grey11' : ( 28, 28, 28), 'grey12' : ( 31, 31, 31), 'grey13' : ( 33, 33, 33), 'grey14' : ( 36, 36, 36), 'grey15' : ( 38, 38, 38), 'grey16' : ( 41, 41, 41), 'grey17' : ( 43, 43, 43), 'grey18' : ( 46, 46, 46), 'grey19' : ( 48, 48, 48), 'grey2' : ( 5, 5, 5), 'grey20' : ( 51, 51, 51), 'grey21' : ( 54, 54, 54), 'grey22' : ( 56, 56, 56), 'grey23' : ( 59, 59, 59), 'grey24' : ( 61, 61, 61), 'grey25' : ( 64, 64, 64), 'grey26' : ( 66, 66, 66), 'grey27' : ( 69, 69, 69), 'grey28' : ( 71, 71, 71), 'grey29' : ( 74, 74, 74), 'grey3' : ( 8, 8, 8), 'grey30' : ( 77, 77, 77), 'grey31' : ( 79, 79, 79), 'grey32' : ( 82, 82, 82), 'grey33' : ( 84, 84, 84), 'grey34' : ( 87, 87, 87), 'grey35' : ( 89, 89, 89), 'grey36' : ( 92, 92, 92), 'grey37' : ( 94, 94, 94), 'grey38' : ( 97, 97, 97), 'grey39' : ( 99, 99, 99), 'grey4' : ( 10, 10, 10), 'grey40' : (102,102,102), 'grey41' : (105,105,105), 'grey42' : (107,107,107), 'grey43' : (110,110,110), 'grey44' : (112,112,112), 'grey45' : (115,115,115), 'grey46' : (117,117,117), 'grey47' : (120,120,120), 'grey48' : (122,122,122), 'grey49' : (125,125,125), 'grey5' : ( 13, 13, 13), 'grey50' : (127,127,127), 'grey51' : (130,130,130), 'grey52' : (133,133,133), 'grey53' : (135,135,135), 'grey54' : (138,138,138), 'grey55' : (140,140,140), 'grey56' : (143,143,143), 'grey57' : (145,145,145), 'grey58' : (148,148,148), 'grey59' : (150,150,150), 'grey6' : ( 15, 15, 15), 'grey60' : (153,153,153), 'grey61' : (156,156,156), 'grey62' : (158,158,158), 'grey63' : (161,161,161), 'grey64' : (163,163,163), 'grey65' : (166,166,166), 'grey66' : (168,168,168), 'grey67' : (171,171,171), 'grey68' : (173,173,173), 'grey69' : (176,176,176), 'grey7' : ( 18, 18, 18), 'grey70' : (179,179,179), 'grey71' : (181,181,181), 'grey72' : (184,184,184), 'grey73' : (186,186,186), 'grey74' : (189,189,189), 'grey75' : (191,191,191), 'grey76' : (194,194,194), 'grey77' : (196,196,196), 'grey78' : (199,199,199), 'grey79' : (201,201,201), 'grey8' : ( 20, 20, 20), 'grey80' : (204,204,204), 'grey81' : (207,207,207), 'grey82' : (209,209,209), 'grey83' : (212,212,212), 'grey84' : (214,214,214), 'grey85' : (217,217,217), 'grey86' : (219,219,219), 'grey87' : (222,222,222), 'grey88' : (224,224,224), 'grey89' : (227,227,227), 'grey9' : ( 23, 23, 23), 'grey90' : (229,229,229), 'grey91' : (232,232,232), 'grey92' : (235,235,235), 'grey93' : (237,237,237), 'grey94' : (240,240,240), 'grey95' : (242,242,242), 'grey96' : (245,245,245), 'grey97' : (247,247,247), 'grey98' : (250,250,250), 'grey99' : (252,252,252), 'honeydew' : (240,255,240), 'honeydew1' : (240,255,240), 'honeydew2' : (224,238,224), 'honeydew3' : (193,205,193), 'honeydew4' : (131,139,131), 'hotpink' : (255,105,180), 'hotpink1' : (255,110,180), 'hotpink2' : (238,106,167), 'hotpink3' : (205, 96,144), 'hotpink4' : (139, 58, 98), 'indianred' : (205, 92, 92), 'indianred1' : (255,106,106), 'indianred2' : (238, 99, 99), 'indianred3' : (205, 85, 85), 'indianred4' : (139, 58, 58), 'ivory' : (255,255,240), 'ivory1' : (255,255,240), 'ivory2' : (238,238,224), 'ivory3' : (205,205,193), 'ivory4' : (139,139,131), 'khaki' : (240,230,140), 'khaki1' : (255,246,143), 'khaki2' : (238,230,133), 'khaki3' : (205,198,115), 'khaki4' : (139,134, 78), 'lavender' : (230,230,250), 'lavenderblush' : (255,240,245), 'lavenderblush1' : (255,240,245), 'lavenderblush2' : (238,224,229), 'lavenderblush3' : (205,193,197), 'lavenderblush4' : (139,131,134), 'lawngreen' : (124,252, 0), 'lemonchiffon' : (255,250,205), 'lemonchiffon1' : (255,250,205), 'lemonchiffon2' : (238,233,191), 'lemonchiffon3' : (205,201,165), 'lemonchiffon4' : (139,137,112), 'lightblue' : (173,216,230), 'lightblue1' : (191,239,255), 'lightblue2' : (178,223,238), 'lightblue3' : (154,192,205), 'lightblue4' : (104,131,139), 'lightcoral' : (240,128,128), 'lightcyan' : (224,255,255), 'lightcyan1' : (224,255,255), 'lightcyan2' : (209,238,238), 'lightcyan3' : (180,205,205), 'lightcyan4' : (122,139,139), 'lightgoldenrod' : (238,221,130), 'lightgoldenrod1' : (255,236,139), 'lightgoldenrod2' : (238,220,130), 'lightgoldenrod3' : (205,190,112), 'lightgoldenrod4' : (139,129, 76), 'lightgoldenrodyellow' : (250,250,210), 'lightgray' : (211,211,211), 'lightgreen' : (144,238,144), 'lightgrey' : (211,211,211), 'lightpink' : (255,182,193), 'lightpink1' : (255,174,185), 'lightpink2' : (238,162,173), 'lightpink3' : (205,140,149), 'lightpink4' : (139, 95,101), 'lightsalmon' : (255,160,122), 'lightsalmon1' : (255,160,122), 'lightsalmon2' : (238,149,114), 'lightsalmon3' : (205,129, 98), 'lightsalmon4' : (139, 87, 66), 'lightseagreen' : ( 32,178,170), 'lightskyblue' : (135,206,250), 'lightskyblue1' : (176,226,255), 'lightskyblue2' : (164,211,238), 'lightskyblue3' : (141,182,205), 'lightskyblue4' : ( 96,123,139), 'lightslateblue' : (132,112,255), 'lightslategray' : (119,136,153), 'lightslategrey' : (119,136,153), 'lightsteelblue' : (176,196,222), 'lightsteelblue1' : (202,225,255), 'lightsteelblue2' : (188,210,238), 'lightsteelblue3' : (162,181,205), 'lightsteelblue4' : (110,123,139), 'lightyellow' : (255,255,224), 'lightyellow1' : (255,255,224), 'lightyellow2' : (238,238,209), 'lightyellow3' : (205,205,180), 'lightyellow4' : (139,139,122), 'limegreen' : ( 50,205, 50), 'linen' : (250,240,230), 'magenta' : (255, 0,255), 'magenta1' : (255, 0,255), 'magenta2' : (238, 0,238), 'magenta3' : (205, 0,205), 'magenta4' : (139, 0,139), 'maroon' : (176, 48, 96), 'maroon1' : (255, 52,179), 'maroon2' : (238, 48,167), 'maroon3' : (205, 41,144), 'maroon4' : (139, 28, 98), 'mediumaquamarine' : (102,205,170), 'mediumblue' : ( 0, 0,205), 'mediumorchid' : (186, 85,211), 'mediumorchid1' : (224,102,255), 'mediumorchid2' : (209, 95,238), 'mediumorchid3' : (180, 82,205), 'mediumorchid4' : (122, 55,139), 'mediumpurple' : (147,112,219), 'mediumpurple1' : (171,130,255), 'mediumpurple2' : (159,121,238), 'mediumpurple3' : (137,104,205), 'mediumpurple4' : ( 93, 71,139), 'mediumseagreen' : ( 60,179,113), 'mediumslateblue' : (123,104,238), 'mediumspringgreen' : ( 0,250,154), 'mediumturquoise' : ( 72,209,204), 'mediumvioletred' : (199, 21,133), 'midnightblue' : ( 25, 25,112), 'mintcream' : (245,255,250), 'mistyrose' : (255,228,225), 'mistyrose1' : (255,228,225), 'mistyrose2' : (238,213,210), 'mistyrose3' : (205,183,181), 'mistyrose4' : (139,125,123), 'moccasin' : (255,228,181), 'navajowhite' : (255,222,173), 'navajowhite1' : (255,222,173), 'navajowhite2' : (238,207,161), 'navajowhite3' : (205,179,139), 'navajowhite4' : (139,121, 94), 'navy' : ( 0, 0,128), 'navyblue' : ( 0, 0,128), 'oldlace' : (253,245,230), 'olivedrab' : (107,142, 35), 'olivedrab1' : (192,255, 62), 'olivedrab2' : (179,238, 58), 'olivedrab3' : (154,205, 50), 'olivedrab4' : (105,139, 34), 'orange' : (255,165, 0), 'orange1' : (255,165, 0), 'orange2' : (238,154, 0), 'orange3' : (205,133, 0), 'orange4' : (139, 90, 0), 'orangered' : (255, 69, 0), 'orangered1' : (255, 69, 0), 'orangered2' : (238, 64, 0), 'orangered3' : (205, 55, 0), 'orangered4' : (139, 37, 0), 'orchid' : (218,112,214), 'orchid1' : (255,131,250), 'orchid2' : (238,122,233), 'orchid3' : (205,105,201), 'orchid4' : (139, 71,137), 'palegoldenrod' : (238,232,170), 'palegreen' : (152,251,152), 'palegreen1' : (154,255,154), 'palegreen2' : (144,238,144), 'palegreen3' : (124,205,124), 'palegreen4' : ( 84,139, 84), 'paleturquoise' : (175,238,238), 'paleturquoise1' : (187,255,255), 'paleturquoise2' : (174,238,238), 'paleturquoise3' : (150,205,205), 'paleturquoise4' : (102,139,139), 'palevioletred' : (219,112,147), 'palevioletred1' : (255,130,171), 'palevioletred2' : (238,121,159), 'palevioletred3' : (205,104,137), 'palevioletred4' : (139, 71, 93), 'papayawhip' : (255,239,213), 'peachpuff' : (255,218,185), 'peachpuff1' : (255,218,185), 'peachpuff2' : (238,203,173), 'peachpuff3' : (205,175,149), 'peachpuff4' : (139,119,101), 'peru' : (205,133, 63), 'pink' : (255,192,203), 'pink1' : (255,181,197), 'pink2' : (238,169,184), 'pink3' : (205,145,158), 'pink4' : (139, 99,108), 'plum' : (221,160,221), 'plum1' : (255,187,255), 'plum2' : (238,174,238), 'plum3' : (205,150,205), 'plum4' : (139,102,139), 'powderblue' : (176,224,230), 'purple' : (160, 32,240), 'purple1' : (155, 48,255), 'purple2' : (145, 44,238), 'purple3' : (125, 38,205), 'purple4' : ( 85, 26,139), 'red' : (255, 0, 0), 'red1' : (255, 0, 0), 'red2' : (238, 0, 0), 'red3' : (205, 0, 0), 'red4' : (139, 0, 0), 'rosybrown' : (188,143,143), 'rosybrown1' : (255,193,193), 'rosybrown2' : (238,180,180), 'rosybrown3' : (205,155,155), 'rosybrown4' : (139,105,105), 'royalblue' : ( 65,105,225), 'royalblue1' : ( 72,118,255), 'royalblue2' : ( 67,110,238), 'royalblue3' : ( 58, 95,205), 'royalblue4' : ( 39, 64,139), 'saddlebrown' : (139, 69, 19), 'salmon' : (250,128,114), 'salmon1' : (255,140,105), 'salmon2' : (238,130, 98), 'salmon3' : (205,112, 84), 'salmon4' : (139, 76, 57), 'sandybrown' : (244,164, 96), 'seagreen' : ( 46,139, 87), 'seagreen1' : ( 84,255,159), 'seagreen2' : ( 78,238,148), 'seagreen3' : ( 67,205,128), 'seagreen4' : ( 46,139, 87), 'seashell' : (255,245,238), 'seashell1' : (255,245,238), 'seashell2' : (238,229,222), 'seashell3' : (205,197,191), 'seashell4' : (139,134,130), 'sienna' : (160, 82, 45), 'sienna1' : (255,130, 71), 'sienna2' : (238,121, 66), 'sienna3' : (205,104, 57), 'sienna4' : (139, 71, 38), 'skyblue' : (135,206,235), 'skyblue1' : (135,206,255), 'skyblue2' : (126,192,238), 'skyblue3' : (108,166,205), 'skyblue4' : ( 74,112,139), 'slateblue' : (106, 90,205), 'slateblue1' : (131,111,255), 'slateblue2' : (122,103,238), 'slateblue3' : (105, 89,205), 'slateblue4' : ( 71, 60,139), 'slategray' : (112,128,144), 'slategray1' : (198,226,255), 'slategray2' : (185,211,238), 'slategray3' : (159,182,205), 'slategray4' : (108,123,139), 'slategrey' : (112,128,144), 'snow' : (255,250,250), 'snow1' : (255,250,250), 'snow2' : (238,233,233), 'snow3' : (205,201,201), 'snow4' : (139,137,137), 'springgreen' : ( 0,255,127), 'springgreen1' : ( 0,255,127), 'springgreen2' : ( 0,238,118), 'springgreen3' : ( 0,205,102), 'springgreen4' : ( 0,139, 69), 'steelblue' : ( 70,130,180), 'steelblue1' : ( 99,184,255), 'steelblue2' : ( 92,172,238), 'steelblue3' : ( 79,148,205), 'steelblue4' : ( 54,100,139), 'tan' : (210,180,140), 'tan1' : (255,165, 79), 'tan2' : (238,154, 73), 'tan3' : (205,133, 63), 'tan4' : (139, 90, 43), 'thistle' : (216,191,216), 'thistle1' : (255,225,255), 'thistle2' : (238,210,238), 'thistle3' : (205,181,205), 'thistle4' : (139,123,139), 'tomato' : (255, 99, 71), 'tomato1' : (255, 99, 71), 'tomato2' : (238, 92, 66), 'tomato3' : (205, 79, 57), 'tomato4' : (139, 54, 38), 'turquoise' : ( 64,224,208), 'turquoise1' : ( 0,245,255), 'turquoise2' : ( 0,229,238), 'turquoise3' : ( 0,197,205), 'turquoise4' : ( 0,134,139), 'violet' : (238,130,238), 'violetred' : (208, 32,144), 'violetred1' : (255, 62,150), 'violetred2' : (238, 58,140), 'violetred3' : (205, 50,120), 'violetred4' : (139, 34, 82), 'wheat' : (245,222,179), 'wheat1' : (255,231,186), 'wheat2' : (238,216,174), 'wheat3' : (205,186,150), 'wheat4' : (139,126,102), 'white' : (255,255,255), 'whitesmoke' : (245,245,245), 'yellow' : (255,255, 0), 'yellow1' : (255,255, 0), 'yellow2' : (238,238, 0), 'yellow3' : (205,205, 0), 'yellow4' : (139,139, 0), 'yellowgreen' : (154,205, 50), } def __init__(self, colorChoice='White'): """Create a new Color instance (default 'White'). The parameter can be either: - a string with the name of the color - an (r,g,b) tuple - an existing Color instance (which will be cloned) """ # we intentionally have Drawable objects using a color # register with the color instance, so that when the color is # mutated, the object can be informed that it has changed self._drawables = [] if isinstance(colorChoice,str): try: self.setByName(colorChoice) except ValueError, ve: raise ValueError(str(ve)) elif isinstance(colorChoice,tuple): try: self.setByValue(colorChoice) except ValueError, ve: raise ValueError(str(ve)) elif isinstance(colorChoice,Color): self._colorName = colorChoice._colorName self._transparent = colorChoice._transparent self._colorValue = colorChoice._colorValue else: raise TypeError('Invalid color specification') def setByName(self, colorName): """Set the color to colorName. colorName a string representing a valid name If colorName is 'Transparent' the resulting color will not show up on a canvas. """ if not isinstance(colorName,str): raise TypeError('string expected as color name') self._colorName = colorName.lower().replace(' ','') if self._colorName == 'transparent': self._transparent = True self._colorValue = (0,0,0) else: try: self._transparent = False self._colorValue = Color._colorValues[self._colorName.lower()] except KeyError: raise ValueError('%s is not a valid color name' % colorName) self._informDrawables() def getColorName(self): """Return the name of the color. If the color was set by RGB value, it returns 'Custom'. """ return self._colorName def setByValue(self, rgbTuple): """Set the color to the given tuple of (red, green, blue) values.""" if not isinstance(rgbTuple,tuple): raise TypeError('(r,g,b) tuple expected') if len(rgbTuple)!=3: raise ValueError('(r,g,b) tuple must have three components') for val in rgbTuple: if not isinstance(val,(int,float)): raise TypeError('tuple entries must be numeric') elif not 0 <= val <= 255: raise ValueError('tuple entries must be from 0 to 255') self._transparent = False self._colorName = 'Custom' self._colorValue = rgbTuple self._informDrawables() def getColorValue(self): """Return a tuple of the (red, green, blue) color components.""" return (self._colorValue[0], self._colorValue[1], self._colorValue[2]) def isTransparent(self): """Return a boolean variable indicating if the current color is transparent.""" return self._transparent def randomColor(): """Return a random color.""" return Color((_random.randint(0,255),_random.randint(0,255),_random.randint(0,255))) randomColor = staticmethod(randomColor) def __repr__(self): """Return the name of the color, if named. Otherwise return the (r,g,b) value.""" if self._colorName == 'Custom': return self._colorValue.__repr__() else: return self._colorName def _register(self, drawable): """Called to register a Drawable with this Color instance""" if drawable not in self._drawables: self._drawables.append(drawable) def _unregister(self, drawable): """Called to unregister a Drawable with this Color instance""" if drawable in self._drawables: self._drawables.remove(drawable) def _informDrawables(self): """When the Color instance has been mutated, we must inform those registered drawables.""" for drawable in self._drawables: drawable._objectChanged() class _GraphicsContainer(object): def __init__(self): self._contents = [] def __contains__(self, obj): """Return True if obj is currently in the container; False otherwise.""" return obj in self._contents def add(self, drawable): """Add the Drawable object to the container.""" # not doing error checking here, as we want tailored messages for Canvas and Layer self._contents.append(drawable) def remove(self, drawable): """Remove the Drawable object from the container.""" # not doing error checking here, as we want tailored messages for Canvas and Layer self._contents.remove(drawable) def clear(self): """Remove all objects from the container.""" contents = list(self._contents) # intentional clone for drawable in contents: self.remove(drawable) def getContents(self): """Return the contents of the container, sorted by depth.""" contentsPair = list() for drawable in self._contents: contentsPair.append( (drawable._depth, drawable) ) contentsPair.sort() contentsPair.reverse() if _debug >= 3: print "Contents of container (depth, item):", contentsPair contents = list() for pair in contentsPair: contents.append(pair[1]) return contents class Event(object): """An event typically triggered by the user interface.""" def __init__(self): self._eventType = "" self._x, self._y = 0, 0 self._prevx, self._prevy = 0,0 self._key = "" self._trigger = None def getDescription(self): """Return a text description of the event. Possibilities include: 'mouse click', 'mouse release', 'mouse drag', 'keyboard, 'timer' """ return self._eventType def getMouseLocation(self): """Return a Point designating the location of the mouse at the time of the event.""" return Point(self._x, self._y) def getOldMouseLocation(self): """Return a Point designating the location of the mouse at the start of a mouse drag.""" return Point(self._prevx, self._prevy) def getTrigger(self): """Return a reference to the object that triggered the event.""" return self._trigger def getKey(self): """Return a string designating the key pressed for a keyboard event.""" return self._key class EventHandler(object): """A base class for creating new event handlers. The handle method for this base class does not do anything. """ def __init__(self): """Create a new event handler. Children of this class must call this constructor. """ pass def handle(self, event): """Handle an event. Child classes must override this method, but do not need to call it. """ pass class _ReleaseHandler(EventHandler): def __init__(self, lock): self._lock = lock self._event = None self._lock.acquire() def handle(self, event): if event.getDescription() in ['keyboard', 'mouse click']: self._event = event self._lock.release() class _EventTrigger(object): def __init__(self): pass def wait(self): """Wait for an event to occur. When an event occurs, an Event instance is returned with information about what has happened. """ lock = _threading.Lock() rh = _ReleaseHandler(lock) self.addHandler(rh) lock.acquire() self.removeHandler(rh) return rh._event def addHandler(self, handler): """Register an EventHandler instance with this object.""" if not isinstance(handler, EventHandler): raise TypeError('Only instance of EventHandler (or child class) can handle events') try: _graphicsManager.addHandler(self, handler) except ValueError: raise ValueError('Handler is already handling events for this object') def removeHandler(self, handler): """Unregister an EventHandler instance from this object.""" if not isinstance(handler, EventHandler): raise TypeError('Parameter is not an instance of EventHandler (or child class)') try: _graphicsManager.removeHandler(self, handler) except ValueError: raise ValueError('The handler is not currently associated with this object.') class _EventThread(_threading.Thread): def __init__(self, handler, event): _threading.Thread.__init__(self) self._handler = handler self._event = event def run(self): self._handler.handle(self._event) class Canvas(_GraphicsContainer, _EventTrigger): """A window that can be drawn upon.""" def __init__(self, w=200, h=200, bgColor=None, title="Graphics canvas", autoRefresh=True): """Create a new drawing canvas. A new canvas will be created. w width of drawing area (default 200) h height of drawing area (default 200) bgColor color of the background (default 'White') title window title (default 'Graphics Canvas') autoRefresh whether auto-refresh mode is used (default True) """ global _graphicsManager _GraphicsContainer.__init__(self) _EventTrigger.__init__(self) self._base_Transformation = _Transformation() self._canvasUpdated = True if not bgColor: bgColor = 'white' if not isinstance(w, (int,float)): raise TypeError('width must be numeric') if not isinstance(h, (int,float)): raise TypeError('height must be numeric') if not isinstance(title,str): raise TypeError('title must be a string') if not isinstance(autoRefresh,bool): raise TypeError('autoRefresh flag must be a boolean value') if isinstance(bgColor,Color): self._backgroundColor = bgColor else: try: self._backgroundColor = Color(bgColor) except TypeError,te: raise TypeError(str(te)) except ValueError,ve: raise ValueError(str(ve)) self._width = w self._height = h self._title = title self._autoRefresh = autoRefresh self._canvasOpen = True _graphicsManager._openCanvases.add(self) _graphicsManager.addCommandToQueue(('create canvas', self, w, h, self._backgroundColor, title, autoRefresh)) def setBackgroundColor(self, color): """Set the background color. The parameter can be either: - a string with the name of the color - an (r,g,b) tuple - an existing Color instance """ if isinstance(color,Color): self._backgroundColor = color else: try: self._backgroundColor = Color(color) except TypeError,te: raise TypeError(str(te)) except ValueError,ve: raise ValueError(str(ve)) self._canvasChanged() def getBackgroundColor(self): """Return the background color as a Color instance.""" return self._backgroundColor def setWidth(self, w): """Reset the canvas width to w.""" if not isinstance(w, (int,float)): raise TypeError('width must be numeric value') if w <= 0: raise ValueError('width must be positive') self._width = w self._canvasChanged() def getWidth(self): """Return the width of the canvas.""" return self._width def setHeight(self, h): """Reset the canvas height to h.""" if not isinstance(h, (int,float)): raise TypeError('height must be numeric value') if h <= 0: raise ValueError('height must be positive') self._height = h self._canvasChanged() def getHeight(self): """Return the height of the canvas.""" return self._height def setTitle(self, title): """Set the title for the canvas window to given string.""" if not isinstance(title,str): raise TypeError('title must be a string') self._title = title self._canvasChanged() def getTitle(self): """Return the title of the window.""" return self._title def close(self): """Close the canvas window (if not already closed). The window can be reopened with a subsequent call to open(). """ if self._canvasOpen: _graphicsManager.addCommandToQueue(("close canvas", self)) _graphicsManager._openCanvases.remove(self) self._canvasOpen = False def open(self): """Opens a graphic window (if not already open). The window can be closed with a subsequent call to close(). """ if not self._canvasOpen: self._canvasOpen = True _graphicsManager._openCanvases.add(self) if not self._canvas: _graphicsManager.addCommandToQueue(('create canvas', self, self._width, self._height, self._backgroundColor, self._title, self._autoRefresh)) else: _graphicsManager.addCommandToQueue(("open canvas", self)) if self._autoRefresh: self.refresh(True) def add(self, drawable): """Add the Drawable object to the canvas.""" if not isinstance(drawable,Drawable): raise TypeError('Only Drawable objects can be added to a Canvas') if drawable in self._contents: raise ValueError('Object already on the Canvas') _GraphicsContainer.add(self, drawable) if '_transform' not in vars(drawable): raise StandardError('Drawable instance not properly initialized (was parent constructor called?)') try: drawable._draw except: raise StandardError('Child class of Drawable must provide a _draw method') if _graphicsManager._canvases.has_key(drawable): _graphicsManager._canvases[drawable].add(self) else: _graphicsManager._canvases[drawable] = set([self]) self._canvasChanged() def remove(self, drawable): """Remove the drawable object from the canvas.""" if drawable not in self._contents: raise ValueError('Object not currently on the Canvas') _GraphicsContainer.remove(self, drawable) self._canvasChanged() def setAutoRefresh(self, autoRefresh=True): """Change the auto-refresh mode. When True (the default), every change to the canvas or to an object drawn upon the canvas will be immediately rendered to the screen. When False, all changes are recorded internally, yet not shown on the screen until the next subsequent call to the refresh() method of this canvas. This allows multiple changes to be buffered and rendered all at once. """ if not isinstance(autoRefresh,bool): raise TypeError('autoRefresh flag should be a bool') if autoRefresh and not self._autoRefresh: self.refresh() # flush the current queue self._autoRefresh = autoRefresh def refresh(self, force=False): """ Forces all internal changes to be rendered to the screen. This method is only necessary when the auto-refresh property of the canvas has previously been turned off. If force is True then the entire window is redrawn regardless of need. """ self._trueRefresh(force, None, True, True, True) def _trueRefresh(self, force, needsUpdating, position, properties, depth): _graphicsManager._refreshLock.acquire() try: _graphicsManager.addCommandToQueue(("begin refresh", self, force)) _graphicsManager._needsUpdatingInfo = (position, properties, depth) if not needsUpdating: _graphicsManager.addCommandToQueue(("update canvas", self, self._height, self._width, self._backgroundColor, self._title)) for drawable in self.getContents(): drawable._draw() else: for chain in needsUpdating: _graphicsManager.addCommandToQueue(("set chain", chain)) chain[-1][0]._draw() _graphicsManager.addCommandToQueue(("complete refresh", self), True) except: if _debug >= 1: print "Exception thrown in refresh" _graphicsManager._refreshLock.release() def saveToFile(self, filename): """Save a picture of the current canvas to a file.""" if not isinstance(filename, str): raise TypeError('filename must be a string') background = Rectangle(self.getWidth()+2, self.getHeight()+2) background.move( float(self.getWidth())/2, float(self.getHeight())/2 ) background.setFillColor(self.getBackgroundColor()) background.setBorderColor(self.getBackgroundColor()) maxDepth = 0 for o in self._contents: if o.getDepth() > maxDepth: maxDepth = o.getDepth() background.setDepth(maxDepth+1) self.add(background) self.refresh(True) _graphicsManager.addCommandToQueue(('save to file', self, filename)) self.remove(background) self.refresh() def _canvasChanged(self): self._canvasUpdated = True if self._autoRefresh: self.refresh() class Drawable(_EventTrigger): """An object that can be drawn to a graphics canvas.""" def __init__(self, reference=None): """Create a Drawable instance. referencePoint local reference point for scaling, rotating and flipping (default Point(0,0) ) """ _EventTrigger.__init__(self) if reference: if not isinstance(reference,Point): raise TypeError('reference point must be a Point instance') self._reference = reference else: self._reference = Point() self._transform = _Transformation() self._depth = [50, _ourRandom.random()] # Give an autoupdate feature to user-defined methods if not vars(self.__class__).has_key('_noAutomaticCall'): keys = vars(self.__class__).keys() for n in keys: v = vars(self.__class__)[n] try: v.__call__ if n not in ['__new__', '__init__', '_draw']: self._replaceMethod(n, v) except: pass def _replaceMethod(self, name, cs1graphicsWrapper): def wrap(self, *args, **kw): result = cs1graphicsWrapper(self, *args, **kw) self._objectChanged() return result setattr(self.__class__, name, wrap) def move(self, dx, dy): """Move the object dx units along X-axis and dy units along Y-axis. For the default coordinate system, positive dx is rightward and negative is leftward; positive dy is downard and negative is upward. """ if not isinstance(dx, (int,float)): raise TypeError('dx must be numeric') if not isinstance(dy, (int,float)): raise TypeError('dy must be numeric') self._transform = _Transformation( (1.,0.,0.,1.,dx,dy)) * self._transform self._objectChanged(True,False,False) def moveTo(self, x, y): """Move the object to align its reference point with (x,y)""" if not isinstance(x, (int,float)): raise TypeError('x must be numeric') if not isinstance(y, (int,float)): raise TypeError('y must be numeric') curRef = self.getReferencePoint() self.move(x-curRef.getX(), y-curRef.getY()) self._objectChanged(True,False,False) def rotate(self, angle): """Rotate the object around its current reference point. angle number of degrees of clockwise rotation """ if not isinstance(angle, (int,float)): raise TypeError('angle must be specified numerically') angle = -_math.pi*angle/180. p = self._localToGlobal(self._reference) trans = _Transformation((1.,0.,0.,1.)+p.get()) rot = _Transformation((_math.cos(angle),_math.sin(angle), -_math.sin(angle),_math.cos(angle),0.,0.)) self._transform = trans*(rot*(trans.inv()*self._transform)) self._objectChanged(True,False,False) def scale(self, factor): """Scale the object relative to its current reference point. factor scale is multiplied by this number (must be positive) """ if not isinstance(factor, (int,float)): raise TypeError, 'scaling factor must be a positive number' if factor<=0: raise ValueError, 'scaling factor must be a positive number' p = self._localToGlobal(self._reference) trans = _Transformation((1.,0.,0.,1.)+p.get()) sca = _Transformation((factor,0.,0.,factor,0.,0.)) self._transform = trans*(sca*(trans.inv()*self._transform)) self._objectChanged(True,False,False) def stretch(self, xFactor, yFactor, angle=0): """Stretch the shape in mutltiple direction. By default the x-axis is scaled by a factor of xFactor and the y-axis is scaled by a factor of yFactor. The optional parameter rotates the directions that the streching is performed along. """ if not isinstance(xFactor, (int,float)) or not isinstance(yFactor, (int,float)): raise TypeError, 'stretch factor must be a positive number' if xFactor<=0 or yFactor<=0: raise ValueError, 'stretch factor must be a positive number' p = self._localToGlobal(self._reference) trans = _Transformation((1.,0.,0.,1.)+p.get()) rot = _Transformation((_math.cos(angle),_math.sin(angle), -_math.sin(angle),_math.cos(angle),0.,0.)) rotinv = rot.inv() sca = _Transformation((xFactor,0.,0.,yFactor,0.,0.)) self._transform = trans*(rotinv*(sca*(rot*(trans.inv()*self._transform)))) self._objectChanged(True,False,False) def flip(self, angle=0): """Flip the object reflected about its current reference point. By default the flip is a left-to-right flip with a vertical axis of symmetry. angle a clockwise rotation of the axis of symmetry away from vertical """ if not isinstance(angle, (int,float)): raise TypeError('angle must be numeric') angle = _math.pi*angle/180. p = self._localToGlobal(self._reference) trans = _Transformation((1.,0.,0.,1.)+p.get()) rot = _Transformation((_math.cos(angle),_math.sin(angle), -_math.sin(angle),_math.cos(angle),0.,0.)) rotinv = rot.inv() invert = _Transformation((-1.,0.,0.,1.,0.,0.)) self._transform = trans*(rotinv*(invert*(rot*(trans.inv()*self._transform)))) self._objectChanged(True,False,False) def shear(self, shear, angle=0): """Shear the object relative to its current reference point. By default, points with the same y-coordinate as the reference point are left unchanged. A point d units above the reference point is shifted d * shear units to the right. The optional angle parameter rotates the axis that the shearing occurs along. angle clockwise angle for shear """ if not isinstance(shear, (int,float)): raise TypeError('shear factor must be numeric') if not isinstance(angle, (int,float)): raise TypeError('angle must be numeric') angle = _math.pi*angle/180. p = self._localToGlobal(self._reference) trans = _Transformation((1.,0.,0.,1.)+p.get()) rot = _Transformation((_math.cos(angle),_math.sin(angle), -_math.sin(angle),_math.cos(angle),0.,0.)) rotinv = rot.inv() sh = _Transformation((1.,-shear,0.,1.,0.,0.)) self._transform = trans*(rotinv*(sh*(rot*(trans.inv()*self._transform)))) self._objectChanged(True,False,False) def getReferencePoint(self): """Return a copy of the current reference point. Note that mutating that copy has no effect on the Drawable object. """ return self._localToGlobal(self._reference) def adjustReference(self, dx, dy): """Move the local reference point relative to its current position. Note that the object is not moved at all. """ if not isinstance(dx, (int,float)): raise TypeError('dx must be numeric') if not isinstance(dy, (int,float)): raise TypeError('dy must be numeric') p = self._localToGlobal(self._reference) p = Point(p.getX()+dx, p.getY()+dy) self._reference = self._globalToLocal(p) def setDepth(self, depth): """Set the depth of the object. Objects with a higher depth will be rendered behind those with lower depths. """ if not isinstance(depth, (int,float)): raise TypeError('depth must be numeric') self._depth[-2] = depth self._objectChanged(False,False,True) def getDepth(self): """Return the depth of the object.""" return self._depth[-2] def clone(self): """Return a duplicate of the drawable object. Note that the duplicate is not automatically added to any canvases or layers, even if original is currently so. """ return _copy.deepcopy(self) def _localToGlobal(self, point): if not isinstance(point,Point): raise TypeError('parameter must be a Point instance') return self._transform.image(point) def _globalToLocal(self, point): if not isinstance(point,Point): raise TypeError('parameter must be a Point instance') return self._transform.inv().image(point) def _beginDraw(self): _graphicsManager.addCommandToQueue(("begin draw", self)) def _completeDraw(self): _graphicsManager.addCommandToQueue(("complete draw", self)) def _draw(self): """Cause the object to be drawn (typically, the method is not called directly).""" if _debug>=2: print "within Drawable._draw for self=",self raise NotImplementedError('_draw() method must be implemented for each shape.') def _objectChanged(self, position=True, properties=True, depth=True): """Designate that some trait of this object has been mutated. As a result, all of its rendered images may need to be updated. """ if _graphicsManager: _graphicsManager.objectChanged(self, position, properties, depth) class Layer(Drawable, _GraphicsContainer): """A composite that represents a group of shapes as a single drawable object. Objects are placed onto the layer relative to the coordinate system of the layer itself. The layer can then be placed onto a canvas (or even onto another layer). """ def __init__(self): """Construct a new Layer instance. The layer is initially empty. The reference point of that layer is initially the origin in its own coordinate system, (0,0). """ Drawable.__init__(self) _GraphicsContainer.__init__(self) def _noAutomaticCall(self): pass def add(self, drawable): """Add the Drawable object to the layer.""" if _debug>=2: print 'Call to Layer.add with self=',self,' drawable=',drawable if not isinstance(drawable,Drawable): raise TypeError('parameter must be an instance of a Drawable object') if drawable in self._contents: raise ValueError('The object is already on the Layer') if '_transform' not in vars(drawable): raise StandardError('Drawable not properly initialized (was parent constructor called?)') try: drawable._draw except: raise StandardError('Drawable class must have a _draw method') _GraphicsContainer.add(self, drawable) self._objectChanged() def remove(self, drawable): """Remove the Drawable object from the layer. A ValueError is raised if the drawable is not currently in the layer. """ if drawable not in self._contents: raise ValueError('object not currently on the Layer') _GraphicsContainer.remove(self,drawable) self._objectChanged() def _draw(self): self._beginDraw() for shape in self.getContents(): shape._draw() self._completeDraw() class Shape(Drawable): """A drawable objects that has a border.""" def __init__(self, reference=None): """Construct a Shape instance. reference the initial placement of the shape's reference point. (default Point(0,0) ) """ if reference and not isinstance(reference,Point): raise TypeError('reference point must be a Point instance') Drawable.__init__(self, reference) self._borderColor = Color("Black") self._borderWidth = 1 def setBorderColor(self, color): """ Set the border color to a copy of the indicated color. The parameter can be either: - a string with the name of the color - an (r,g,b) tuple - an existing Color instance """ old = self._borderColor if isinstance(color,Color): self._borderColor = color else: try: self._borderColor = Color(color) except TypeError,te: raise TypeError(str(te)) except ValueError,ve: raise ValueError(str(ve)) self._objectChanged(False,True,False) if self._borderColor is not old: self._borderColor._register(self) if not isinstance(self,FillableShape) or old is not self._fillColor: # this shape no longer using the old color old._unregister(self) def getBorderColor(self): """Return the color of the object's border.""" return self._borderColor def setBorderWidth(self, width): """ Set the width of the border to the indicated width. """ if not isinstance(width, (int,float)): raise TypeError('Border width must be non-negative number') if width < 0: raise ValueError("A shape's border width cannot be negative.") self._borderWidth = width self._objectChanged(False,True,False) def getBorderWidth(self): """Return the width of the border.""" return self._borderWidth class FillableShape(Shape): """A shape that can be filled with an interior color.""" def __init__(self, reference=None): """Construct a new FillableShape instance. The interior color defaults to 'Transparent'. reference the initial placement of the shape's reference point. (default Point(0,0) ) """ if reference and not isinstance(reference,Point): raise TypeError('reference point must be a Point instance') Shape.__init__(self, reference) self._fillColor = Color("Transparent") def setFillColor(self, color): """Set the interior color of the shape to the color. The parameter can be either: - a string with the name of the color - an (r,g,b) tuple - an existing Color instance """ old = self._fillColor if isinstance(color,Color): self._fillColor = color else: try: self._fillColor = Color(color) except TypeError,te: raise TypeError(str(te)) except ValueError,ve: raise ValueError(str(ve)) self._objectChanged(False,True,False) if self._fillColor is not old: self._fillColor._register(self) if self._borderColor is not old: # no longer using the old color old._unregister(self) def getFillColor(self): """Return the color of the shape's interior.""" return self._fillColor class Circle(FillableShape): """A circle that can be drawn to a canvas.""" def __init__(self, radius=10, centerPt=None): """Construct a new instance of Circle. radius the circle's radius (default 10) centerPt a Point representing the placement of the circle's center (default Point(0,0) ) The reference point for a circle is originally its center. """ if not isinstance(radius, (int,float)): raise TypeError('Radius must be a number') if radius <= 0: raise ValueError("The circle's radius must be positive.") if centerPt and not isinstance(centerPt,Point): raise TypeError("The circle's center must be specified as a Point") FillableShape.__init__(self) # intentionally not sending center if not centerPt: centerPt = Point() self._transform = _Transformation( (radius,0.,0.,radius,centerPt.getX(),centerPt.getY()) ) def _noAutomaticCall(self): pass def setRadius(self, r): """Set the radius of the circle to r.""" if not isinstance(r, (int,float)): raise TypeError('Radius must be a number') if r <= 0: raise ValueError("The circle's radius must be positive.") factor = float(r)/self.getRadius() self._transform = self._transform * _Transformation((factor,0.,0.,factor,0.,0.)) self._objectChanged(True,False,False) def getRadius(self): """Return the radius of the circle.""" return _math.sqrt(self._transform._matrix[0]**2 + self._transform._matrix[1]**2) def _draw(self): self._beginDraw() _graphicsManager.addCommandToQueue(('draw circle', self)) self._completeDraw() class Rectangle(FillableShape): """A rectangle that can be drawn to the canvas.""" def __init__(self, w=20, h=10, centerPt=None): """ Construct a new instance of a Rectangle. The reference point for a rectangle is its center. w the width of the rectangle (default 20) h the height of the rectangle (default 10) centerPt a Point representing the placement of the rectangle's center (default Point(0,0) ) """ if not isinstance(w, (int,float)): raise TypeError('Width must be a number') if w <= 0: raise ValueError('The width must be positive.') if not isinstance(h, (int,float)): raise TypeError('Height must be a number') if h <= 0: raise ValueError('The height must be positive.') if centerPt and not isinstance(centerPt,Point): raise TypeError('center must be specified as a Point') FillableShape.__init__(self) # intentionally not sending center point if not centerPt: centerPt = Point(0,0) self._transform = _Transformation( (w, 0., 0., h, centerPt.getX(), centerPt.getY()) ) def _noAutomaticCall(self): pass def getWidth(self): """Return the width of the rectangle.""" return _math.sqrt(self._transform._matrix[0]**2 + self._transform._matrix[2]**2) def getHeight(self): """Return the height of the rectangle.""" return _math.sqrt(self._transform._matrix[1]**2 + self._transform._matrix[3]**2) def setWidth(self, w): """Set the width of the rectangle to w.""" if not isinstance(w, (int,float)): raise TypeError('Width must be a positive number') if w <= 0: raise ValueError("The rectangle's width must be positive") factor = float(w) / self.getWidth() p = self._localToGlobal(self._reference) trans = _Transformation((1.,0.,0.,1.)+p.get()) sca = _Transformation((factor,0.,0.,1.,0.,0.)) self._transform = trans*(sca*(trans.inv()*self._transform)) self._objectChanged(True,False,False) def setHeight(self, h): """Set the height of the rectangle to h.""" if not isinstance(h, (int,float)): raise TypeError('Height must be a positive number') if h <= 0: raise ValueError("The rectangle's height must be positive") factor = float(h) / self.getHeight() p = self._localToGlobal(self._reference) trans = _Transformation((1.,0.,0.,1.)+p.get()) sca = _Transformation((1.,0.,0.,factor,0.,0.)) self._transform = trans*(sca*(trans.inv()*self._transform)) self._objectChanged(True,False,False) def _draw(self): self._beginDraw() _graphicsManager.addCommandToQueue(('draw rectangle', self)) self._completeDraw() class Square(Rectangle): """A square that can be drawn to the canvas.""" def __init__(self, size=10, centerPt=None): """ Construct a new Square instance. The reference point for a square is its center. size the dimension of the square (default 10) centerPt a Point representing the placement of the rectangle's center (defaults Point(0,0) ) """ if not isinstance(size, (int,float)): raise TypeError('size must be a number') if size <= 0: raise ValueError("The size must be positive.") if centerPt and not isinstance(centerPt,Point): raise TypeError('center must be specified as a Point') Rectangle.__init__(self, size, size, centerPt) def _noAutomaticCall(self): pass def getSize(self): """Return the length of a side of the square.""" return self.getWidth() def setSize(self, s): """Set the width and height of the square to s.""" if not isinstance(s, (int,float)): raise TypeError('size must be a number') if s <= 0: raise ValueError('The size must be positive.') Rectangle.setWidth(self, s) Rectangle.setHeight(self, s) def setWidth(self, w): """Set the width and height of the square to w.""" if not isinstance(w, (int,float)): raise TypeError('width must be a positive number') if w <= 0: raise ValueError("The square's width must be positive") self.setSize(w) def setHeight(self, h): """Set the width and height of the square to h.""" if not isinstance(h, (int,float)): raise TypeError('height must be a positive number') if h <= 0: raise ValueError("The square's height must be positive") self.setSize(h) class Path(Shape): """ A path that can be drawn to a canvas. """ def __init__(self, *points): """ Construct a new instance of a Path. The path is described as a series of points that are connected in order. These points can be initialized by sending each individual Point as a separate parameter, or by sending a single parameter containing a sequence of Points. If no parameters are sent, the path initially has zero points. The reference point for a path is initially aligned with the first point of the path. """ Shape.__init__(self) if len(points)==1: try: points = tuple(points[0]) except: pass # original parameter might be a single Point for p in points: if not isinstance(p,Point): raise TypeError('non-Point specified as parameter') self._points = list(points) if len(self._points)>=1: self.adjustReference(self._points[0].getX(), self._points[0].getY()) def _noAutomaticCall(self): pass def addPoint(self, point, index=-1): """Add a new point to the Path. point a Point instance index designates where on the path the new point is placed (at the end, by default) """ if not isinstance(point,Point): raise TypeError('parameter must be a Point instance') if index>-1: self._points.insert(index, point) else: self._points.append(point) if len(self._points)==1: # first point added self._reference = Point(point.getX(), point.getY()) self._objectChanged(True,False,False) def deletePoint(self, index=-1): """Remove the Point at the given index. By default, deletes the last point.""" if not isinstance(index,int): raise TypeError('index must be an integer') try: self._points.pop(index) except: raise IndexError('index out of range') self._objectChanged(True,False,False) def clearPoints(self): """Remove all points, setting this back to an empty Path.""" self._points = list() self._objectChanged(True,False,False) def getNumberOfPoints(self): """Return the current number of points.""" return len(self._points) def getPoint(self, index): """Return a copy of the Point at the given index. Subsequently mutating that copy has no effect on the Path. """ if not isinstance(index,int): raise TypeError('index must be an integer') try: p = self._points[index] except: raise IndexError('index out of range') return Point(p.getX(), p.getY()) def setPoint(self, point, index=-1): """Change the Point at the given index to a new value. By default, the last point is changed. """ if not isinstance(index,int): raise TypeError('index must be an integer') if not isinstance(point,Point): raise TypeError('first parameter must be a Point instance') try: self._points[index] = point except: raise IndexError('index out of range') self._objectChanged(True,False,False) def getPoints(self): """Return a list of Point instances that are copies of the points on the Path.""" return list(self._points) def _draw(self): self._beginDraw() _graphicsManager.addCommandToQueue(("draw path", self)) self._completeDraw() class Polygon(Path,FillableShape): """A polygon that can be drawn to a canvas.""" def __init__(self, *points): """Construct a new Polygon instance. The polygon is described as a series of points that are connected in order. The last point is automatically connected back to the first to close the polygon. These points can be initialized by sending each individual Point as a separate parameter, or by sending a single parameter containing a sequence of Points. If no parameters are sent, the polygon initially has zero points. The reference point for a polygon is initially aligned with the first point of the polygon. """ FillableShape.__init__(self) try: Path.__init__(self, points) except TypeError,te: raise TypeError(str(te)) def _noAutomaticCall(self): pass def _draw(self): self._beginDraw() _graphicsManager.addCommandToQueue(("draw polygon", self)) self._completeDraw() #class Image(Drawable): #def __init__(self, filename): #Drawable.__init__(self) #if not isinstance(filename,str): #raise TypeError('filename must be a string') #self._filename = filename # of course, we won't know if this is legitimate until it is added to a canvas #def rotate(self,angle): #"""Not yet implemented.""" #raise NotImplementedError('rotating image is not yet implemented') #def scale(self,factor): #"""Not yet implemented.""" #raise NotImplementedError('scaling image is not yet implemented') #def _draw(self): #self._beginDraw() #_graphicsManager.addCommandToQueue(("draw image", self)) #self._completeDraw() class Text(Drawable): """A piece of text that can be drawn to a canvas.""" def __init__(self, message='', fontsize=12, centerPt=None): """ Construct a new Text instance. The text color is initially black, although this can be changed by setColor. The reference point for the text is initially its center. message a string which is to be displayed (default empty string) fontsize the font size (default 12) centerPt where to locate the center of the text (default Point(0,0) ) """ if not isinstance(message,str): raise TypeError('message must be a string') if not isinstance(fontsize,int): raise TypeError('fontsize must be an integer') if fontsize <= 0: raise ValueError('fontsize must be positive') if centerPt and not isinstance(centerPt, Point): raise TypeError('center must be a Point') Drawable.__init__(self) self._text = message self._size = fontsize self._color = Color("black") if centerPt: self.move(centerPt.getX(), centerPt.getY()) def _noAutomaticCall(self): pass def setMessage(self, message): """Set the string to be displayed. message a string """ if not isinstance(message,str): raise TypeError('message must be a string') self._text = message self._objectChanged(False,True,False) def getMessage(self): """Return the current string.""" return self._text def setFontColor(self, color): """Set the color of the font. The parameter can be either: - a string with the name of the color - an (r,g,b) tuple - an existing Color instance """ old = self._color if isinstance(color,Color): self._color = color else: try: self._color = Color(color) except TypeError,te: raise TypeError(str(te)) except ValueError,ve: raise ValueError(str(ve)) if self._color is not old: self._color._register(self) # no longer using the old color old._unregister(self) self._objectChanged(False,True,False) def getFontColor(self): """Return the current font color.""" return self._color def setFontSize(self, fontsize): """Set the font size.""" if not isinstance(fontsize,int): raise TypeError('fontsize must be an integer') if fontsize <= 0: raise ValueError('fontsize must be positive') self._size = fontsize self._objectChanged(False,True,False) def getFontSize(self): """Return the current font size.""" return self._size def rotate(self,angle): """Not yet implemented.""" raise NotImplementedError('rotating text is not yet implemented') def scale(self,factor): """Not yet implemented.""" raise NotImplementedError('scaling text is not yet implemented. Use setSize to change font') def getDimensions(self): """Return a (width,height) tuple measuring visual dimensions of currently displayed message.""" #return .75*self._size*(2+len(self._text)), 2.*self._size return _graphicsManager.addCommandToQueue(("get text size", self._text, self._size), True) def _draw(self): self._beginDraw() _graphicsManager.addCommandToQueue(("draw text", self)) self._completeDraw() class Button(Text, Rectangle, EventHandler): """A button that can respond to events.""" def __init__(self, message="", centerPt=None): """Create a new button. The width and height of the button automatically adjust to the size of the displayed text. message the text to display on the button centerPt where to place the center of the button """ Text.__init__(self, message) w, h = self.getDimensions() Rectangle.__init__(self, w+self._size, h+self._size, centerPt) EventHandler.__init__(self) self._baseBorderWidth = self._borderWidth self.setFillColor("white") self.addHandler(self) def _noAutomaticCall(self): pass def _resize(self): w, h = self.getDimensions() self.setWidth(w+self._size) self.setHeight(h+self._size) def handle(self, event): """Highlight the button when the button is clicked.""" if _debug >= 3: print "Button self handler" if event._eventType == "mouse click": Rectangle.setBorderWidth(self, self._baseBorderWidth + 2) elif event._eventType == "mouse release": Rectangle.setBorderWidth(self, self._baseBorderWidth) def _draw(self): self._beginDraw() Rectangle._draw(self) Text._draw(self) self._completeDraw() def setBorderWidth(self, width): self._baseBorderWidth = width Rectangle.setBorderWidth(self, width) def setMessage(self, message): """Changes the button's text to message and automatically resizes the button.""" Text.setMessage(self, message) self._resize() def setFontSize(self, fontsize): """Changes the button's text size and automatically resizes the button.""" Text.setFontSize(self, fontsize) self._resize() class TextBox(Text, Rectangle, EventHandler): """Widget for text entry.""" def __init__(self, width=100, height=50, centerPt=None): """Construct a box to enter text into. width the width of the box height the height of the box centerPt the location of the boxes center """ Text.__init__(self, '', 12, centerPt) Rectangle.__init__(self, width, height, centerPt) self.setFillColor('white') # rectangle interior was transparent by default EventHandler.__init__(self) self.addHandler(self) def _noAutomaticCall(self): pass def _draw(self): self._beginDraw() Rectangle._draw(self) # access the overridden Rectangle version of _draw Text._draw(self) # access the overridden Text version of _draw self._completeDraw() def handle(self, event): """When the text box is in focus append any keypress to the display text.""" if event._eventType == 'keyboard': if event.getKey() == '\b': self.setMessage(self.getMessage()[:-1]) else: self.setMessage(self.getMessage() + event.getKey()) class Timer(_EventTrigger): def __init__(self, delay, repeat=False): _EventTrigger.__init__(self) self._delay = delay self._repeat = repeat self._running = False self._handlers = list() def start(self, force=False): if not self._running or force: self._running = True self._thread = _TimerThread(self, self._delay) self._thread.start() def stop(self): self._running = False def addHandler(self, handler): if not isinstance(handler, EventHandler): raise TypeError('Only child classes of EventHandler can handle events') if handler not in self._handlers: self._handlers.append(handler) else: raise ValueError('Handler is already associated to the shape') def removeHandler(self, handler): if handler in self._handlers: self._handlers.remove(handler) else: raise ValueError('Cannot remove hander from shape it is not associated to') class _TimerThread(_threading.Thread): def __init__(self, timer, delay): _threading.Thread.__init__(self) self._timer = timer self._delay = delay def run(self): _time.sleep(self._delay) if self._timer._repeat and self._timer._running: self._timer.start(True) if self._timer._running: for handler in self._timer._handlers: e = Event() e._eventType = 'timer' handler.handle(e) class Monitor(object): """Monitor class for thread synchronization.""" def __init__(self): """Create a new monitor instance.""" self._lock = _threading.Lock() self._lock.acquire() def wait(self): """Wait for the monitor to be released by another thread.""" self._lock.acquire() def release(self): """Release a thread that is waiting on the monitor.""" if self._lock.locked(): self._lock.release() try: import Tkinter as _Tkinter _tkroot = None _underlyingLibrary = 'Tkinter' class _TkGraphicsManager(_GraphicsManager): def removeUnderlying(self, chain): if _debug >= 2: print "Removing underlying object", chain chain[0][0]._canvas._canvas.delete(self._underlyingObject[chain]._object) self._underlyingObject.pop(chain) _UnderlyingManager = _TkGraphicsManager class _RenderedCanvas(object): def __init__(self, canvas, w, h, background, title, refresh): if _debug >= 2: print "Creating _RenderedCanvas" self._parent = canvas self._tkWin = _Tkinter.Toplevel() self._tkWin.protocol("WM_DELETE_WINDOW", self._parent.close) self._tkWin.title(title) self._canvas = _Tkinter.Canvas(self._tkWin,width=w,height=h,background=getTkColor(background)) self._canvas.pack(expand=False,side=_Tkinter.TOP) self._tkWin.resizable(0,0) # Setup function to deal with events callback = lambda event : self._handleEvent(event) self._canvas.bind('