/*
* Copyright (C) 2012 Jason Gedge <http://www.gedge.ca>
*
* This file is part of the OpGraph project.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package ca.gedge.opgraph.app.components.canvas;
import java.awt.AWTEvent;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.LayoutManager;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.ClipboardOwner;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DropTarget;
import java.awt.dnd.DropTargetAdapter;
import java.awt.dnd.DropTargetDragEvent;
import java.awt.dnd.DropTargetDropEvent;
import java.awt.event.AWTEventListener;
import java.awt.event.ActionEvent;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;
import javax.swing.AbstractAction;
import javax.swing.JComponent;
import javax.swing.JLayeredPane;
import javax.swing.JPopupMenu;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.text.JTextComponent;
import ca.gedge.opgraph.ContextualItem;
import ca.gedge.opgraph.InputField;
import ca.gedge.opgraph.OpLink;
import ca.gedge.opgraph.OpGraph;
import ca.gedge.opgraph.OpNode;
import ca.gedge.opgraph.OutputField;
import ca.gedge.opgraph.OpNodeListener;
import ca.gedge.opgraph.Processor;
import ca.gedge.opgraph.OpGraphListener;
import ca.gedge.opgraph.app.GraphDocument;
import ca.gedge.opgraph.app.GraphEditorModel;
import ca.gedge.opgraph.app.MenuProvider;
import ca.gedge.opgraph.app.components.ErrorDialog;
import ca.gedge.opgraph.app.components.NullLayout;
import ca.gedge.opgraph.app.components.PathAddressableMenuImpl;
import ca.gedge.opgraph.app.components.ResizeGrip;
import ca.gedge.opgraph.app.components.canvas.CanvasNodeField.AnchorFillState;
import ca.gedge.opgraph.app.edits.graph.AddLinkEdit;
import ca.gedge.opgraph.app.edits.graph.AddNodeEdit;
import ca.gedge.opgraph.app.edits.graph.MoveNodesEdit;
import ca.gedge.opgraph.app.edits.graph.RemoveLinkEdit;
import ca.gedge.opgraph.app.edits.notes.MoveNoteEdit;
import ca.gedge.opgraph.app.edits.notes.ResizeNoteEdit;
import ca.gedge.opgraph.app.extensions.NodeMetadata;
import ca.gedge.opgraph.app.extensions.Note;
import ca.gedge.opgraph.app.extensions.NoteComponent;
import ca.gedge.opgraph.app.extensions.Notes;
import ca.gedge.opgraph.app.util.CollectionListener;
import ca.gedge.opgraph.app.util.GUIHelper;
import ca.gedge.opgraph.dag.CycleDetectedException;
import ca.gedge.opgraph.dag.VertexNotFoundException;
import ca.gedge.opgraph.exceptions.ItemMissingException;
import ca.gedge.opgraph.extensions.CompositeNode;
import ca.gedge.opgraph.extensions.Publishable;
import ca.gedge.opgraph.extensions.Publishable.PublishedInput;
import ca.gedge.opgraph.extensions.Publishable.PublishedOutput;
import ca.gedge.opgraph.library.NodeData;
import ca.gedge.opgraph.util.BreadcrumbListener;
import ca.gedge.opgraph.util.Pair;
/**
* A canvas for creating/modifying an {@link OpGraph}.
*
* TODO Autoscrolling when moving a node, or moving/resizing notes, or really
* anytime when dealing with children components
*
* XXX Use a delegate for handling some of this class' inner functionality.
* Some good places would be having the delegate handling the double
* clicking of nodes (specifically, macros). Then we could extract the
* breadcrumb from this class also, which doesn't really feel like it
* belongs here. This class should never set the model on itself.
*/
public class GraphCanvas extends JLayeredPane implements ClipboardOwner {
/** Logger */
private static final Logger LOGGER = Logger.getLogger(GraphCanvas.class.getName());
/** The application model this canvas uses */
private GraphEditorModel model;
/** The document model this canvas uses */
private GraphDocument document;
/** The mapping of nodes to node components */
private HashMap<OpNode, CanvasNode> nodes;
/** The layer that displays a grid */
private final GridLayer gridLayer;
/** The layer that displays links between nodes */
private final LinksLayer linksLayer;
/** The layer that overlays the whole canvas */
private final CanvasOverlay canvasOverlay;
/** The debug layer that overlays the whole canvas */
private final DebugOverlay canvasDebugOverlay;
//
// Drag-based members
//
/**
* If link dragging is happening, <code>null</code> if this should be a new
* link, or a reference to an existing link if editing a link.
*/
private OpLink currentlyDraggedLink;
/** If link dragging is happening, the input field from which this link originates */
private CanvasNodeField currentlyDraggedLinkInputField;
/** If link dragging is happening, the current location of the destination end */
private Point currentDragLinkLocation;
/**
* If link dragging is started, specifies whether or not the current
* position of the drag is a valid drop location for the link.
*/
private boolean dragLinkIsValid;
/** The selection rectangle, or <code>null</code> if none */
private Rectangle selectionRect;
/** The initial click point (screen coordinates) within the canvas */
private Point clickLocation;
/** Component(s) whose location(s) will move during a mouse drag operation */
private List<Pair<Component, Point>> componentsToMove = new ArrayList<Pair<Component, Point>>();
//
// Layers
//
private static final Integer GRID_LAYER = 1;
@SuppressWarnings("unused")
private static final Integer BACKGROUND_LAYER = 2;
private static final Integer NOTES_LAYER = 10;
private static final Integer LINKS_LAYER = 100;
private static final Integer NODES_LAYER = 200;
private static final Integer OVERLAY_LAYER = 10000;
private static final Integer DEBUG_OVERLAY_LAYER = 10001;
@SuppressWarnings("unused")
private static final Integer FOREGROUND_LAYER = Integer.MAX_VALUE;
//
// Listener objects
//
private class MetaListener implements PropertyChangeListener {
private OpNode node;
public MetaListener(OpNode node) {
this.node = node;
}
@Override
public void propertyChange(PropertyChangeEvent evt) {
if(evt.getSource() instanceof NodeMetadata) {
final NodeMetadata meta = (NodeMetadata)evt.getSource();
final CanvasNode canvasNode = nodes.get(node);
if(canvasNode != null) {
if(evt.getPropertyName().equals(NodeMetadata.LOCATION_PROPERTY)) {
canvasNode.setLocation(meta.getX(), meta.getY());
} else if(evt.getPropertyName().equals(NodeMetadata.DEFAULTS_PROPERTY)) {
updateAnchorFillStates(node);
}
}
}
}
};
/**
* Constructs a canvas that displays a given graph model.
*
* @param model the graph model
*/
public GraphCanvas(GraphEditorModel model) {
super();
// Components
this.gridLayer = new GridLayer();
this.linksLayer = new LinksLayer(this);
this.canvasOverlay = new CanvasOverlay(this);
this.canvasDebugOverlay = new DebugOverlay(this);
// Class setup
this.model = model;
this.nodes = new HashMap<OpNode, CanvasNode>();
this.document = new GraphDocument(this);
this.document.getBreadcrumb().addBreadcrumbListener(breadcrumbListener);
this.document.getSelectionModel().addSelectionListener(canvasSelectionListener);
changeGraph(null, this.document.getGraph());
// Initialize component
setDoubleBuffered(true);
setLayout(new NullLayout());
setFocusable(true);
setOpaque(false);
setFocusCycleRoot(true);
setDropTarget(new DropTarget(this, DnDConstants.ACTION_COPY, dropTargetAdapter, true));
addMouseListener(mouseAdapter);
addMouseMotionListener(mouseMotionAdapter);
final long eventMask = AWTEvent.MOUSE_EVENT_MASK | AWTEvent.MOUSE_MOTION_EVENT_MASK;
Toolkit.getDefaultToolkit().addAWTEventListener(awtEventListener, eventMask);
//
// Actions
//
getInputMap(WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("ESCAPE"), "cancel");
getActionMap().put("cancel", new AbstractAction("Cancel") {
@Override
public void actionPerformed(ActionEvent e) {
// If dragging link, null out these guys so selected nodes
// don't get moved on mouse release
if(currentlyDraggedLinkInputField != null)
clickLocation = null;
selectionRect = null;
currentlyDraggedLink = null;
currentlyDraggedLinkInputField = null;
currentDragLinkLocation = null;
repaint();
}
});
// Other layers
add(gridLayer, GRID_LAYER);
add(linksLayer, LINKS_LAYER);
add(canvasOverlay, OVERLAY_LAYER);
add(canvasDebugOverlay, DEBUG_OVERLAY_LAYER);
}
/**
* Gets the bounding rectangle of the items currently being moved.
*
* @return the bounding rectangle
*/
private Rectangle getBoundingRectOfMoved() {
int xmin = Integer.MAX_VALUE;
int xmax = Integer.MIN_VALUE;
int ymin = Integer.MAX_VALUE;
int ymax = Integer.MIN_VALUE;
for(Pair<Component, Point> compLoc : componentsToMove) {
final Point loc = compLoc.getSecond();
final Component comp = compLoc.getFirst();
final Dimension pref = comp.getPreferredSize();
xmin = Math.min(xmin, loc.x);
xmax = Math.max(xmax, loc.x + pref.width);
ymin = Math.min(ymin, loc.y);
ymax = Math.max(ymax, loc.y + pref.height);
}
return new Rectangle(xmin, ymin, xmax-xmin, ymax-ymin);
}
/**
* Gets the selection model this canvas is using.
*
* @return the selection model
*/
public GraphCanvasSelectionModel getSelectionModel() {
return document.getSelectionModel();
}
/**
* Gets the document this canvas is editing.
*
* @return the document
*/
public GraphDocument getDocument() {
return document;
}
/**
* Gets a mapping from {@link OpNode} to the respective node
* component that displays that node.
*
* @return the mapping
*/
public Map<OpNode, CanvasNode> getNodeMap() {
return Collections.unmodifiableMap(nodes);
}
/**
* Gets the node displaying the given node.
*
* @param node the node
*
* @return the node displaying the given node, or <code>null</code>
* if no such node exists
*/
public CanvasNode getNode(OpNode node) {
return nodes.get(node);
}
/**
* Gets the link currently being dragged.
*
* @return the link, or <code>null</code> if no link being dragged
*/
public OpLink getCurrentlyDraggedLink() {
return currentlyDraggedLink;
}
/**
* Gets the input field of the current drag link.
*
* @return the input field, or <code>null</code> if no link being dragged
*/
public CanvasNodeField getCurrentlyDraggedLinkInputField() {
return currentlyDraggedLinkInputField;
}
/**
* Gets the location of the current link being dragged.
*
* @return the location of the drag link, or <code>null</code> if no link
* being dragged
*/
public Point getCurrentDragLinkLocation() {
return currentDragLinkLocation;
}
/**
* Gets whether or not the currently dragged link is at a valid drop spot.
*
* @return <code>true</code> if the currently dragged link can be dropped
* at the curent drag location, <code>false</code> otherwise
*/
public boolean isDragLinkValid() {
return dragLinkIsValid;
}
/**
* Constructs a popup menu for a node.
*
* @param event the mouse event that created the popup
*
* @return an appropriate popup menu for the given node
*/
public JPopupMenu constructPopup(MouseEvent event) {
Object context = document.getGraph();
// Try to find a more specific context
final CanvasNode node = GUIHelper.getAncestorOrSelfOfClass(CanvasNode.class, event.getComponent());
if(node != null) {
context = node.getNode();
} else {
final NoteComponent note = GUIHelper.getAncestorOrSelfOfClass(NoteComponent.class, event.getComponent());
if(note != null)
context = note.getNote();
}
final JPopupMenu popup = new JPopupMenu();
if(context != null) {
final PathAddressableMenuImpl addressable = new PathAddressableMenuImpl(popup);
for(MenuProvider menuProvider : model.getMenuProviders())
menuProvider.installPopupItems(context, event, model, addressable);
}
if(popup.getComponentCount() == 0)
return null;
return popup;
}
/**
* Updates the debug state for this canvas. Currently, the debug state
* simply highlights the node being processed.
*
* @param context the processing context, or <code>null</code> if no debugging
*/
public void updateDebugState(Processor context) {
if(context == null) {
setEnabled(true);
} else {
setEnabled(false);
if(document.getBreadcrumb().containsState(context.getGraph())) {
document.getBreadcrumb().gotoState(context.getGraph());
} else {
// Given the current processing context, find the path that
// gets to the current node
final LinkedList<Pair<OpGraph, String>> path = new LinkedList<Pair<OpGraph, String>>();
String id = context.getGraphOfContext().getId();
Processor activeContext = context;
while(activeContext != null) {
final OpGraph graph = activeContext.getGraphOfContext();
path.addLast(new Pair<OpGraph, String>(graph, id));
if(activeContext.getCurrentNodeOfContext() != null)
id = activeContext.getCurrentNodeOfContext().getName();
else
id = "Unknown";
activeContext = activeContext.getMacroContext();
}
document.getBreadcrumb().set(path);
}
getSelectionModel().setSelectedNode(context.getCurrentNode());
}
canvasDebugOverlay.repaint();
}
/**
* Updates the fill states of all anchors for a given node.
*
* @param node the node to update
*/
public void updateAnchorFillStates(OpNode node) {
final CanvasNode canvasNode = nodes.get(node);
if(canvasNode != null) {
final Map<ContextualItem, CanvasNodeField> fields = canvasNode.getFieldsMap();
final NodeMetadata meta = node.getExtension(NodeMetadata.class);
final Publishable publishable = document.getGraph().getExtension(Publishable.class);
for(InputField field : node.getInputFields()) {
final CanvasNodeField canvasField = fields.get(field);
if(canvasField != null) {
// Check to see if we should fill for a published field first
boolean isPublished = false;
if(publishable != null) {
for(PublishedInput input : publishable.getPublishedInputs()) {
if(node == input.destinationNode && field.equals(input.nodeInputField)) {
canvasField.updateAnchorFillState(AnchorFillState.PUBLISHED);
isPublished = true;
break;
}
}
}
// It's not published, so is there a default value?
if(!isPublished) {
if(meta == null || meta.getDefault(field) == null)
canvasField.updateAnchorFillState(AnchorFillState.NONE);
else
canvasField.updateAnchorFillState(AnchorFillState.DEFAULT);
}
}
}
// Fill for published output fields
if(publishable != null) {
for(OutputField field : node.getOutputFields()) {
final CanvasNodeField canvasField = fields.get(field);
if(canvasField != null) {
for(PublishedOutput output : publishable.getPublishedOutputs()) {
if(node == output.sourceNode && field == output.nodeOutputField) {
canvasField.updateAnchorFillState(AnchorFillState.PUBLISHED);
break;
}
}
}
}
}
}
}
/**
* Start a drag operation for a given field.
*
* @param fieldComponent the field
*/
public void startLinkDrag(CanvasNodeField fieldComponent) {
if(!isEnabled()) return;
currentlyDraggedLink = null;
currentlyDraggedLinkInputField = null;
currentDragLinkLocation = getMousePosition();
CanvasNode node = (CanvasNode)SwingUtilities.getAncestorOfClass(CanvasNode.class, fieldComponent);
if(node != null) {
ContextualItem field = fieldComponent.getField();
if(field instanceof InputField) {
// Check if link exists and, if so, start editing it
for(OpLink e : document.getGraph().getIncomingEdges(node.getNode())) {
if(e.getDestinationField() == field) {
currentlyDraggedLink = e;
break;
}
}
// Found a link, but currentlyDraggedLinkField needs to be on
// source end, so use the link we found to update those fields
if(currentlyDraggedLink != null) {
node = nodes.get(currentlyDraggedLink.getSource());
if(node != null) {
fieldComponent = node.getFieldsMap().get(currentlyDraggedLink.getSourceField());
if(field != null) {
currentlyDraggedLinkInputField = fieldComponent;
repaint();
}
}
}
} else if(field instanceof OutputField) {
currentlyDraggedLinkInputField = fieldComponent;
repaint();
}
}
}
/**
* Called to update link dragging status.
*
* @param p the current point of the drag, in the coordinate system of this component
*/
public void updateLinkDrag(Point p) {
if(!isEnabled()) return;
if(currentlyDraggedLinkInputField == null) {
dragLinkIsValid = false;
return;
}
currentDragLinkLocation = p;
dragLinkIsValid = true;
// Get the source node
final CanvasNode source = (CanvasNode)SwingUtilities.getAncestorOfClass(CanvasNode.class, currentlyDraggedLinkInputField);
if(source == null)
return;
// Find the destination node
for(Component comp : getComponentsInLayer(NODES_LAYER)) {
final CanvasNode dest = (CanvasNode)comp;
final Point nodeP = SwingUtilities.convertPoint(this, p, dest);
if(dest.contains(nodeP)) {
// See if we're hovering over a field
CanvasNodeField field = dest.getFieldAt(nodeP);
if(field != null) {
// At this point, we default to an invalid link. If we're
// not hovering over an InputField, then it isn't valid
dragLinkIsValid = false;
if(field.getField() instanceof InputField) {
try {
final OutputField out = (OutputField)currentlyDraggedLinkInputField.getField();
final InputField in = (InputField)field.getField();
final OpLink link = new OpLink(source.getNode(), out, dest.getNode(), in);
// Now make sure the link can be added, and that it is a valid link
dragLinkIsValid = (document.getGraph().canAddEdge(link) && link.isValid());
} catch(ItemMissingException exc) {}
}
}
break;
}
}
repaint();
}
/**
* Called when link dragging should end.
*
* @param p the end point of the drag, in the coordinate system of this component
*/
public void endLinkDrag(Point p) {
if(!isEnabled()) return;
updateLinkDrag(p);
// If the drag link is valid, check to see which field this link
// was fed into and try to add a new link
if(currentlyDraggedLinkInputField != null) {
final OpGraph graph = document.getGraph();
if(dragLinkIsValid) {
final CanvasNode sourceNode = (CanvasNode)SwingUtilities.getAncestorOfClass(CanvasNode.class, currentlyDraggedLinkInputField);
if(sourceNode == null)
return;
boolean destinationFound = false;
for(Component comp : getComponentsInLayer(NODES_LAYER)) {
final CanvasNode destinationNode = (CanvasNode)comp;
final Point nodeP = SwingUtilities.convertPoint(this, p, destinationNode);
if(destinationNode.contains(nodeP)) {
final CanvasNodeField destinationField = destinationNode.getFieldAt(nodeP);
if(destinationField != null && destinationField.getField() instanceof InputField) {
final OpNode source = sourceNode.getNode();
final OpNode destination = destinationNode.getNode();
final OutputField sourceField = (OutputField)currentlyDraggedLinkInputField.getField();
final InputField destField = (InputField)destinationField.getField();
// If no link being edited, just add the new link,
// otherwise we need to see if any changes made
try {
final OpLink link = new OpLink(source, sourceField, destination, destField);
if(currentlyDraggedLink == null) {
document.getUndoSupport().postEdit(new AddLinkEdit(graph, link));
} else if(!link.equals(currentlyDraggedLink)) {
document.getUndoSupport().beginUpdate();
document.getUndoSupport().postEdit(new RemoveLinkEdit(graph, currentlyDraggedLink));
document.getUndoSupport().postEdit(new AddLinkEdit(graph, link));
document.getUndoSupport().endUpdate();
}
} catch(ItemMissingException exc) {
ErrorDialog.showError(exc);
} catch(VertexNotFoundException exc) {
ErrorDialog.showError(exc);
} catch(CycleDetectedException exc) {
ErrorDialog.showError(exc);
}
destinationFound = true;
}
break;
}
}
// No destination found, so this means we were dragging over
// the canvas area. If we were editing an existing link,
// remove it
if(!destinationFound && currentlyDraggedLink != null) {
if(!graph.contains(currentlyDraggedLink)) {
try {
graph.add(currentlyDraggedLink);
} catch (VertexNotFoundException e) {
} catch (CycleDetectedException e) {
}
}
document.getUndoSupport().postEdit(new RemoveLinkEdit(graph, currentlyDraggedLink));
}
} else if(currentlyDraggedLink != null) {
if(!graph.contains(currentlyDraggedLink)) {
try {
graph.add(currentlyDraggedLink);
} catch (VertexNotFoundException e) {
} catch (CycleDetectedException e) {
}
}
// Invalid link, and we are editing an existing link, so remove it
document.getUndoSupport().postEdit(new RemoveLinkEdit(graph, currentlyDraggedLink));
}
}
currentlyDraggedLink = null;
currentlyDraggedLinkInputField = null;
currentDragLinkLocation = null;
repaint();
}
//
// Overrides
//
@Override
public void setLayout(LayoutManager mgr) {
if(mgr != null && !(mgr instanceof NullLayout))
throw new UnsupportedOperationException("GraphCanvas cannot use a custom layout");
super.setLayout(mgr);
}
/**
* Gets the selection rectangle.
*
* @return the selection rectangle, or <code>null</code> if there is
* currently no selection rectangle
*/
public Rectangle getSelectionRect() {
Rectangle ret = null;
if(selectionRect != null) {
int x = selectionRect.x;
int y = selectionRect.y;
int w = selectionRect.width;
int h = selectionRect.height;
if(w < 0) {
x += w;
w = -w;
}
if(h < 0) {
y += h;
h = -h;
}
ret = new Rectangle(x, y, w, h);
}
return ret;
}
/**
* Switches the graph this canvas is viewing.
*
* @param oldGraph the graph that was displayed previously
* @param graph the new graph to display
*/
protected void changeGraph(OpGraph oldGraph, OpGraph graph) {
synchronized(getTreeLock()) {
// Remove old components
if(oldGraph != null) {
final Notes notes = oldGraph.getExtension(Notes.class);
if(notes != null) {
for(Note note : notes)
notesAdapter.elementRemoved(notes, note);
}
for(OpLink link : oldGraph.getEdges())
graphAdapter.linkRemoved(oldGraph, link);
for(OpNode node : oldGraph.getVertices())
graphAdapter.nodeRemoved(oldGraph, node);
}
// Add new ones
if(graph != null) {
graph.addGraphListener(graphAdapter);
for(OpNode node : document.getGraph().getVertices())
graphAdapter.nodeAdded(graph, node);
for(OpLink link : document.getGraph().getEdges())
graphAdapter.linkAdded(graph, link);
// Add any notes, or if none exist, make sure the extension
// exists on the given graph
Notes notes = document.getGraph().getExtension(Notes.class);
if(notes == null) {
notes = new Notes();
document.getGraph().putExtension(Notes.class, notes);
} else {
for(Note note : notes)
notesAdapter.elementAdded(notes, note);
}
notes.addCollectionListener(notesAdapter);
}
}
// Update selection
for(OpNode node : getSelectionModel().getSelectedNodes()) {
if(nodes.containsKey(node))
nodes.get(node).setSelected(true);
}
// Update anchor fill states
if(graph != null) {
for(OpNode node : document.getGraph().getVertices())
updateAnchorFillStates(node);
}
revalidate();
repaint();
}
//
// MouseAdapter
//
private final MouseAdapter mouseAdapter = new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
selectionRect = new Rectangle(e.getPoint());
}
@Override
public void mouseReleased(MouseEvent e) {
// Find selected nodes, if necessary
if(selectionRect != null) {
final Rectangle rect = getSelectionRect();
final Set<OpNode> selected = new HashSet<OpNode>();
for(Component comp : getComponentsInLayer(NODES_LAYER)) {
final Rectangle compRect = comp.getBounds();
if((comp instanceof CanvasNode) && rect.intersects(compRect))
selected.add( ((CanvasNode)comp).getNode() );
}
getSelectionModel().setSelectedNodes(selected);
}
// Reset variables
selectionRect = null;
repaint();
}
};
//
// MouseMotionListener
//
private final MouseMotionAdapter mouseMotionAdapter = new MouseMotionAdapter() {
@Override
public void mouseDragged(MouseEvent e) {
final Point p = SwingUtilities.convertPoint(e.getComponent(), e.getPoint(), GraphCanvas.this);
scrollRectToVisible(new Rectangle(p.x, p.y, 1, 1));
if(selectionRect != null) {
final Point src = selectionRect.getLocation();
selectionRect.setSize(p.x - src.x, p.y - src.y);
}
repaint();
}
};
//
// GraphCanvasModelListener
//
private final GraphCanvasAdapter graphAdapter = new GraphCanvasAdapter();
private class GraphCanvasAdapter implements OpGraphListener, OpNodeListener {
@Override
public void nodePropertyChanged(String propertyName, Object oldValue, Object newValue) {}
@Override
public void nodeAdded(final OpGraph graph, final OpNode v) {
if(!nodes.containsKey(v)) {
final CanvasNode node = new CanvasNode(v);
final int cx = (int)getVisibleRect().getCenterX();
final int cy = (int)getVisibleRect().getCenterY();
// Place this node at the center if it has a negative location
NodeMetadata meta = v.getExtension(NodeMetadata.class);
if(meta == null) {
meta = new NodeMetadata(cx, cy);
v.putExtension(NodeMetadata.class, meta);
} else {
if(meta.getX() < 0) meta.setX(cx);
if(meta.getY() < 0) meta.setY(cy);
}
node.setLocation(meta.getX(), meta.getY());
meta.addPropertyChangeListener(new MetaListener(v));
// Adjust links when component moves or resizes
node.addComponentListener(new ComponentAdapter() {
@Override
public void componentResized(ComponentEvent e) {
for(OpLink link : document.getGraph().getIncomingEdges(v)) linksLayer.updateLink(link);
for(OpLink link : document.getGraph().getOutgoingEdges(v)) linksLayer.updateLink(link);
GraphCanvas.this.revalidate();
}
@Override
public void componentMoved(ComponentEvent e) {
for(OpLink link : document.getGraph().getIncomingEdges(v)) linksLayer.updateLink(link);
for(OpLink link : document.getGraph().getOutgoingEdges(v)) linksLayer.updateLink(link);
GraphCanvas.this.revalidate();
}
});
node.addUndoableEditListener(document.getUndoManager());
nodes.put(v, node);
add(node, NODES_LAYER, 0);
v.addNodeListener(this);
v.putExtension(JComponent.class, node);
revalidate();
repaint();
}
}
@Override
public void nodeRemoved(OpGraph graph, OpNode v) {
if(nodes.containsKey(v)) {
remove(nodes.get(v));
nodes.get(v).removeUndoableEditListener(document.getUndoManager());
nodes.remove(v);
getSelectionModel().removeNodeFromSelection(v);
v.removeNodeListener(this);
v.putExtension(JComponent.class, null);
revalidate();
repaint();
}
}
@Override
public void linkAdded(OpGraph graph, OpLink e) {
final CanvasNode src = nodes.get(e.getSource());
final CanvasNode dst = nodes.get(e.getDestination());
final CanvasNodeField srcField = src.getFieldsMap().get(e.getSourceField());
final CanvasNodeField dstField = dst.getFieldsMap().get(e.getDestinationField());
if(srcField != null) srcField.setAnchorFillState(AnchorFillState.LINK);
if(dstField != null) dstField.setAnchorFillState(AnchorFillState.LINK);
linksLayer.updateLink(e);
repaint();
}
@Override
public void linkRemoved(OpGraph graph, OpLink e) {
final CanvasNode src = nodes.get(e.getSource());
final CanvasNode dst = nodes.get(e.getDestination());
// Multiple outgoing links can exist, so before removing this link, make
// sure there are no more outgoing links
if(graph.getOutgoingEdges(src.getNode()).size() == 0) {
final CanvasNodeField field = src.getFieldsMap().get(e.getSourceField());
if(field != null)
field.setAnchorFillState(AnchorFillState.NONE);
}
// Decide whether the anchor fill state is to be set to NONE or DEFAULT
final CanvasNodeField dstField = dst.getFieldsMap().get(e.getDestinationField());
if(dstField != null) {
final NodeMetadata meta = dst.getNode().getExtension(NodeMetadata.class);
if(meta == null || meta.getDefault(e.getDestinationField()) == null)
dstField.setAnchorFillState(AnchorFillState.NONE);
else
dstField.setAnchorFillState(AnchorFillState.DEFAULT);
}
linksLayer.removeLink(e);
// Remove link reference and repaint
repaint();
}
@Override
public void fieldAdded(OpNode node, InputField field) {
final CanvasNode canvasNode = nodes.get(node);
if(canvasNode != null)
repaint();
}
@Override
public void fieldRemoved(OpNode node, InputField field) {
final CanvasNode canvasNode = nodes.get(node);
if(canvasNode != null)
repaint();
}
@Override
public void fieldAdded(OpNode node, OutputField field) {
final CanvasNode canvasNode = nodes.get(node);
if(canvasNode != null)
repaint();
}
@Override
public void fieldRemoved(OpNode node, OutputField field) {
final CanvasNode canvasNode = nodes.get(node);
if(canvasNode != null)
repaint();
}
}
//
// GraphCanvasSelectionListener
//
private final GraphCanvasSelectionListener canvasSelectionListener = new GraphCanvasSelectionListener() {
@Override
public void nodeSelectionChanged(Collection<OpNode> old, Collection<OpNode> selected) {
for(OpNode node : old) {
if(nodes.containsKey(node))
nodes.get(node).setSelected(false);
}
for(OpNode node : selected) {
if(nodes.containsKey(node))
nodes.get(node).setSelected(true);
}
repaint();
}
};
//
// AWTEventListener
//
private final AWTEventListener awtEventListener = new AWTEventListener() {
@Override
public void eventDispatched(AWTEvent e) {
// Only registered for mouse events
if(!(e instanceof MouseEvent))
return;
final Component source = (Component)e.getSource();
final MouseEvent me = (MouseEvent)e;
if(!SwingUtilities.isDescendingFrom(source, GraphCanvas.this))
return;
if(e.getID() == MouseEvent.MOUSE_PRESSED) {
// If the mouse is pressed, update the selection. If pressed
// on the canvas area, clear the selection. If on a node, and
// it isn't already selected, select it. Otherwise, do nothing.
//
// Request focus if not clicked in text field that is being edited
if(source instanceof JTextComponent) {
if( ((JTextComponent)source).hasFocus() == false )
requestFocusInWindow();
} else {
requestFocusInWindow();
}
// Make sure the component the event was dispatched to is a child
clickLocation = me.getLocationOnScreen();
componentsToMove.clear();
// No CanvasNode parent? Select nothing, otherwise select its node
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
final CanvasNode canvasNode = GUIHelper.getAncestorOrSelfOfClass(CanvasNode.class, source);
if(canvasNode == null) {
getSelectionModel().setSelectedNode(null);
final NoteComponent note = GUIHelper.getAncestorOrSelfOfClass(NoteComponent.class, source);
if(note != null) {
moveToFront(note);
final Component comp = (source instanceof ResizeGrip) ? source : note;
final Point initialLocation = comp.getLocation();
componentsToMove.add(new Pair<Component, Point>(comp, initialLocation));
}
} else {
// If it's not already selected, then select it
if(!getSelectionModel().getSelectedNodes().contains(canvasNode.getNode()))
getSelectionModel().setSelectedNode(canvasNode.getNode());
// Bring it to the top of the nodes layer
moveToFront(canvasNode);
// Set the selected nodes as the components to move on drag
for(OpNode node : getSelectionModel().getSelectedNodes()) {
final JComponent comp = node.getExtension(JComponent.class);
if(comp != null) {
final Point initialLocation = comp.getLocation();
componentsToMove.add(new Pair<Component, Point>(comp, initialLocation));
}
}
}
}
});
} else if(e.getID() == MouseEvent.MOUSE_CLICKED && ((MouseEvent)e).getClickCount() == 2) {
// If double clicked, we'll descend into a composite node
boolean shouldDescend = true;
// Check to see if this is an editable text component, and if
// it isn't, we can go into a composite node
if(source instanceof JTextComponent)
shouldDescend = !((JTextComponent)source).isEditable();
// If double-clicked on a composite node, start editing it
if(shouldDescend) {
final CanvasNode node = GUIHelper.getAncestorOrSelfOfClass(CanvasNode.class, source);
if(node != null) {
final CompositeNode composite = node.getNode().getExtension(CompositeNode.class);
final GraphDocument document = GraphCanvas.this.document;
if(composite != null) {
// Put the publishing extension in the graph (even if it's null)
final Publishable publishable = node.getNode().getExtension(Publishable.class);
composite.getGraph().putExtension(Publishable.class, publishable);
// Set up the breadcrumb
document.getBreadcrumb().addState(composite.getGraph(), node.getNode().getName());
}
}
}
}
// Show popup if a popup trigger
if(me.isPopupTrigger()) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
final Point loc = SwingUtilities.convertPoint(source, me.getPoint(), GraphCanvas.this);
final JPopupMenu popup = constructPopup(me);
if(popup != null)
popup.show(GraphCanvas.this, loc.x, loc.y);
}
});
}
// All other events won't be processed if the canvas is disabled
if(!isEnabled())
return;
if(e.getID() == MouseEvent.MOUSE_DRAGGED) {
// Move the selected nodes, if not creating a link
if(clickLocation != null && currentlyDraggedLinkInputField == null && selectionRect == null) {
int deltaX = me.getLocationOnScreen().x - clickLocation.x;
int deltaY = me.getLocationOnScreen().y - clickLocation.y;
// Snap to grid if top left corner of selection is close to grid
final Point topLeftBound = getBoundingRectOfMoved().getLocation();
topLeftBound.translate(deltaX, deltaY);
final Point snapDelta = gridLayer.snap(topLeftBound);
deltaX += snapDelta.x;
deltaY += snapDelta.y;
// First, make sure that one of the components to move is
// an ancestor of the source of the drag
for(Pair<Component, Point> compLoc : componentsToMove) {
final Component comp = compLoc.getFirst();
if(!(comp instanceof ResizeGrip)) {
final Point initialLoc = compLoc.getSecond();
comp.setLocation(initialLoc.x + deltaX, initialLoc.y + deltaY);
}
}
}
} else if(e.getID() == MouseEvent.MOUSE_RELEASED) {
// Post an undoable event for any dragging that occurred
if(clickLocation != null && currentlyDraggedLinkInputField == null && selectionRect == null) {
// XXX Right now this works, but generalizing this would be better. What if
// we could select both nodes and notes and move them simultaneously?
int deltaX = me.getXOnScreen() - clickLocation.x;
int deltaY = me.getYOnScreen() - clickLocation.y;
if(deltaX != 0 || deltaY != 0) {
final Collection<OpNode> selected = getSelectionModel().getSelectedNodes();
if(selected.size() > 0) {
// If nodes selected, post special edit for them
document.getUndoSupport().postEdit(new MoveNodesEdit(selected, deltaX, deltaY));
} else {
// Otherwise, assume we have a note
//
// TODO generalized element movement
//
document.getUndoSupport().beginUpdate();
for(Pair<Component, Point> compLoc : componentsToMove) {
final Component comp = compLoc.getFirst();
if(comp instanceof NoteComponent) {
final Note note = ((NoteComponent)comp).getNote();
comp.setLocation(comp.getX() - deltaX, comp.getY() - deltaY);
document.getUndoSupport().postEdit(new MoveNoteEdit(note, deltaX, deltaY));
}
}
document.getUndoSupport().endUpdate();
}
}
}
clickLocation = null;
}
}
};
//
// CollectionListener<Notes.Note>
//
// TODO the xxxMouseListener calls below are ugly, so find a nicer way to do this
private final CollectionListener<Notes, Note> notesAdapter = new CollectionListener<Notes, Note>() {
@Override
public void elementAdded(Notes source, Note element) {
final JComponent comp = element.getExtension(JComponent.class);
if(comp != null) {
add(comp, NOTES_LAYER);
((NoteComponent)comp).getResizeGrip().addMouseListener(notesMouseAdapter);
comp.revalidate();
}
}
@Override
public void elementRemoved(Notes source, Note element) {
final JComponent comp = element.getExtension(JComponent.class);
if(comp != null) {
remove(comp);
((NoteComponent)comp).getResizeGrip().removeMouseListener(notesMouseAdapter);
repaint(comp.getBounds());
}
}
};
//
// BreadcrumbListener
//
private final BreadcrumbListener<OpGraph, String> breadcrumbListener = new BreadcrumbListener<OpGraph, String>() {
@Override
public void stateChanged(OpGraph oldGraph, OpGraph newGraph) {
// XXX Could this move to changeGraph instead?
if(oldGraph != null){
oldGraph.removeGraphListener(graphAdapter);
final Notes notes = oldGraph.getExtension(Notes.class);
if(notes != null)
notes.removeCollectionListener(notesAdapter);
}
changeGraph(oldGraph, newGraph);
}
@Override
public void stateAdded(OpGraph state, String value) {}
};
//
// Adapter for creating undoable edits when notes are resized
//
private final MouseAdapter notesMouseAdapter = new MouseAdapter() {
@Override
public void mouseReleased(MouseEvent e) {
if(e.getComponent() instanceof ResizeGrip) {
final ResizeGrip grip = (ResizeGrip)e.getComponent();
if(grip.getComponent() instanceof NoteComponent) {
final Note note = ((NoteComponent)grip.getComponent()).getNote();
final Dimension initialSize = grip.getInitialComponentSize();
final Dimension newSize = grip.getComponent().getSize();
document.getUndoSupport().postEdit(new ResizeNoteEdit(note, initialSize, newSize));
}
}
}
};
//
// DropTargetListener
//
private static DataFlavor accepted = new DataFlavor(NodeData.class, "NodeData");
private final DropTargetAdapter dropTargetAdapter = new DropTargetAdapter() {
@Override
public void dragEnter(DropTargetDragEvent dtde) {
if(!dtde.isDataFlavorSupported(accepted))
dtde.rejectDrag();
}
@Override
public void dragOver(DropTargetDragEvent dtde) {
scrollRectToVisible(new Rectangle(dtde.getLocation(), new Dimension(1, 1)));
if(!dtde.isDataFlavorSupported(accepted))
dtde.rejectDrag();
}
@Override
public void drop(final DropTargetDropEvent dtde) {
if(dtde.isDataFlavorSupported(accepted)) {
NodeData info = null;
try {
info = (NodeData)dtde.getTransferable().getTransferData(accepted);
// Set up the initial location metadata and post the edit
final int x = dtde.getLocation().x;
final int y = dtde.getLocation().y;
final OpGraph graph = document.getGraph();
document.getUndoSupport().postEdit(new AddNodeEdit(graph, info, x, y));
} catch(UnsupportedFlavorException e) {
LOGGER.warning("Drop event says it supports NodeData flavor, but can't get data for that flavor");
} catch(IOException e) {
LOGGER.warning("IOException on drop, which should never happen");
} catch(InstantiationException e) {
LOGGER.warning("Could not instantiate node '" + info.name + "' from drop");
}
// Drag complete!
dtde.acceptDrop(DnDConstants.ACTION_COPY);
dtde.dropComplete(true);
} else {
dtde.rejectDrop();
}
}
@Override
public void dropActionChanged(DropTargetDragEvent dtde) {
if(!dtde.isDataFlavorSupported(accepted))
dtde.rejectDrag();
}
};
//
// ClipboardOwner
//
@Override
public void lostOwnership(Clipboard clipboard, Transferable contents) { }
}