[fixes #358] Implement multi-component component tree drag
This commit is contained in:
parent
91b93debec
commit
493883bd87
@ -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<RocketComponent> componentsFromPaths(TreePath[] paths) {
|
||||
List<RocketComponent> result = new LinkedList<>();
|
||||
for (TreePath path : paths) {
|
||||
result.add(componentFromPath(path));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
|
@ -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<RocketComponent> 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<SourceTarget> 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<SourceTarget> 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 <code>null</code> if invalid.
|
||||
* @return list of sources and targets, or <code>null</code> if invalid.
|
||||
*/
|
||||
private SourceTarget getSourceAndTarget(TransferHandler.TransferSupport support) {
|
||||
private List<SourceTarget> 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<RocketComponent> 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<SourceTarget> 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 {
|
||||
|
@ -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 <sampo.niskanen@iki.fi>
|
||||
*/
|
||||
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<RocketComponent> components;
|
||||
|
||||
public RocketComponentTransferable(RocketComponent component) {
|
||||
this.component = component;
|
||||
public RocketComponentTransferable(List<RocketComponent> 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<RocketComponent> getComponents() {
|
||||
return components;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
Loading…
x
Reference in New Issue
Block a user