1# Copyright (c) 2004 Divmod. 2# See LICENSE for details. 3 4from zope.interface import implements 5 6from twisted.internet import defer 7from twisted.python import log 8 9from nevow import inevow, rend, loaders, static, url, tags, util 10from nevow.flat import flatten 11from nevow.stan import Proto, Tag 12from itertools import count 13 14cn = count().next 15cookie = lambda: str(cn()) 16 17_hookup = {} 18 19## If we need to use Canvas through a CGI which forwards to the appserver, 20## then we will need to listen with the canvas protocol on another socket 21## so the canvas movie can push data to us. Here is where we will keep it. 22_canvasCGIService = None 23 24# m = method 25# a = argument 26# <m n="moveTo" t="canvas"> 27# <a v="16" /> 28# <a v="16" /> 29# </m> 30 31 32m = Proto('m') # method call; contains arguments 33a = Proto('a') # argument; has v="" attribute for simple value, or <l> or <d> child for list or dict value 34l = Proto('l') # list; has <a> children; <a> children must be simple values currently 35d = Proto('d') # dict; has <i> children 36i = Proto('i') # dict item; has k="" for key and v="" for simple value (no nested dicts yet) 37 38 39def squish(it): 40 if isinstance(it, Tag): 41 return a[it] 42 return a(v=it) 43 44 45class _Remoted(object): 46 def __init__(self, cookie, canvas): 47 self.cookie = cookie 48 self.canvas = canvas 49 50 51class Text(_Remoted): 52 x = 0 53 y = 0 54 def change(self, text): 55 self.text = text 56 self.canvas.call('changeText', self.cookie, text) 57 58 def move(self, x, y): 59 self.x = x 60 self.y = y 61 self.canvas.call('moveText', self.cookie, x, y) 62 63 def listFonts(self): 64 if hasattr(self.canvas, '_fontList'): 65 return defer.succeed(self.canvas._fontList) 66 cook = cookie() 67 self.canvas.deferreds[cook] = d = defer.Deferred() 68 self.canvas.call('listFonts', cook) 69 def _cb(l): 70 L = l.split(',') 71 self.canvas._fontList = L 72 return L 73 return d.addCallback(_cb) 74 75 def font(self, font): 76 self.canvas.call('font', self.cookie, font) 77 78 def size(self, size): 79 self.canvas.call('size', self.cookie, size) 80 81 82class Image(_Remoted): 83 def move(self, x, y): 84 self.canvas.call('moveImage', self.cookie, x, y) 85 86 def scale(self, x, y): 87 self.canvas.call('scaleImage', self.cookie, x, y) 88 89 def alpha(self, alpha): 90 self.canvas.call('alphaImage', self.cookie, alpha) 91 92 def rotate(self, angle): 93 self.canvas.call('rotateImage', self.cookie, angle) 94 95 96class Sound(_Remoted): 97 def play(self, offset=0, timesLoop=0): 98 """Play the sound, starting at "offset", in seconds. Loop the sound "timesLoop" 99 times. 100 """ 101 self.canvas.call('playSound', self.cookie, offset, timesLoop) 102 103 104class GroupBase(object): 105 def call(self, method, *args): 106 """Call a client-side method with the given arguments. Arguments 107 will be converted to strings. You should probably use the other higher-level 108 apis instead. 109 """ 110 flatcall = flatten( 111 m(n=method, t=self.groupName)[[ 112 squish(x) for x in args if x is not None]]) 113 self.socket.write(flatcall + '\0') 114 115 groupx = 0 116 groupy = 0 117 def reposition(self, x, y): 118 """Reposition all the elements in this group 119 """ 120 self.groupx = x 121 self.groupy = y 122 self.call('reposition', x, y) 123 124 def rotate(self, angle): 125 """Rotate all the elements of this group 126 """ 127 self.call('rotate', angle) 128 129 _alpha = 100 130 def alpha(self, percent): 131 """Set the alpha value of this group 132 """ 133 self._alpha = percent 134 self.call('alpha', percent) 135 136 def line(self, x, y): 137 """Draw a line from the current point to the given point. 138 139 (0,0) is in the center of the canvas. 140 """ 141 self.call('line', x, y) 142 143 x = 0 144 y = 0 145 def move(self, x, y): 146 """Move the pen to the given point. 147 148 (0, 0) is in the center of the canvas. 149 """ 150 self.x = x 151 self.y = y 152 self.call('move', x, y) 153 154 def pen(self, width=None, rgb=None, alpha=None): 155 """Change the current pen attributes. 156 157 width: an integer between 0 and 255; the pen thickness, in pixels. 158 rgb: an integer between 0x000000 and 0xffffff 159 alpha: an integer between 0 and 100; the opacity of the pen 160 """ 161 self.call('pen', width, rgb, alpha) 162 163 def clear(self): 164 """Clear the current pen attributes. 165 """ 166 self.call('clear') 167 168 def fill(self, rgb, alpha=100): 169 """Set the current fill. Fill will not be drawn until close is called. 170 171 rgb: color of fill, integer between 0x000000 and 0xffffff 172 alpha: an integer between 0 and 100; the opacity of the fill 173 """ 174 self.call('fill', rgb, alpha) 175 176 def close(self): 177 """Close the current shape. A line will be drawn from the end point 178 to the start point, and the shape will be filled with the current fill. 179 """ 180 self.call('close') 181 182 def curve(self, controlX, controlY, anchorX, anchorY): 183 """Draw a curve 184 """ 185 self.call('curve', controlX, controlY, anchorX, anchorY) 186 187 def gradient(self, type, colors, alphas, ratios, matrix): 188 """Draw a gradient. Currently the API for this sucks, see the flash documentation 189 for info. Higher level objects for creating gradients will hopefully be developed 190 eventually. 191 """ 192 self.call('gradient', type, 193 l[[a(v=x) for x in colors]], 194 l[[a(v=x) for x in alphas]], 195 l[[a(v=x) for x in ratios]], 196 d[[i(k=k, v=v) for (k, v) in matrix.items()]]) 197 198 def text(self, text, x, y, height, width): 199 """Place the given text on the canvas using the given x, y, height and width. 200 The result is a Text object which can be further manipulated to affect the text. 201 """ 202 cook = cookie() 203 t = Text(cook, self) 204 t.text = text 205 self.call('text', cook, text, x, y, height, width) 206 return t 207 208 def image(self, where): 209 """Load an image from the URL "where". The result is an Image object which 210 can be further manipulated to move it or change rotation. 211 """ 212 cook = cookie() 213 I = Image(cook, self) 214 self.call('image', cook, where) 215 print "IMAGE", where 216 return I 217 218 def sound(self, where, stream=True): 219 """Load an mp3 from the URL "where". The result is a Sound object which 220 can be further manipulated. 221 222 If stream is True, the sound will play as soon as possible. If false, 223 """ 224 cook = cookie() 225 S = Sound(cook, self) 226 self.call('sound', cook, where, stream and 1 or 0) 227 return S 228 229 def group(self): 230 """Create a new group of shapes. The returned object will 231 have all of the same APIs for drawing, except the grouped 232 items can all be moved simultaneously, deleted, etc. 233 """ 234 cook = cookie() 235 G = Group('%s.G_%s' % (self.groupName, cook), self.socket, self) 236 self.call('group', cook) 237 return G 238 239 240class Group(GroupBase): 241 def __init__(self, groupName, socket, canvas): 242 self.groupName = groupName 243 self.socket = socket 244 self.canvas = canvas 245 self.deferreds = canvas.deferreds 246 247 closed = property(lambda self: self.canvas.closed) 248 249 def setMask(self, other=None): 250 """Set the mask of self to the group "other". "other" must be a Group 251 instance, if provided. If not provided, any previous mask will be removed 252 from self. 253 """ 254 if other is None: 255 self.call('setMask', '') 256 else: 257 self.call('setMask', other.groupName) 258 259 def setVisible(self, visible): 260 self.call('setVisible', str(bool(visible))) 261 262 xscale = 100 263 yscale = 100 264 def scale(self, x, y): 265 self.call('scale', x, y) 266 267 def swapDepth(self, intOrGroup): 268 """Swap the z-order depth of this group with another. 269 If an int is provided, the group will be placed at that depth, 270 regardless of whether there is an existing clip there. 271 If a group is provided, the z depth of self and the other group 272 are swapped. 273 """ 274 if isinstance(intOrGroup, Group): 275 self.call('swapGroup', intOrGroup.groupName) 276 else: 277 self.call('swapInt', intOrGroup) 278 279 def depth(self): 280 """Return a deferred which will fire the depth of this group. 281 XXX TODO 282 """ 283 return 0 284 285 286class CanvasSocket(GroupBase): 287 """An object which represents the client-side canvas. Defines APIs for drawing 288 on the canvas. An instance of this class will be passed to your onload callback. 289 """ 290 implements(inevow.IResource) 291 292 groupName = 'canvas' 293 294 closed = False 295 def __init__(self): 296 self.canvas = self 297 self.d = defer.Deferred().addErrback(log.err) 298 299 def locateChild(self, ctx, segs): 300 self.cookie = segs[0] 301 return (self, ()) 302 303 def renderHTTP(self, ctx): 304 try: 305 self.deferreds = {} 306 self.buffer = '' 307 ## Don't try this at home kids! You'll blow your arm off! 308 self.socket = inevow.IRequest(ctx).transport 309 ## We be hijackin' 310 self.socket.protocol = self 311 ## This request never finishes until the user leaves the page 312 self.delegate = _hookup[self.cookie] 313 self.delegate.onload(self) 314 del _hookup[self.cookie] 315 except: 316 log.err() 317 return self.d 318 319 def dataReceived(self, data): 320 self.buffer += data 321 while '\0' in self.buffer: 322 I = self.buffer.index('\0') 323 message = self.buffer[:I] 324 self.buffer = self.buffer[I+1:] 325 self.gotMessage(message) 326 327 def gotMessage(self, message): 328 I = message.index(' ') 329 handler = getattr(self, 'handle_%s' % (message[:I], ), None) 330 if handler is not None: 331 handler(message[I+1:]) 332 else: 333 self.deferreds[message[:I]].callback(message[I+1:]) 334 del self.deferreds[message[:I]] 335 336 def connectionLost(self, reason): 337 self.closed = True 338 del self.socket 339 340 def done(self): 341 """Done drawing; close the connection with the movie 342 """ 343 ## All done with the request object 344 self.closed = True 345 self.d.callback('') 346 347 def handle_onKeyDown(self, info): 348 if self.delegate.onKeyDown: 349 self.delegate.onKeyDown(self, chr(int(info))) 350 351 def handle_onKeyUp(self, info): 352 if self.delegate.onKeyUp: 353 self.delegate.onKeyUp(self, chr(int(info))) 354 355 def handle_onMouseUp(self, info): 356 if self.delegate.onMouseUp: 357 self.delegate.onMouseUp(self, *map(int, map(float, info.split()))) 358 359 def handle_onMouseDown(self, info): 360 if self.delegate.onMouseDown: 361 self.delegate.onMouseDown(self, *map(int, map(float, info.split()))) 362 363 def handle_onMouseMove(self, info): 364 if self.delegate.onMouseMove: 365 self.delegate.onMouseMove(self, *map(int, map(float, info.split()))) 366 367 def handle_diagnostic(self, info): 368 print "Trace", info 369 370canvasServerMessage = loaders.stan(tags.html["This server dispatches for nevow canvas events."]) 371 372 373def canvas(width, height, delegate, useCGI=False): 374 C = cookie() 375 if useCGI: 376 global _canvasCGIService 377 if _canvasCGIService is None: 378 from nevow import appserver 379 # Import reactor here to avoid installing default at startup 380 from twisted.internet import reactor 381 _canvasCGIService = reactor.listenTCP(0, appserver.NevowSite(Canvas(docFactory=canvasServerMessage))) 382 _canvasCGIService.dispatchMap = {} 383 port = _canvasCGIService.getHost().port 384 prefix = '/' 385 movie_url = url.here.click('/').secure(False, port) 386 else: 387 movie_url = url.here 388 port = lambda c, d: inevow.IRequest(c).transport.server.port 389 def prefix(c, d): 390 pre = inevow.IRequest(c).path 391 if pre.endswith('/'): 392 return pre 393 return pre + '/' 394 395 _hookup[C] = delegate 396 handlerInfo = [] 397 for handlerName in ['onMouseMove', 'onMouseDown', 'onMouseUp', 'onKeyDown', 'onKeyUp']: 398 if getattr(delegate, handlerName, None) is not None: 399 handlerInfo.append((handlerName, 1)) 400 401 movie_url = movie_url.child('nevow_canvas_movie.swf').add('cookie', C).add('port', port).add('prefix', prefix) 402 for (k, v) in handlerInfo: 403 movie_url = movie_url.add(k, v) 404 405 return tags._object(classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000", 406 codebase="http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=7,0,0,0", 407 width=width, height=height, id=("Canvas-", C), align="middle")[ 408 tags.param(name="allowScriptAccess", value="sameDomain"), 409 tags.param(name="movie", value=movie_url), 410 tags.param(name="quality", value="high"), 411 tags.param(name="scale", value="noscale"), 412 tags.param(name="bgcolor", value="#ffffff"), 413 Tag('embed')( 414 src=movie_url, 415 quality="high", 416 scale="noscale", 417 bgcolor="#ffffff", 418 width=width, 419 height=height, 420 name=("Canvas-", C), 421 align="middle", 422 allowScriptAccess="sameDomain", 423 type="application/x-shockwave-flash", 424 pluginspage="http://www.macromedia.com/go/getflashplayer")] 425 426 427class Canvas(rend.Page): 428 """A page which can embed canvases. Simplest usage is to subclass and 429 override width, height and onload. Then, putting render_canvas in the 430 template will output that canvas there. 431 432 You can also embed more than one canvas in a page using the canvas 433 helper function, canvas(width, height, onload). The resulting stan 434 will cause a canvas of the given height and width to be embedded in 435 the page at that location, and the given onload callable to be called 436 with a CanvasSocket when the connection is established. 437 """ 438 addSlash = True 439 def __init__(self, original=None, width=None, height=None, onload=None, 440 onMouseMove=None, onMouseDown=None, onMouseUp=None, 441 onKeyDown=None, onKeyUp=None, **kw): 442 rend.Page.__init__(self, original, **kw) 443 if width: self.width = width 444 if height: self.height = height 445 if onload: self.onload = onload 446 if onMouseMove: self.onMouseMove = onMouseMove 447 if onMouseDown: self.onMouseDown = onMouseDown 448 if onMouseUp: self.onMouseUp = onMouseUp 449 if onKeyDown: self.onKeyDown = onKeyDown 450 if onKeyUp: self.onKeyUp = onKeyUp 451 452 def child_canvas_socket(self, ctx): 453 return CanvasSocket() 454 455 width = 1000 456 height = 500 457 458 onload = None 459 onMouseDown = None 460 onMouseUp = None 461 onMouseMove = None 462 onKeyUp = None 463 onKeyDown = None 464 465 def render_canvas(self, ctx, data): 466 return canvas( 467 self.width, self.height, self) 468 469 docFactory = loaders.stan(tags.html[render_canvas]) 470 471setattr(Canvas, 'child_nevow_canvas_movie.swf', static.File( 472 util.resource_filename('nevow', 'Canvas.swf'), 473 'application/x-shockwave-flash')) 474 475