1 package net.sf.openrocket.document;
3 import java.awt.event.ActionEvent;
5 import java.util.ArrayList;
6 import java.util.LinkedList;
9 import javax.swing.AbstractAction;
10 import javax.swing.Action;
12 import net.sf.openrocket.document.events.DocumentChangeEvent;
13 import net.sf.openrocket.document.events.DocumentChangeListener;
14 import net.sf.openrocket.document.events.SimulationChangeEvent;
15 import net.sf.openrocket.gui.main.ExceptionHandler;
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.BugException;
24 import net.sf.openrocket.util.Icons;
27 * Class describing an entire OpenRocket document, including a rocket and
28 * simulations. This class also handles undo/redo operations for the rocket structure.
30 * @author Sampo Niskanen <sampo.niskanen@iki.fi>
32 public class OpenRocketDocument implements ComponentChangeListener {
33 private static final LogHelper log = Application.getLogger();
36 * The minimum number of undo levels that are stored.
38 public static final int UNDO_LEVELS = 50;
40 * The margin of the undo levels. After the number of undo levels exceeds
41 * UNDO_LEVELS by this amount the undo is purged to that length.
43 public static final int UNDO_MARGIN = 10;
45 public static final String SIMULATION_NAME_PREFIX = "Simulation ";
47 /** Whether an undo error has already been reported to the user */
48 private static boolean undoErrorReported = false;
50 private final Rocket rocket;
51 private final Configuration configuration;
53 private final ArrayList<Simulation> simulations = new ArrayList<Simulation>();
57 * The undo/redo variables and mechanism are documented in doc/undo-redo-flow.*
61 * The undo history of the rocket. Whenever a new undo position is created while the
62 * rocket is in "dirty" state, the rocket is copied here.
64 private LinkedList<Rocket> undoHistory = new LinkedList<Rocket>();
65 private LinkedList<String> undoDescription = new LinkedList<String>();
68 * The position in the undoHistory we are currently at. If modifications have been
69 * made to the rocket, the rocket is in "dirty" state and this points to the previous
72 private int undoPosition = -1; // Illegal position, init in constructor
75 * The description of the next action that modifies this rocket.
77 private String nextDescription = null;
78 private String storedDescription = null;
81 private File file = null;
82 private int savedID = -1;
84 private final StorageOptions storageOptions = new StorageOptions();
87 private final List<DocumentChangeListener> listeners =
88 new ArrayList<DocumentChangeListener>();
90 /* These must be initialized after undo history is set up. */
91 private final UndoRedoAction undoAction;
92 private final UndoRedoAction redoAction;
95 public OpenRocketDocument(Rocket rocket) {
96 this(rocket.getDefaultConfiguration());
100 private OpenRocketDocument(Configuration configuration) {
101 this.configuration = configuration;
102 this.rocket = configuration.getRocket();
106 undoAction = new UndoRedoAction(UndoRedoAction.UNDO);
107 redoAction = new UndoRedoAction(UndoRedoAction.REDO);
109 rocket.addComponentChangeListener(this);
115 public Rocket getRocket() {
120 public Configuration getDefaultConfiguration() {
121 return configuration;
125 public File getFile() {
129 public void setFile(File file) {
134 public boolean isSaved() {
135 return rocket.getModID() == savedID;
138 public void setSaved(boolean saved) {
142 this.savedID = rocket.getModID();
146 * Retrieve the default storage options for this document.
148 * @return the storage options.
150 public StorageOptions getDefaultStorageOptions() {
151 return storageOptions;
158 @SuppressWarnings("unchecked")
159 public List<Simulation> getSimulations() {
160 return (ArrayList<Simulation>) simulations.clone();
163 public int getSimulationCount() {
164 return simulations.size();
167 public Simulation getSimulation(int n) {
168 return simulations.get(n);
171 public int getSimulationIndex(Simulation simulation) {
172 return simulations.indexOf(simulation);
175 public void addSimulation(Simulation simulation) {
176 simulations.add(simulation);
177 fireDocumentChangeEvent(new SimulationChangeEvent(simulation));
180 public void addSimulation(Simulation simulation, int n) {
181 simulations.add(n, simulation);
182 fireDocumentChangeEvent(new SimulationChangeEvent(simulation));
185 public void removeSimulation(Simulation simulation) {
186 simulations.remove(simulation);
187 fireDocumentChangeEvent(new SimulationChangeEvent(simulation));
190 public Simulation removeSimulation(int n) {
191 Simulation simulation = simulations.remove(n);
192 fireDocumentChangeEvent(new SimulationChangeEvent(simulation));
197 * Return a unique name suitable for the next simulation. The name begins
198 * with {@link #SIMULATION_NAME_PREFIX} and has a unique number larger than any
199 * previous simulation.
201 * @return the new name.
203 public String getNextSimulationName() {
204 // Generate unique name for the simulation
206 for (Simulation s : simulations) {
207 String name = s.getName();
208 if (name.startsWith(SIMULATION_NAME_PREFIX)) {
210 maxValue = Math.max(maxValue,
211 Integer.parseInt(name.substring(SIMULATION_NAME_PREFIX.length())));
212 } catch (NumberFormatException ignore) {
216 return SIMULATION_NAME_PREFIX + (maxValue + 1);
221 * Adds an undo point at this position. This method should be called *before* any
222 * action that is to be undoable. All actions after the call will be undone by a
223 * single "Undo" action.
225 * The description should be a short, descriptive string of the actions that will
226 * follow. This is shown to the user e.g. in the Edit-menu, for example
227 * "Undo (Modify body tube)". If the actions are not known (in general should not
228 * be the case!) description may be null.
230 * If this method is called successively without any change events occurring between the
231 * calls, only the last call will have any effect.
233 * @param description A short description of the following actions.
235 public void addUndoPosition(String description) {
237 if (storedDescription != null) {
238 logUndoError("addUndoPosition called while storedDescription=" + storedDescription +
239 " description=" + description);
242 // Check whether modifications have been done since last call
243 if (isCleanState()) {
245 log.info("Adding undo position '" + description + "' to " + this + ", document was in clean state");
246 nextDescription = description;
250 log.info("Adding undo position '" + description + "' to " + this + ", document is in unclean state");
253 * Modifications have been made to the rocket. We should be at the end of the
254 * undo history, but check for consistency and try to recover.
256 if (undoPosition != undoHistory.size() - 1) {
257 logUndoError("undo position inconsistency");
259 while (undoPosition < undoHistory.size() - 1) {
260 undoHistory.removeLast();
261 undoDescription.removeLast();
265 // Add the current state to the undo history
266 undoHistory.add(rocket.copyWithOriginalID());
267 undoDescription.add(null);
268 nextDescription = description;
272 // Maintain maximum undo size
273 if (undoHistory.size() > UNDO_LEVELS + UNDO_MARGIN && undoPosition > UNDO_MARGIN) {
274 for (int i = 0; i < UNDO_MARGIN; i++) {
275 undoHistory.removeFirst();
276 undoDescription.removeFirst();
284 * Start a time-limited undoable operation. After the operation {@link #stopUndo()}
285 * must be called, which will restore the previous undo description into effect.
286 * Only one level of start-stop undo descriptions is supported, i.e. start-stop
287 * undo cannot be nested, and no other undo operations may be called between
288 * the start and stop calls.
290 * @param description Description of the following undoable operations.
292 public void startUndo(String description) {
293 if (storedDescription != null) {
294 logUndoError("startUndo called while storedDescription=" + storedDescription +
295 " description=" + description);
297 log.info("Starting time-limited undoable operation '" + description + "' for " + this);
298 String store = nextDescription;
299 addUndoPosition(description);
300 storedDescription = store;
304 * End the previous time-limited undoable operation. This must be called after
305 * {@link #startUndo(String)} has been called before any other undo operations are
308 public void stopUndo() {
309 log.info("Ending time-limited undoable operation for " + this + " nextDescription=" +
310 nextDescription + " storedDescription=" + storedDescription);
311 String stored = storedDescription;
312 storedDescription = null;
313 addUndoPosition(stored);
317 public Action getUndoAction() {
322 public Action getRedoAction() {
328 * Clear the undo history.
330 public void clearUndo() {
331 log.info("Clearing undo history of " + this);
333 undoDescription.clear();
335 undoHistory.add(rocket.copyWithOriginalID());
336 undoDescription.add(null);
339 if (undoAction != null)
340 undoAction.setAllValues();
341 if (redoAction != null)
342 redoAction.setAllValues();
347 public void componentChanged(ComponentChangeEvent e) {
349 if (!e.isUndoChange()) {
350 if (undoPosition < undoHistory.size() - 1) {
351 log.info("Rocket changed while in undo history, removing redo information for " + this +
352 " undoPosition=" + undoPosition + " undoHistory.size=" + undoHistory.size() +
353 " isClean=" + isCleanState());
355 // Remove any redo information if available
356 while (undoPosition < undoHistory.size() - 1) {
357 undoHistory.removeLast();
358 undoDescription.removeLast();
361 // Set the latest description
362 undoDescription.set(undoPosition, nextDescription);
365 undoAction.setAllValues();
366 redoAction.setAllValues();
371 * Return whether undo action is available.
372 * @return <code>true</code> if undo can be performed
374 public boolean isUndoAvailable() {
375 if (undoPosition > 0)
378 return !isCleanState();
382 * Return the description of what action would be undone if undo is called.
383 * @return the description what would be undone, or <code>null</code> if description unavailable.
385 public String getUndoDescription() {
386 if (!isUndoAvailable())
389 if (isCleanState()) {
390 return undoDescription.get(undoPosition - 1);
392 return undoDescription.get(undoPosition);
398 * Return whether redo action is available.
399 * @return <code>true</code> if redo can be performed
401 public boolean isRedoAvailable() {
402 return undoPosition < undoHistory.size() - 1;
406 * Return the description of what action would be redone if redo is called.
407 * @return the description of what would be redone, or <code>null</code> if description unavailable.
409 public String getRedoDescription() {
410 if (!isRedoAvailable())
413 return undoDescription.get(undoPosition);
418 * Perform undo operation on the rocket.
421 log.info("Performing undo for " + this + " undoPosition=" + undoPosition +
422 " undoHistory.size=" + undoHistory.size() + " isClean=" + isCleanState());
423 if (!isUndoAvailable()) {
424 logUndoError("Undo not available");
425 undoAction.setAllValues();
426 redoAction.setAllValues();
429 if (storedDescription != null) {
430 logUndoError("undo() called with storedDescription=" + storedDescription);
433 // Update history position
435 if (isCleanState()) {
436 // We are in a clean state, simply move backwards in history
439 if (undoPosition != undoHistory.size() - 1) {
440 logUndoError("undo position inconsistency");
442 // Modifications have been made, save the state and restore previous state
443 undoHistory.add(rocket.copyWithOriginalID());
444 undoDescription.add(null);
447 rocket.loadFrom(undoHistory.get(undoPosition).copyWithOriginalID());
452 * Perform redo operation on the rocket.
455 log.info("Performing redo for " + this + " undoPosition=" + undoPosition +
456 " undoHistory.size=" + undoHistory.size() + " isClean=" + isCleanState());
457 if (!isRedoAvailable()) {
458 logUndoError("Redo not available");
459 undoAction.setAllValues();
460 redoAction.setAllValues();
463 if (storedDescription != null) {
464 logUndoError("redo() called with storedDescription=" + storedDescription);
469 rocket.loadFrom(undoHistory.get(undoPosition).copyWithOriginalID());
473 private boolean isCleanState() {
474 return rocket.getModID() == undoHistory.get(undoPosition).getModID();
479 * Log a non-fatal undo/redo error or inconsistency. Reports it to the user the first
480 * time it occurs, but not on subsequent times. Logs automatically the undo system state.
482 private void logUndoError(String error) {
483 log.error(1, error + ": this=" + this + " undoPosition=" + undoPosition +
484 " undoHistory.size=" + undoHistory.size() + " isClean=" + isCleanState() +
485 " nextDescription=" + nextDescription + " storedDescription=" + storedDescription,
486 new TraceException());
488 if (!undoErrorReported) {
489 undoErrorReported = true;
490 ExceptionHandler.handleErrorCondition("Undo/Redo error: " + error);
496 public void addDocumentChangeListener(DocumentChangeListener listener) {
497 listeners.add(listener);
500 public void removeDocumentChangeListener(DocumentChangeListener listener) {
501 listeners.remove(listener);
504 protected void fireDocumentChangeEvent(DocumentChangeEvent event) {
505 DocumentChangeListener[] array = listeners.toArray(new DocumentChangeListener[0]);
506 for (DocumentChangeListener l : array) {
507 l.documentChanged(event);
515 * Inner class to implement undo/redo actions.
517 private class UndoRedoAction extends AbstractAction {
518 public static final int UNDO = 1;
519 public static final int REDO = 2;
521 private final int type;
524 public UndoRedoAction(int type) {
525 if (type != UNDO && type != REDO) {
526 throw new IllegalArgumentException("Unknown type = " + type);
533 // Actual action to make
534 public void actionPerformed(ActionEvent e) {
537 log.user("Performing undo, event=" + e);
542 log.user("Performing redo, event=" + e);
549 // Set all the values correctly (name and enabled/disabled status)
550 public void setAllValues() {
552 boolean actionEnabled;
557 desc = getUndoDescription();
558 actionEnabled = isUndoAvailable();
559 this.putValue(SMALL_ICON, Icons.EDIT_UNDO);
564 desc = getRedoDescription();
565 actionEnabled = isRedoAvailable();
566 this.putValue(SMALL_ICON, Icons.EDIT_REDO);
570 throw new BugException("illegal type=" + type);
574 name = name + " (" + desc + ")";
576 putValue(NAME, name);
577 setEnabled(actionEnabled);