Bug fixes and startup checks
[debian/openrocket] / src / net / sf / openrocket / gui / main / RocketActions.java
1 package net.sf.openrocket.gui.main;
2
3
4 import java.awt.event.ActionEvent;
5 import java.awt.event.KeyEvent;
6 import java.util.ArrayList;
7 import java.util.List;
8
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;
17
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;
27
28
29
30 /**
31  * A class that holds Actions for common rocket and simulation operations such as
32  * cut/copy/paste/delete etc.
33  * 
34  * @author Sampo Niskanen <sampo.niskanen@iki.fi>
35  */
36 public class RocketActions {
37
38         private static RocketComponent clipboard = null;
39         private static List<RocketAction> clipboardListeners = new ArrayList<RocketAction>();
40
41         private final OpenRocketDocument document;
42         private final Rocket rocket;
43         private final JFrame parentFrame;
44         private final TreeSelectionModel selectionModel;
45
46
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;
55         
56
57         public RocketActions(OpenRocketDocument document, TreeSelectionModel selectionModel,
58                         JFrame parentFrame) {
59                 this.document = document;
60                 this.rocket = document.getRocket();
61                 this.selectionModel = selectionModel;
62                 this.parentFrame = parentFrame;
63
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();
73
74                 updateActions();
75
76                 // Update all actions when tree selection or rocket changes
77
78                 selectionModel.addTreeSelectionListener(new TreeSelectionListener() {
79                         @Override
80                         public void valueChanged(TreeSelectionEvent e) {
81                                 updateActions();
82                         }
83                 });
84                 document.getRocket().addComponentChangeListener(new ComponentChangeListener() {
85                         @Override
86                         public void componentChanged(ComponentChangeEvent e) {
87                                 updateActions();
88                         }
89                 });
90         }
91
92         /**
93          * Update the state of all of the actions.
94          */
95         private void updateActions() {
96                 deleteAction.update();
97                 cutAction.update();
98                 copyAction.update();
99                 pasteAction.update();
100                 editAction.update();
101                 newStageAction.update();
102                 moveUpAction.update();
103                 moveDownAction.update();
104         }
105         
106         
107         /**
108          * Update the state of all actions that depend on the clipboard.
109          */
110         private void updateClipboardActions() {
111                 RocketAction[] array = clipboardListeners.toArray(new RocketAction[0]);
112                 for (RocketAction a: array) {
113                         a.update();
114                 }
115         }
116
117         
118
119         public Action getDeleteAction() {
120                 return deleteAction;
121         }
122
123         public Action getCutAction() {
124                 return cutAction;
125         }
126         
127         public Action getCopyAction() {
128                 return copyAction;
129         }
130         
131         public Action getPasteAction() {
132                 return pasteAction;
133         }
134         
135         public Action getEditAction() {
136                 return editAction;
137         }
138         
139         public Action getNewStageAction() {
140                 return newStageAction;
141         }
142         
143         public Action getMoveUpAction() {
144                 return moveUpAction;
145         }
146         
147         public Action getMoveDownAction() {
148                 return moveDownAction;
149         }
150
151         
152         
153         ////////  Helper methods for the actions
154
155         /**
156          * Return the currently selected rocket component, or null if none selected.
157          * 
158          * @return      the currently selected component.
159          */
160         private RocketComponent getSelectedComponent() {
161                 RocketComponent c = null;
162                 TreePath p = selectionModel.getSelectionPath();
163                 if (p != null)
164                         c = (RocketComponent) p.getLastPathComponent();
165
166                 if (c != null && c.getRocket() != rocket) {
167                         throw new IllegalStateException("Selection not same as document rocket, "
168                                         + "report bug!");
169                 }
170                 return c;
171         }
172         
173         private void setSelectedComponent(RocketComponent component) {
174                 TreePath path = ComponentTreeModel.makeTreePath(component);
175                 selectionModel.setSelectionPath(path);
176         }
177
178
179         private boolean isDeletable(RocketComponent c) {
180                 // Sanity check
181                 if (c == null || c.getParent() == null)
182                         return false;
183
184                 // Cannot remove Rocket
185                 if (c instanceof Rocket)
186                         return false;
187
188                 // Cannot remove last stage
189                 if ((c instanceof Stage) && (c.getParent().getChildCount() == 1)) {
190                         return false;
191                 }
192
193                 return true;
194         }
195
196         private void delete(RocketComponent c) {
197                 if (!isDeletable(c)) {
198                         throw new IllegalArgumentException("Report bug!  Component " + c + " not deletable.");
199                 }
200
201                 RocketComponent parent = c.getParent();
202                 parent.removeChild(c);
203         }
204
205         private boolean isCopyable(RocketComponent c) {
206                 if (c==null)
207                         return false;
208                 if (c instanceof Rocket)
209                         return false;
210                 return true;
211         }
212
213         
214
215
216         /**
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.
220          * 
221          * @return  a Pair with both components defined, or null.
222          */
223         private Pair<RocketComponent, Integer> getPastePosition() {
224                 RocketComponent selected = getSelectedComponent();
225                 if (selected == null)
226                         return null;
227
228                 if (clipboard == null)
229                         return null;
230
231                 if (selected.isCompatible(clipboard))
232                         return new Pair<RocketComponent, Integer>(selected, selected.getChildCount());
233
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);
238                 }
239
240                 return null;
241         }
242         
243         
244         
245         
246
247         ///////  Action classes
248
249         private abstract class RocketAction extends AbstractAction {
250                 public abstract void update();
251         }
252
253
254         /**
255          * Action that deletes the selected component.
256          */
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);
264                         update();
265                 }
266
267                 @Override
268                 public void actionPerformed(ActionEvent e) {
269                         RocketComponent c = getSelectedComponent();
270                         if (!isDeletable(c))
271                                 return;
272
273                         ComponentConfigDialog.hideDialog();
274
275                         document.addUndoPosition("Delete " + c.getComponentName());
276                         delete(c);
277                 }
278
279                 @Override
280                 public void update() {
281                         this.setEnabled(isDeletable(getSelectedComponent()));
282                 }
283         }
284
285
286         
287         /**
288          * Action the cuts the selected component (copies to clipboard and deletes).
289          */
290         private class CutAction extends RocketAction {
291                 public CutAction() {
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);
299                         update();
300                 }
301
302                 @Override
303                 public void actionPerformed(ActionEvent e) {
304                         RocketComponent c = getSelectedComponent();
305                         if (!isDeletable(c) || !isCopyable(c))
306                                 return;
307
308                         ComponentConfigDialog.hideDialog();
309
310                         document.addUndoPosition("Cut " + c.getComponentName());
311                         clipboard = c.copy();
312                         delete(c);
313                         updateClipboardActions();
314                 }
315
316                 @Override
317                 public void update() {
318                         RocketComponent c = getSelectedComponent();
319                         this.setEnabled(isDeletable(c) && isCopyable(c));
320                 }
321         }
322
323
324
325         /**
326          * Action that copies the selected component to the clipboard.
327          */
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 "
335                                         + "the clipboard.");
336                         this.putValue(SMALL_ICON, Icons.EDIT_COPY);
337                         update();
338                 }
339
340                 @Override
341                 public void actionPerformed(ActionEvent e) {
342                         RocketComponent c = getSelectedComponent();
343                         if (!isCopyable(c))
344                                 return;
345
346                         clipboard = c.copy();
347                         updateClipboardActions();
348                 }
349
350                 @Override
351                 public void update() {
352                         this.setEnabled(isCopyable(getSelectedComponent()));
353                 }
354                 
355         }
356
357
358
359         /**
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. 
363          */
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);
373                         update();
374                         
375                         // Listen to when the clipboard changes
376                         clipboardListeners.add(this);
377                 }
378
379                 @Override
380                 public void actionPerformed(ActionEvent e) {
381                         Pair<RocketComponent, Integer> position = getPastePosition();
382                         if (position == null)
383                                 return;
384
385                         ComponentConfigDialog.hideDialog();
386
387                         RocketComponent pasted = clipboard.copy();
388                         document.addUndoPosition("Paste " + pasted.getComponentName());
389                         position.getU().addChild(pasted, position.getV());
390                         setSelectedComponent(pasted);
391                 }
392
393                 @Override
394                 public void update() {
395                         this.setEnabled(getPastePosition() != null);
396                 }
397         }
398         
399         
400         
401         
402
403         
404         /**
405          * Action to edit the currently selected component.
406          */
407         private class EditAction extends RocketAction {
408                 public EditAction() {
409                         this.putValue(NAME, "Edit");
410                         this.putValue(SHORT_DESCRIPTION, "Edit the selected component.");
411                         update();
412                 }
413
414                 @Override
415                 public void actionPerformed(ActionEvent e) {
416                         RocketComponent c = getSelectedComponent();
417                         if (c == null)
418                                 return;
419                         
420                         ComponentConfigDialog.showDialog(parentFrame, document, c);
421                 }
422
423                 @Override
424                 public void update() {
425                         this.setEnabled(getSelectedComponent() != null);
426                 }
427         }
428
429
430         
431         
432         
433         
434         
435         /**
436          * Action to add a new stage to the rocket.
437          */
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.");
442                         update();
443                 }
444
445                 @Override
446                 public void actionPerformed(ActionEvent e) {
447                         
448                         ComponentConfigDialog.hideDialog();
449
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);
457                         
458                 }
459
460                 @Override
461                 public void update() {
462                         this.setEnabled(true);
463                 }
464         }
465
466
467
468         
469         /**
470          * Action to move the selected component upwards in the parent's child list.
471          */
472         private class MoveUpAction extends RocketAction {
473                 public MoveUpAction() {
474                         this.putValue(NAME, "Move up");
475                         this.putValue(SHORT_DESCRIPTION, "Move this component upwards.");
476                         update();
477                 }
478
479                 @Override
480                 public void actionPerformed(ActionEvent e) {
481                         RocketComponent selected = getSelectedComponent();
482                         if (!canMove(selected))
483                                 return;
484                         
485                         ComponentConfigDialog.hideDialog();
486
487                         RocketComponent parent = selected.getParent();
488                         document.addUndoPosition("Move "+selected.getComponentName());
489                         parent.moveChild(selected, parent.getChildPosition(selected)-1);
490                         setSelectedComponent(selected);
491                 }
492
493                 @Override
494                 public void update() {
495                         this.setEnabled(canMove(getSelectedComponent()));
496                 }
497                 
498                 private boolean canMove(RocketComponent c) {
499                         if (c == null || c.getParent() == null)
500                                 return false;
501                         RocketComponent parent = c.getParent();
502                         if (parent.getChildPosition(c) > 0)
503                                 return true;
504                         return false;
505                 }
506         }
507
508
509
510         /**
511          * Action to move the selected component down in the parent's child list.
512          */
513         private class MoveDownAction extends RocketAction {
514                 public MoveDownAction() {
515                         this.putValue(NAME, "Move down");
516                         this.putValue(SHORT_DESCRIPTION, "Move this component downwards.");
517                         update();
518                 }
519
520                 @Override
521                 public void actionPerformed(ActionEvent e) {
522                         RocketComponent selected = getSelectedComponent();
523                         if (!canMove(selected))
524                                 return;
525                         
526                         ComponentConfigDialog.hideDialog();
527
528                         RocketComponent parent = selected.getParent();
529                         document.addUndoPosition("Move "+selected.getComponentName());
530                         parent.moveChild(selected, parent.getChildPosition(selected)+1);
531                         setSelectedComponent(selected);
532                 }
533
534                 @Override
535                 public void update() {
536                         this.setEnabled(canMove(getSelectedComponent()));
537                 }
538                 
539                 private boolean canMove(RocketComponent c) {
540                         if (c == null || c.getParent() == null)
541                                 return false;
542                         RocketComponent parent = c.getParent();
543                         if (parent.getChildPosition(c) < parent.getChildCount()-1)
544                                 return true;
545                         return false;
546                 }
547         }
548
549
550
551 }