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