1 package net.sf.openrocket.document;
3 import java.awt.event.ActionEvent;
5 import java.util.LinkedList;
8 import javax.swing.AbstractAction;
9 import javax.swing.Action;
11 import net.sf.openrocket.document.events.DocumentChangeEvent;
12 import net.sf.openrocket.document.events.DocumentChangeListener;
13 import net.sf.openrocket.document.events.SimulationChangeEvent;
14 import net.sf.openrocket.gui.main.ExceptionHandler;
15 import net.sf.openrocket.l10n.Translator;
16 import net.sf.openrocket.logging.LogHelper;
17 import net.sf.openrocket.logging.TraceException;
18 import net.sf.openrocket.rocketcomponent.ComponentChangeEvent;
19 import net.sf.openrocket.rocketcomponent.ComponentChangeListener;
20 import net.sf.openrocket.rocketcomponent.Configuration;
21 import net.sf.openrocket.rocketcomponent.Rocket;
22 import net.sf.openrocket.startup.Application;
23 import net.sf.openrocket.util.ArrayList;
24 import net.sf.openrocket.util.BugException;
25 import net.sf.openrocket.util.Icons;
28 * Class describing an entire OpenRocket document, including a rocket and
29 * simulations. The document contains:
31 * - the rocket definition
32 * - a default Configuration
33 * - Simulation instances
34 * - the stored file and file save information
35 * - undo/redo information
37 * @author Sampo Niskanen <sampo.niskanen@iki.fi>
39 public class OpenRocketDocument implements ComponentChangeListener {
40 private static final LogHelper log = Application.getLogger();
41 private static final Translator trans = Application.getTranslator();
44 * The minimum number of undo levels that are stored.
46 public static final int UNDO_LEVELS = 50;
48 * The margin of the undo levels. After the number of undo levels exceeds
49 * UNDO_LEVELS by this amount the undo is purged to that length.
51 public static final int UNDO_MARGIN = 10;
53 public static final String SIMULATION_NAME_PREFIX = "Simulation ";
55 /** Whether an undo error has already been reported to the user */
56 private static boolean undoErrorReported = false;
60 private final Rocket rocket;
61 private final Configuration configuration;
63 private final ArrayList<Simulation> simulations = new ArrayList<Simulation>();
67 * The undo/redo variables and mechanism are documented in doc/undo-redo-flow.*
71 * The undo history of the rocket. Whenever a new undo position is created while the
72 * rocket is in "dirty" state, the rocket is copied here.
74 private LinkedList<Rocket> undoHistory = new LinkedList<Rocket>();
75 private LinkedList<String> undoDescription = new LinkedList<String>();
78 * The position in the undoHistory we are currently at. If modifications have been
79 * made to the rocket, the rocket is in "dirty" state and this points to the previous
82 private int undoPosition = -1; // Illegal position, init in constructor
85 * The description of the next action that modifies this rocket.
87 private String nextDescription = null;
88 private String storedDescription = null;
91 private File file = null;
92 private int savedID = -1;
94 private final StorageOptions storageOptions = new StorageOptions();
97 private final List<DocumentChangeListener> listeners =
98 new ArrayList<DocumentChangeListener>();
100 /* These must be initialized after undo history is set up. */
101 private final UndoRedoAction undoAction;
102 private final UndoRedoAction redoAction;
105 public OpenRocketDocument(Rocket rocket) {
106 this(rocket.getDefaultConfiguration());
110 private OpenRocketDocument(Configuration configuration) {
111 this.configuration = configuration;
112 this.rocket = configuration.getRocket();
116 undoAction = new UndoRedoAction(UndoRedoAction.UNDO);
117 redoAction = new UndoRedoAction(UndoRedoAction.REDO);
119 rocket.addComponentChangeListener(this);
125 public Rocket getRocket() {
130 public Configuration getDefaultConfiguration() {
131 return configuration;
135 public File getFile() {
139 public void setFile(File file) {
144 public boolean isSaved() {
145 return rocket.getModID() == savedID;
148 public void setSaved(boolean saved) {
152 this.savedID = rocket.getModID();
156 * Retrieve the default storage options for this document.
158 * @return the storage options.
160 public StorageOptions getDefaultStorageOptions() {
161 return storageOptions;
168 public List<Simulation> getSimulations() {
169 return simulations.clone();
172 public int getSimulationCount() {
173 return simulations.size();
176 public Simulation getSimulation(int n) {
177 return simulations.get(n);
180 public int getSimulationIndex(Simulation simulation) {
181 return simulations.indexOf(simulation);
184 public void addSimulation(Simulation simulation) {
185 simulations.add(simulation);
186 fireDocumentChangeEvent(new SimulationChangeEvent(simulation));
189 public void addSimulation(Simulation simulation, int n) {
190 simulations.add(n, simulation);
191 fireDocumentChangeEvent(new SimulationChangeEvent(simulation));
194 public void removeSimulation(Simulation simulation) {
195 simulations.remove(simulation);
196 fireDocumentChangeEvent(new SimulationChangeEvent(simulation));
199 public Simulation removeSimulation(int n) {
200 Simulation simulation = simulations.remove(n);
201 fireDocumentChangeEvent(new SimulationChangeEvent(simulation));
206 * Return a unique name suitable for the next simulation. The name begins
207 * with {@link #SIMULATION_NAME_PREFIX} and has a unique number larger than any
208 * previous simulation.
210 * @return the new name.
212 public String getNextSimulationName() {
213 // Generate unique name for the simulation
215 for (Simulation s : simulations) {
216 String name = s.getName();
217 if (name.startsWith(SIMULATION_NAME_PREFIX)) {
219 maxValue = Math.max(maxValue,
220 Integer.parseInt(name.substring(SIMULATION_NAME_PREFIX.length())));
221 } catch (NumberFormatException ignore) {
225 return SIMULATION_NAME_PREFIX + (maxValue + 1);
230 * Adds an undo point at this position. This method should be called *before* any
231 * action that is to be undoable. All actions after the call will be undone by a
232 * single "Undo" action.
234 * The description should be a short, descriptive string of the actions that will
235 * follow. This is shown to the user e.g. in the Edit-menu, for example
236 * "Undo (Modify body tube)". If the actions are not known (in general should not
237 * be the case!) description may be null.
239 * If this method is called successively without any change events occurring between the
240 * calls, only the last call will have any effect.
242 * @param description A short description of the following actions.
244 public void addUndoPosition(String description) {
246 if (storedDescription != null) {
247 logUndoError("addUndoPosition called while storedDescription=" + storedDescription +
248 " description=" + description);
251 // Check whether modifications have been done since last call
252 if (isCleanState()) {
254 log.info("Adding undo position '" + description + "' to " + this + ", document was in clean state");
255 nextDescription = description;
259 log.info("Adding undo position '" + description + "' to " + this + ", document is in unclean state");
262 * Modifications have been made to the rocket. We should be at the end of the
263 * undo history, but check for consistency and try to recover.
265 if (undoPosition != undoHistory.size() - 1) {
266 logUndoError("undo position inconsistency");
268 while (undoPosition < undoHistory.size() - 1) {
269 undoHistory.removeLast();
270 undoDescription.removeLast();
274 // Add the current state to the undo history
275 undoHistory.add(rocket.copyWithOriginalID());
276 undoDescription.add(null);
277 nextDescription = description;
281 // Maintain maximum undo size
282 if (undoHistory.size() > UNDO_LEVELS + UNDO_MARGIN && undoPosition > UNDO_MARGIN) {
283 for (int i = 0; i < UNDO_MARGIN; i++) {
284 undoHistory.removeFirst();
285 undoDescription.removeFirst();
293 * Start a time-limited undoable operation. After the operation {@link #stopUndo()}
294 * must be called, which will restore the previous undo description into effect.
295 * Only one level of start-stop undo descriptions is supported, i.e. start-stop
296 * undo cannot be nested, and no other undo operations may be called between
297 * the start and stop calls.
299 * @param description Description of the following undoable operations.
301 public void startUndo(String description) {
302 if (storedDescription != null) {
303 logUndoError("startUndo called while storedDescription=" + storedDescription +
304 " description=" + description);
306 log.info("Starting time-limited undoable operation '" + description + "' for " + this);
307 String store = nextDescription;
308 addUndoPosition(description);
309 storedDescription = store;
313 * End the previous time-limited undoable operation. This must be called after
314 * {@link #startUndo(String)} has been called before any other undo operations are
317 public void stopUndo() {
318 log.info("Ending time-limited undoable operation for " + this + " nextDescription=" +
319 nextDescription + " storedDescription=" + storedDescription);
320 String stored = storedDescription;
321 storedDescription = null;
322 addUndoPosition(stored);
326 public Action getUndoAction() {
331 public Action getRedoAction() {
337 * Clear the undo history.
339 public void clearUndo() {
340 log.info("Clearing undo history of " + this);
342 undoDescription.clear();
344 undoHistory.add(rocket.copyWithOriginalID());
345 undoDescription.add(null);
348 if (undoAction != null)
349 undoAction.setAllValues();
350 if (redoAction != null)
351 redoAction.setAllValues();
356 public void componentChanged(ComponentChangeEvent e) {
358 if (!e.isUndoChange()) {
359 if (undoPosition < undoHistory.size() - 1) {
360 log.info("Rocket changed while in undo history, removing redo information for " + this +
361 " undoPosition=" + undoPosition + " undoHistory.size=" + undoHistory.size() +
362 " isClean=" + isCleanState());
364 // Remove any redo information if available
365 while (undoPosition < undoHistory.size() - 1) {
366 undoHistory.removeLast();
367 undoDescription.removeLast();
370 // Set the latest description
371 undoDescription.set(undoPosition, nextDescription);
374 undoAction.setAllValues();
375 redoAction.setAllValues();
380 * Return whether undo action is available.
381 * @return <code>true</code> if undo can be performed
383 public boolean isUndoAvailable() {
384 if (undoPosition > 0)
387 return !isCleanState();
391 * Return the description of what action would be undone if undo is called.
392 * @return the description what would be undone, or <code>null</code> if description unavailable.
394 public String getUndoDescription() {
395 if (!isUndoAvailable())
398 if (isCleanState()) {
399 return undoDescription.get(undoPosition - 1);
401 return undoDescription.get(undoPosition);
407 * Return whether redo action is available.
408 * @return <code>true</code> if redo can be performed
410 public boolean isRedoAvailable() {
411 return undoPosition < undoHistory.size() - 1;
415 * Return the description of what action would be redone if redo is called.
416 * @return the description of what would be redone, or <code>null</code> if description unavailable.
418 public String getRedoDescription() {
419 if (!isRedoAvailable())
422 return undoDescription.get(undoPosition);
427 * Perform undo operation on the rocket.
430 log.info("Performing undo for " + this + " undoPosition=" + undoPosition +
431 " undoHistory.size=" + undoHistory.size() + " isClean=" + isCleanState());
432 if (!isUndoAvailable()) {
433 logUndoError("Undo not available");
434 undoAction.setAllValues();
435 redoAction.setAllValues();
438 if (storedDescription != null) {
439 logUndoError("undo() called with storedDescription=" + storedDescription);
442 // Update history position
444 if (isCleanState()) {
445 // We are in a clean state, simply move backwards in history
448 if (undoPosition != undoHistory.size() - 1) {
449 logUndoError("undo position inconsistency");
451 // Modifications have been made, save the state and restore previous state
452 undoHistory.add(rocket.copyWithOriginalID());
453 undoDescription.add(null);
456 rocket.checkComponentStructure();
457 undoHistory.get(undoPosition).checkComponentStructure();
458 undoHistory.get(undoPosition).copyWithOriginalID().checkComponentStructure();
459 rocket.loadFrom(undoHistory.get(undoPosition).copyWithOriginalID());
460 rocket.checkComponentStructure();
465 * Perform redo operation on the rocket.
468 log.info("Performing redo for " + this + " undoPosition=" + undoPosition +
469 " undoHistory.size=" + undoHistory.size() + " isClean=" + isCleanState());
470 if (!isRedoAvailable()) {
471 logUndoError("Redo not available");
472 undoAction.setAllValues();
473 redoAction.setAllValues();
476 if (storedDescription != null) {
477 logUndoError("redo() called with storedDescription=" + storedDescription);
482 rocket.loadFrom(undoHistory.get(undoPosition).copyWithOriginalID());
486 private boolean isCleanState() {
487 return rocket.getModID() == undoHistory.get(undoPosition).getModID();
492 * Log a non-fatal undo/redo error or inconsistency. Reports it to the user the first
493 * time it occurs, but not on subsequent times. Logs automatically the undo system state.
495 private void logUndoError(String error) {
496 log.error(1, error + ": this=" + this + " undoPosition=" + undoPosition +
497 " undoHistory.size=" + undoHistory.size() + " isClean=" + isCleanState() +
498 " nextDescription=" + nextDescription + " storedDescription=" + storedDescription,
499 new TraceException());
501 if (!undoErrorReported) {
502 undoErrorReported = true;
503 ExceptionHandler.handleErrorCondition("Undo/Redo error: " + error);
510 * Return a copy of this document. The rocket is copied with original ID's, the default
511 * motor configuration ID is maintained and the simulations are copied to the new rocket.
512 * No undo/redo information or file storage information is maintained.
514 * @return a copy of this document.
516 public OpenRocketDocument copy() {
517 Rocket rocketCopy = rocket.copyWithOriginalID();
518 OpenRocketDocument documentCopy = new OpenRocketDocument(rocketCopy);
519 documentCopy.getDefaultConfiguration().setMotorConfigurationID(configuration.getMotorConfigurationID());
520 for (Simulation s : simulations) {
521 documentCopy.addSimulation(s.duplicateSimulation(rocketCopy));
530 public void addDocumentChangeListener(DocumentChangeListener listener) {
531 listeners.add(listener);
534 public void removeDocumentChangeListener(DocumentChangeListener listener) {
535 listeners.remove(listener);
538 protected void fireDocumentChangeEvent(DocumentChangeEvent event) {
539 DocumentChangeListener[] array = listeners.toArray(new DocumentChangeListener[0]);
540 for (DocumentChangeListener l : array) {
541 l.documentChanged(event);
549 * Inner class to implement undo/redo actions.
551 private class UndoRedoAction extends AbstractAction {
552 public static final int UNDO = 1;
553 public static final int REDO = 2;
555 private final int type;
558 public UndoRedoAction(int type) {
559 if (type != UNDO && type != REDO) {
560 throw new IllegalArgumentException("Unknown type = " + type);
567 // Actual action to make
569 public void actionPerformed(ActionEvent e) {
572 log.user("Performing undo, event=" + e);
577 log.user("Performing redo, event=" + e);
584 // Set all the values correctly (name and enabled/disabled status)
585 public void setAllValues() {
587 boolean actionEnabled;
592 name = trans.get("OpenRocketDocument.Undo");
593 desc = getUndoDescription();
594 actionEnabled = isUndoAvailable();
595 this.putValue(SMALL_ICON, Icons.EDIT_UNDO);
600 name = trans.get("OpenRocketDocument.Redo");
601 desc = getRedoDescription();
602 actionEnabled = isRedoAvailable();
603 this.putValue(SMALL_ICON, Icons.EDIT_REDO);
607 throw new BugException("illegal type=" + type);
611 name = name + " (" + desc + ")";
613 putValue(NAME, name);
614 setEnabled(actionEnabled);