1 package net.sf.openrocket.gui.main;
4 import java.awt.event.ActionEvent;
5 import java.awt.event.KeyEvent;
6 import java.util.ArrayList;
9 import javax.swing.AbstractAction;
10 import javax.swing.Action;
11 import javax.swing.JFrame;
12 import javax.swing.KeyStroke;
13 import javax.swing.event.TreeSelectionEvent;
14 import javax.swing.event.TreeSelectionListener;
15 import javax.swing.tree.TreePath;
16 import javax.swing.tree.TreeSelectionModel;
18 import net.sf.openrocket.document.OpenRocketDocument;
19 import net.sf.openrocket.gui.configdialog.ComponentConfigDialog;
20 import net.sf.openrocket.rocketcomponent.ComponentChangeEvent;
21 import net.sf.openrocket.rocketcomponent.ComponentChangeListener;
22 import net.sf.openrocket.rocketcomponent.Rocket;
23 import net.sf.openrocket.rocketcomponent.RocketComponent;
24 import net.sf.openrocket.rocketcomponent.Stage;
25 import net.sf.openrocket.util.Icons;
26 import net.sf.openrocket.util.Pair;
31 * A class that holds Actions for common rocket operations such as
32 * cut/copy/paste/delete etc.
34 * @author Sampo Niskanen <sampo.niskanen@iki.fi>
36 public class RocketActions {
38 private static RocketComponent clipboard = null;
39 private static List<RocketAction> clipboardListeners = new ArrayList<RocketAction>();
41 private final OpenRocketDocument document;
42 private final Rocket rocket;
43 private final JFrame parentFrame;
44 private final TreeSelectionModel selectionModel;
47 private final RocketAction deleteAction;
48 private final RocketAction cutAction;
49 private final RocketAction copyAction;
50 private final RocketAction pasteAction;
51 private final RocketAction editAction;
52 private final RocketAction newStageAction;
53 private final RocketAction moveUpAction;
54 private final RocketAction moveDownAction;
57 public RocketActions(OpenRocketDocument document, TreeSelectionModel selectionModel,
59 this.document = document;
60 this.rocket = document.getRocket();
61 this.selectionModel = selectionModel;
62 this.parentFrame = parentFrame;
64 // Add action also to updateActions()
65 this.deleteAction = new DeleteAction();
66 this.cutAction = new CutAction();
67 this.copyAction = new CopyAction();
68 this.pasteAction = new PasteAction();
69 this.editAction = new EditAction();
70 this.newStageAction = new NewStageAction();
71 this.moveUpAction = new MoveUpAction();
72 this.moveDownAction = new MoveDownAction();
76 // Update all actions when tree selection or rocket changes
78 selectionModel.addTreeSelectionListener(new TreeSelectionListener() {
80 public void valueChanged(TreeSelectionEvent e) {
84 document.getRocket().addComponentChangeListener(new ComponentChangeListener() {
86 public void componentChanged(ComponentChangeEvent e) {
93 * Update the state of all of the actions.
95 private void updateActions() {
96 deleteAction.update();
101 newStageAction.update();
102 moveUpAction.update();
103 moveDownAction.update();
108 * Update the state of all actions that depend on the clipboard.
110 private void updateClipboardActions() {
111 RocketAction[] array = clipboardListeners.toArray(new RocketAction[0]);
112 for (RocketAction a: array) {
119 public Action getDeleteAction() {
123 public Action getCutAction() {
127 public Action getCopyAction() {
131 public Action getPasteAction() {
135 public Action getEditAction() {
139 public Action getNewStageAction() {
140 return newStageAction;
143 public Action getMoveUpAction() {
147 public Action getMoveDownAction() {
148 return moveDownAction;
153 //////// Helper methods for the actions
156 * Return the currently selected rocket component, or null if none selected.
158 * @return the currently selected component.
160 private RocketComponent getSelectedComponent() {
161 RocketComponent c = null;
162 TreePath p = selectionModel.getSelectionPath();
164 c = (RocketComponent) p.getLastPathComponent();
166 if (c != null && c.getRocket() != rocket) {
167 throw new IllegalStateException("Selection not same as document rocket, "
173 private void setSelectedComponent(RocketComponent component) {
174 TreePath path = ComponentTreeModel.makeTreePath(component);
175 selectionModel.setSelectionPath(path);
179 private boolean isDeletable(RocketComponent c) {
181 if (c == null || c.getParent() == null)
184 // Cannot remove Rocket
185 if (c instanceof Rocket)
188 // Cannot remove last stage
189 if ((c instanceof Stage) && (c.getParent().getChildCount() == 1)) {
196 private void delete(RocketComponent c) {
197 if (!isDeletable(c)) {
198 throw new IllegalArgumentException("Report bug! Component " + c + " not deletable.");
201 RocketComponent parent = c.getParent();
202 parent.removeChild(c);
205 private boolean isCopyable(RocketComponent c) {
208 if (c instanceof Rocket)
217 * Return the component and position to which the current clipboard
218 * should be pasted. Returns null if the clipboard is empty or if the
219 * clipboard cannot be pasted to the current selection.
221 * @return a Pair with both components defined, or null.
223 private Pair<RocketComponent, Integer> getPastePosition() {
224 RocketComponent selected = getSelectedComponent();
225 if (selected == null)
228 if (clipboard == null)
231 if (selected.isCompatible(clipboard))
232 return new Pair<RocketComponent, Integer>(selected, selected.getChildCount());
234 RocketComponent parent = selected.getParent();
235 if (parent != null && parent.isCompatible(clipboard)) {
236 int index = parent.getChildPosition(selected) + 1;
237 return new Pair<RocketComponent, Integer>(parent, index);
247 /////// Action classes
249 private abstract class RocketAction extends AbstractAction {
250 public abstract void update();
255 * Action that deletes the selected component.
257 private class DeleteAction extends RocketAction {
258 public DeleteAction() {
259 this.putValue(NAME, "Delete");
260 this.putValue(SHORT_DESCRIPTION, "Delete the selected component and subcomponents.");
261 this.putValue(MNEMONIC_KEY, KeyEvent.VK_D);
262 this.putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0));
263 this.putValue(SMALL_ICON, Icons.EDIT_DELETE);
268 public void actionPerformed(ActionEvent e) {
269 RocketComponent c = getSelectedComponent();
273 ComponentConfigDialog.hideDialog();
275 document.addUndoPosition("Delete " + c.getComponentName());
280 public void update() {
281 this.setEnabled(isDeletable(getSelectedComponent()));
288 * Action the cuts the selected component (copies to clipboard and deletes).
290 private class CutAction extends RocketAction {
292 this.putValue(NAME, "Cut");
293 this.putValue(MNEMONIC_KEY, KeyEvent.VK_T);
294 this.putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_X,
295 ActionEvent.CTRL_MASK));
296 this.putValue(SHORT_DESCRIPTION, "Cut this component (and subcomponents) to "
297 + "the clipboard and remove from this design");
298 this.putValue(SMALL_ICON, Icons.EDIT_CUT);
303 public void actionPerformed(ActionEvent e) {
304 RocketComponent c = getSelectedComponent();
305 if (!isDeletable(c) || !isCopyable(c))
308 ComponentConfigDialog.hideDialog();
310 document.addUndoPosition("Cut " + c.getComponentName());
311 clipboard = c.copy();
313 updateClipboardActions();
317 public void update() {
318 RocketComponent c = getSelectedComponent();
319 this.setEnabled(isDeletable(c) && isCopyable(c));
326 * Action that copies the selected component to the clipboard.
328 private class CopyAction extends RocketAction {
329 public CopyAction() {
330 this.putValue(NAME, "Copy");
331 this.putValue(MNEMONIC_KEY, KeyEvent.VK_C);
332 this.putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_C,
333 ActionEvent.CTRL_MASK));
334 this.putValue(SHORT_DESCRIPTION, "Copy this component (and subcomponents) to "
336 this.putValue(SMALL_ICON, Icons.EDIT_COPY);
341 public void actionPerformed(ActionEvent e) {
342 RocketComponent c = getSelectedComponent();
346 clipboard = c.copy();
347 updateClipboardActions();
351 public void update() {
352 this.setEnabled(isCopyable(getSelectedComponent()));
360 * Action that pastes the current clipboard to the selected position.
361 * It first tries to paste the component to the end of the selected component
362 * as a child, and after that as a sibling after the selected component.
364 private class PasteAction extends RocketAction {
365 public PasteAction() {
366 this.putValue(NAME, "Paste");
367 this.putValue(MNEMONIC_KEY, KeyEvent.VK_P);
368 this.putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_V,
369 ActionEvent.CTRL_MASK));
370 this.putValue(SHORT_DESCRIPTION, "Paste the component (and subcomponents) on "
371 + "the clipboard to the design.");
372 this.putValue(SMALL_ICON, Icons.EDIT_PASTE);
375 // Listen to when the clipboard changes
376 clipboardListeners.add(this);
380 public void actionPerformed(ActionEvent e) {
381 Pair<RocketComponent, Integer> position = getPastePosition();
382 if (position == null)
385 ComponentConfigDialog.hideDialog();
387 RocketComponent pasted = clipboard.copy();
388 document.addUndoPosition("Paste " + pasted.getComponentName());
389 position.getU().addChild(pasted, position.getV());
390 setSelectedComponent(pasted);
394 public void update() {
395 this.setEnabled(getPastePosition() != null);
405 * Action to edit the currently selected component.
407 private class EditAction extends RocketAction {
408 public EditAction() {
409 this.putValue(NAME, "Edit");
410 this.putValue(SHORT_DESCRIPTION, "Edit the selected component.");
415 public void actionPerformed(ActionEvent e) {
416 RocketComponent c = getSelectedComponent();
420 ComponentConfigDialog.showDialog(parentFrame, document, c);
424 public void update() {
425 this.setEnabled(getSelectedComponent() != null);
436 * Action to add a new stage to the rocket.
438 private class NewStageAction extends RocketAction {
439 public NewStageAction() {
440 this.putValue(NAME, "New stage");
441 this.putValue(SHORT_DESCRIPTION, "Add a new stage to the rocket design.");
446 public void actionPerformed(ActionEvent e) {
448 ComponentConfigDialog.hideDialog();
450 RocketComponent stage = new Stage();
451 stage.setName("Booster stage");
452 document.addUndoPosition("Add stage");
453 rocket.addChild(stage);
454 rocket.getDefaultConfiguration().setAllStages();
455 setSelectedComponent(stage);
456 ComponentConfigDialog.showDialog(parentFrame, document, stage);
461 public void update() {
462 this.setEnabled(true);
470 * Action to move the selected component upwards in the parent's child list.
472 private class MoveUpAction extends RocketAction {
473 public MoveUpAction() {
474 this.putValue(NAME, "Move up");
475 this.putValue(SHORT_DESCRIPTION, "Move this component upwards.");
480 public void actionPerformed(ActionEvent e) {
481 RocketComponent selected = getSelectedComponent();
482 if (!canMove(selected))
485 ComponentConfigDialog.hideDialog();
487 RocketComponent parent = selected.getParent();
488 document.addUndoPosition("Move "+selected.getComponentName());
489 parent.moveChild(selected, parent.getChildPosition(selected)-1);
490 setSelectedComponent(selected);
494 public void update() {
495 this.setEnabled(canMove(getSelectedComponent()));
498 private boolean canMove(RocketComponent c) {
499 if (c == null || c.getParent() == null)
501 RocketComponent parent = c.getParent();
502 if (parent.getChildPosition(c) > 0)
511 * Action to move the selected component down in the parent's child list.
513 private class MoveDownAction extends RocketAction {
514 public MoveDownAction() {
515 this.putValue(NAME, "Move down");
516 this.putValue(SHORT_DESCRIPTION, "Move this component downwards.");
521 public void actionPerformed(ActionEvent e) {
522 RocketComponent selected = getSelectedComponent();
523 if (!canMove(selected))
526 ComponentConfigDialog.hideDialog();
528 RocketComponent parent = selected.getParent();
529 document.addUndoPosition("Move "+selected.getComponentName());
530 parent.moveChild(selected, parent.getChildPosition(selected)+1);
531 setSelectedComponent(selected);
535 public void update() {
536 this.setEnabled(canMove(getSelectedComponent()));
539 private boolean canMove(RocketComponent c) {
540 if (c == null || c.getParent() == null)
542 RocketComponent parent = c.getParent();
543 if (parent.getChildPosition(c) < parent.getChildCount()-1)