/******************************************************************************* * 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.diagram.impl; import java.awt.Color; import java.awt.geom.AffineTransform; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; import org.simantics.g2d.canvas.Hints; import org.simantics.g2d.canvas.ICanvasContext; import org.simantics.g2d.canvas.ICanvasParticipant; import org.simantics.g2d.canvas.IContentContext; import org.simantics.g2d.canvas.IMouseCaptureContext; import org.simantics.g2d.canvas.IMouseCursorContext; import org.simantics.g2d.canvas.impl.MouseCaptureContext; import org.simantics.g2d.canvas.impl.MouseCursorContext; import org.simantics.g2d.canvas.impl.PaintableContextImpl; import org.simantics.g2d.chassis.ITooltipProvider; import org.simantics.g2d.diagram.DiagramClass; import org.simantics.g2d.diagram.DiagramHints; import org.simantics.g2d.diagram.IDiagram; import org.simantics.g2d.diagram.participant.DiagramParticipant; import org.simantics.g2d.diagram.participant.ElementInteractor; import org.simantics.g2d.diagram.participant.ElementPainter; import org.simantics.g2d.diagram.participant.Selection; import org.simantics.g2d.diagram.participant.TerminalPainter; import org.simantics.g2d.diagram.participant.ZOrderHandler; import org.simantics.g2d.diagram.participant.pointertool.PointerInteractor; import org.simantics.g2d.element.IElement; import org.simantics.g2d.multileveldiagram.TransitionFunction; import org.simantics.g2d.multileveldiagram.ZoomTransitionParticipant; import org.simantics.g2d.participant.BackgroundPainter; import org.simantics.g2d.participant.CanvasGrab; import org.simantics.g2d.participant.GridPainter; import org.simantics.g2d.participant.HandPainter; import org.simantics.g2d.participant.KeyToCommand; import org.simantics.g2d.participant.KeyUtil; import org.simantics.g2d.participant.MouseUtil; import org.simantics.g2d.participant.PointerPainter; import org.simantics.g2d.participant.RulerPainter; import org.simantics.g2d.participant.SubCanvas; import org.simantics.g2d.participant.SymbolUtil; import org.simantics.g2d.participant.TimeParticipant; import org.simantics.g2d.participant.TransformUtil; import org.simantics.g2d.scenegraph.SceneGraphConstants; import org.simantics.scenegraph.g2d.G2DParentNode; import org.simantics.scenegraph.g2d.G2DSceneGraph; import org.simantics.scenegraph.g2d.events.Event; import org.simantics.scenegraph.g2d.events.EventHandlerStack; import org.simantics.scenegraph.g2d.events.EventQueue; import org.simantics.scenegraph.g2d.events.IEventHandlerStack; import org.simantics.scenegraph.g2d.events.IEventQueue; import org.simantics.scenegraph.g2d.events.MouseEvent; import org.simantics.scenegraph.g2d.events.MouseEventCoalescer; import org.simantics.scenegraph.g2d.events.EventHandlerReflection.EventHandler; import org.simantics.scenegraph.g2d.events.IEventQueue.IEventQueueListener; import org.simantics.scenegraph.g2d.events.MouseEvent.MouseButtonEvent; import org.simantics.scenegraph.g2d.events.MouseEvent.MouseButtonPressedEvent; import org.simantics.scenegraph.g2d.events.MouseEvent.MouseButtonReleasedEvent; import org.simantics.scenegraph.g2d.events.MouseEvent.MouseClickEvent; import org.simantics.scenegraph.g2d.events.MouseEvent.MouseDoubleClickedEvent; import org.simantics.scenegraph.g2d.events.MouseEvent.MouseDragBegin; import org.simantics.scenegraph.g2d.events.MouseEvent.MouseEnterEvent; import org.simantics.scenegraph.g2d.events.MouseEvent.MouseExitEvent; import org.simantics.scenegraph.g2d.events.MouseEvent.MouseMovedEvent; import org.simantics.scenegraph.g2d.events.MouseEvent.MouseWheelMovedEvent; import org.simantics.scenegraph.g2d.events.command.CommandKeyBinding; import org.simantics.utils.datastructures.context.Context; import org.simantics.utils.datastructures.context.IContext; import org.simantics.utils.datastructures.context.IContextListener; import org.simantics.utils.datastructures.hints.HintContext; import org.simantics.utils.datastructures.hints.HintStack; import org.simantics.utils.datastructures.hints.IHintContext; import org.simantics.utils.datastructures.hints.IHintListener; import org.simantics.utils.datastructures.hints.IHintStack; import org.simantics.utils.threads.IThreadWorkQueue; import org.simantics.utils.threads.ThreadUtils; /** * Creates subcanvas on top of parent canvas. Size and position of the subcanvas * can be changed. * * Beware: every CanvasParticipant that handles mouseEvents must be replaced * since mouse coordinates must be translated. Currently Implemented * participants: - MouseUtil - ElementInteractor * * @author Marko Luukkainen * * TODO: CanvasContext implementation needs to implement scene graph methods too */ public class ElementDiagram implements IDiagram { private final ICanvasContext ctx; private final ICanvasContext parentCtx; private IDiagram diagram; private final SubCanvas subCanvas; private Rectangle2D canvasRect = new Rectangle2D.Double(0, 0, 200, 200); private int canvasPosX = 100; private int canvasPosY = 100; public ElementDiagram(ICanvasContext parentCtx) { this.parentCtx = parentCtx; ctx = createCanvas(parentCtx.getThreadAccess(),parentCtx.getSceneGraph()); ICanvasParticipant mouseUtil = ctx.getSingleItem(MouseUtil.class); if (mouseUtil != null) { ctx.remove(mouseUtil); ctx.add(new ElementDiagramMouseUtil()); // ctx.add(new MouseUtil()); } ICanvasParticipant elementInteractor = ctx.getSingleItem(ElementInteractor.class); if (elementInteractor != null) { ctx.remove(elementInteractor); ctx.add(new ElementDiagramElementInteractor()); } diagram = createDiagram(); ctx.getDefaultHintContext().setHint(DiagramHints.KEY_DIAGRAM, diagram); subCanvas = new SubCanvas(ctx, 10000, Integer.MAX_VALUE - 100, 10000); parentCtx.add(subCanvas); } public void setSize(int x, int y, int width, int height) { canvasPosX = x; canvasPosY = y; canvasRect = new Rectangle2D.Double(0, 0, width, height); getCanvas().getCanvasNode().setTransform(new AffineTransform(1,0,0,1,x,y)); } public Rectangle2D getSize() { return canvasRect; } /** * Override this for custom functionality * * @return */ public IDiagram createDiagram() { IDiagram d = Diagram.spawnNew(DiagramClass.DEFAULT); return d; } /** * Override this for custom functionality * * @param thread * @return */ public ICanvasContext createCanvas(IThreadWorkQueue thread, G2DSceneGraph sg) { return createDefaultCanvas(thread,sg); } public void setDiagram(IDiagram diagram) { this.diagram = diagram; ctx.getDefaultHintContext().setHint(DiagramHints.KEY_DIAGRAM, diagram); } public ICanvasContext getCanvas() { return ctx; } public ICanvasContext getParentCanvas() { return parentCtx; } public SubCanvas getSubCanvas() { return subCanvas; } public IDiagram getDiagram() { return diagram; } @Override public void addElement(IElement element) { diagram.addElement(element); } @Override public void removeElement(IElement element) { diagram.removeElement(element); } @Override public void addCompositionListener(CompositionListener listener) { diagram.addCompositionListener(listener); } @Override public void addCompositionVetoListener(CompositionVetoListener listener) { diagram.addCompositionVetoListener(listener); } @Override public void addHintListener(IHintListener listener) { diagram.addHintListener(listener); } @Override public void addHintListener(IThreadWorkQueue threadAccess, IHintListener listener) { diagram.addHintListener(threadAccess, listener); } @Override public void addKeyHintListener(IThreadWorkQueue threadAccess, Key key, IHintListener listener) { diagram.addKeyHintListener(threadAccess, key, listener); } @Override public void addKeyHintListener(Key key, IHintListener listener) { diagram.addKeyHintListener(key, listener); } @Override public boolean bringToTop(IElement e) { return diagram.bringToTop(e); } @Override public boolean bringUp(IElement e) { return diagram.bringUp(e); } @Override public void destroy() { diagram.destroy(); } @Override public void dispose() { diagram.dispose(); } @Override public DiagramClass getDiagramClass() { return diagram.getDiagramClass(); } @Override public boolean containsElement(IElement element) { return diagram.containsElement(element); } @Override public List getElements() { return diagram.getElements(); } @Override public void sort(Comparator comparator) { diagram.sort(comparator); } @Override public List getSnapshot() { return diagram.getSnapshot(); } @Override public void clearWithoutNotification() { diagram.clearWithoutNotification(); } @Override public boolean containsHint(Key key) { return diagram.containsHint(key); } @Override public E getHint(Key key) { return diagram.getHint(key); } @Override public Map getHints() { return diagram.getHints(); } @Override public Map getHintsUnsafe() { return diagram.getHintsUnsafe(); } @Override public Map getHintsOfClass(Class clazz) { return diagram.getHintsOfClass(clazz); } @Override public boolean moveTo(IElement e, int position) { return diagram.moveTo(e, position); } @Override public void removeCompositionListener(CompositionListener listener) { diagram.removeCompositionListener(listener); } @Override public void removeCompositionVetoListener(CompositionVetoListener listener) { diagram.removeCompositionVetoListener(listener); } @Override public E removeHint(Key key) { return diagram.removeHint(key); } @Override public void removeHintListener(IHintListener listener) { diagram.removeHintListener(listener); } @Override public void removeHintListener(IThreadWorkQueue threadAccess, IHintListener listener) { diagram.removeHintListener(threadAccess, listener); } @Override public void removeKeyHintListener(IThreadWorkQueue threadAccess, Key key, IHintListener listener) { diagram.removeKeyHintListener(threadAccess, key, listener); } @Override public void removeKeyHintListener(Key key, IHintListener listener) { diagram.removeKeyHintListener(key, listener); } @Override public boolean sendDown(IElement e) { return diagram.sendDown(e); } @Override public boolean sendToBottom(IElement e) { return diagram.sendToBottom(e); } @Override public void setHint(Key key, Object value) { diagram.setHint(key, value); } @Override public void setHints(Map hints) { diagram.setHints(hints); } public ICanvasContext createDefaultCanvas(IThreadWorkQueue thread, G2DSceneGraph sg) { // Create canvas context and a layer of interactors ICanvasContext canvasContext = new ElementDiagramCanvasContext(thread,sg); IHintContext h = canvasContext.getDefaultHintContext(); //canvasContext.add(new PanZoomRotateHandler()); // Must be before // TransformUtil // Support & Util Participants canvasContext.add(new TransformUtil()); canvasContext.add(new MouseUtil()); canvasContext.add(new KeyUtil()); canvasContext.add(new CanvasGrab()); canvasContext.add(new SymbolUtil()); canvasContext.add(new TimeParticipant()); // Debug participant(s) // canvasContext.add( new PointerPainter() ); canvasContext.add(new HandPainter()); h.setHint(PointerPainter.KEY_PAINT_POINTER, true); // Pan & Zoom & Rotate // canvasContext.add( new MousePanZoomInteractor() ); // canvasContext.add( new MultitouchPanZoomRotateInteractor() ); // canvasContext.add( new OrientationRestorer() ); // Grid & Ruler & Background canvasContext.add(new GridPainter()); canvasContext.add(new RulerPainter()); canvasContext.add(new BackgroundPainter()); h.setHint(Hints.KEY_GRID_COLOR, new Color(0.9f, 0.9f, 0.9f)); h.setHint(Hints.KEY_BACKGROUND_COLOR, Color.LIGHT_GRAY); // Key bindings canvasContext.add(new KeyToCommand(CommandKeyBinding.DEFAULT_BINDINGS)); // //// Diagram Participants ////// canvasContext.add(new PointerInteractor()); canvasContext.add(new ElementInteractor()); canvasContext.add(new Selection()); canvasContext.add(new DiagramParticipant()); canvasContext.add(new ElementPainter()); canvasContext.add(new TerminalPainter(true, true, false, true)); //canvasContext.add(new ElementHeartbeater()); canvasContext.add(new ZOrderHandler()); canvasContext.add(new ZoomTransitionParticipant(TransitionFunction.SIGMOID)); h.setHint(Hints.KEY_TOOL, Hints.PANTOOL); return canvasContext; } public class ElementDiagramCanvasContext extends Context implements ICanvasContext { protected HintStack hintStack = new HintStack(); protected HintContext bottomHintContext = new HintContext(); protected IEventHandlerStack eventHandlerStack = null; protected boolean eventHandlingOrdered = false; protected EventQueue eventQueue = null; protected IContentContext paintableCtx = new PaintableContextImpl(); protected final IThreadWorkQueue thread; protected IMouseCaptureContext mouseCaptureCtx = new MouseCaptureContext(); protected IMouseCursorContext mouseCursorCtx = new MouseCursorContext(); protected G2DSceneGraph sg; protected G2DParentNode canvasNode = null; protected ITooltipProvider tooltip; /** * * @param thread context thread, or null if sync policy not used */ public ElementDiagramCanvasContext(IThreadWorkQueue thread, G2DSceneGraph sg) { super(ICanvasParticipant.class); if (thread == null) throw new IllegalArgumentException("null"); this.thread = thread; eventHandlerStack = new EventHandlerStack(thread); eventQueue = new EventQueue(eventHandlerStack); hintStack.addHintContext(bottomHintContext, Integer.MIN_VALUE); this.sg = sg; canvasNode = sg.addNode("elementd" + SceneGraphConstants.NAVIGATION_NODE_NAME , G2DParentNode.class); // Add dummy parent node canvasNode.setZIndex(1000); this.addContextListener(thread, new IContextListener() { @Override public void itemAdded(IContext sender, ICanvasParticipant item) { item.addedToContext(ElementDiagramCanvasContext.this); } @Override public void itemRemoved(IContext sender, ICanvasParticipant item) { item.removedFromContext(ElementDiagramCanvasContext.this); } }); eventQueue.addEventCoalesceler(MouseEventCoalescer.INSTANCE); // Order event handling if events are added to the queue eventQueue.addQueueListener(new IEventQueueListener() { @Override public void onEventAdded(IEventQueue queue, Event e, int index) { asyncHandleEvents(); } @Override public void onQueueEmpty(IEventQueue queue) { } }); } public IHintStack getHintStack() { assertNotDisposed(); return hintStack; } private final Runnable eventHandling = new Runnable() { @Override public void run() { eventHandlingOrdered = false; eventQueue.handleEvents(); } }; synchronized void asyncHandleEvents() { if (eventHandlingOrdered) return; eventHandlingOrdered = true; ThreadUtils.asyncExec(thread, eventHandling); } synchronized void syncHandleEvents() { if (eventHandlingOrdered) return; eventHandlingOrdered = true; ThreadUtils.syncExec(thread, eventHandling); } @Override public IEventHandlerStack getEventHandlerStack() { assertNotDisposed(); return eventHandlerStack; } @Override public IThreadWorkQueue getThreadAccess() { // assertNotDisposed(); return thread; } @Override protected void doDispose() { ThreadUtils.syncExec(getThreadAccess(), new Runnable() { @Override public void run() { clear(); // HN: added to decrease memory leaks if (sg != null) { // Makes sure that scene graph nodes free their resources! sg.removeNodes(); sg = null; } } }); } @Override public IHintContext getDefaultHintContext() { return bottomHintContext; } @Override public IMouseCursorContext getMouseCursorContext() { return mouseCursorCtx; } @Override public void setMouseCursorContext(IMouseCursorContext ctx) { this.mouseCursorCtx = ctx; } @Override public IMouseCaptureContext getMouseCaptureContext() { return mouseCaptureCtx; } @Override public void setMouseCaptureContext(IMouseCaptureContext mctx) { this.mouseCaptureCtx = mctx; } @Override public IEventQueue getEventQueue() { return eventQueue; } @Override public IContentContext getContentContext() { return paintableCtx; } @Override public void setContentContext(IContentContext ctx) { this.paintableCtx = ctx; } @Override public G2DSceneGraph getSceneGraph() { return sg; } @Override public G2DParentNode getCanvasNode() { return canvasNode; } @Override public void setCanvasNode(G2DParentNode node) { throw new RuntimeException("Cannot set canvasNode"); } protected final AtomicBoolean locked = new AtomicBoolean(false); @Override public boolean isLocked() { return this.locked.get(); } @Override public void setLocked(boolean locked) { boolean previous = this.locked.getAndSet(locked); if (!locked && previous != locked) { // The context was unlocked! getContentContext().setDirty(); } } @Override public ITooltipProvider getTooltipProvider() { return tooltip; } @Override public void setTooltipProvider(ITooltipProvider tooltipProvider) { this.tooltip = tooltipProvider; } } public class ElementDiagramMouseUtil extends MouseUtil { @Override @EventHandler(priority = Integer.MAX_VALUE) public boolean handleMouseEvent(MouseEvent e) { MouseEvent ne = createMouseEvent(e); if (ne != null) return handleMouseEvent2(ne); return false; } /** * Copy-pasted MouseUtil.handleMouseEvent with one modification; * Generating MouseClickEvents has been removed, because it created * duplicated events (with incorrect coordinates). * * @param e * @return */ public boolean handleMouseEvent2(MouseEvent e) { assertDependencies(); if (e instanceof MouseEnterEvent) { Point2D canvasPosition = util.controlToCanvas(e.controlPosition, null); // Reset mouse MouseInfo mi = new MouseInfo(e.mouseId, e.controlPosition, canvasPosition, e.buttons); miceInfo.put(e.mouseId, mi); } else if (e instanceof MouseExitEvent) { miceInfo.remove(e.mouseId); } else if (e instanceof MouseMovedEvent) { Point2D canvasPosition = util.controlToCanvas(e.controlPosition, null); double deltaDistance = 0; MouseInfo mi = miceInfo.get(e.mouseId); if (mi == null) { mi = new MouseInfo(e.mouseId, e.controlPosition, canvasPosition, 0/*e.buttons*/); miceInfo.put(e.mouseId, mi); } else { deltaDistance = e.controlPosition.distance(mi.controlPosition); mi.controlPosition = e.controlPosition; mi.canvasPosition = canvasPosition; } if (deltaDistance > 0) mi.addDistanceForButtons(deltaDistance); // Send mouse drag events. for (ButtonInfo bi : mi.buttonPressInfo.values()) { if (!bi.down) continue; if (bi.deltaMotion <= profile.movementTolerance) continue; if (bi.drag) continue; bi.drag = true; MouseDragBegin db = new MouseDragBegin(this, e.time, e.mouseId, e.buttons, e.stateMask, bi.button, bi.canvasPosition, bi.controlPosition, e.controlPosition, e.screenPosition); getContext().getEventQueue().queueFirst(db); } } else if (e instanceof MouseButtonPressedEvent) { Point2D canvasPosition = util.controlToCanvas(e.controlPosition, null); MouseButtonPressedEvent me = (MouseButtonPressedEvent) e; MouseInfo mi = miceInfo.get(e.mouseId); if (mi == null) { mi = new MouseInfo(e.mouseId, e.controlPosition, canvasPosition, e.buttons); miceInfo.put(e.mouseId, mi); } else { mi.controlPosition = e.controlPosition; mi.canvasPosition = canvasPosition; } mi.setButtonPressed(me.button, e.stateMask, e.controlPosition, canvasPosition, e.time); } else if (e instanceof MouseButtonReleasedEvent) { MouseButtonReleasedEvent me = (MouseButtonReleasedEvent) e; Point2D canvasPosition = util.controlToCanvas(me.controlPosition, null); MouseInfo mi = miceInfo.get(me.mouseId); if (mi == null) { mi = new MouseInfo(e.mouseId, me.controlPosition, canvasPosition, 0/*me.buttons*/); miceInfo.put(me.mouseId, mi); } else { mi.controlPosition = me.controlPosition; mi.canvasPosition = canvasPosition; } ButtonInfo bi = mi.releaseButton(me.button, me.time); if (bi == null) return false; if (me.holdTime > profile.clickHoldTimeTolerance) return false; if (bi.deltaMotion > profile.movementTolerance) return false; // This is a click long timeSinceLastClick = me.time - bi.lastClickEventTime; bi.lastClickEventTime = me.time; // reset click counter if (timeSinceLastClick > profile.consecutiveToleranceTime) bi.clickCount = 0; else bi.clickCount++; } return false; } } public class ElementDiagramElementInteractor extends ElementInteractor { @Override @EventHandler(priority = INTERACTOR_PRIORITY) public boolean handleMouseEvent(MouseEvent me) { MouseEvent ne = createMouseEvent(me); if (ne != null) return super.handleMouseEvent(ne); return false; } } public MouseEvent createMouseEvent(MouseEvent e) { MouseEvent newEvent = null; double x = e.controlPosition.getX(); double y = e.controlPosition.getY(); Point2D newPos = new Point2D.Double(x - canvasPosX, y - canvasPosY); if (!canvasRect.contains(newPos)) return new MouseExitEvent(e.context, e.time, e.mouseId, e.buttons, e.stateMask, newPos, e.screenPosition); if (e instanceof MouseButtonPressedEvent) { newEvent = new MouseButtonPressedEvent(e.context, e.time, e.mouseId, e.buttons, e.stateMask, ((MouseButtonEvent) e).button, newPos, e.screenPosition); } else if (e instanceof MouseButtonReleasedEvent) { newEvent = new MouseButtonReleasedEvent(e.context, e.time, e.mouseId, e.buttons, e.stateMask, ((MouseButtonEvent) e).button, ((MouseButtonReleasedEvent) e).holdTime, newPos, e.screenPosition); } else if (e instanceof MouseDoubleClickedEvent) { newEvent = new MouseDoubleClickedEvent(e.context, e.time, e.mouseId, e.buttons, e.stateMask, ((MouseButtonEvent) e).button, newPos, e.screenPosition); } else if (e instanceof MouseClickEvent) { newEvent = new MouseClickEvent(e.context, e.time, e.mouseId, e.buttons, e.stateMask, ((MouseButtonEvent) e).button, ((MouseClickEvent) e).clickCount, newPos, e.screenPosition); } else if (e instanceof MouseDragBegin) { newEvent = new MouseDragBegin(e.context, e.time, e.mouseId, e.buttons, e.stateMask, ((MouseButtonEvent) e).button, ((MouseDragBegin) e).startCanvasPos, ((MouseDragBegin) e).startControlPos, newPos, e.screenPosition); } else if (e instanceof MouseEnterEvent) { newEvent = new MouseEnterEvent(e.context, e.time, e.mouseId, e.buttons, e.stateMask, newPos, e.screenPosition); } else if (e instanceof MouseExitEvent) { newEvent = new MouseExitEvent(e.context, e.time, e.mouseId, e.buttons, e.stateMask, newPos, e.screenPosition); } else if (e instanceof MouseMovedEvent) { newEvent = new MouseMovedEvent(e.context, e.time, e.mouseId, e.buttons, e.stateMask, newPos, e.screenPosition); } else if (e instanceof MouseWheelMovedEvent) { newEvent = new MouseWheelMovedEvent(e.context, e.time, e.mouseId, e.buttons, e.stateMask, newPos, e.screenPosition, ((MouseWheelMovedEvent) e).scrollType, ((MouseWheelMovedEvent) e).scrollAmount, ((MouseWheelMovedEvent) e).wheelRotation); } else { throw new Error("Unknow event " + e.getClass() + " " + e); } // System.out.println(newPos + " " + newEvent + " "+ e); return newEvent; } }