Allow bypassing VolatileImage rendering to gain performance
[simantics/platform.git] / bundles / org.simantics.g2d / src / org / simantics / g2d / chassis / AWTChassis.java
1 /*******************************************************************************
2  * Copyright (c) 2007, 2010 Association for Decentralized Information Management
3  * in Industry THTH ry.
4  * All rights reserved. This program and the accompanying materials
5  * are made available under the terms of the Eclipse Public License v1.0
6  * which accompanies this distribution, and is available at
7  * http://www.eclipse.org/legal/epl-v10.html
8  *
9  * Contributors:
10  *     VTT Technical Research Centre of Finland - initial API and implementation
11  *******************************************************************************/
12 package org.simantics.g2d.chassis;
13
14 import java.awt.Color;
15 import java.awt.Container;
16 import java.awt.Cursor;
17 import java.awt.Graphics;
18 import java.awt.Graphics2D;
19 import java.awt.Rectangle;
20 import java.awt.image.VolatileImage;
21 import java.lang.reflect.Method;
22
23 import javax.swing.JComponent;
24 import javax.swing.RepaintManager;
25
26 import org.simantics.g2d.canvas.ICanvasContext;
27 import org.simantics.g2d.canvas.IContentContext;
28 import org.simantics.g2d.canvas.IContentContext.IContentListener;
29 import org.simantics.g2d.canvas.IMouseCursorContext;
30 import org.simantics.g2d.canvas.IMouseCursorListener;
31 import org.simantics.g2d.dnd.DragInteractor;
32 import org.simantics.g2d.dnd.DropInteractor;
33 import org.simantics.g2d.internal.DebugPolicy;
34 import org.simantics.scenegraph.g2d.G2DRenderingHints;
35 import org.simantics.scenegraph.g2d.events.Event;
36 import org.simantics.scenegraph.g2d.events.IEventQueue;
37 import org.simantics.scenegraph.g2d.events.IEventQueue.IEventQueueListener;
38 import org.simantics.scenegraph.g2d.events.adapter.AWTFocusAdapter;
39 import org.simantics.scenegraph.g2d.events.adapter.AWTKeyEventAdapter;
40 import org.simantics.scenegraph.g2d.events.adapter.AWTMouseEventAdapter;
41 import org.simantics.utils.datastructures.hints.HintContext;
42 import org.simantics.utils.datastructures.hints.IHintContext;
43 import org.simantics.utils.threads.AWTThread;
44 import org.simantics.utils.threads.Executable;
45 import org.simantics.utils.threads.IThreadWorkQueue;
46 import org.simantics.utils.threads.SyncListenerList;
47 import org.simantics.utils.threads.ThreadUtils;
48
49 /**
50  * Swing component chassis for a canvas context.
51  *
52  * @author Toni Kalajainen
53  */
54 public class AWTChassis extends JComponent implements ICanvasChassis {
55
56 //    private static final String PROBLEM = "Encountered VolatileImage issue, please reopen editor.";
57
58     private static final long serialVersionUID = 1L;
59
60     /** Hint context */
61     protected IHintContext                       hintCtx          = new HintContext();
62
63     protected SyncListenerList<IChassisListener> listeners        = new SyncListenerList<IChassisListener>(IChassisListener.class);
64     protected DropInteractor                     drop;
65     protected DragInteractor                     drag;
66
67     AWTMouseEventAdapter                         mouseAdapter;
68     AWTKeyEventAdapter                           keyAdapter;
69     AWTFocusAdapter                              focusAdapter;
70
71     Container holder = null; // Holder for possible swing components. Scenegraph handles event and painting, thus the size of the holder should be 0x0
72
73     IMouseCursorListener cursorListener = new IMouseCursorListener() {
74         @Override
75         public void onCursorSet(IMouseCursorContext sender, int mouseId, Cursor cursor) {
76             if (mouseId==0) setCursor(cursor);
77         }
78     };
79
80     private transient boolean                    dirty            = false;
81     private transient boolean                    closed           = false;
82     protected ICanvasContext                     canvasContext;
83
84     private boolean useVolatileImage = true;
85     
86     // Marks the content dirty
87     protected IContentListener contentListener = new IContentListener() {
88         @Override
89         public void onDirty(IContentContext sender) {
90             dirty = true;
91             ICanvasContext ctx = canvasContext;
92             if (ctx==null) return;
93             if (ctx.getEventQueue().isEmpty())
94             {
95                 RepaintManager rm = RepaintManager.currentManager(AWTChassis.this);
96                 rm.markCompletelyDirty(AWTChassis.this);
97             }
98         }
99     };
100
101     // Paints dirty contents after all events are handled
102     protected IEventQueueListener queueListener = new IEventQueueListener() {
103         @Override
104         public void onEventAdded(IEventQueue queue, Event e, int index) {
105         }
106         @Override
107         public void onQueueEmpty(IEventQueue queue) {
108             if (dirty) {
109                 RepaintManager rm = RepaintManager.currentManager(AWTChassis.this);
110                 rm.markCompletelyDirty(AWTChassis.this);
111             }
112         }
113     };
114
115     protected boolean hookKeyEvents;
116
117     /**
118      * Background buffer for rendered canvas. Allocated on-demand in
119      * {@link #paintComponent(Graphics)}. Nullified by
120      * {@link #setCanvasContext(ICanvasContext)} when canvas context is null to
121      * prevent leakage.
122      */
123     private VolatileImage buffer = null;
124 //  private final int imageNo = 0;
125
126
127     public AWTChassis() {
128         this(true);
129     }
130
131     public AWTChassis(boolean hookKeyEvents) {
132         super();
133         setFocusable(true);
134         this.hookKeyEvents = hookKeyEvents;
135         this.setDoubleBuffered(false);
136         this.setOpaque(true);
137         this.setBackground(Color.WHITE);
138     }
139
140     @Override
141     public void setCanvasContext(final ICanvasContext canvasContext) {
142         // FIXME: this should be true but is currently not.
143         //assert AWTThread.getThreadAccess().currentThreadAccess();
144         if (this.canvasContext == canvasContext) return;
145         // Unhook old context
146         if (this.canvasContext!=null) {
147             this.canvasContext.getHintStack().removeHintContext(hintCtx);
148             this.canvasContext.getContentContext().removePaintableContextListener(contentListener);
149             this.canvasContext.getEventQueue().removeQueueListener(queueListener);
150             this.canvasContext.getMouseCursorContext().removeCursorListener(cursorListener);
151             this.canvasContext.remove(drop);
152             this.canvasContext.remove(drag);
153             removeMouseListener(mouseAdapter);
154             removeMouseMotionListener(mouseAdapter);
155             removeMouseWheelListener(mouseAdapter);
156             if (hookKeyEvents)
157                 removeKeyListener(keyAdapter);
158             removeFocusListener(focusAdapter);
159
160             // SceneGraph event handling
161             removeMouseListener(this.canvasContext.getSceneGraph().getEventDelegator());
162             removeMouseMotionListener(this.canvasContext.getSceneGraph().getEventDelegator());
163             removeMouseWheelListener(this.canvasContext.getSceneGraph().getEventDelegator());
164             removeKeyListener(this.canvasContext.getSceneGraph().getEventDelegator());
165             removeFocusListener(this.canvasContext.getSceneGraph().getEventDelegator());
166             this.canvasContext.setTooltipProvider( null );
167
168             this.canvasContext = null;
169             this.focusAdapter = null;
170             this.mouseAdapter = null;
171             this.keyAdapter = null;
172             this.drop = null;
173             this.drag = null;
174             this.holder = null;
175             removeAll();
176             
177         }
178         this.canvasContext = canvasContext;
179         // Hook new context
180         if (canvasContext!=null) {
181
182             // SceneGraph event handling
183             addMouseListener(canvasContext.getSceneGraph().getEventDelegator());
184             addMouseMotionListener(canvasContext.getSceneGraph().getEventDelegator());
185             addMouseWheelListener(canvasContext.getSceneGraph().getEventDelegator());
186             addKeyListener(canvasContext.getSceneGraph().getEventDelegator());
187             addFocusListener(canvasContext.getSceneGraph().getEventDelegator());
188             canvasContext.setTooltipProvider( new AWTTooltipProvider() );
189
190             //Create canvas context and a layer of interactors
191             canvasContext.getHintStack().addHintContext(hintCtx, 0);
192             canvasContext.getContentContext().addPaintableContextListener(contentListener);
193             canvasContext.getEventQueue().addQueueListener(queueListener);
194             canvasContext.getMouseCursorContext().addCursorListener(cursorListener);
195
196             mouseAdapter = new AWTMouseEventAdapter(canvasContext, canvasContext.getEventQueue());
197             if (hookKeyEvents) {
198                 keyAdapter = new AWTKeyEventAdapter(canvasContext, canvasContext.getEventQueue());
199                 addKeyListener(keyAdapter);
200             }
201             focusAdapter = new AWTFocusAdapter(canvasContext, canvasContext.getEventQueue());
202             addMouseListener(mouseAdapter);
203             addMouseMotionListener(mouseAdapter);
204             addMouseWheelListener(mouseAdapter);
205             addFocusListener(focusAdapter);
206
207             canvasContext.add( drag=new DragInteractor(this) );
208             canvasContext.add( drop=new DropInteractor(this) );
209
210             // FIXME: hack to work around this:
211             // FIXME: this should be true but is currently not.
212             //assert AWTThread.getThreadAccess().currentThreadAccess();
213             Runnable initializeHolder = new Runnable() {
214                 @Override
215                 public void run() {
216                     if (canvasContext.isDisposed())
217                         return;
218                     if (holder == null) {
219                         holder = new Holder();
220 //                        holder.setLayout(new FlowLayout(FlowLayout.LEFT, 0, 0));
221                         holder.setSize(1, 1);
222                         holder.setLocation(0, 0);
223                         holder.setFocusable(false);
224                         holder.setEnabled(true);
225                         holder.setVisible(true);
226                         holder.addMouseListener(canvasContext.getSceneGraph().getEventDelegator());
227                         holder.addMouseMotionListener(canvasContext.getSceneGraph().getEventDelegator());
228                         holder.addMouseWheelListener(canvasContext.getSceneGraph().getEventDelegator());
229                         holder.addKeyListener(canvasContext.getSceneGraph().getEventDelegator());
230                         holder.addFocusListener(canvasContext.getSceneGraph().getEventDelegator());
231                         //System.err.println("layout: " + holder.getLayout());
232                         AWTChassis.this.add(holder);
233                     }
234
235                     // Use dummy holder as root pane. Swing components need a root pane, but we don't want swing to handle painting..
236                     canvasContext.getSceneGraph().setRootPane(holder, AWTChassis.this);
237
238                     holder.addMouseListener(mouseAdapter);
239                     holder.addMouseMotionListener(mouseAdapter);
240                     holder.addMouseWheelListener(mouseAdapter);
241                     holder.addFocusListener(focusAdapter);
242                 }
243             };
244             if (AWTThread.getThreadAccess().currentThreadAccess())
245                 initializeHolder.run();
246             else
247                 AWTThread.getThreadAccess().asyncExec(initializeHolder);
248         }
249         buffer = null;
250         repaint();
251     }
252
253     @Override
254     public ICanvasContext getCanvasContext() {
255         return canvasContext;
256     }
257
258     @Override
259     public void addChassisListener(IThreadWorkQueue thread, IChassisListener listener) {
260         listeners.add(thread, listener);
261     }
262
263     @Override
264     public void removeChassisListener(IThreadWorkQueue thread, IChassisListener listener) {
265         listeners.remove(thread, listener);
266     }
267
268     @Override
269     public void addChassisListener(IChassisListener listener) {
270         listeners.add(listener);
271     }
272
273     @Override
274     public void removeChassisListener(IChassisListener listener) {
275         listeners.remove(listener);
276     }
277
278     @Override
279     public IHintContext getHintContext() {
280         return hintCtx;
281     }
282     
283     public void setUseVolatileImage(boolean useVolatileImage) {
284                 this.useVolatileImage = useVolatileImage;
285         }
286     
287     public boolean isUseVolatileImage() {
288                 return useVolatileImage;
289         }
290
291     private void paintScenegraph(Graphics2D g2d, Rectangle controlBounds) {
292         Color bg = getBackground();
293         if (bg == null)
294             bg = Color.WHITE;
295         g2d.setBackground(bg);
296         g2d.clearRect(controlBounds.x, controlBounds.y, controlBounds.width, controlBounds.height);
297         g2d.setClip(controlBounds.x, controlBounds.y, controlBounds.width, controlBounds.height);
298         g2d.setRenderingHint(G2DRenderingHints.KEY_CONTROL_BOUNDS, controlBounds);
299         if (!canvasContext.isLocked())
300             canvasContext.getSceneGraph().render(g2d);
301     }
302
303     /**
304      * Perform a best effort to render the current scene graph into a
305      * VolatileImage compatible with this chassis.
306      * 
307      * @param g2d
308      * @param b
309      * @return the VolatileImage rendered if successful, <code>null</code> if
310      *         rendering fails
311      */
312     private VolatileImage paintToVolatileImage(Graphics2D g2d, Rectangle b) {
313         int attempts = 0;
314         do {
315             // Prevent eternal looping experienced by Kalle.
316             // Caused by buffer.contentsLost failing constantly
317             // for the buffer created by createVolatileImage(w, h) below.
318             // This happens often with the following sequence:
319             //   - two displays, application window on the second display
320             //   - lock windows, (possibly wait a while/get coffee)
321             //   - unlock windows
322             // Also using an attempt count to let the code fall back to a
323             // slower rendering path if volatile images are causing trouble.
324             if (closed || attempts >= 10)
325                 return null;
326
327             if (buffer == null
328                     || b.width != buffer.getWidth()
329                     || b.height != buffer.getHeight()
330                     || buffer.validate(g2d.getDeviceConfiguration()) == VolatileImage.IMAGE_INCOMPATIBLE)
331             {
332                 
333                 buffer = createVolatileImage(b.width, b.height);
334             }
335             if (buffer == null)
336                 return null;
337
338 //            ImageCapabilities caps = g2d.getDeviceConfiguration().getImageCapabilities();
339 //            System.out.println("CAPS: " + caps + ", accelerated=" + caps.isAccelerated() + ", true volatile=" + caps.isTrueVolatile());
340 //            caps = buffer.getCapabilities();
341 //            System.out.println("CAPS2: " + caps + ", accelerated=" + caps.isAccelerated() + ", true volatile=" + caps.isTrueVolatile());
342
343             Graphics2D bg = buffer.createGraphics();
344             paintScenegraph(bg, b);
345             bg.dispose();
346
347             ++attempts;
348         } while (buffer.contentsLost());
349
350         // Successfully rendered to buffer!
351         return buffer;
352     }
353
354     @Override
355     public void paintComponent(Graphics g) {
356         dirty = false;
357         if (canvasContext == null)
358             return;
359
360         Graphics2D g2d = (Graphics2D) g;
361         Rectangle b = getBounds();
362
363         long startmem = 0, start = 0;
364         if (DebugPolicy.PERF_CHASSIS_RENDER_FRAME) {
365             startmem = Runtime.getRuntime().freeMemory();
366             start = System.nanoTime();
367         }
368         VolatileImage buffer = null;
369         if (useVolatileImage)
370                 buffer = paintToVolatileImage(g2d, b);
371         if (closed)
372             return;
373         if (DebugPolicy.PERF_CHASSIS_RENDER_FRAME) {
374             long end = System.nanoTime();
375             long endmem = Runtime.getRuntime().freeMemory();
376             System.out.println("frame render: " + ((end-start)*1e-6) + " ms, " + (startmem-endmem)/(1024.0*1024.0) + " MB");
377         }
378
379         if (buffer != null) {
380             // Successfully buffered the scenegraph, copy the image to screen.
381
382             // DEBUG
383 //          try {
384 //              File frame = new File("d:/frames/frame" + (++imageNo) + ".png");
385 //              System.out.println("writing frame: " + frame);
386 //              ImageIO.write(buffer.getSnapshot(), "PNG", frame);
387 //          } catch (IOException e) {
388 //              e.printStackTrace();
389 //          }
390
391             g2d.drawImage(buffer, 0, 0, null);
392         } else {
393             // Failed to paint volatile image, paint directly to the provided
394             // graphics context.
395             paintScenegraph(g2d, b);
396
397 //            g2d.setFont(Font.getFont("Arial 14"));
398 //            g2d.setColor(Color.RED);
399 //            FontMetrics fm = g2d.getFontMetrics();
400 //            Rectangle2D r = fm.getStringBounds(PROBLEM, g2d);
401 //            g2d.drawString(PROBLEM, (int) (b.width-r.getWidth()), (int) (b.getHeight()-fm.getMaxDescent()));
402         }
403     }
404
405     private final static Method CLOSED_METHOD = SyncListenerList.getMethod(IChassisListener.class, "chassisClosed");
406     protected void fireChassisClosed()
407     {
408         closed = true;
409         Executable e[] = listeners.getExecutables(CLOSED_METHOD, this);
410         ThreadUtils.multiSyncExec(e);
411     }
412     
413     @Override
414         public boolean contains(int x, int y) {
415                 // TODO Auto-generated method stub
416                 return super.contains(x, y);
417         }
418     
419 }