001: /*
002: *
003: *
004: * Copyright 1990-2007 Sun Microsystems, Inc. All Rights Reserved.
005: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER
006: *
007: * This program is free software; you can redistribute it and/or
008: * modify it under the terms of the GNU General Public License version
009: * 2 only, as published by the Free Software Foundation.
010: *
011: * This program is distributed in the hope that it will be useful, but
012: * WITHOUT ANY WARRANTY; without even the implied warranty of
013: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
014: * General Public License version 2 for more details (a copy is
015: * included at /legal/license.txt).
016: *
017: * You should have received a copy of the GNU General Public License
018: * version 2 along with this work; if not, write to the Free Software
019: * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
020: * 02110-1301 USA
021: *
022: * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa
023: * Clara, CA 95054 or visit www.sun.com if you need additional
024: * information or have any questions.
025: */
026:
027: package com.sun.perseus.midp;
028:
029: import com.sun.perseus.builder.ModelBuilder;
030:
031: import com.sun.perseus.model.SimpleCanvasManager;
032: import com.sun.perseus.model.CanvasUpdateListener;
033: import com.sun.perseus.model.DocumentNode;
034: import com.sun.perseus.model.ModelEvent;
035: import com.sun.perseus.model.ModelNode;
036: import com.sun.perseus.model.SMILSample;
037: import com.sun.perseus.model.Time;
038:
039: import com.sun.perseus.j2d.RenderGraphics;
040:
041: import com.sun.perseus.util.SVGConstants;
042: import com.sun.perseus.util.RunnableQueue;
043:
044: import org.w3c.dom.events.Event;
045: import org.w3c.dom.events.EventListener;
046:
047: import javax.microedition.m2g.SVGEventListener;
048:
049: import javax.microedition.lcdui.Canvas;
050: import javax.microedition.lcdui.Graphics;
051:
052: import com.sun.pisces.PiscesRenderer;
053: import com.sun.pisces.RendererBase;
054: import com.sun.pisces.NativeSurface;
055: import com.sun.pisces.GraphicsSurfaceDestination;
056:
057: /**
058: * This class provides support for an LCDUI Canvas extension which can display
059: * an SVG Document.
060: *
061: * @version $Id: SVGCanvas.java,v 1.16 2006/04/21 06:40:56 st125089 Exp $
062: */
063: class SVGCanvas extends Canvas implements CanvasUpdateListener {
064: /**
065: * Color used to clear the canvas' background.
066: */
067: public static final int CLEAR_COLOR = 0xffffffff;
068:
069: /**
070: * Initial state.
071: */
072: public final static int STATE_STOPPED = 1;
073:
074: /**
075: * Playing state, i.e., playing animations and repainting buffer.
076: */
077: public final static int STATE_PLAYING = 2;
078:
079: /**
080: * Paused state, i.e., repainting buffer but no longer advancing the
081: * time.
082: */
083: public final static int STATE_PAUSED = 3;
084:
085: /**
086: * SMIL Animation's frame length, in milliseconds
087: */
088: public static final int SMIL_ANIMATION_FRAME_LENGTH = 1000;
089:
090: /**
091: * Last x position on a pointer pressed event.
092: */
093: protected int lastX;
094:
095: /**
096: * Last y position on a pointer pressed event.
097: */
098: protected int lastY;
099:
100: /**
101: * True if the last pointer event was a pointer pressed event.
102: */
103: protected boolean lastWasPressed;
104:
105: /**
106: * The current player state.
107: */
108: protected int state = STATE_STOPPED;
109:
110: /**
111: * The <code>SimpleCanvasManager</code> manages the area where the SVG
112: * content is rendered.
113: */
114: protected SimpleCanvasManager canvasManager;
115:
116: /**
117: * This component displays a DocumentNode object, which
118: * is built from the URI
119: */
120: protected DocumentNode documentNode;
121:
122: /**
123: * Offscreen image
124: */
125: protected NativeSurface offscreen;
126:
127: /**
128: * Used to blit the offscreen onto the graphics destination.
129: */
130: protected GraphicsSurfaceDestination gsd;
131:
132: /**
133: * Offscreen width
134: */
135: protected int offscreenWidth;
136:
137: /**
138: * Offscreen height
139: */
140: protected int offscreenHeight;
141:
142: /**
143: * The PiscesRenderer associated with the offscreen.
144: */
145: protected PiscesRenderer pr;
146:
147: /**
148: * RenderGraphics used to draw into the current offscreen
149: */
150: protected RenderGraphics rg;
151:
152: /**
153: * The associated SVGEventListener.
154: */
155: protected SVGEventListener svgEventListener;
156:
157: /**
158: * The RunnableQueue is the _only_ valid way to access the
159: * model tree. No access to the model should be done other
160: * than from the RunnableQueue thread.
161: */
162: protected RunnableQueue updateQueue = null;
163:
164: /**
165: * The animation sampler, which runs animations in the update thread.
166: */
167: protected SMILSample smilSample = null;
168:
169: /**
170: * The animation clock.
171: */
172: protected SMILSample.DocumentWallClock clock = null;
173:
174: /**
175: * The time increment for the animation.
176: */
177: protected float timeIncrement = 0.1f;
178:
179: /**
180: * The last mouse event target.
181: */
182: protected ModelNode lastMouseTarget = null;
183:
184: /**
185: * Boolean flag used to control when the SVGCanvas ignores a
186: * canvas manager update because it asked for a a full paint
187: * in response to a prior repaint. This avoid queuing an extra
188: * initial repaint() when building a new offscreen buffer.
189: */
190: private boolean ignoreCanvasUpdate = false;
191:
192: /**
193: * @param documentNode the documentNode this component will render. The input
194: * DocumentNode must be fully loaded before this method is called.
195: * Note: if the DocumentNode already has an associated RunnableQueue,
196: * it is simply replaced. It is the responsibility of the caller to
197: * stop that RunnableQueue if need be.
198: * @throws IllegalArgumentException see {@link #setURI setURI}.
199: */
200: public SVGCanvas(final DocumentNode documentNode) {
201: if (documentNode == null) {
202: throw new NullPointerException();
203: }
204:
205: if (!documentNode.isLoaded()) {
206: throw new IllegalStateException();
207: }
208:
209: this .documentNode = documentNode;
210:
211: // Set-up RunnableQueue
212: updateQueue = RunnableQueue.getDefault();
213: documentNode.setUpdateQueue(updateQueue);
214:
215: // Hook in the SimpleCanvasManager after creating the offscreen buffer.
216: buildOffscreen(1, 1);
217: canvasManager = new SimpleCanvasManager(rg, documentNode, this );
218: canvasManager.turnOff(); // disabled until we call play or pause.
219: documentNode.setRunnableHandler(canvasManager);
220:
221: // Create a SMILSample instance that will be scheduled with the
222: // RunnableQueue whenever the component plays.
223: clock = new SMILSample.DocumentWallClock(documentNode);
224: smilSample = new SMILSample(documentNode, clock);
225:
226: // Initialize the timing engine.
227: documentNode.initializeTimingEngine();
228:
229: // Apply animations at time 0
230: documentNode.sample(new Time(0));
231: documentNode.applyAnimations();
232: }
233:
234: /**
235: * @see javax.microedition.lcdui.Canvas#paint
236: */
237: protected void paint(final Graphics g) {
238: checkOffscreen();
239: int x = g.getClipX();
240: int y = g.getClipY();
241: int w = g.getClipWidth();
242: int h = g.getClipHeight();
243:
244: synchronized (canvasManager.lock) {
245: if (x != 0 || y != 0 || w != documentNode.getWidth()
246: || h != documentNode.getHeight()) {
247: // The repaint area is not exactly the same as the viewport area
248: // so we need to clear the background first.
249: g.setColor(CLEAR_COLOR);
250: g.fillRect(x, y, w, h);
251: }
252:
253: if (gsd == null) {
254: gsd = new GraphicsSurfaceDestination(g);
255: }
256: gsd.drawSurface(offscreen, 0, 0, 0, 0, offscreenWidth,
257: offscreenHeight, 1);
258: canvasManager.consume();
259: }
260: }
261:
262: /**
263: * Checks if the offscreen buffer needs to be built or rebuilt.
264: */
265: protected void checkOffscreen() {
266: if (offscreen == null) {
267: // This is the very first time we build an offscreen.
268: buildOffscreen(getWidth(), getHeight());
269: } else {
270: // Check that the offscreen is large enough for the current size.
271: int width = getWidth();
272: int height = getHeight();
273:
274: // We use an offscreen size with is the smallest of the viewport
275: // size and the canvas size.
276: if (width > documentNode.getWidth()) {
277: width = documentNode.getWidth();
278: }
279:
280: if (height > documentNode.getHeight()) {
281: height = documentNode.getHeight();
282: }
283:
284: if (width != offscreenWidth || height != offscreenHeight) {
285: buildOffscreen(width, height);
286: }
287: }
288: }
289:
290: /**
291: * The offscreen buffer has the size of the component. This method
292: * is called in the MIDP painting thread.
293: *
294: * @param width the requested minimum buffer width
295: * @param height the requested minimum buffer height
296: */
297: protected void buildOffscreen(final int width, final int height) {
298: if (width > 0 && height > 0) {
299: // We build an offscreen of the requested size.
300: offscreen = new NativeSurface(width, height);
301: offscreenWidth = width;
302: offscreenHeight = height;
303: } else {
304: // This is a degenerate case, just build with 1x1 offscreen
305: if (offscreenWidth == 1 && offscreenHeight == 1) {
306: return;
307: }
308:
309: offscreen = new NativeSurface(1, 1);
310: offscreenWidth = 1;
311: offscreenHeight = 1;
312: }
313:
314: // Build a new PiscesRenderer for the new rendering surface.
315: pr = new PiscesRenderer(offscreen, offscreenWidth,
316: offscreenHeight, 0, offscreenWidth, 1,
317: RendererBase.TYPE_INT_ARGB);
318:
319: // Build a corresponding RenderGraphics
320: rg = new RenderGraphics(pr, offscreenWidth, offscreenHeight);
321:
322: if (canvasManager != null) {
323: // We need to force painting the offscreen buffer.
324: // Offscreen buffer rendering happens in the update
325: // thread.
326: try {
327: updateQueue.invokeAndWait(new Runnable() {
328: public void run() {
329: synchronized (canvasManager.lock) {
330: // Automatically adjust the SVG image's viewport size.
331: documentNode.setSize(width, height);
332:
333: // Switch the SimpleCanvasManager to the new RenderGraphics
334: canvasManager.setRenderGraphics(rg);
335:
336: // Set the consumed flag to true to force painting
337: // immediately.
338: canvasManager.consume();
339: }
340:
341: // Now, update the new canvas.
342: // We set the ignoreCanvasUpdate flag to true so that the
343: // canvas update does not trigger a repaint() request.
344: ignoreCanvasUpdate = true;
345: canvasManager.updateCanvas();
346: ignoreCanvasUpdate = false;
347: }
348: }, null);
349: } catch (InterruptedException ie) {
350: // This is a serious error, because it means the
351: // default Runnable Queue thread has been
352: // interrupted.
353: ie.printStackTrace();
354: }
355: } else {
356: pr.setColor(255, 255, 255);
357: pr.clearRect(0, 0, offscreenWidth, offscreenHeight);
358: }
359: }
360:
361: // ========================================================================
362: // CanvasUpdateListener implementation
363: // ========================================================================
364:
365: /**
366: * Invoked by the <code>SimpleCanvasManager</code> when it is done updating the
367: * canvas. This is used during the progressive rendering loading phase and
368: * when a Runnable has been invoked on the RunnableQueue associated with the
369: * SVG image. This method is called in the RunnableQueue thread.
370: *
371: * @param canvasManager the <code>SimpleCanvasManager</code> which is reporting
372: * the update.
373: */
374: public void updateComplete(final Object canvasManager) {
375: if (!ignoreCanvasUpdate) {
376: repaint(0, 0, documentNode.getWidth(), documentNode
377: .getHeight());
378: }
379: }
380:
381: /**
382: * Called by the <code>SimpleCanvasManager</code> when the initial load is
383: * complete. This method is called in the RunnableQueue thread.
384: *
385: * @param e if not null, it means that the initial load failed due to
386: * this exception.
387: */
388: public void initialLoadComplete(final Exception e) {
389: if (e != null) {
390: e.printStackTrace();
391: }
392: }
393:
394: // ========================================================================
395:
396: /**
397: * Event Listeners used to turn MIDP Events into DOM Events. It also
398: * switches between the MIDP event thread and the document's update
399: * thread (i.e., the <code>RunnableQueue</code>'s thread.
400: */
401:
402: /**
403: * Invoked when a mouse button has been pressed on a component.
404: * @param x the x-axis coordinate of the pointer event
405: * @param y the y-axis coordinate of the pointer event
406: */
407: protected void pointerPressed(final int x, final int y) {
408: if (svgEventListener != null) {
409: svgEventListener.pointerPressed(x, y);
410: }
411:
412: lastX = x;
413: lastY = y;
414: lastWasPressed = true;
415:
416: float[] pt = { x, y };
417: dispatchPointerEvent(SVGConstants.SVG_MOUSEDOWN_EVENT_TYPE, pt);
418: }
419:
420: /**
421: * Invoked when a mouse button has been released on a component.
422: * @param x the x-axis coordinate of the pointer event
423: * @param y the y-axis coordinate of the pointer event
424: */
425: protected void pointerReleased(final int x, final int y) {
426: if (svgEventListener != null) {
427: svgEventListener.pointerReleased(x, y);
428: }
429:
430: float[] pt = { x, y };
431: dispatchPointerEvent(SVGConstants.SVG_MOUSEUP_EVENT_TYPE, pt);
432:
433: if (lastWasPressed && lastX == x && lastY == y) {
434: dispatchPointerEvent(SVGConstants.SVG_CLICK_EVENT_TYPE, pt);
435: }
436:
437: lastWasPressed = false;
438: }
439:
440: /**
441: * Dispatches a mouse event to the DOM tree.
442: *
443: * @param eventType the DOM event type.
444: * @param pt the mouse event coordinates.
445: */
446: protected void dispatchPointerEvent(final String eventType,
447: final float[] pt) {
448: if (state == STATE_STOPPED) {
449: return;
450: }
451:
452: invokeLater(new Runnable() {
453: public void run() {
454: ModelNode target = documentNode.nodeHitAt(pt);
455: if (target == null) {
456: target = documentNode;
457: }
458:
459: // If the target is different from the lastMouseTarget
460: // dispatch a 'mouseout' event to the lastMouseTarget
461: // and dispatch a 'mouseover' to the new target
462: if (lastMouseTarget != target) {
463: if (lastMouseTarget != null
464: && lastMouseTarget != documentNode) {
465: ModelEvent e = new ModelEvent(
466: SVGConstants.SVG_MOUSEOUT_EVENT_TYPE,
467: lastMouseTarget);
468: documentNode.dispatchEvent(e);
469: }
470: ModelEvent e = new ModelEvent(
471: SVGConstants.SVG_MOUSEOVER_EVENT_TYPE,
472: target);
473: documentNode.dispatchEvent(e);
474: lastMouseTarget = target;
475: }
476:
477: // Map the event type
478: // Build the DOM Event
479: ModelEvent evt = new ModelEvent(eventType, target);
480:
481: // Dispatch to the target tree
482: documentNode.dispatchEvent(evt);
483: }
484: });
485: }
486:
487: /**
488: * Invoked when a key has been pressed.
489: * @param keyCode the code of the event key
490: */
491: protected void keyPressed(int keyCode) {
492: if (svgEventListener != null) {
493: svgEventListener.keyPressed(keyCode);
494: }
495: dispatchKeyEvent(SVGConstants.SVG_KEYDOWN_EVENT_TYPE, keyCode);
496: }
497:
498: /**
499: * Dispatches a key event to the DOM tree.
500: *
501: * @param eventType the DOM event type.
502: * @param keyCode the key code.
503: */
504: protected void dispatchKeyEvent(final String eventType,
505: final int keyCode) {
506: Runnable r = new Runnable() {
507: public void run() {
508: documentNode.dispatchEvent(new ModelEvent(eventType,
509: documentNode, (char) keyCode));
510: }
511: };
512:
513: if (state != STATE_STOPPED) {
514: invokeLater(r);
515: }
516: }
517:
518: /**
519: * Invoked when a key has been released.
520: * @param keyCode the code of the event key
521: */
522: protected void keyReleased(int keyCode) {
523: if (svgEventListener != null) {
524: svgEventListener.keyReleased(keyCode);
525: }
526: dispatchKeyEvent(SVGConstants.SVG_KEYUP_EVENT_TYPE, keyCode);
527: }
528:
529: /**
530: * Invoked when the component's size changes.
531: *
532: * @param w the new width
533: * @param h the new height
534: */
535: protected void sizeChanged(final int w, final int h) {
536: if (svgEventListener != null) {
537: svgEventListener.sizeChanged(w, h);
538: }
539: }
540:
541: /**
542: * Invoked when the component is hidden.
543: */
544: protected void hideNotify() {
545: if (svgEventListener != null) {
546: svgEventListener.hideNotify();
547: }
548: }
549:
550: /**
551: * Invoked when the component is shown.
552: */
553: protected void showNotify() {
554: if (svgEventListener != null) {
555: svgEventListener.showNotify();
556: }
557: }
558:
559: // ========================================================================
560:
561: /**
562: * Associate the specified <code>SVGEventListener</code> with this
563: * <code>SVGAnimator</code>.
564: *
565: * @param svgEventListener the SVGEventListener that will receive
566: * events forwarded by this <code>SVGAnimator</code>. If null,
567: * events will not be forwarded by the <code>SVGAnimator</code>.
568: */
569: public void setSVGEventListener(SVGEventListener svgEventListener) {
570: this .svgEventListener = svgEventListener;
571: }
572:
573: /**
574: * Set the time increment to be used for animation rendering.
575: *
576: * @param timeIncrement the minimal period of time, in seconds, that
577: * should elapse between frame. Must be greater than zero.
578: * @throws IllegalArgumentException if timeIncrement is less than or equal to
579: * zero.
580: * @see #getTimeIncrement
581: */
582: public void setTimeIncrement(float timeIncrement) {
583: if (timeIncrement <= 0) {
584: throw new IllegalArgumentException();
585: }
586:
587: this .timeIncrement = timeIncrement;
588:
589: if (state == STATE_PLAYING) {
590: updateQueue.unschedule(smilSample);
591: updateQueue.scheduleAtFixedRate(smilSample, canvasManager,
592: (long) (1000 * timeIncrement));
593: }
594: }
595:
596: /**
597: * Get the current time increment for animation rendering. The
598: * SVGAnimator increments the SVG document's current time by this amount
599: * upon each rendering. The default value is 0.1 (100 milliseconds).
600: *
601: * @return the current time increment, in seconds, used for animation
602: * rendering.
603: * @see #setTimeIncrement
604: */
605: public float getTimeIncrement() {
606: return timeIncrement;
607: }
608:
609: /**
610: * Transition this <code>SVGAnimator</code> to the <i>playing</i>
611: * state. In the <i>playing</i> state, both Animation and SVGImage
612: * updates cause rendering updates. Note that in the playing state,
613: * when the document's current time changes, the animator will seek
614: * to the new time, and continue to play animations from this place.
615: *
616: * @throws IllegalStateException if the animator is not currently in
617: * the <i>stopped</i> or <i>paused</i> state.
618: */
619: public void play() {
620: if (state == STATE_PLAYING) {
621: throw new IllegalStateException(Messages.formatMessage(
622: Messages.ERROR_INVALID_STATE, new Object[] {
623: getClass().getName(), stateToString(),
624: "play()", "stopped, paused" }));
625: }
626:
627: // Mark the document as playing.
628: updateQueue.preemptLater(new Runnable() {
629: public void run() {
630: documentNode.setPlaying(true);
631: }
632: }, canvasManager);
633:
634: // Now, schedule the SMILSampler
635: clock.start();
636: updateQueue.scheduleAtFixedRate(smilSample, canvasManager,
637: (long) (1000 * timeIncrement));
638:
639: state = STATE_PLAYING;
640:
641: // Turn on any updates to the offscreen canvas.
642: canvasManager.turnOn();
643: }
644:
645: /**
646: * Transition this <code>SVGAnimator</code> to the <i>paused</i> state.
647: * The <code>SVGAnimator</code> stops advancing the document's current time
648: * automatically (see the SVGDocument's setCurrentTime method). In consequence,
649: * animation playback will be paused until another call to the <code>play</code> method
650: * is made, at which points animations will resume from the document's current
651: * time. SVGImage updates (through API calls) cause a rendering update
652: * while the <code>SVGAnimator</code> is in the <i>paused</i> state.
653: *
654: * @throws IllegalStateException if the animator is not in the <i>playing</i>
655: * state.
656: */
657: public void pause() {
658: if (state != STATE_PLAYING) {
659: throw new IllegalStateException(Messages.formatMessage(
660: Messages.ERROR_INVALID_STATE, new Object[] {
661: getClass().getName(), stateToString(),
662: "pause()", "playing" }));
663: }
664:
665: state = STATE_PAUSED;
666:
667: // Mark the document as _not_ playing.
668: updateQueue.preemptLater(new Runnable() {
669: public void run() {
670: documentNode.setPlaying(false);
671: }
672: }, canvasManager);
673:
674: // Remove the SMILSampler
675: updateQueue.unschedule(smilSample);
676:
677: // Turn on any updates to the offscreen canvas.
678: canvasManager.turnOn();
679:
680: }
681:
682: /**
683: * Transition this <code>SVGAnimator</code> to the <i>stopped</i> state.
684: * In this state, no rendering updates are performed.
685: *
686: * @throws IllegalStateException if the animator is not in the <i>playing</i>
687: * or <i>paused</i> state.
688: */
689: public void stop() {
690: if (state == STATE_STOPPED) {
691: throw new IllegalStateException(Messages.formatMessage(
692: Messages.ERROR_INVALID_STATE, new Object[] {
693: getClass().getName(), stateToString(),
694: "stop()", "paused, playing" }));
695: }
696:
697: state = STATE_STOPPED;
698:
699: // Remove the SMILSampler
700: updateQueue.unschedule(smilSample);
701:
702: // Mark the document as _not_ playing.
703: documentNode.setPlaying(false);
704:
705: // To unlock the canvasManager if it is waiting on the
706: // consumed flag.
707: canvasManager.consume();
708:
709: // Turn off any updates to the offscreen canvas.
710: canvasManager.turnOff();
711: }
712:
713: /**
714: * Invoke the Runnable in the Document update thread and
715: * return only after this Runnable has finished.
716: *
717: * @param runnable the new Runnable to invoke.
718: * @throws InterruptedException if the current thread is waiting,
719: * sleeping, or otherwise paused for a long time and another thread
720: * interrupts it.
721: * @throws NullPointerException if <code>runnable</code> is null.
722: * @throws IllegalStateException if the animator is in the <i>stopped</i> state.
723: */
724: void invokeAndWait(Runnable runnable) throws InterruptedException {
725: if (runnable == null) {
726: throw new NullPointerException();
727: }
728:
729: if (state == STATE_STOPPED) {
730: throw new IllegalStateException(Messages.formatMessage(
731: Messages.ERROR_INVALID_STATE, new Object[] {
732: getClass().getName(), stateToString(),
733: "invokeAndWait()", "paused, playing" }));
734: }
735:
736: updateQueue.invokeAndWait(runnable, canvasManager);
737: }
738:
739: /**
740: * Schedule execution of the input Runnable in the update thread at a later time.
741: *
742: * @param runnable the new Runnable to execute in the Document's update
743: * thread when time permits.
744: * @throws NullPointerException if <code>runnable</code> is null.
745: * @throws IllegalStateException if the animator is in the <i>stopped</i> state.
746: */
747: void invokeLater(Runnable runnable) {
748: if (runnable == null) {
749: throw new NullPointerException();
750: }
751:
752: if (state == STATE_STOPPED) {
753: throw new IllegalStateException(Messages.formatMessage(
754: Messages.ERROR_INVALID_STATE, new Object[] {
755: getClass().getName(), stateToString(),
756: "invokeLater()", "paused, playing" }));
757: }
758:
759: updateQueue.invokeLater(runnable, canvasManager);
760: }
761:
762: /**
763: * Helper method. Converts the current state to a String.
764: */
765: String stateToString() {
766: switch (state) {
767: case STATE_PLAYING:
768: return "playing";
769: case STATE_PAUSED:
770: return "paused";
771: case STATE_STOPPED:
772: default:
773: return "stopped";
774: }
775: }
776:
777: }
|