[fixes #358] Implement multi-component component tree drag

This commit is contained in:
SiboVG 2022-02-20 17:13:40 +01:00
parent 91b93debec
commit 493883bd87
3 changed files with 220 additions and 118 deletions

View File

@ -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;
}
/**

View File

@ -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 {

View File

@ -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