[fixes #358] Implement multi-component component tree drag
This commit is contained in:
parent
91b93debec
commit
493883bd87
@ -176,6 +176,20 @@ public class ComponentTreeModel implements TreeModel, ComponentChangeListener {
|
|||||||
return (RocketComponent) last;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a TreePath corresponding to the specified rocket component.
|
* Return a TreePath corresponding to the specified rocket component.
|
||||||
|
@ -5,6 +5,9 @@ import java.awt.datatransfer.Transferable;
|
|||||||
import java.awt.datatransfer.UnsupportedFlavorException;
|
import java.awt.datatransfer.UnsupportedFlavorException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import javax.swing.JComponent;
|
import javax.swing.JComponent;
|
||||||
import javax.swing.JTree;
|
import javax.swing.JTree;
|
||||||
@ -13,6 +16,8 @@ import javax.swing.TransferHandler;
|
|||||||
import javax.swing.tree.TreeModel;
|
import javax.swing.tree.TreeModel;
|
||||||
import javax.swing.tree.TreePath;
|
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.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
@ -52,25 +57,41 @@ public class ComponentTreeTransferHandler extends TransferHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Transferable createTransferable(JComponent component) {
|
public Transferable createTransferable(JComponent treeComp) {
|
||||||
if (!(component instanceof JTree)) {
|
if (!(treeComp instanceof JTree)) {
|
||||||
throw new BugException("TransferHandler called with component " + component);
|
throw new BugException("TransferHandler called with component " + treeComp);
|
||||||
}
|
}
|
||||||
|
|
||||||
JTree tree = (JTree) component;
|
JTree tree = (JTree) treeComp;
|
||||||
TreePath path = tree.getSelectionPath();
|
TreePath[] paths = tree.getSelectionPaths();
|
||||||
if (path == null) {
|
if (paths == null || paths.length == 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
RocketComponent c = ComponentTreeModel.componentFromPath(path);
|
List<RocketComponent> components = new ArrayList<>(ComponentTreeModel.componentsFromPaths(paths));
|
||||||
if (c instanceof Rocket) {
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
log.info("Attempting to create transferable from Rocket");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
sb.append(components.get(i).getComponentName());
|
||||||
|
if (i < components.size() - 1) {
|
||||||
|
sb.append(", ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
log.info("Creating transferable from component " + c.getComponentName());
|
log.info("Creating transferable from component " + sb.toString());
|
||||||
return new RocketComponentTransferable(c);
|
return new RocketComponentTransferable(components);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -85,19 +106,33 @@ public class ComponentTreeTransferHandler extends TransferHandler {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean canImport(TransferHandler.TransferSupport support) {
|
public boolean canImport(TransferHandler.TransferSupport support) {
|
||||||
SourceTarget data = getSourceAndTarget(support);
|
List<SourceTarget> targets = getSourceAndTarget(support);
|
||||||
|
|
||||||
if (data == null) {
|
if (targets == null || targets.size() == 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean allowed = data.destParent.isCompatible(data.child);
|
for (SourceTarget target : targets) {
|
||||||
log.trace("Checking validity of drag-drop " + data.toString() + " allowed:" + allowed);
|
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
|
// Ensure we're not dropping a component onto a child component
|
||||||
RocketComponent path = data.destParent;
|
RocketComponent path = target.destParent;
|
||||||
while (path != null) {
|
while (path != null) {
|
||||||
if (path.equals(data.child)) {
|
if (path.equals(target.child)) {
|
||||||
log.trace("Drop would cause cycle in tree, disallowing.");
|
log.trace("Drop would cause cycle in tree, disallowing.");
|
||||||
allowed = false;
|
allowed = false;
|
||||||
break;
|
break;
|
||||||
@ -106,14 +141,12 @@ public class ComponentTreeTransferHandler extends TransferHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If drag-dropping to another rocket always copy
|
// 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);
|
support.setDropAction(COPY);
|
||||||
}
|
}
|
||||||
|
|
||||||
return allowed;
|
return allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean importData(TransferHandler.TransferSupport support) {
|
public boolean importData(TransferHandler.TransferSupport support) {
|
||||||
|
|
||||||
@ -125,67 +158,56 @@ public class ComponentTreeTransferHandler extends TransferHandler {
|
|||||||
|
|
||||||
// Sun JRE silently ignores any RuntimeExceptions in importData, yeech!
|
// Sun JRE silently ignores any RuntimeExceptions in importData, yeech!
|
||||||
try {
|
try {
|
||||||
|
List<SourceTarget> targets = getSourceAndTarget(support);
|
||||||
SourceTarget data = getSourceAndTarget(support);
|
if (targets == null || targets.size() == 0) {
|
||||||
|
|
||||||
// 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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check what action to perform
|
||||||
|
int action = support.getDropAction();
|
||||||
|
|
||||||
|
int destIndex = targets.get(0).destIndex;
|
||||||
|
|
||||||
|
// Add undo positions
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case MOVE:
|
case MOVE:
|
||||||
log.info(Markers.USER_MARKER, "Performing DnD move operation: " + data);
|
if (targets.size() == 1) {
|
||||||
|
|
||||||
// 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");
|
document.startUndo("Move component");
|
||||||
try {
|
} else {
|
||||||
data.srcParent.getRocket().freeze();
|
document.startUndo("Move components");
|
||||||
data.srcParent.removeChild(data.srcIndex);
|
|
||||||
data.destParent.addChild(data.child, index);
|
|
||||||
} finally {
|
|
||||||
data.srcParent.getRocket().thaw();
|
|
||||||
}
|
}
|
||||||
} finally {
|
break;
|
||||||
document.stopUndo();
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case COPY:
|
case COPY:
|
||||||
log.info(Markers.USER_MARKER, "Performing DnD copy operation: " + data);
|
if (targets.size() == 1) {
|
||||||
RocketComponent copy = data.child.copy();
|
|
||||||
try {
|
|
||||||
document.startUndo("Copy component");
|
document.startUndo("Copy component");
|
||||||
data.destParent.addChild(copy, data.destIndex);
|
} else {
|
||||||
} finally {
|
document.startUndo("Copy components");
|
||||||
document.stopUndo();
|
|
||||||
}
|
}
|
||||||
return true;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
log.warn("Unknown transfer action " + action);
|
log.warn("Unknown transfer action " + action);
|
||||||
return false;
|
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) {
|
} catch (final RuntimeException e) {
|
||||||
// Open error dialog later if an exception has occurred
|
// Open error dialog later if an exception has occurred
|
||||||
SwingUtilities.invokeLater(new Runnable() {
|
SwingUtilities.invokeLater(new Runnable() {
|
||||||
@ -198,16 +220,68 @@ public class ComponentTreeTransferHandler extends TransferHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 source and target for the DnD action. This method does not perform
|
* 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.
|
* checks on whether this action is allowed based on component positioning rules.
|
||||||
*
|
*
|
||||||
* @param support the transfer support
|
* @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
|
// We currently only support drop, not paste
|
||||||
if (!support.isDrop()) {
|
if (!support.isDrop()) {
|
||||||
log.warn("Import action is not a drop action");
|
log.warn("Import action is not a drop action");
|
||||||
@ -232,18 +306,22 @@ public class ComponentTreeTransferHandler extends TransferHandler {
|
|||||||
|
|
||||||
// Fetch the transferred component (child component)
|
// Fetch the transferred component (child component)
|
||||||
Transferable transferable = support.getTransferable();
|
Transferable transferable = support.getTransferable();
|
||||||
RocketComponent child;
|
List<RocketComponent> children;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
child = (RocketComponent) transferable.getTransferData(
|
RocketComponentTransferable transfer = (RocketComponentTransferable) transferable.getTransferData(
|
||||||
RocketComponentTransferable.ROCKET_COMPONENT_DATA_FLAVOR);
|
RocketComponentTransferable.ROCKET_COMPONENT_DATA_FLAVOR);
|
||||||
} catch (IOException e) {
|
children = transfer.getComponents();
|
||||||
throw new BugException(e);
|
if (children == null || children.size() == 0) {
|
||||||
} catch (UnsupportedFlavorException e) {
|
return null;
|
||||||
|
}
|
||||||
|
} catch (IOException | UnsupportedFlavorException e) {
|
||||||
throw new BugException(e);
|
throw new BugException(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<SourceTarget> targets = new LinkedList<>();
|
||||||
|
|
||||||
|
for (RocketComponent child : children) {
|
||||||
// Get the source component & index
|
// Get the source component & index
|
||||||
RocketComponent srcParent = child.getParent();
|
RocketComponent srcParent = child.getParent();
|
||||||
if (srcParent == null) {
|
if (srcParent == null) {
|
||||||
@ -260,7 +338,10 @@ public class ComponentTreeTransferHandler extends TransferHandler {
|
|||||||
destIndex = 0;
|
destIndex = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new SourceTarget(srcParent, srcIndex, destParent, destIndex, child);
|
targets.add(new SourceTarget(srcParent, srcIndex, destParent, destIndex, child));
|
||||||
|
}
|
||||||
|
|
||||||
|
return targets;
|
||||||
}
|
}
|
||||||
|
|
||||||
private class SourceTarget {
|
private class SourceTarget {
|
||||||
|
@ -4,6 +4,7 @@ import java.awt.datatransfer.DataFlavor;
|
|||||||
import java.awt.datatransfer.Transferable;
|
import java.awt.datatransfer.Transferable;
|
||||||
import java.awt.datatransfer.UnsupportedFlavorException;
|
import java.awt.datatransfer.UnsupportedFlavorException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import net.sf.openrocket.rocketcomponent.RocketComponent;
|
import net.sf.openrocket.rocketcomponent.RocketComponent;
|
||||||
|
|
||||||
@ -14,15 +15,17 @@ import net.sf.openrocket.rocketcomponent.RocketComponent;
|
|||||||
*/
|
*/
|
||||||
public class RocketComponentTransferable implements Transferable {
|
public class RocketComponentTransferable implements Transferable {
|
||||||
|
|
||||||
public static final DataFlavor ROCKET_COMPONENT_DATA_FLAVOR = new DataFlavor(
|
/**
|
||||||
DataFlavor.javaJVMLocalObjectMimeType + "; class=" + RocketComponent.class.getCanonicalName(),
|
* Data flavor that allows a RocketComponent to be extracted from a transferable object
|
||||||
"OpenRocket component");
|
*/
|
||||||
|
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) {
|
public RocketComponentTransferable(List<RocketComponent> components) {
|
||||||
this.component = component;
|
this.components = components;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -31,7 +34,11 @@ public class RocketComponentTransferable implements Transferable {
|
|||||||
if (!isDataFlavorSupported(flavor)) {
|
if (!isDataFlavorSupported(flavor)) {
|
||||||
throw new UnsupportedFlavorException(flavor);
|
throw new UnsupportedFlavorException(flavor);
|
||||||
}
|
}
|
||||||
return component;
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<RocketComponent> getComponents() {
|
||||||
|
return components;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
Loading…
x
Reference in New Issue
Block a user