diff --git a/swing/src/net/sf/openrocket/gui/main/BasicFrame.java b/swing/src/net/sf/openrocket/gui/main/BasicFrame.java
index 1cd414091..a084939f7 100644
--- a/swing/src/net/sf/openrocket/gui/main/BasicFrame.java
+++ b/swing/src/net/sf/openrocket/gui/main/BasicFrame.java
@@ -181,7 +181,7 @@ public class BasicFrame extends JFrame {
// Create the component tree selection model that will be used
componentSelectionModel = new DefaultTreeSelectionModel();
- componentSelectionModel.setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
+ componentSelectionModel.setSelectionMode(TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION);
// Obtain the simulation selection model that will be used
simulationPanel = new SimulationPanel(document);
diff --git a/swing/src/net/sf/openrocket/gui/main/DocumentSelectionModel.java b/swing/src/net/sf/openrocket/gui/main/DocumentSelectionModel.java
index e69997c36..5d9277df7 100644
--- a/swing/src/net/sf/openrocket/gui/main/DocumentSelectionModel.java
+++ b/swing/src/net/sf/openrocket/gui/main/DocumentSelectionModel.java
@@ -79,9 +79,20 @@ public class DocumentSelectionModel {
* @return the currently selected rocket component, or null.
*/
public RocketComponent getSelectedComponent() {
- if (componentSelection == null || componentSelection.size() == 0) return null;
+ if (componentSelection.size() == 0) return null;
return componentSelection.get(0);
}
+
+ /**
+ * Return the currently selected rocket components. Returns null
+ * if no rocket component is selected.
+ *
+ * @return the currently selected rocket components, or null.
+ */
+ public List getSelectedComponents() {
+ if (componentSelection.size() == 0) return null;
+ return componentSelection;
+ }
public void setSelectedComponent(RocketComponent component) {
componentSelection.clear();
@@ -142,7 +153,7 @@ public class DocumentSelectionModel {
public void clearComponentSelection() {
- if (componentSelection == null || componentSelection.size() == 0)
+ if (componentSelection.size() == 0)
return;
componentSelection.clear();
@@ -177,15 +188,17 @@ public class DocumentSelectionModel {
@Override
public void valueChanged(TreeSelectionEvent e) {
- TreePath path = componentTreeSelectionModel.getSelectionPath();
- if (path == null) {
+ TreePath[] paths = componentTreeSelectionModel.getSelectionPaths();
+ if (paths == null || paths.length == 0) {
componentSelection.clear();
fireDocumentSelection(DocumentSelectionListener.COMPONENT_SELECTION_CHANGE);
return;
}
componentSelection.clear();
- componentSelection.add((RocketComponent)path.getLastPathComponent());
+ for (TreePath path : paths) {
+ componentSelection.add((RocketComponent)path.getLastPathComponent());
+ }
clearSimulationSelection();
fireDocumentSelection(DocumentSelectionListener.COMPONENT_SELECTION_CHANGE);
diff --git a/swing/src/net/sf/openrocket/gui/main/OpenRocketClipboard.java b/swing/src/net/sf/openrocket/gui/main/OpenRocketClipboard.java
index 5541efb0c..cda0a7e85 100644
--- a/swing/src/net/sf/openrocket/gui/main/OpenRocketClipboard.java
+++ b/swing/src/net/sf/openrocket/gui/main/OpenRocketClipboard.java
@@ -1,6 +1,8 @@
package net.sf.openrocket.gui.main;
import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.LinkedList;
import java.util.List;
import net.sf.openrocket.document.Simulation;
@@ -8,10 +10,10 @@ import net.sf.openrocket.rocketcomponent.RocketComponent;
public final class OpenRocketClipboard {
- private static RocketComponent clipboardComponent = null;
+ private static final List clipboardComponents = new LinkedList<>();
private static Simulation[] clipboardSimulations = null;
- private static List listeners = new ArrayList();
+ private static final List listeners = new ArrayList();
private OpenRocketClipboard() {
// Disallow instantiation
@@ -26,16 +28,91 @@ public final class OpenRocketClipboard {
* @return the rocket component contained in the clipboard, or null
* if the clipboard does not currently contain a rocket component.
*/
- public static RocketComponent getClipboardComponent() {
- return clipboardComponent;
+ public static List getClipboardComponents() {
+ return clipboardComponents;
}
- public static void setClipboard(RocketComponent component) {
- clipboardComponent = component;
+ public static void setClipboard(List components) {
+ clipboardComponents.clear();
+ components = RocketActions.copyComponentsMaintainParent(components);
+ if (components != null) {
+ clipboardComponents.addAll(components);
+ }
+ filterClipboardComponents(new LinkedList<>(clipboardComponents));
clipboardSimulations = null;
fireClipboardChanged();
}
+
+ /**
+ * Filters out children from clipboardComponents and the children's list of the components, based on the selection
+ * (see {@link #filterClipboardComponent} to see how the actual filtering is done.
+ * @param components components to be filtered
+ */
+ private static void filterClipboardComponents(List components) {
+ if (components == null || components.size() == 0) return;
+ List checkedComponents = new ArrayList<>();
+ for (RocketComponent component : components) {
+ // Make sure a parent component is processed before the child components
+ RocketComponent temp = component;
+ while (temp.getParent() != null && clipboardComponents.contains(temp.getParent())
+ && !checkedComponents.contains(temp.getParent())) {
+ filterClipboardComponent(temp.getParent());
+ checkedComponents.add(temp.getParent());
+ temp = temp.getParent();
+ }
+ filterClipboardComponent(component);
+ checkedComponents.add(component);
+ }
+ }
+
+ /**
+ * Iteratively (over all children of component and their children), filter clipboard components according to
+ * the following rules:
+ * - If all children of component are selected, then remove those children from clipboardComponents, but keep them
+ * as children of component
+ * - If one of the children of component is selected, but some are not, then remove the unselected children as child
+ * from component, but keep the selected children as children of component + remove the selected children from
+ * clipboardComponents
+ * - If no children are selected, then no children are removed from component + no children are removed from
+ * clipboardComponents
+ * @param component component to filter
+ */
+ private static void filterClipboardComponent(RocketComponent component) {
+ if (component == null) return;
+
+ boolean allChildrenSelected = clipboardComponents.containsAll(component.getChildren());
+ boolean someChildrenSelected = false;
+ for (RocketComponent child : component.getChildren()) {
+ if (clipboardComponents.contains(child)) {
+ someChildrenSelected = true;
+ break;
+ }
+ }
+
+ if (allChildrenSelected) {
+ for (RocketComponent child : component.getChildren()) {
+ clipboardComponents.remove(child);
+ filterClipboardComponents(child.getChildren());
+ }
+ return;
+ }
+
+ if (someChildrenSelected) {
+ for (RocketComponent child : component.getChildren()) {
+ if (!clipboardComponents.contains(child)) {
+ component.removeChild(child);
+ } else {
+ clipboardComponents.remove(child);
+ filterClipboardComponents(child.getChildren());
+ }
+ }
+ } else {
+ for (RocketComponent child : component.getChildren()) {
+ filterClipboardComponents(child.getChildren());
+ }
+ }
+ }
public static Simulation[] getClipboardSimulations() {
@@ -46,7 +123,7 @@ public final class OpenRocketClipboard {
public static void setClipboard(Simulation[] simulations) {
clipboardSimulations = simulations.clone();
- clipboardComponent = null;
+ clipboardComponents.clear();
fireClipboardChanged();
}
diff --git a/swing/src/net/sf/openrocket/gui/main/RocketActions.java b/swing/src/net/sf/openrocket/gui/main/RocketActions.java
index a220c9e8b..7f8a6c083 100644
--- a/swing/src/net/sf/openrocket/gui/main/RocketActions.java
+++ b/swing/src/net/sf/openrocket/gui/main/RocketActions.java
@@ -5,6 +5,9 @@ import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.LinkedList;
+import java.util.List;
import javax.swing.AbstractAction;
import javax.swing.Action;
@@ -181,6 +184,14 @@ public class RocketActions {
return true;
}
+ private boolean isDeletable(List components) {
+ if (components == null || components.size() == 0) return false;
+ for (RocketComponent component : components) {
+ if (!isDeletable(component)) return false;
+ }
+ return true;
+ }
+
private void delete(RocketComponent c) {
if (!isDeletable(c)) {
throw new IllegalArgumentException("Report bug! Component " + c +
@@ -191,6 +202,16 @@ public class RocketActions {
parent.removeChild(c);
}
+ private void delete(List components) {
+ if (!isDeletable(components)) {
+ throw new IllegalArgumentException("Report bug! Components not deletable.");
+ }
+
+ for (RocketComponent component : components) {
+ delete(component);
+ }
+ }
+
private boolean isCopyable(RocketComponent c) {
if (c==null)
return false;
@@ -199,6 +220,119 @@ public class RocketActions {
return true;
}
+ private boolean isCopyable(List components) {
+ if (components == null || components.size() == 0) return false;
+ for (RocketComponent component : components) {
+ if (!isCopyable(component)) return false;
+ }
+ return true;
+ }
+
+ /**
+ * Copies components, but with maintaining parent-relations with the newly copied components
+ * @param components components to copy
+ * @return copied components
+ */
+ public static List copyComponentsMaintainParent(List components) {
+ if (components == null) return null;
+ List result = new LinkedList<>();
+ for (RocketComponent component : components) {
+ result.add(component.copy());
+ }
+
+ for (int i = 0; i < components.size(); i++) {
+ if (components.contains(components.get(i).getParent())) {
+ RocketComponent oldChild = components.get(i);
+ RocketComponent oldParent = oldChild.getParent();
+
+ int index = components.indexOf(oldParent);
+ int childPos = oldParent.getChildPosition(oldChild);
+
+ RocketComponent newChild = result.get(i);
+ RocketComponent newParent = result.get(index);
+
+ // Add the newly copied child to the parent
+ newParent.addChild(newChild, childPos);
+
+ // Remove the old child from the parent
+ newParent.removeChild(oldChild);
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Iteratively checks whether the list of components contains the parent or super-parent (parent of parent of parent of...)
+ * of component.
+ * @param components list of components that may contain the parent
+ * @param component component to check the parent for
+ * @return true if the list contains the parent, false if not
+ */
+ public static boolean listContainsParent(List components, RocketComponent component) {
+ RocketComponent c = component;
+ while (c.getParent() != null) {
+ if (components.contains(c.getParent())) {
+ return true;
+ }
+ c = c.getParent();
+ }
+ return false;
+ }
+
+ /**
+ * If the children of a parent are not selected, add them to the selection. Do this recursively for the children
+ * of the children as well.
+ * @param selections list of currently selected components
+ * @param parent parent component to parse the children of
+ */
+ private void selectAllUnselectedInParent(List selections, RocketComponent parent) {
+ if (parent.getChildCount() == 0) return;
+
+ boolean noChildrenSelected = true;
+ for (RocketComponent child : parent.getChildren()) {
+ if (selections.contains(child)) {
+ noChildrenSelected = false;
+ break;
+ }
+ }
+
+ // Add children to selection if none of them were selected
+ if (noChildrenSelected) {
+ selections.addAll(parent.getChildren());
+ }
+
+ // Recursively select all unselected children. Unselected children will not undergo this recursive updating.
+ for (RocketComponent child : parent.getChildren()) {
+ if (!noChildrenSelected && selections.contains(child)) {
+ selectAllUnselectedInParent(selections, child);
+ }
+ }
+ }
+
+ /**
+ * This method fills in some selections in selections. If there is a parent where none of its children are selected,
+ * add all the children to the selection. If there is a component whose parent is not selected, but it does have
+ * a super-parent in the selection, add the parent to the selection.
+ * @param selections component selections to be filled up
+ */
+ private void fillInMissingSelections(List selections) {
+ List initSelections = new LinkedList<>(selections);
+ for (RocketComponent component : initSelections) {
+ selectAllUnselectedInParent(selections, component);
+
+ // If there is a component in the selection, but its parent (or the parent of the parent) is still
+ // not selected, add it to the selection
+ RocketComponent temp = component;
+ if (listContainsParent(selections, temp) && !selections.contains(temp.getParent())) {
+ while (!selections.contains(temp.getParent())) {
+ selections.add(temp.getParent());
+ temp = temp.getParent();
+ }
+ }
+ }
+ }
+
private boolean isSimulationSelected() {
Simulation[] selection = selectionModel.getSelectedSimulations();
@@ -269,8 +403,26 @@ public class RocketActions {
return null;
}
-
-
+
+ /**
+ * Return the component and position to which the current clipboard
+ * should be pasted. Returns null if the clipboard is empty or if the
+ * clipboard cannot be pasted to the current selection.
+ *
+ * @param clipboard the component on the clipboard.
+ * @return a Pair with both components defined, or null.
+ */
+ private List> getPastePositions(List clipboard) {
+ List> result = new LinkedList<>();
+ for (RocketComponent component : clipboard) {
+ Pair position = getPastePosition(component);
+ if (position != null) {
+ result.add(position);
+ }
+ }
+ return result;
+ }
+
@@ -306,15 +458,30 @@ public class RocketActions {
@Override
public void actionPerformed(ActionEvent e) {
- RocketComponent c = selectionModel.getSelectedComponent();
+ List components = new ArrayList<>(selectionModel.getSelectedComponents());
+ if (components.size() == 0) return;
+ components.sort(Comparator.comparing(c -> c.getParent().getChildPosition(c)));
- if (isDeletable(c)) {
+ if (components.size() == 1) {
+ document.addUndoPosition("Delete " + components.get(0).getComponentName());
+ } else {
+ document.addUndoPosition("Delete components");
+ }
+
+ for (RocketComponent component : components) {
+ deleteComponent(component);
+ }
+ }
+
+ private void deleteComponent(RocketComponent component) {
+ if (isDeletable(component)) {
ComponentConfigDialog.hideDialog();
- c.getRocket().removeComponentChangeListener(ComponentConfigDialog.getDialog());
+ try {
+ component.getRocket().removeComponentChangeListener(ComponentConfigDialog.getDialog());
- document.addUndoPosition("Delete " + c.getComponentName());
- delete(c);
+ delete(component);
+ } catch (IllegalStateException ignored) { }
}
}
@@ -419,15 +586,28 @@ public class RocketActions {
@Override
public void actionPerformed(ActionEvent e) {
- RocketComponent c = selectionModel.getSelectedComponent();
+ List components = selectionModel.getSelectedComponents();
+ if (components != null) {
+ components = new ArrayList<>(components);
+ fillInMissingSelections(components);
+
+ components.sort(Comparator.comparing(c -> c.getParent() != null ? -c.getParent().getChildPosition(c) : 0));
+ }
Simulation[] sims = selectionModel.getSelectedSimulations();
- if (isDeletable(c) && isCopyable(c)) {
+ if (isDeletable(components) && isCopyable(components)) {
ComponentConfigDialog.hideDialog();
-
- document.addUndoPosition("Cut " + c.getComponentName());
- OpenRocketClipboard.setClipboard(c.copy());
- delete(c);
+
+ if (components.size() == 1) {
+ document.addUndoPosition("Cut " + components.get(0).getComponentName());
+ } else {
+ document.addUndoPosition("Cut components");
+ }
+
+ List copiedComponents = new LinkedList<>(copyComponentsMaintainParent(components));
+
+ OpenRocketClipboard.setClipboard(copiedComponents);
+ delete(components);
parentFrame.selectTab(BasicFrame.COMPONENT_TAB);
} else if (isSimulationSelected()) {
@@ -446,8 +626,8 @@ public class RocketActions {
@Override
public void clipboardChanged() {
- RocketComponent c = selectionModel.getSelectedComponent();
- this.setEnabled((isDeletable(c) && isCopyable(c)) || isSimulationSelected());
+ List components = selectionModel.getSelectedComponents();
+ this.setEnabled((isDeletable(components) && isCopyable(components)) || isSimulationSelected());
}
}
@@ -472,13 +652,21 @@ public class RocketActions {
@Override
public void actionPerformed(ActionEvent e) {
- RocketComponent c = selectionModel.getSelectedComponent();
+ List components = selectionModel.getSelectedComponents();
+ if (components != null) {
+ components = new ArrayList<>(components);
+ fillInMissingSelections(components);
+
+ components.sort(Comparator.comparing(c -> c.getParent() != null ? -c.getParent().getChildPosition(c) : 0));
+ }
Simulation[] sims = selectionModel.getSelectedSimulations();
- if (isCopyable(c)) {
- OpenRocketClipboard.setClipboard(c.copy());
+ if (isCopyable(components)) {
+ List copiedComponents = new LinkedList<>(copyComponentsMaintainParent(components));
+
+ OpenRocketClipboard.setClipboard(copiedComponents);
parentFrame.selectTab(BasicFrame.COMPONENT_TAB);
- } else if (sims.length >= 0) {
+ } else if (sims.length > 0) {
Simulation[] simsCopy = new Simulation[sims.length];
for (int i=0; i < sims.length; i++) {
@@ -520,17 +708,33 @@ public class RocketActions {
@Override
public void actionPerformed(ActionEvent e) {
- RocketComponent clipboard = OpenRocketClipboard.getClipboardComponent();
+ List components = new LinkedList<>(OpenRocketClipboard.getClipboardComponents());
Simulation[] sims = OpenRocketClipboard.getClipboardSimulations();
-
- Pair position = getPastePosition(clipboard);
- if (position != null) {
+
+ if (components.size() > 0) {
ComponentConfigDialog.hideDialog();
- RocketComponent pasted = clipboard.copy();
- document.addUndoPosition("Paste " + pasted.getComponentName());
- position.getU().addChild(pasted, position.getV());
- selectionModel.setSelectedComponent(pasted);
+ List pasted = new LinkedList<>();
+ for (RocketComponent component : components) {
+ pasted.add(component.copy());
+ }
+
+ List> positions = new LinkedList<>();
+ for (RocketComponent component : pasted) {
+ positions.add(getPastePosition(component));
+ }
+
+ if (pasted.size() == 1) {
+ document.addUndoPosition("Paste " + pasted.get(0).getComponentName());
+ } else {
+ document.addUndoPosition("Paste components");
+ }
+
+ for (int i = 0; i < pasted.size(); i++) {
+ positions.get(i).getU().addChild(pasted.get(i), positions.get(i).getV());
+ }
+
+ selectionModel.setSelectedComponents(pasted);
parentFrame.selectTab(BasicFrame.COMPONENT_TAB);
@@ -556,7 +760,7 @@ public class RocketActions {
@Override
public void clipboardChanged() {
this.setEnabled(
- (getPastePosition(OpenRocketClipboard.getClipboardComponent()) != null) ||
+ (getPastePositions(OpenRocketClipboard.getClipboardComponents()).size() > 0) ||
(OpenRocketClipboard.getClipboardSimulations() != null));
}
}
@@ -656,22 +860,40 @@ public class RocketActions {
@Override
public void actionPerformed(ActionEvent e) {
- RocketComponent selected = selectionModel.getSelectedComponent();
- if (!canMove(selected))
+ List components = selectionModel.getSelectedComponents();
+ if (components == null || components.size() == 0) return;
+ components = new ArrayList<>(components);
+ components.sort(Comparator.comparing(c -> c.getParent() != null ? c.getParent().getChildPosition(c) : 0));
+
+ if (components.size() == 1) {
+ document.addUndoPosition("Move " + components.get(0).getComponentName());
+ } else {
+ document.addUndoPosition("Move components");
+ }
+
+ for (RocketComponent component : components) {
+ // Only move top components, don't move its children
+ if (!listContainsParent(components, component)) {
+ moveUp(component);
+ }
+ }
+ rocket.fireComponentChangeEvent(ComponentChangeEvent.TREE_CHANGE);
+ selectionModel.setSelectedComponents(components);
+ }
+
+ private void moveUp(RocketComponent component) {
+ if (!canMove(component))
return;
-
+
ComponentConfigDialog.hideDialog();
- RocketComponent parent = selected.getParent();
- document.addUndoPosition("Move "+selected.getComponentName());
- parent.moveChild(selected, parent.getChildPosition(selected)-1);
- rocket.fireComponentChangeEvent( ComponentChangeEvent.TREE_CHANGE );
- selectionModel.setSelectedComponent(selected);
+ RocketComponent parent = component.getParent();
+ parent.moveChild(component, parent.getChildPosition(component) - 1);
}
@Override
public void clipboardChanged() {
- this.setEnabled(canMove(selectionModel.getSelectedComponent()));
+ this.setEnabled(canMove(selectionModel.getSelectedComponents()));
}
private boolean canMove(RocketComponent c) {
@@ -682,6 +904,17 @@ public class RocketActions {
return true;
return false;
}
+
+ private boolean canMove(List components) {
+ if (components == null || components.size() == 0)
+ return false;
+
+ for (RocketComponent component : components) {
+ if (!listContainsParent(components, component) && !canMove(component))
+ return false;
+ }
+ return true;
+ }
}
@@ -702,22 +935,40 @@ public class RocketActions {
@Override
public void actionPerformed(ActionEvent e) {
- RocketComponent selected = selectionModel.getSelectedComponent();
- if (!canMove(selected))
+ List components = selectionModel.getSelectedComponents();
+ if (components == null || components.size() == 0) return;
+ components = new ArrayList<>(components);
+ components.sort(Comparator.comparing(c -> c.getParent() != null ? -c.getParent().getChildPosition(c) : 0));
+
+ if (components.size() == 1) {
+ document.addUndoPosition("Move " + components.get(0).getComponentName());
+ } else {
+ document.addUndoPosition("Move components");
+ }
+
+ for (RocketComponent component : components) {
+ // Only move top components, don't move its children
+ if (!listContainsParent(components, component)) {
+ moveDown(component);
+ }
+ }
+ rocket.fireComponentChangeEvent(ComponentChangeEvent.TREE_CHANGE);
+ selectionModel.setSelectedComponents(components);
+ }
+
+ private void moveDown(RocketComponent component) {
+ if (!canMove(component))
return;
-
+
ComponentConfigDialog.hideDialog();
- RocketComponent parent = selected.getParent();
- document.addUndoPosition("Move "+selected.getComponentName());
- parent.moveChild(selected, parent.getChildPosition(selected)+1);
- rocket.fireComponentChangeEvent( ComponentChangeEvent.TREE_CHANGE );
- selectionModel.setSelectedComponent(selected);
+ RocketComponent parent = component.getParent();
+ parent.moveChild(component, parent.getChildPosition(component) + 1);
}
@Override
public void clipboardChanged() {
- this.setEnabled(canMove(selectionModel.getSelectedComponent()));
+ this.setEnabled(canMove(selectionModel.getSelectedComponents()));
}
private boolean canMove(RocketComponent c) {
@@ -728,6 +979,17 @@ public class RocketActions {
return true;
return false;
}
+
+ private boolean canMove(List components) {
+ if (components == null || components.size() == 0)
+ return false;
+
+ for (RocketComponent component : components) {
+ if (!listContainsParent(components, component) && !canMove(component))
+ return false;
+ }
+ return true;
+ }
}