/* * @(#)GraphTransferHandler.java 1.0 31-DEC-04 * * Copyright (c) 2001-2004 Gaudenz Alder * */ package org.jgraph.graph; import java.awt.Point; import java.awt.datatransfer.DataFlavor; import java.awt.datatransfer.Transferable; import java.awt.event.InputEvent; import java.awt.event.MouseEvent; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.util.Hashtable; import java.util.Iterator; import java.util.Map; import javax.swing.JComponent; import javax.swing.TransferHandler; import org.jgraph.JGraph; /** * @author Gaudenz Alder * * Default datatransfer handler. */ @SuppressWarnings ( "all" ) public class GraphTransferHandler extends TransferHandler { /** * Controls if all inserts should be handled as external drops even if all * cells are already in the graph model. This is useful if the enclosing * component does not allow moving. */ protected boolean alwaysReceiveAsCopyAction = false; /* Pointer to the last inserted array of cells. */ protected Object out, in; /* How many times the last transferable was inserted. */ protected int inCount = 0; public boolean canImport(JComponent comp, DataFlavor[] flavors) { for (int i = 0; i < flavors.length; i++) if (flavors[i] == GraphTransferable.dataFlavor) return true; return false; } /* Public entry point to create a Transferable. */ public Transferable createTransferableForGraph(JGraph graph) { return createTransferable(graph); } protected Transferable createTransferable(JComponent c) { if (c instanceof JGraph) { JGraph graph = (JGraph) c; if (!graph.isSelectionEmpty()) { return createTransferable(graph, graph.getSelectionCells()); } } return null; } protected Transferable createTransferable(JGraph graph, Object[] cells) { Object[] flat = graph.getDescendants(graph.order(cells)); ParentMap pm = ParentMap.create(graph.getModel(), flat, false, true); ConnectionSet cs = ConnectionSet.create(graph.getModel(), flat, false); Map viewAttributes = GraphConstants.createAttributes(flat, graph .getGraphLayoutCache()); Rectangle2D bounds = graph.getCellBounds(graph.getSelectionCells()); bounds = new AttributeMap.SerializableRectangle2D(bounds.getX(), bounds .getY(), bounds.getWidth(), bounds.getHeight()); out = flat; return create(graph, flat, viewAttributes, bounds, cs, pm); } protected GraphTransferable create(JGraph graph, Object[] cells, Map viewAttributes, Rectangle2D bounds, ConnectionSet cs, ParentMap pm) { return new GraphTransferable(cells, viewAttributes, bounds, cs, pm); } protected void exportDone(JComponent comp, Transferable data, int action) { if (comp instanceof JGraph && data instanceof GraphTransferable) { JGraph graph = (JGraph) comp; if (action == TransferHandler.MOVE) { Object[] cells = ((GraphTransferable) data).getCells(); graph.getGraphLayoutCache().remove(cells); } graph.getUI().updateHandle(); graph.getUI().setInsertionLocation(null); } } public int getSourceActions(JComponent c) { return COPY_OR_MOVE; } // NOTE: 1. We abuse return value to signal removal to the sender. // 2. We always clone cells when transferred between two models // This is because they contain parts of the model's data. // 3. Transfer is passed to importDataImpl for unsupported // dataflavors (becaue method may return false, see 1.) public boolean importData(JComponent comp, Transferable t) { try { if (comp instanceof JGraph) { JGraph graph = (JGraph) comp; GraphModel model = graph.getModel(); GraphLayoutCache cache = graph.getGraphLayoutCache(); if (t.isDataFlavorSupported(GraphTransferable.dataFlavor) && graph.isEnabled()) { // May be null Point p = graph.getUI().getInsertionLocation(); // Get Local Machine Flavor Object obj = t .getTransferData(GraphTransferable.dataFlavor); GraphTransferable gt = (GraphTransferable) obj; // Get Transferred Cells Object[] cells = gt.getCells(); // Check if all cells are in the model boolean allInModel = true; for (int i = 0; i < cells.length && allInModel; i++) allInModel = allInModel && model.contains(cells[i]); // Count repetitive inserts if (in == cells) inCount++; else inCount = (allInModel) ? 1 : 0; in = cells; // Delegate to handle if (p != null && in == out && graph.getUI().getHandle() != null) { int mod = (graph.getUI().getDropAction() == TransferHandler.COPY) ? InputEvent.CTRL_MASK : 0; graph.getUI().getHandle().mouseReleased( new MouseEvent(comp, 0, 0, mod, p.x, p.y, 1, false)); return false; } // Get more Transfer Data Rectangle2D bounds = gt.getBounds(); Map nested = gt.getAttributeMap(); ConnectionSet cs = gt.getConnectionSet(); ParentMap pm = gt.getParentMap(); // Move across models or via clipboard always clones if (!allInModel || p == null || alwaysReceiveAsCopyAction || graph.getUI().getDropAction() == TransferHandler.COPY) { // Translate cells double dx = 0, dy = 0; // Cloned via Drag and Drop if (nested != null) { if (p != null && bounds != null) { Point2D insert = graph.fromScreen(graph .snap((Point2D) p.clone())); dx = insert.getX() - bounds.getX(); dy = insert.getY() - bounds.getY(); // Cloned via Clipboard } else { Point2D insertPoint = getInsertionOffset(graph, inCount, bounds); if (insertPoint != null) { dx = insertPoint.getX(); dy = insertPoint.getY(); } } } handleExternalDrop(graph, cells, nested, cs, pm, dx, dy); // Signal sender to remove only if moved between // different models return (graph.getUI().getDropAction() == TransferHandler.MOVE && !allInModel); } // We are dealing with a move across multiple views // of the same model else { // Moved via Drag and Drop if (p != null) { // Scale insertion location Point2D insert = graph.fromScreen(graph .snap(new Point(p))); // Compute translation vector and translate all // attribute maps. if (bounds != null && nested != null) { double dx = insert.getX() - bounds.getX(); double dy = insert.getY() - bounds.getY(); AttributeMap.translate(nested.values(), dx, dy); } else if (bounds == null) { // Prevents overwriting view-local // attributes // for known cells. Note: This is because // if bounds is null, the caller wants // to signal that the bounds were // not available, which is typically the // case if no graph layout cache // is at hand. To avoid overriding the // local attributes such as the bounds // with the default bounds from the model, // we remove all attributes that travel // along with the transferable. (Since // all cells are already in the model // no information is lost by doing this.) double gs2 = 2 * graph.getGridSize(); nested = new Hashtable(); Map emptyMap = new Hashtable(); for (int i = 0; i < cells.length; i++) { // This also gives us the chance to // provide useful default location and // resize if there are no useful bounds // that travel along with the cells. if (!model.isEdge(cells[i]) && !model.isPort(cells[i])) { // Check if there are useful bounds // defined in the model, otherwise // resize, // because the view does not yet exist. Rectangle2D tmp = graph .getCellBounds(cells[i]); if (tmp == null) tmp = GraphConstants .getBounds(model .getAttributes(cells[i])); // Clone the rectangle to force a // repaint if (tmp != null) tmp = (Rectangle2D) tmp.clone(); Hashtable attrs = new Hashtable(); Object parent = model .getParent(cells[i]); if (tmp == null) { tmp = new Rectangle2D.Double(p .getX(), p.getY(), gs2 / 2, gs2); GraphConstants.setResize(attrs, true); // Shift p.setLocation(p.getX() + gs2, p .getY() + gs2); graph.snap(p); // If parent processed then childs // are already located } else if (parent == null || !nested .keySet() .contains( model .getParent(cells[i]))) { CellView view = graph .getGraphLayoutCache() .getMapping(cells[i], false); if (view != null && !view.isLeaf()) { double dx = p.getX() - tmp.getX(); double dy = p.getY() - tmp.getY(); GraphLayoutCache .translateViews( new CellView[] { view }, dx, dy); } else { tmp.setFrame(p.getX(), p.getY(), tmp .getWidth(), tmp.getHeight()); } // Shift p.setLocation(p.getX() + gs2, p .getY() + gs2); graph.snap(p); } GraphConstants.setBounds(attrs, tmp); nested.put(cells[i], attrs); } else { nested.put(cells[i], emptyMap); } } } // Edit cells (and make visible) cache.edit(nested, null, null, null); } // Select topmost cells in group-structure graph.setSelectionCells(DefaultGraphModel .getTopmostCells(model, cells)); // Don't remove at sender return false; } } else return importDataImpl(comp, t); } } catch (Exception exception) { // System.err.println("Cannot import: " + // exception.getMessage()); exception.printStackTrace(); } return false; } /** * Hook method to determine offset of cells cloned via the clipboard * @param graph the graph the insertion is occurring on * @param inCount the number of time the insert has been applied * @param bounds the bounds of the transferred graph * @return the offset from the cloned cell(s) */ protected Point2D getInsertionOffset(JGraph graph, int inCount, Rectangle2D bounds) { Point2D result = null; if (graph != null) { result = new Point2D.Double(inCount * graph.getGridSize(), inCount * graph.getGridSize()); } return result; } protected void handleExternalDrop(JGraph graph, Object[] cells, Map nested, ConnectionSet cs, ParentMap pm, double dx, double dy) { // Removes all connections for which the port is neither // passed in the parent map nor already in the model. Iterator it = cs.connections(); while (it.hasNext()) { ConnectionSet.Connection conn = (ConnectionSet.Connection) it .next(); if (!pm.getChangedNodes().contains(conn.getPort()) && !graph.getModel().contains(conn.getPort())) { it.remove(); } } Map clones = graph.cloneCells(cells); graph.getGraphLayoutCache().insertClones(cells, clones, nested, cs, pm, dx, dy); } // For subclassers if above does not handle the insertion protected boolean importDataImpl(JComponent comp, Transferable t) { return false; } /** * @return Returns the alwaysReceiveAsCopyAction. */ public boolean isAlwaysReceiveAsCopyAction() { return alwaysReceiveAsCopyAction; } /** * @param alwaysReceiveAsCopyAction * The alwaysReceiveAsCopyAction to set. */ public void setAlwaysReceiveAsCopyAction(boolean alwaysReceiveAsCopyAction) { this.alwaysReceiveAsCopyAction = alwaysReceiveAsCopyAction; } }