diff --git a/swing/src/net/sf/openrocket/gui/main/componenttree/ComponentTreeModel.java b/swing/src/net/sf/openrocket/gui/main/componenttree/ComponentTreeModel.java index b71c93c59..b33089118 100644 --- a/swing/src/net/sf/openrocket/gui/main/componenttree/ComponentTreeModel.java +++ b/swing/src/net/sf/openrocket/gui/main/componenttree/ComponentTreeModel.java @@ -175,6 +175,20 @@ public class ComponentTreeModel implements TreeModel, ComponentChangeListener { } return (RocketComponent) last; } + + /** + * Return the rocket components that an array of TreePath objects are referring to. + * + * @param paths the TreePaths + * @return the list of RocketComponents the paths are referring to. + */ + public static List componentsFromPaths(TreePath[] paths) { + List result = new LinkedList<>(); + for (TreePath path : paths) { + result.add(componentFromPath(path)); + } + return result; + } /** diff --git a/swing/src/net/sf/openrocket/gui/main/componenttree/ComponentTreeTransferHandler.java b/swing/src/net/sf/openrocket/gui/main/componenttree/ComponentTreeTransferHandler.java index 4431725e7..6750b98b2 100644 --- a/swing/src/net/sf/openrocket/gui/main/componenttree/ComponentTreeTransferHandler.java +++ b/swing/src/net/sf/openrocket/gui/main/componenttree/ComponentTreeTransferHandler.java @@ -5,6 +5,9 @@ import java.awt.datatransfer.Transferable; import java.awt.datatransfer.UnsupportedFlavorException; import java.io.IOException; import java.util.Arrays; +import java.util.Comparator; +import java.util.LinkedList; +import java.util.List; import javax.swing.JComponent; import javax.swing.JTree; @@ -13,6 +16,8 @@ import javax.swing.TransferHandler; import javax.swing.tree.TreeModel; import javax.swing.tree.TreePath; +import net.sf.openrocket.gui.main.RocketActions; +import net.sf.openrocket.util.ArrayList; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -52,25 +57,41 @@ public class ComponentTreeTransferHandler extends TransferHandler { } @Override - public Transferable createTransferable(JComponent component) { - if (!(component instanceof JTree)) { - throw new BugException("TransferHandler called with component " + component); + public Transferable createTransferable(JComponent treeComp) { + if (!(treeComp instanceof JTree)) { + throw new BugException("TransferHandler called with component " + treeComp); } - JTree tree = (JTree) component; - TreePath path = tree.getSelectionPath(); - if (path == null) { + JTree tree = (JTree) treeComp; + TreePath[] paths = tree.getSelectionPaths(); + if (paths == null || paths.length == 0) { return null; } - RocketComponent c = ComponentTreeModel.componentFromPath(path); - if (c instanceof Rocket) { - log.info("Attempting to create transferable from Rocket"); - return null; + List components = new ArrayList<>(ComponentTreeModel.componentsFromPaths(paths)); + components.sort(Comparator.comparing(c -> -c.getParent().getChildPosition(c))); + + // When the parent of a child is in the selection, don't include the child in components + for (RocketComponent component : new ArrayList<>(components)) { + if (RocketActions.listContainsParent(components, component)) { + components.remove(component); + } } - - log.info("Creating transferable from component " + c.getComponentName()); - return new RocketComponentTransferable(c); + + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < components.size(); i++) { + if (components.get(i) instanceof Rocket) { + log.info("Attempting to create transferable from Rocket"); + return null; + } + sb.append(components.get(i).getComponentName()); + if (i < components.size() - 1) { + sb.append(", "); + } + } + + log.info("Creating transferable from component " + sb.toString()); + return new RocketComponentTransferable(components); } @@ -85,35 +106,47 @@ public class ComponentTreeTransferHandler extends TransferHandler { @Override public boolean canImport(TransferHandler.TransferSupport support) { - SourceTarget data = getSourceAndTarget(support); - - if (data == null) { + List targets = getSourceAndTarget(support); + + if (targets == null || targets.size() == 0) { return false; } - - boolean allowed = data.destParent.isCompatible(data.child); - log.trace("Checking validity of drag-drop " + data.toString() + " allowed:" + allowed); - + + for (SourceTarget target : targets) { + Boolean allowed = canImportTarget(support, target); + if (allowed == null || !allowed) { + return false; + } + } + return true; + } + + private Boolean canImportTarget(TransferHandler.TransferSupport support, SourceTarget target) { + if (target == null) { + return null; + } + + boolean allowed = target.destParent.isCompatible(target.child); + log.trace("Checking validity of drag-drop " + target.toString() + " allowed:" + allowed); + // Ensure we're not dropping a component onto a child component - RocketComponent path = data.destParent; + RocketComponent path = target.destParent; while (path != null) { - if (path.equals(data.child)) { + if (path.equals(target.child)) { log.trace("Drop would cause cycle in tree, disallowing."); allowed = false; break; } path = path.getParent(); } - + // If drag-dropping to another rocket always copy - if (support.getDropAction() == MOVE && data.srcParent.getRoot() != data.destParent.getRoot()) { + if (support.getDropAction() == MOVE && target.srcParent.getRoot() != target.destParent.getRoot()) { support.setDropAction(COPY); } - return allowed; } - - + @Override public boolean importData(TransferHandler.TransferSupport support) { @@ -125,67 +158,56 @@ public class ComponentTreeTransferHandler extends TransferHandler { // Sun JRE silently ignores any RuntimeExceptions in importData, yeech! try { - - SourceTarget data = getSourceAndTarget(support); + List targets = getSourceAndTarget(support); + if (targets == null || targets.size() == 0) { + return false; + } // Check what action to perform int action = support.getDropAction(); - if (data.srcParent.getRoot() != data.destParent.getRoot()) { - // If drag-dropping to another rocket always copy - log.info("Performing DnD between different rockets, forcing copy action"); - action = TransferHandler.COPY; - } - - - // Check whether move action would be a no-op - if ((action == MOVE) && (data.srcParent == data.destParent) && - (data.destIndex == data.srcIndex || data.destIndex == data.srcIndex + 1)) { - log.info(Markers.USER_MARKER, "Dropped component at the same place as previously: " + data); - return false; - } - - + + int destIndex = targets.get(0).destIndex; + + // Add undo positions switch (action) { - case MOVE: - log.info(Markers.USER_MARKER, "Performing DnD move operation: " + data); - - // If parents are the same, check whether removing the child changes the insert position - int index = data.destIndex; - if (data.srcParent == data.destParent && data.srcIndex < data.destIndex) { - index--; - } - - // Mark undo and freeze rocket. src and dest are in same rocket, need to freeze only one - try { - document.startUndo("Move component"); - try { - data.srcParent.getRocket().freeze(); - data.srcParent.removeChild(data.srcIndex); - data.destParent.addChild(data.child, index); - } finally { - data.srcParent.getRocket().thaw(); + case MOVE: + if (targets.size() == 1) { + document.startUndo("Move component"); + } else { + document.startUndo("Move components"); } - } finally { - document.stopUndo(); - } - return true; - - case COPY: - log.info(Markers.USER_MARKER, "Performing DnD copy operation: " + data); - RocketComponent copy = data.child.copy(); - try { - document.startUndo("Copy component"); - data.destParent.addChild(copy, data.destIndex); - } finally { - document.stopUndo(); - } - return true; - - default: - log.warn("Unknown transfer action " + action); - return false; + break; + case COPY: + if (targets.size() == 1) { + document.startUndo("Copy component"); + } else { + document.startUndo("Copy components"); + } + break; + default: + log.warn("Unknown transfer action " + action); + return false; } - + + for (SourceTarget target : targets) { + int targetAction = action; + + if (target.srcParent.getRoot() != target.destParent.getRoot()) { + // If drag-dropping to another rocket always copy + log.info("Performing DnD between different rockets, forcing copy action"); + targetAction = TransferHandler.COPY; + } + + if (!checkImportAction(targetAction, target, destIndex)) { + document.stopUndo(); + return false; + } + + destIndex = importDataTarget(targetAction, target, destIndex); + } + document.stopUndo(); + return true; + } catch (final RuntimeException e) { // Open error dialog later if an exception has occurred SwingUtilities.invokeLater(new Runnable() { @@ -197,17 +219,69 @@ public class ComponentTreeTransferHandler extends TransferHandler { return false; } } - - - + /** - * Fetch the source and target for the DnD action. This method does not perform + * Checks + * @param action + * @param target + * @return + */ + private boolean checkImportAction(int action, SourceTarget target, int destIndex) { + // Check whether move action would be a no-op + if ((action == MOVE) && (target.srcParent == target.destParent) && + (destIndex == target.srcIndex || destIndex == target.srcIndex + 1)) { + log.info(Markers.USER_MARKER, "Dropped component at the same place as previously: " + target); + return false; + } + + return true; + } + + /** + * Moves or copies a RocketComponent in target. + * + * @param action action to perform + * @param target target object containing the RocketComponent to edit + * @param destIndex destination index for the component + * @return new destination index + */ + private int importDataTarget(int action, SourceTarget target, int destIndex) { + switch (action) { + case MOVE: + log.info(Markers.USER_MARKER, "Performing DnD move operation: " + target); + + // If parents are the same, check whether removing the child changes the insert position + if (target.srcParent == target.destParent && target.srcIndex < destIndex) { + destIndex--; + } + + // Freeze rocket. src and dest are in same rocket, need to freeze only one + try { + target.srcParent.getRocket().freeze(); + target.srcParent.removeChild(target.child); + target.destParent.addChild(target.child, destIndex); + } finally { + target.srcParent.getRocket().thaw(); // Unfreeze + } + break; + case COPY: + log.info(Markers.USER_MARKER, "Performing DnD copy operation: " + target); + RocketComponent copy = target.child.copy(); + target.destParent.addChild(copy, target.destIndex); + break; + } + return destIndex; + } + + + /** + * Fetch the sources and targets for the DnD action. This method does not perform * checks on whether this action is allowed based on component positioning rules. * * @param support the transfer support - * @return the source and targer, or null if invalid. + * @return list of sources and targets, or null if invalid. */ - private SourceTarget getSourceAndTarget(TransferHandler.TransferSupport support) { + private List getSourceAndTarget(TransferHandler.TransferSupport support) { // We currently only support drop, not paste if (!support.isDrop()) { log.warn("Import action is not a drop action"); @@ -232,35 +306,42 @@ public class ComponentTreeTransferHandler extends TransferHandler { // Fetch the transferred component (child component) Transferable transferable = support.getTransferable(); - RocketComponent child; + List children; try { - child = (RocketComponent) transferable.getTransferData( + RocketComponentTransferable transfer = (RocketComponentTransferable) transferable.getTransferData( RocketComponentTransferable.ROCKET_COMPONENT_DATA_FLAVOR); - } catch (IOException e) { - throw new BugException(e); - } catch (UnsupportedFlavorException e) { + children = transfer.getComponents(); + if (children == null || children.size() == 0) { + return null; + } + } catch (IOException | UnsupportedFlavorException e) { throw new BugException(e); } - - - // Get the source component & index - RocketComponent srcParent = child.getParent(); - if (srcParent == null) { - log.debug("Attempting to drag root component"); - return null; - } - int srcIndex = srcParent.getChildPosition(child); - - - // Get destination component & index - RocketComponent destParent = ComponentTreeModel.componentFromPath(location.path); - int destIndex = location.index; - if (destIndex < 0) { - destIndex = 0; + + List targets = new LinkedList<>(); + + for (RocketComponent child : children) { + // Get the source component & index + RocketComponent srcParent = child.getParent(); + if (srcParent == null) { + log.debug("Attempting to drag root component"); + return null; + } + int srcIndex = srcParent.getChildPosition(child); + + + // Get destination component & index + RocketComponent destParent = ComponentTreeModel.componentFromPath(location.path); + int destIndex = location.index; + if (destIndex < 0) { + destIndex = 0; + } + + targets.add(new SourceTarget(srcParent, srcIndex, destParent, destIndex, child)); } - return new SourceTarget(srcParent, srcIndex, destParent, destIndex, child); + return targets; } private class SourceTarget { diff --git a/swing/src/net/sf/openrocket/gui/main/componenttree/RocketComponentTransferable.java b/swing/src/net/sf/openrocket/gui/main/componenttree/RocketComponentTransferable.java index e9379dae9..94f11860a 100644 --- a/swing/src/net/sf/openrocket/gui/main/componenttree/RocketComponentTransferable.java +++ b/swing/src/net/sf/openrocket/gui/main/componenttree/RocketComponentTransferable.java @@ -4,6 +4,7 @@ import java.awt.datatransfer.DataFlavor; import java.awt.datatransfer.Transferable; import java.awt.datatransfer.UnsupportedFlavorException; import java.io.IOException; +import java.util.List; import net.sf.openrocket.rocketcomponent.RocketComponent; @@ -13,16 +14,18 @@ import net.sf.openrocket.rocketcomponent.RocketComponent; * @author Sampo Niskanen */ public class RocketComponentTransferable implements Transferable { - - public static final DataFlavor ROCKET_COMPONENT_DATA_FLAVOR = new DataFlavor( - DataFlavor.javaJVMLocalObjectMimeType + "; class=" + RocketComponent.class.getCanonicalName(), - "OpenRocket component"); + + /** + * Data flavor that allows a RocketComponent to be extracted from a transferable object + */ + public static final DataFlavor ROCKET_COMPONENT_DATA_FLAVOR = new DataFlavor(RocketComponentTransferable.class, + "Drag and drop list"); - private final RocketComponent component; + private final List components; - public RocketComponentTransferable(RocketComponent component) { - this.component = component; + public RocketComponentTransferable(List components) { + this.components = components; } @@ -31,7 +34,11 @@ public class RocketComponentTransferable implements Transferable { if (!isDataFlavorSupported(flavor)) { throw new UnsupportedFlavorException(flavor); } - return component; + return this; + } + + public List getComponents() { + return components; } @Override