/******************************************************************************* * Copyright (c) 2007, 2011 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.scenegraph.g2d.events; import java.awt.Component; import java.awt.dnd.DnDConstants; import java.awt.dnd.DragGestureEvent; import java.awt.dnd.DragGestureListener; import java.awt.dnd.DragSource; import java.awt.dnd.DragSourceDragEvent; import java.awt.dnd.DragSourceDropEvent; import java.awt.dnd.DragSourceEvent; import java.awt.dnd.DragSourceListener; import java.awt.event.InputEvent; import java.awt.geom.Point2D; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.List; import org.simantics.scenegraph.INode; import org.simantics.scenegraph.g2d.G2DFocusManager; import org.simantics.scenegraph.g2d.G2DSceneGraph; import org.simantics.scenegraph.g2d.IG2DNode; import org.simantics.scenegraph.g2d.events.MouseEvent.MouseButtonPressedEvent; import org.simantics.scenegraph.g2d.events.MouseEvent.MouseDragBegin; import org.simantics.scenegraph.g2d.events.adapter.AWTMouseEventAdapter; import org.simantics.scenegraph.g2d.events.command.CommandEvent; /** * Delivers events (mouse, key, focus, command, time) to scene graph nodes that * have registered to receive them. * * @author Tuukka Lehtonen */ public class NodeEventHandler implements IEventHandler { private static final boolean DEBUG_EVENTS = false; private static final boolean DEBUG_HANDLER_SORT = false; private static final IEventHandler[] NONE = {}; public static class TreePreOrderComparator implements Comparator { static enum Order { ASCENDING, DESCENDING } static class Temp { ArrayList path1 = new ArrayList(); ArrayList path2 = new ArrayList(); } private transient ThreadLocal temp = new ThreadLocal() { protected Temp initialValue() { return new Temp(); } }; Order order; public TreePreOrderComparator(Order order) { this.order = order; } void getTreePath(INode node, ArrayList result) { result.clear(); for (INode parent = node.getParent(); parent != null; parent = parent.getParent()) result.add(parent); } void notSameGraph(INode o1, INode o2) { throw new IllegalStateException("nodes " + o1 + " and " + o2 + " not part of same scene graph.\n\t root 1: " + o1.getRootNode() + "\n\troot 2: " + o2.getRootNode()); } @Override public int compare(IEventHandler e1, IEventHandler e2) { if (e1 == e2) return 0; Temp tmp = temp.get(); ArrayList path1 = tmp.path1; ArrayList path2 = tmp.path2; // Get path to root node for both nodes INode o1 = (INode) e1; INode o2 = (INode) e2; getTreePath(o1, path1); getTreePath(o2, path2); // Sanity checks: nodes part of same scene graph INode root1 = path1.isEmpty() ? o1 : path1.get(path1.size() - 1); INode root2 = path2.isEmpty() ? o2 : path2.get(path2.size() - 1); if (root1 != root2) notSameGraph(o1, o2); try { // Find first non-matching nodes in the paths starting from the root node int i1 = path1.size() - 1; int i2 = path2.size() - 1; for (; i1 >= 0 && i2 >= 0; --i1, --i2) { INode p1 = path1.get(i1); INode p2 = path2.get(i2); if (p1 != p2) { break; } } // Pre-order: a node that is on the tree path of another node is first if (i1 < 0) return Order.ASCENDING == order ? -1 : 1; if (i2 < 0) return Order.ASCENDING == order ? 1 : -1; INode n1 = path1.get(i1); INode n2 = path2.get(i2); IG2DNode g1 = n1 instanceof IG2DNode ? (IG2DNode) n1 : null; IG2DNode g2 = n2 instanceof IG2DNode ? (IG2DNode) n2 : null; if (g1 != null && g2 != null) { int z1 = g1.getZIndex(); int z2 = g2.getZIndex(); int c = compare(z1, z2); return order == Order.ASCENDING ? c : -c; } // Can't sort non-IG2DNodes. return 0; } finally { // Don't hold on to objects unnecessarily path1.clear(); path2.clear(); } } private int compare(int v1, int v2) { return v1 < v2 ? -1 : (v1 > v2 ? 1 : 0); } }; TreePreOrderComparator COMPARATOR = new TreePreOrderComparator(TreePreOrderComparator.Order.DESCENDING); /** * {@link FocusEvent} are propagated first to the scene graph focus node, * then to event handler nodes in scene graph tree pre-order. */ protected List focusListeners = new ArrayList(); protected IEventHandler[] sortedFocusListeners = null; /** * {@link TimeEvent} are propagated first to the scene graph focus node, * then to event handler nodes in scene graph tree pre-order. */ protected List timeListeners = new ArrayList(); protected IEventHandler[] sortedTimeListeners = null; /** * {@link CommandEvent} are propagated first to the scene graph focus node, * then to event handler nodes in scene graph tree pre-order. */ protected List commandListeners = new ArrayList(); protected IEventHandler[] sortedCommandListeners = null; /** * {@link KeyEvent} are propagated first to the scene graph focus node, then * to event handler nodes in scene graph tree pre-order. */ protected List keyListeners = new ArrayList(); protected IEventHandler[] sortedKeyListeners = null; /** * {@link MouseEvent} are propagated first to the scene graph focus node, * then to event handler nodes in scene graph tree pre-order. */ protected List mouseListeners = new ArrayList(); protected IEventHandler[] sortedMouseListeners = null; /** * {@link MouseDragBegin} events are propagated first to the scene graph focus node, then * to event handler nodes in scene graph tree pre-order. */ protected List mouseDragBeginListeners = new ArrayList(); protected IEventHandler[] sortedMouseDragBeginListeners = null; /** * The scene graph this instance handles event propagation for. */ protected G2DSceneGraph sg; /** * For proper initiation of native DnD operations within this AWT-based * scenegraph system. */ protected DragSource ds = new DragSource(); public NodeEventHandler(G2DSceneGraph sg) { this.sg = sg; } @SuppressWarnings("unused") private IEventHandler[] sort(IEventHandler[] sort) { if (DEBUG_HANDLER_SORT) debug("copy sort " + sort.length + " handlers"); return sortInplace(Arrays.copyOf(sort, sort.length)); } private IEventHandler[] sortInplace(IEventHandler[] sort) { if (DEBUG_HANDLER_SORT) debug("in-place sort " + sort.length + " handlers"); Arrays.sort(sort, COMPARATOR); return sort; } public void setRootPane(Component rootPane) { final DragSourceListener dsl = new DragSourceListener() { @Override public void dropActionChanged(DragSourceDragEvent dsde) { } @Override public void dragOver(DragSourceDragEvent dsde) { } @Override public void dragExit(DragSourceEvent dse) { } @Override public void dragEnter(DragSourceDragEvent dsde) { } @Override public void dragDropEnd(DragSourceDropEvent dsde) { } }; DragGestureListener dgl = new DragGestureListener() { @Override public void dragGestureRecognized(DragGestureEvent dge) { InputEvent ie = dge.getTriggerEvent(); if (ie instanceof java.awt.event.MouseEvent) { java.awt.event.MouseEvent e = (java.awt.event.MouseEvent) ie; Point2D controlPos = AWTMouseEventAdapter.getControlPosition(e); MouseDragBegin event = new MouseDragBegin(NodeEventHandler.this, e.getWhen(), 0, AWTMouseEventAdapter.getButtonStatus(e), AWTMouseEventAdapter.getStateMask(e), AWTMouseEventAdapter.getMouseButton(e), // TODO: fix canvas position if necessary new Point2D.Double(), controlPos, controlPos, AWTMouseEventAdapter.getScreenPosition(e)); // Send MouseDragBegin to the scenegraph and see // if anyone sets event.transferable to start DnD. handleMouseDragBeginEvent(event, EventTypes.MouseDragBegin); if (event.transferable != null) { ds.startDrag(dge, null, event.transferable, dsl); if (DEBUG_EVENTS) debug("dragGestureRecognized: startDrag " + event.transferable); } } } }; ds.createDefaultDragGestureRecognizer( rootPane, DnDConstants.ACTION_COPY_OR_MOVE | DnDConstants.ACTION_LINK, dgl); ds.addDragSourceListener(dsl); } public boolean mousePressed(MouseButtonPressedEvent event) { G2DFocusManager.INSTANCE.clearFocus(); try { // Point op = event.getPoint(); // for (MouseListener l : mouseListeners.getListeners()) { // MouseEvent e = (MouseEvent) NodeUtil.transformEvent(event,(IG2DNode) l); // l.mousePressed(e); // event.translatePoint((int)(op.getX()-event.getX()), (int)(op.getY()-event.getY())); // if (e.isConsumed()) // break; // } return false; } finally { if (sg.getRootPane() != null) { if (G2DFocusManager.INSTANCE.getFocusOwner() == null) { sg.getRootPane().requestFocusInWindow(); //sg.getRootPane().repaint(); //TODO : why repaint here? FocusOwner seems to be always null, so this causes unnecessary delays when interacting the canvas. } } } } private boolean handleEvent(Event e, IG2DNode focusNode, IEventHandler[] handlers) { int typeMask = EventTypes.toTypeMask(e); if (focusNode instanceof IEventHandler) { IEventHandler h = (IEventHandler) focusNode; if (eats(h.getEventMask(), typeMask)) { if (h.handleEvent(e)) return true; } } for (IEventHandler l : handlers) { if (eats(l.getEventMask(), typeMask)) { if (l.handleEvent(e)) return true; } } return false; } private boolean handleMouseEvent(MouseEvent e, int eventType) { IEventHandler[] sorted = sortedMouseListeners; if (sorted == null) sortedMouseListeners = sorted = sortInplace(mouseListeners.toArray(NONE)); return handleEvent(e, sg.getFocusNode(), sorted); } private boolean handleMouseDragBeginEvent(MouseEvent e, int eventType) { IEventHandler[] sorted = sortedMouseDragBeginListeners; if (sorted == null) sortedMouseDragBeginListeners = sorted = sortInplace(mouseDragBeginListeners.toArray(NONE)); // Give null for focusNode because we want to propagate // this event in scene tree pre-order only. return handleEvent(e, null, sorted); } private boolean handleFocusEvent(FocusEvent e) { IEventHandler[] sorted = sortedFocusListeners; if (sorted == null) sortedFocusListeners = sorted = sortInplace(focusListeners.toArray(NONE)); return handleEvent(e, null, sorted); } private boolean handleTimeEvent(TimeEvent e) { IEventHandler[] sorted = sortedTimeListeners; if (sorted == null) sortedTimeListeners = sorted = sortInplace(timeListeners.toArray(NONE)); return handleEvent(e, null, sorted); } private boolean handleCommandEvent(CommandEvent e) { IEventHandler[] sorted = sortedCommandListeners; if (sorted == null) sortedCommandListeners = sorted = sortInplace(commandListeners.toArray(NONE)); return handleEvent(e, sg.getFocusNode(), sorted); } private boolean handleKeyEvent(KeyEvent e) { IEventHandler[] sorted = sortedKeyListeners; if (sorted == null) sortedKeyListeners = sorted = sortInplace(keyListeners.toArray(NONE)); return handleEvent(e, sg.getFocusNode(), sorted); } @Override public int getEventMask() { return EventTypes.AnyMask; } @Override public boolean handleEvent(Event e) { if (DEBUG_EVENTS) debug("handle event: " + e); int eventType = EventTypes.toType(e); switch (eventType) { case EventTypes.Command: return handleCommandEvent((CommandEvent) e); case EventTypes.FocusGained: case EventTypes.FocusLost: return handleFocusEvent((FocusEvent) e); case EventTypes.KeyPressed: case EventTypes.KeyReleased: return handleKeyEvent((KeyEvent) e); case EventTypes.MouseDragBegin: return handleMouseDragBeginEvent((MouseEvent) e, eventType); case EventTypes.MouseButtonPressed: case EventTypes.MouseButtonReleased: case EventTypes.MouseClick: case EventTypes.MouseDoubleClick: case EventTypes.MouseEnter: case EventTypes.MouseExit: case EventTypes.MouseMoved: case EventTypes.MouseWheel: return handleMouseEvent((MouseEvent) e, eventType); case EventTypes.Time: return handleTimeEvent((TimeEvent) e); } return false; } public void add(IEventHandler item) { if (!(item instanceof IG2DNode)) throw new IllegalArgumentException("event handler must be an IG2DNode"); int mask = item.getEventMask(); if (eats(mask, EventTypes.CommandMask)) { commandListeners.add(item); sortedCommandListeners = null; } if (eats(mask, EventTypes.FocusMask)) { focusListeners.add(item); sortedFocusListeners = null; } if (eats(mask, EventTypes.KeyMask)) { keyListeners.add(item); sortedKeyListeners = null; } if (eats(mask, EventTypes.MouseDragBeginMask)) { mouseDragBeginListeners.add(item); sortedMouseDragBeginListeners = null; } if (eats(mask, EventTypes.MouseMask & ~EventTypes.MouseDragBeginMask)) { mouseListeners.add(item); sortedMouseListeners = null; } if (eats(mask, EventTypes.TimeMask)) { timeListeners.add(item); sortedTimeListeners = null; } } public boolean remove(IEventHandler item) { if (!(item instanceof IG2DNode)) throw new IllegalArgumentException("event handler must be an IG2DNode"); int mask = item.getEventMask(); boolean removed = false; if (eats(mask, EventTypes.CommandMask)) { removed |= commandListeners.remove(item); sortedCommandListeners = null; } if (eats(mask, EventTypes.FocusMask)) { removed |= focusListeners.remove(item); sortedFocusListeners = null; } if (eats(mask, EventTypes.KeyMask)) { removed |= keyListeners.remove(item); sortedKeyListeners = null; } if (eats(mask, EventTypes.MouseDragBeginMask)) { removed |= mouseDragBeginListeners.remove(item); sortedMouseDragBeginListeners = null; } if (eats(mask, EventTypes.MouseMask & ~EventTypes.MouseDragBeginMask)) { removed |= mouseListeners.remove(item); sortedMouseListeners = null; } if (eats(mask, EventTypes.TimeMask)) { removed |= timeListeners.remove(item); sortedTimeListeners = null; } return removed; } private static boolean eats(int handlerMask, int eventTypeMask) { return (handlerMask & eventTypeMask) != 0; } private void debug(String msg) { System.out.println(getClass().getSimpleName() + ": " + msg); } public void dispose() { commandListeners.clear(); commandListeners = null; focusListeners.clear(); focusListeners = null; keyListeners.clear(); keyListeners = null; mouseListeners.clear(); mouseListeners = null; sg = null; sortedCommandListeners = null; sortedKeyListeners = null; sortedMouseListeners = null; timeListeners.clear(); timeListeners = null; } }