/*
* NanoGraph, a small footprint java graph drawing component
*
* Copyright 2004 Jeroen van Grondelle
* 2013 Xander Uiterlinden
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.nanograph.interaction;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import org.nanograph.drawing.NanoGraph;
import org.nanograph.drawing.graphicsadapter.GraphicsAdapter;
import org.nanograph.interaction.events.GraphActionEvent;
import org.nanograph.interaction.events.GraphActionListener;
import org.nanograph.interaction.events.GraphSelectionEvent;
import org.nanograph.interaction.events.GraphSelectionListener;
import org.nanograph.interaction.selection.Selection;
/**
* InteractionManager implements generic interaction behaviour for managing
* selections and event listeners by interpreting mouse clicks and keyboard
* shortcuts.
*
* The interactionmanager uses the Strategy pattern to implement complex
* selection manipulation: each possible function a mouse operation has is
* implemented in a Strategy class, the strategies decide on their own succesor.
*
* @author Jeroen van Grondelle
*/
public class InteractionManager {
public final static int MODIFIER_CTRL = 1, MODIFIER_SHIFT = 2, MODIFIER_ALT = 4, MODIFIER_RIGHTCLICK = 8;
public final SelectionMode DEFAULT = new DefaultSelectionMode(), SELECT = new GroupSelectSelectionMode(), MOVE_GROUP = new MoveSelectionMode(), MOVE_NODE = new MoveNodeMode(), ACTION = new ActionMode();
NanoGraph nanograph = null;
Selection nodeSelection = new Selection();
Object selectedEdge = null;
SelectionMode selectionMode = DEFAULT;
Rectangle2D selectionBox = null, moveBox = null;
private boolean interactionEnabled = true;
private boolean draggingSelection = false;
public InteractionManager(NanoGraph nanograph) {
this.nanograph = nanograph;
}
public void handleMouseDown(int x, int y, int modifiers) {
if (!interactionEnabled) {
return;
}
fireGraphActionResetEvent();
selectionMode.startSelection(x, y, modifiers);
}
public void handleMouseDrag(int x, int y) {
if (!interactionEnabled) {
return;
}
if (!draggingSelection && !nodeSelection.isEmpty()) {
draggingSelection = true;
}
selectionMode.dragSelection(x, y);
}
public void handleMouseUp(int x, int y) {
if (!interactionEnabled) {
return;
}
if (draggingSelection && !nodeSelection.isEmpty()) {
draggingSelection = false;
fireSelectionMoveEvent();
}
selectionMode.endSelection(x, y);
}
public void handleKeyboardShortcut(int character, int modifiers) {
if (!interactionEnabled) {
return;
}
if ((modifiers & MODIFIER_CTRL) == 1) {
if (character == 'a' || character == 'A') {
// ctrl+a
// select all
nodeSelection.reset();
selectedEdge = null;
for (int i = 0; i < nanograph.getModel().getNodeCount(); i++) {
nodeSelection.add(nanograph.getModel().getNode(i));
}
setNodeSelection(nodeSelection);
fireSelectionChangedEvent();
}
if (character == 'c' || character == 'C') {
// ctrl+c
}
if (character == 'x' || character == 'X') {
// ctrl+x
}
if (character == 'v' || character == 'V') {
// ctrl+v
}
if (character == 'z' || character == 'Z') {
// ctrl+z
}
}
}
private void switchMode(SelectionMode mode) {
selectionMode = mode;
}
private interface SelectionMode {
void startSelection(int x, int y, int modifiers);
void dragSelection(int x, int y);
void endSelection(int x, int y);
}
private class DefaultSelectionMode implements SelectionMode {
Object selectedNode = null;
int dragCount = 0, modifiers;
int xdown = 0, ydown = 0;
public void startSelection(int x, int y, int modifiers) {
// empty selection
dragCount = 0;
this.modifiers = modifiers;
xdown = x;
ydown = y;
selectedNode = nanograph.getNodeForLocation(new Point2D.Double(x, y));
selectedEdge = nanograph.getEdgeForLocation(new Point2D.Double(x, y));
if (selectedNode != null) {
nanograph.setSelectedEdge(null);
} else {
nanograph.setSelectedEdge(selectedEdge);
nodeSelection.reset();
fireSelectionChangedEvent();
}
// handle rightclick
if ((modifiers & MODIFIER_RIGHTCLICK) != 0) {
switchMode(ACTION);
selectionMode.startSelection(x, y, modifiers);
// handle others
} else if (selectedNode == null && selectedEdge == null) {
// A click outside nodes suggest a group selection operation
switchMode(SELECT);
selectionMode.startSelection(x, y, modifiers);
}
}
public void dragSelection(int x, int y) {
dragCount++;
if (selectedNode != null && !nodeSelection.contains(selectedNode)) {
nodeSelection.reset();
fireSelectionResetEvent();
nodeSelection.add(selectedNode);
fireSelectionChangedEvent();
}
// a drag on one of the selected nodes indicates a move
if (nodeSelection.size() == 0) {
} else if (nodeSelection.size() == 1) {
switchMode(MOVE_NODE);
selectionMode.startSelection(xdown, ydown, modifiers);
} else {
switchMode(MOVE_GROUP);
selectionMode.startSelection(xdown, ydown, modifiers);
}
}
public void endSelection(int x, int y) {
if (dragCount == 0) {
if ((modifiers & MODIFIER_CTRL) == 0) {
// ctrl not pressed
nodeSelection.reset();
if (selectedNode != null) {
nodeSelection.toggle(selectedNode);
}
} else {
if (selectedNode != null) {
nodeSelection.toggle(selectedNode);
} else {
nodeSelection.reset();
}
}
}
if (!nodeSelection.isEmpty()) {
fireSelectionChangedEvent();
} else if (selectedEdge == null) {
fireSelectionResetEvent();
}
}
public String toString() {
return "DEFAULT";
}
}
class GroupSelectSelectionMode implements SelectionMode {
int startx = 0, starty = 0;
public void startSelection(int x, int y, int modifiers) {
startx = x;
starty = y;
// if shift is not down
if ((modifiers & MODIFIER_CTRL) == 0) {
nodeSelection.reset();
fireSelectionResetEvent();
}
}
public void dragSelection(int x, int y) {
// update selectionbox
selectionBox = new Rectangle2D.Double(Math.min((double) startx, x), Math.min((double) starty, y), Math.max((double) startx, x) - Math.min((double) startx, x), Math.max((double) starty, y) - Math.min((double) starty, y));
}
public void endSelection(int x, int y) {
// update Selection according to SelectionBox
//
if (selectionBox != null) {
nodeSelection.addAll(nanograph.getNodesForBounds(selectionBox));
fireSelectionChangedEvent();
}
selectionBox = null;
switchMode(DEFAULT);
}
public String toString() {
return "SELECT";
}
}
class MoveSelectionMode implements SelectionMode {
int startx = 0, starty = 0;
// movebox parameters
int deltax = 0, deltay = 0;
double width, height;
public void startSelection(int x, int y, int modifiers) {
startx = x;
starty = y;
// create moveBox
moveBox = nanograph.getBoundsForNodes(nodeSelection.getSelectedObjects());
deltax = (int) moveBox.getMinX() - x;
deltay = (int) moveBox.getMinY() - y;
width = moveBox.getWidth();
height = moveBox.getHeight();
}
public void dragSelection(int x, int y) {
int newX = x + deltax - 10;
int newY = y + deltay - 10;
double mdeltax = newX - moveBox.getX();
double mdeltay = newY - moveBox.getY();
moveBox.setFrame((newX > 0 ? newX : 0), (newY > 0 ? newY : 0), width + 20, height + 20);
//
double moveX = mdeltax;
double moveY = mdeltay;
for (Iterator iter = nodeSelection.getSelectedObjects().iterator(); iter.hasNext();) {
Object node = iter.next();
Point2D p = nanograph.getModel().getLocation(node);
double bnewX = p.getX() + moveX;
double bnewY = p.getY() + moveY;
double tempMoveX = moveX + 0 - bnewX;
double tempMoveY = moveY + 0 - bnewY;
if (newX < 0 && Math.abs(tempMoveX) < Math.abs(moveX)) {
moveX = tempMoveX;
}
if (newY < 0 && Math.abs(tempMoveY) < Math.abs(moveY)) {
moveY = tempMoveY;
}
}
// use a tempmap because we don't know how the locations are stored
// in the model.
// this way, we first calculated the new positions and then set them
// on the model
Map tempMap = new HashMap();
for (Iterator iter = nodeSelection.getSelectedObjects().iterator(); iter.hasNext();) {
Object node = iter.next();
Point2D p = nanograph.getModel().getLocation(node);
double bnewX = p.getX() + moveX;
double bnewY = p.getY() + moveY;
// put the new value in the tempMap
tempMap.put(node, new Point2D.Double((bnewX > 0 ? bnewX : 0), (bnewY > 0 ? bnewY : 0)));
}
// set the new positions on the model
for (Iterator iter = tempMap.keySet().iterator(); iter.hasNext();) {
Object node = iter.next();
nanograph.getModel().setLocation(node, (Point2D) tempMap.get(node));
}
}
public void endSelection(int x, int y) {
moveBox = null;
switchMode(DEFAULT);
}
public String toString() {
return "MOVE GROUP";
}
}
class MoveNodeMode implements SelectionMode {
int startx = 0, starty = 0;
int deltax = 0, deltay = 0;
public void startSelection(int x, int y, int modifiers) {
startx = x;
starty = y;
Point2D p = nanograph.getModel().getLocation(nodeSelection.getSelectedObjects().iterator().next());
if (p != null) {
deltax = (int) p.getX() - x;
deltay = (int) p.getY() - y;
}
}
public void dragSelection(int x, int y) {
int newX = x + deltax;
int newY = y + deltay;
nanograph.getModel().setLocation(nodeSelection.getSelectedObjects().iterator().next(), new Point2D.Double((newX > 0 ? newX : 0), (newY > 0 ? newY : 0)));
}
public void endSelection(int x, int y) {
int newX = x + deltax;
int newY = y + deltay;
nanograph.getModel().setLocation(nodeSelection.getSelectedObjects().iterator().next(), new Point2D.Double((newX > 0 ? newX : 0), (newY > 0 ? newY : 0)));
switchMode(DEFAULT);
}
public String toString() {
return "MOVE NODE";
}
}
class ActionMode implements SelectionMode {
boolean moved = false;
public void startSelection(int x, int y, int modifiers) {
moved = false;
if ((modifiers & MODIFIER_CTRL) == 0) {
nodeSelection.reset();
fireSelectionResetEvent();
}
}
public void dragSelection(int x, int y) {
moved = true;
}
public void endSelection(int x, int y) {
if (!moved) {
Object node = nanograph.getNodeForLocation(new Point2D.Double(x, y));
selectedEdge = nanograph.getEdgeForLocation(new Point2D.Double(x, y));
nanograph.setSelectedEdge(selectedEdge);
if (node != null) {
selectedEdge = null;
nodeSelection.toggle(node);
nanograph.setSelectedEdge(null);
fireGraphActionEvent(node, GraphActionEvent.NODE_ACTION);
} else {
if (selectedEdge != null) {
nodeSelection.reset();
fireGraphActionEvent(selectedEdge, GraphActionEvent.EDGE_ACTION);
}
}
}
switchMode(DEFAULT);
}
public String toString() {
return "ACTION";
}
}
public void setSelectedEdge(Object edge) {
selectedEdge = edge;
nanograph.setSelectedEdge(edge);
fireSelectionChangedEvent();
}
/**
* This method paints the selection box and other visble aspects of
* interaction over the graph.
*
* This methods is invoked by components that use an interactionManager
* after the paintGraph() method.
*
* @param g
* Graphics object
*/
public void paintInteractionMask(GraphicsAdapter g) {
if (selectionBox != null) {
g.setColor("#CCCCCC");
g.drawRectangle((int) selectionBox.getX(), (int) selectionBox.getY(), (int) selectionBox.getWidth(), (int) selectionBox.getHeight());
g.setFillColor("#00FF00");
g.setAlpha(20);
g.fillRectangle((int) selectionBox.getX(), (int) selectionBox.getY(), (int) selectionBox.getWidth(), (int) selectionBox.getHeight());
}
// if (moveBox != null) {
// g2d.setColor(Color.darkGray);
// //g2d.setStroke(new BasicStroke(1, BasicStroke.CAP_BUTT,
// BasicStroke.JOIN_MITER, 10, new float[] { 5, 2, 3, 2 }, 0));
//
// g2d.draw(moveBox);
// }
}
// ///////////////////////////////////////
// Listeners
private ArrayList selectionListeners = new ArrayList();
private ArrayList graphActionListeners = new ArrayList();
private ArrayList selectionMoveListeners = new ArrayList();
public void addSelectionListener(GraphSelectionListener l) {
selectionListeners.add(l);
}
public void removeSelectionListener(GraphSelectionListener l) {
selectionListeners.remove(l);
}
public void addActionListener(GraphActionListener l) {
graphActionListeners.add(l);
}
public void removeActionListener(GraphActionListener l) {
graphActionListeners.remove(l);
}
public void fireSelectionChangedEvent() {
Iterator iter = selectionListeners.iterator();
while (iter.hasNext()) {
GraphSelectionListener l = (GraphSelectionListener) iter.next();
l.selectionChanged(new GraphSelectionEvent(null, nodeSelection));
}
}
public void fireSelectionResetEvent() {
Iterator iter = selectionListeners.iterator();
while (iter.hasNext()) {
GraphSelectionListener l = (GraphSelectionListener) iter.next();
l.selectionReset(new GraphSelectionEvent(null, nodeSelection));
}
}
private void fireSelectionMoveEvent() {
Iterator iter = selectionListeners.iterator();
while (iter.hasNext()) {
GraphSelectionListener l = (GraphSelectionListener) iter.next();
l.selectionMove(new GraphSelectionEvent(null, nodeSelection));
}
}
public void fireGraphActionEvent(Object object, int type) {
Iterator iter = graphActionListeners.iterator();
while (iter.hasNext()) {
GraphActionListener l = (GraphActionListener) iter.next();
l.graphActionPerformed(new GraphActionEvent(null, object, type));
}
}
public void fireGraphActionResetEvent() {
Iterator iter = graphActionListeners.iterator();
while (iter.hasNext()) {
GraphActionListener l = (GraphActionListener) iter.next();
l.graphActionReset();
}
}
// //////////////////
// selection stuff
public Selection getNodeSelection() {
return nodeSelection;
}
public void setNodeSelection(Selection nodeSelection) {
this.nodeSelection = nodeSelection;
}
public void reset() {
nodeSelection.reset();
selectedEdge = null;
fireSelectionResetEvent();
fireGraphActionResetEvent();
}
public boolean isInteractionEnabled() {
return interactionEnabled;
}
public void setInteractionEnabled(boolean interactionEnabled) {
this.interactionEnabled = interactionEnabled;
}
}