/******************************************************************************* * Copyright (c) 2007, 2010 Association for Decentralized Information Management * in Industry THTH ry. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * VTT Technical Research Centre of Finland - initial API and implementation *******************************************************************************/ package org.simantics.g2d.chassis; import java.awt.Color; import java.awt.Container; import java.awt.Cursor; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Rectangle; import java.awt.image.VolatileImage; import java.lang.reflect.Method; import javax.swing.JComponent; import javax.swing.RepaintManager; import org.simantics.g2d.canvas.ICanvasContext; import org.simantics.g2d.canvas.IContentContext; import org.simantics.g2d.canvas.IContentContext.IContentListener; import org.simantics.g2d.canvas.IMouseCursorContext; import org.simantics.g2d.canvas.IMouseCursorListener; import org.simantics.g2d.dnd.DragInteractor; import org.simantics.g2d.dnd.DropInteractor; import org.simantics.g2d.internal.DebugPolicy; import org.simantics.scenegraph.g2d.G2DRenderingHints; import org.simantics.scenegraph.g2d.events.Event; import org.simantics.scenegraph.g2d.events.IEventQueue; import org.simantics.scenegraph.g2d.events.IEventQueue.IEventQueueListener; import org.simantics.scenegraph.g2d.events.adapter.AWTFocusAdapter; import org.simantics.scenegraph.g2d.events.adapter.AWTKeyEventAdapter; import org.simantics.scenegraph.g2d.events.adapter.AWTMouseEventAdapter; import org.simantics.utils.datastructures.hints.HintContext; import org.simantics.utils.datastructures.hints.IHintContext; import org.simantics.utils.threads.AWTThread; import org.simantics.utils.threads.Executable; import org.simantics.utils.threads.IThreadWorkQueue; import org.simantics.utils.threads.SyncListenerList; import org.simantics.utils.threads.ThreadUtils; /** * Swing component chassis for a canvas context. * * @author Toni Kalajainen */ public class AWTChassis extends JComponent implements ICanvasChassis { // private static final String PROBLEM = "Encountered VolatileImage issue, please reopen editor."; private static final long serialVersionUID = 1L; /** Hint context */ protected IHintContext hintCtx = new HintContext(); protected SyncListenerList listeners = new SyncListenerList(IChassisListener.class); protected DropInteractor drop; protected DragInteractor drag; AWTMouseEventAdapter mouseAdapter; AWTKeyEventAdapter keyAdapter; AWTFocusAdapter focusAdapter; Container holder = null; // Holder for possible swing components. Scenegraph handles event and painting, thus the size of the holder should be 0x0 IMouseCursorListener cursorListener = new IMouseCursorListener() { @Override public void onCursorSet(IMouseCursorContext sender, int mouseId, Cursor cursor) { if (mouseId==0) setCursor(cursor); } }; private transient boolean dirty = false; private transient boolean closed = false; protected ICanvasContext canvasContext; private boolean useVolatileImage = true; // Marks the content dirty protected IContentListener contentListener = new IContentListener() { @Override public void onDirty(IContentContext sender) { dirty = true; ICanvasContext ctx = canvasContext; if (ctx==null) return; if (ctx.getEventQueue().isEmpty()) { RepaintManager rm = RepaintManager.currentManager(AWTChassis.this); rm.markCompletelyDirty(AWTChassis.this); } } }; // Paints dirty contents after all events are handled protected IEventQueueListener queueListener = new IEventQueueListener() { @Override public void onEventAdded(IEventQueue queue, Event e, int index) { } @Override public void onQueueEmpty(IEventQueue queue) { if (dirty) { RepaintManager rm = RepaintManager.currentManager(AWTChassis.this); rm.markCompletelyDirty(AWTChassis.this); } } }; protected boolean hookKeyEvents; /** * Background buffer for rendered canvas. Allocated on-demand in * {@link #paintComponent(Graphics)}. Nullified by * {@link #setCanvasContext(ICanvasContext)} when canvas context is null to * prevent leakage. */ private VolatileImage buffer = null; // private final int imageNo = 0; public AWTChassis() { this(true); } public AWTChassis(boolean hookKeyEvents) { super(); setFocusable(true); this.hookKeyEvents = hookKeyEvents; this.setDoubleBuffered(false); this.setOpaque(true); this.setBackground(Color.WHITE); } @Override public void setCanvasContext(final ICanvasContext canvasContext) { // FIXME: this should be true but is currently not. //assert AWTThread.getThreadAccess().currentThreadAccess(); if (this.canvasContext == canvasContext) return; // Unhook old context if (this.canvasContext!=null) { this.canvasContext.getHintStack().removeHintContext(hintCtx); this.canvasContext.getContentContext().removePaintableContextListener(contentListener); this.canvasContext.getEventQueue().removeQueueListener(queueListener); this.canvasContext.getMouseCursorContext().removeCursorListener(cursorListener); this.canvasContext.remove(drop); this.canvasContext.remove(drag); removeMouseListener(mouseAdapter); removeMouseMotionListener(mouseAdapter); removeMouseWheelListener(mouseAdapter); if (hookKeyEvents) removeKeyListener(keyAdapter); removeFocusListener(focusAdapter); // SceneGraph event handling removeMouseListener(this.canvasContext.getSceneGraph().getEventDelegator()); removeMouseMotionListener(this.canvasContext.getSceneGraph().getEventDelegator()); removeMouseWheelListener(this.canvasContext.getSceneGraph().getEventDelegator()); removeKeyListener(this.canvasContext.getSceneGraph().getEventDelegator()); removeFocusListener(this.canvasContext.getSceneGraph().getEventDelegator()); this.canvasContext.setTooltipProvider( null ); this.canvasContext = null; this.focusAdapter = null; this.mouseAdapter = null; this.keyAdapter = null; this.drop = null; this.drag = null; this.holder = null; removeAll(); } this.canvasContext = canvasContext; // Hook new context if (canvasContext!=null) { // SceneGraph event handling addMouseListener(canvasContext.getSceneGraph().getEventDelegator()); addMouseMotionListener(canvasContext.getSceneGraph().getEventDelegator()); addMouseWheelListener(canvasContext.getSceneGraph().getEventDelegator()); addKeyListener(canvasContext.getSceneGraph().getEventDelegator()); addFocusListener(canvasContext.getSceneGraph().getEventDelegator()); canvasContext.setTooltipProvider( new AWTTooltipProvider() ); //Create canvas context and a layer of interactors canvasContext.getHintStack().addHintContext(hintCtx, 0); canvasContext.getContentContext().addPaintableContextListener(contentListener); canvasContext.getEventQueue().addQueueListener(queueListener); canvasContext.getMouseCursorContext().addCursorListener(cursorListener); mouseAdapter = new AWTMouseEventAdapter(canvasContext, canvasContext.getEventQueue()); if (hookKeyEvents) { keyAdapter = new AWTKeyEventAdapter(canvasContext, canvasContext.getEventQueue()); addKeyListener(keyAdapter); } focusAdapter = new AWTFocusAdapter(canvasContext, canvasContext.getEventQueue()); addMouseListener(mouseAdapter); addMouseMotionListener(mouseAdapter); addMouseWheelListener(mouseAdapter); addFocusListener(focusAdapter); canvasContext.add( drag=new DragInteractor(this) ); canvasContext.add( drop=new DropInteractor(this) ); // FIXME: hack to work around this: // FIXME: this should be true but is currently not. //assert AWTThread.getThreadAccess().currentThreadAccess(); Runnable initializeHolder = new Runnable() { @Override public void run() { if (canvasContext.isDisposed()) return; if (holder == null) { holder = new Holder(); // holder.setLayout(new FlowLayout(FlowLayout.LEFT, 0, 0)); holder.setSize(1, 1); holder.setLocation(0, 0); holder.setFocusable(false); holder.setEnabled(true); holder.setVisible(true); holder.addMouseListener(canvasContext.getSceneGraph().getEventDelegator()); holder.addMouseMotionListener(canvasContext.getSceneGraph().getEventDelegator()); holder.addMouseWheelListener(canvasContext.getSceneGraph().getEventDelegator()); holder.addKeyListener(canvasContext.getSceneGraph().getEventDelegator()); holder.addFocusListener(canvasContext.getSceneGraph().getEventDelegator()); //System.err.println("layout: " + holder.getLayout()); AWTChassis.this.add(holder); } // Use dummy holder as root pane. Swing components need a root pane, but we don't want swing to handle painting.. canvasContext.getSceneGraph().setRootPane(holder, AWTChassis.this); holder.addMouseListener(mouseAdapter); holder.addMouseMotionListener(mouseAdapter); holder.addMouseWheelListener(mouseAdapter); holder.addFocusListener(focusAdapter); } }; if (AWTThread.getThreadAccess().currentThreadAccess()) initializeHolder.run(); else AWTThread.getThreadAccess().asyncExec(initializeHolder); } buffer = null; repaint(); } @Override public ICanvasContext getCanvasContext() { return canvasContext; } @Override public void addChassisListener(IThreadWorkQueue thread, IChassisListener listener) { listeners.add(thread, listener); } @Override public void removeChassisListener(IThreadWorkQueue thread, IChassisListener listener) { listeners.remove(thread, listener); } @Override public void addChassisListener(IChassisListener listener) { listeners.add(listener); } @Override public void removeChassisListener(IChassisListener listener) { listeners.remove(listener); } @Override public IHintContext getHintContext() { return hintCtx; } public void setUseVolatileImage(boolean useVolatileImage) { this.useVolatileImage = useVolatileImage; } public boolean isUseVolatileImage() { return useVolatileImage; } private void paintScenegraph(Graphics2D g2d, Rectangle controlBounds) { Color bg = getBackground(); if (bg == null) bg = Color.WHITE; g2d.setBackground(bg); g2d.clearRect(controlBounds.x, controlBounds.y, controlBounds.width, controlBounds.height); g2d.setClip(controlBounds.x, controlBounds.y, controlBounds.width, controlBounds.height); g2d.setRenderingHint(G2DRenderingHints.KEY_CONTROL_BOUNDS, controlBounds); if (!canvasContext.isLocked()) canvasContext.getSceneGraph().render(g2d); } /** * Perform a best effort to render the current scene graph into a * VolatileImage compatible with this chassis. * * @param g2d * @param b * @return the VolatileImage rendered if successful, null if * rendering fails */ private VolatileImage paintToVolatileImage(Graphics2D g2d, Rectangle b) { int attempts = 0; do { // Prevent eternal looping experienced by Kalle. // Caused by buffer.contentsLost failing constantly // for the buffer created by createVolatileImage(w, h) below. // This happens often with the following sequence: // - two displays, application window on the second display // - lock windows, (possibly wait a while/get coffee) // - unlock windows // Also using an attempt count to let the code fall back to a // slower rendering path if volatile images are causing trouble. if (closed || attempts >= 10) return null; if (buffer == null || b.width != buffer.getWidth() || b.height != buffer.getHeight() || buffer.validate(g2d.getDeviceConfiguration()) == VolatileImage.IMAGE_INCOMPATIBLE) { buffer = createVolatileImage(b.width, b.height); } if (buffer == null) return null; // ImageCapabilities caps = g2d.getDeviceConfiguration().getImageCapabilities(); // System.out.println("CAPS: " + caps + ", accelerated=" + caps.isAccelerated() + ", true volatile=" + caps.isTrueVolatile()); // caps = buffer.getCapabilities(); // System.out.println("CAPS2: " + caps + ", accelerated=" + caps.isAccelerated() + ", true volatile=" + caps.isTrueVolatile()); Graphics2D bg = buffer.createGraphics(); paintScenegraph(bg, b); bg.dispose(); ++attempts; } while (buffer.contentsLost()); // Successfully rendered to buffer! return buffer; } @Override public void paintComponent(Graphics g) { dirty = false; if (canvasContext == null) return; Graphics2D g2d = (Graphics2D) g; Rectangle b = getBounds(); long startmem = 0, start = 0; if (DebugPolicy.PERF_CHASSIS_RENDER_FRAME) { startmem = Runtime.getRuntime().freeMemory(); start = System.nanoTime(); } VolatileImage buffer = null; if (useVolatileImage) buffer = paintToVolatileImage(g2d, b); if (closed) return; if (DebugPolicy.PERF_CHASSIS_RENDER_FRAME) { long end = System.nanoTime(); long endmem = Runtime.getRuntime().freeMemory(); System.out.println("frame render: " + ((end-start)*1e-6) + " ms, " + (startmem-endmem)/(1024.0*1024.0) + " MB"); } if (buffer != null) { // Successfully buffered the scenegraph, copy the image to screen. // DEBUG // try { // File frame = new File("d:/frames/frame" + (++imageNo) + ".png"); // System.out.println("writing frame: " + frame); // ImageIO.write(buffer.getSnapshot(), "PNG", frame); // } catch (IOException e) { // e.printStackTrace(); // } g2d.drawImage(buffer, 0, 0, null); } else { // Failed to paint volatile image, paint directly to the provided // graphics context. paintScenegraph(g2d, b); // g2d.setFont(Font.getFont("Arial 14")); // g2d.setColor(Color.RED); // FontMetrics fm = g2d.getFontMetrics(); // Rectangle2D r = fm.getStringBounds(PROBLEM, g2d); // g2d.drawString(PROBLEM, (int) (b.width-r.getWidth()), (int) (b.getHeight()-fm.getMaxDescent())); } } private final static Method CLOSED_METHOD = SyncListenerList.getMethod(IChassisListener.class, "chassisClosed"); protected void fireChassisClosed() { closed = true; Executable e[] = listeners.getExecutables(CLOSED_METHOD, this); ThreadUtils.multiSyncExec(e); } @Override public boolean contains(int x, int y) { // TODO Auto-generated method stub return super.contains(x, y); } }