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.logging.LogHelper;
16 import net.sf.openrocket.logging.TraceException;
17 import net.sf.openrocket.rocketcomponent.ComponentChangeEvent;
18 import net.sf.openrocket.rocketcomponent.ComponentChangeListener;
19 import net.sf.openrocket.rocketcomponent.Configuration;
20 import net.sf.openrocket.rocketcomponent.Rocket;
21 import net.sf.openrocket.startup.Application;
22 import net.sf.openrocket.util.ArrayList;
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 public List<Simulation> getSimulations() {
159 return simulations.clone();
162 public int getSimulationCount() {
163 return simulations.size();
166 public Simulation getSimulation(int n) {
167 return simulations.get(n);
170 public int getSimulationIndex(Simulation simulation) {
171 return simulations.indexOf(simulation);
174 public void addSimulation(Simulation simulation) {
175 simulations.add(simulation);
176 fireDocumentChangeEvent(new SimulationChangeEvent(simulation));
179 public void addSimulation(Simulation simulation, int n) {
180 simulations.add(n, simulation);
181 fireDocumentChangeEvent(new SimulationChangeEvent(simulation));
184 public void removeSimulation(Simulation simulation) {
185 simulations.remove(simulation);
186 fireDocumentChangeEvent(new SimulationChangeEvent(simulation));
189 public Simulation removeSimulation(int n) {
190 Simulation simulation = simulations.remove(n);
191 fireDocumentChangeEvent(new SimulationChangeEvent(simulation));
196 * Return a unique name suitable for the next simulation. The name begins
197 * with {@link #SIMULATION_NAME_PREFIX} and has a unique number larger than any
198 * previous simulation.
200 * @return the new name.
202 public String getNextSimulationName() {
203 // Generate unique name for the simulation
205 for (Simulation s : simulations) {
206 String name = s.getName();
207 if (name.startsWith(SIMULATION_NAME_PREFIX)) {
209 maxValue = Math.max(maxValue,
210 Integer.parseInt(name.substring(SIMULATION_NAME_PREFIX.length())));
211 } catch (NumberFormatException ignore) {
215 return SIMULATION_NAME_PREFIX + (maxValue + 1);
220 * Adds an undo point at this position. This method should be called *before* any
221 * action that is to be undoable. All actions after the call will be undone by a
222 * single "Undo" action.
224 * The description should be a short, descriptive string of the actions that will
225 * follow. This is shown to the user e.g. in the Edit-menu, for example
226 * "Undo (Modify body tube)". If the actions are not known (in general should not
227 * be the case!) description may be null.
229 * If this method is called successively without any change events occurring between the
230 * calls, only the last call will have any effect.
232 * @param description A short description of the following actions.
234 public void addUndoPosition(String description) {
236 if (storedDescription != null) {
237 logUndoError("addUndoPosition called while storedDescription=" + storedDescription +
238 " description=" + description);
241 // Check whether modifications have been done since last call
242 if (isCleanState()) {
244 log.info("Adding undo position '" + description + "' to " + this + ", document was in clean state");
245 nextDescription = description;
249 log.info("Adding undo position '" + description + "' to " + this + ", document is in unclean state");
252 * Modifications have been made to the rocket. We should be at the end of the
253 * undo history, but check for consistency and try to recover.
255 if (undoPosition != undoHistory.size() - 1) {
256 logUndoError("undo position inconsistency");
258 while (undoPosition < undoHistory.size() - 1) {
259 undoHistory.removeLast();
260 undoDescription.removeLast();
264 // Add the current state to the undo history
265 undoHistory.add(rocket.copyWithOriginalID());
266 undoDescription.add(null);
267 nextDescription = description;
271 // Maintain maximum undo size
272 if (undoHistory.size() > UNDO_LEVELS + UNDO_MARGIN && undoPosition > UNDO_MARGIN) {
273 for (int i = 0; i < UNDO_MARGIN; i++) {
274 undoHistory.removeFirst();
275 undoDescription.removeFirst();
283 * Start a time-limited undoable operation. After the operation {@link #stopUndo()}
284 * must be called, which will restore the previous undo description into effect.
285 * Only one level of start-stop undo descriptions is supported, i.e. start-stop
286 * undo cannot be nested, and no other undo operations may be called between
287 * the start and stop calls.
289 * @param description Description of the following undoable operations.
291 public void startUndo(String description) {
292 if (storedDescription != null) {
293 logUndoError("startUndo called while storedDescription=" + storedDescription +
294 " description=" + description);
296 log.info("Starting time-limited undoable operation '" + description + "' for " + this);
297 String store = nextDescription;
298 addUndoPosition(description);
299 storedDescription = store;
303 * End the previous time-limited undoable operation. This must be called after
304 * {@link #startUndo(String)} has been called before any other undo operations are
307 public void stopUndo() {
308 log.info("Ending time-limited undoable operation for " + this + " nextDescription=" +
309 nextDescription + " storedDescription=" + storedDescription);
310 String stored = storedDescription;
311 storedDescription = null;
312 addUndoPosition(stored);
316 public Action getUndoAction() {
321 public Action getRedoAction() {
327 * Clear the undo history.
329 public void clearUndo() {
330 log.info("Clearing undo history of " + this);
332 undoDescription.clear();
334 undoHistory.add(rocket.copyWithOriginalID());
335 undoDescription.add(null);
338 if (undoAction != null)
339 undoAction.setAllValues();
340 if (redoAction != null)
341 redoAction.setAllValues();
346 public void componentChanged(ComponentChangeEvent e) {
348 if (!e.isUndoChange()) {
349 if (undoPosition < undoHistory.size() - 1) {
350 log.info("Rocket changed while in undo history, removing redo information for " + this +
351 " undoPosition=" + undoPosition + " undoHistory.size=" + undoHistory.size() +
352 " isClean=" + isCleanState());
354 // Remove any redo information if available
355 while (undoPosition < undoHistory.size() - 1) {
356 undoHistory.removeLast();
357 undoDescription.removeLast();
360 // Set the latest description
361 undoDescription.set(undoPosition, nextDescription);
364 undoAction.setAllValues();
365 redoAction.setAllValues();
370 * Return whether undo action is available.
371 * @return <code>true</code> if undo can be performed
373 public boolean isUndoAvailable() {
374 if (undoPosition > 0)
377 return !isCleanState();
381 * Return the description of what action would be undone if undo is called.
382 * @return the description what would be undone, or <code>null</code> if description unavailable.
384 public String getUndoDescription() {
385 if (!isUndoAvailable())
388 if (isCleanState()) {
389 return undoDescription.get(undoPosition - 1);
391 return undoDescription.get(undoPosition);
397 * Return whether redo action is available.
398 * @return <code>true</code> if redo can be performed
400 public boolean isRedoAvailable() {
401 return undoPosition < undoHistory.size() - 1;
405 * Return the description of what action would be redone if redo is called.
406 * @return the description of what would be redone, or <code>null</code> if description unavailable.
408 public String getRedoDescription() {
409 if (!isRedoAvailable())
412 return undoDescription.get(undoPosition);
417 * Perform undo operation on the rocket.
420 log.info("Performing undo for " + this + " undoPosition=" + undoPosition +
421 " undoHistory.size=" + undoHistory.size() + " isClean=" + isCleanState());
422 if (!isUndoAvailable()) {
423 logUndoError("Undo not available");
424 undoAction.setAllValues();
425 redoAction.setAllValues();
428 if (storedDescription != null) {
429 logUndoError("undo() called with storedDescription=" + storedDescription);
432 // Update history position
434 if (isCleanState()) {
435 // We are in a clean state, simply move backwards in history
438 if (undoPosition != undoHistory.size() - 1) {
439 logUndoError("undo position inconsistency");
441 // Modifications have been made, save the state and restore previous state
442 undoHistory.add(rocket.copyWithOriginalID());
443 undoDescription.add(null);
446 rocket.checkComponentStructure();
447 undoHistory.get(undoPosition).checkComponentStructure();
448 undoHistory.get(undoPosition).copyWithOriginalID().checkComponentStructure();
449 rocket.loadFrom(undoHistory.get(undoPosition).copyWithOriginalID());
450 rocket.checkComponentStructure();
455 * Perform redo operation on the rocket.
458 log.info("Performing redo for " + this + " undoPosition=" + undoPosition +
459 " undoHistory.size=" + undoHistory.size() + " isClean=" + isCleanState());
460 if (!isRedoAvailable()) {
461 logUndoError("Redo not available");
462 undoAction.setAllValues();
463 redoAction.setAllValues();
466 if (storedDescription != null) {
467 logUndoError("redo() called with storedDescription=" + storedDescription);
472 rocket.loadFrom(undoHistory.get(undoPosition).copyWithOriginalID());
476 private boolean isCleanState() {
477 return rocket.getModID() == undoHistory.get(undoPosition).getModID();
482 * Log a non-fatal undo/redo error or inconsistency. Reports it to the user the first
483 * time it occurs, but not on subsequent times. Logs automatically the undo system state.
485 private void logUndoError(String error) {
486 log.error(1, error + ": this=" + this + " undoPosition=" + undoPosition +
487 " undoHistory.size=" + undoHistory.size() + " isClean=" + isCleanState() +
488 " nextDescription=" + nextDescription + " storedDescription=" + storedDescription,
489 new TraceException());
491 if (!undoErrorReported) {
492 undoErrorReported = true;
493 ExceptionHandler.handleErrorCondition("Undo/Redo error: " + error);
499 public void addDocumentChangeListener(DocumentChangeListener listener) {
500 listeners.add(listener);
503 public void removeDocumentChangeListener(DocumentChangeListener listener) {
504 listeners.remove(listener);
507 protected void fireDocumentChangeEvent(DocumentChangeEvent event) {
508 DocumentChangeListener[] array = listeners.toArray(new DocumentChangeListener[0]);
509 for (DocumentChangeListener l : array) {
510 l.documentChanged(event);
518 * Inner class to implement undo/redo actions.
520 private class UndoRedoAction extends AbstractAction {
521 public static final int UNDO = 1;
522 public static final int REDO = 2;
524 private final int type;
527 public UndoRedoAction(int type) {
528 if (type != UNDO && type != REDO) {
529 throw new IllegalArgumentException("Unknown type = " + type);
536 // Actual action to make
538 public void actionPerformed(ActionEvent e) {
541 log.user("Performing undo, event=" + e);
546 log.user("Performing redo, event=" + e);
553 // Set all the values correctly (name and enabled/disabled status)
554 public void setAllValues() {
556 boolean actionEnabled;
561 desc = getUndoDescription();
562 actionEnabled = isUndoAvailable();
563 this.putValue(SMALL_ICON, Icons.EDIT_UNDO);
568 desc = getRedoDescription();
569 actionEnabled = isRedoAvailable();
570 this.putValue(SMALL_ICON, Icons.EDIT_REDO);
574 throw new BugException("illegal type=" + type);
578 name = name + " (" + desc + ")";
580 putValue(NAME, name);
581 setEnabled(actionEnabled);