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. This class also handles undo/redo operations for the rocket structure.
31 * @author Sampo Niskanen <sampo.niskanen@iki.fi>
33 public class OpenRocketDocument implements ComponentChangeListener {
34 private static final LogHelper log = Application.getLogger();
35 private static final Translator trans = Application.getTranslator();
38 * The minimum number of undo levels that are stored.
40 public static final int UNDO_LEVELS = 50;
42 * The margin of the undo levels. After the number of undo levels exceeds
43 * UNDO_LEVELS by this amount the undo is purged to that length.
45 public static final int UNDO_MARGIN = 10;
47 public static final String SIMULATION_NAME_PREFIX = "Simulation ";
49 /** Whether an undo error has already been reported to the user */
50 private static boolean undoErrorReported = false;
52 private final Rocket rocket;
53 private final Configuration configuration;
55 private final ArrayList<Simulation> simulations = new ArrayList<Simulation>();
59 * The undo/redo variables and mechanism are documented in doc/undo-redo-flow.*
63 * The undo history of the rocket. Whenever a new undo position is created while the
64 * rocket is in "dirty" state, the rocket is copied here.
66 private LinkedList<Rocket> undoHistory = new LinkedList<Rocket>();
67 private LinkedList<String> undoDescription = new LinkedList<String>();
70 * The position in the undoHistory we are currently at. If modifications have been
71 * made to the rocket, the rocket is in "dirty" state and this points to the previous
74 private int undoPosition = -1; // Illegal position, init in constructor
77 * The description of the next action that modifies this rocket.
79 private String nextDescription = null;
80 private String storedDescription = null;
83 private File file = null;
84 private int savedID = -1;
86 private final StorageOptions storageOptions = new StorageOptions();
89 private final List<DocumentChangeListener> listeners =
90 new ArrayList<DocumentChangeListener>();
92 /* These must be initialized after undo history is set up. */
93 private final UndoRedoAction undoAction;
94 private final UndoRedoAction redoAction;
97 public OpenRocketDocument(Rocket rocket) {
98 this(rocket.getDefaultConfiguration());
102 private OpenRocketDocument(Configuration configuration) {
103 this.configuration = configuration;
104 this.rocket = configuration.getRocket();
108 undoAction = new UndoRedoAction(UndoRedoAction.UNDO);
109 redoAction = new UndoRedoAction(UndoRedoAction.REDO);
111 rocket.addComponentChangeListener(this);
117 public Rocket getRocket() {
122 public Configuration getDefaultConfiguration() {
123 return configuration;
127 public File getFile() {
131 public void setFile(File file) {
136 public boolean isSaved() {
137 return rocket.getModID() == savedID;
140 public void setSaved(boolean saved) {
144 this.savedID = rocket.getModID();
148 * Retrieve the default storage options for this document.
150 * @return the storage options.
152 public StorageOptions getDefaultStorageOptions() {
153 return storageOptions;
160 public List<Simulation> getSimulations() {
161 return simulations.clone();
164 public int getSimulationCount() {
165 return simulations.size();
168 public Simulation getSimulation(int n) {
169 return simulations.get(n);
172 public int getSimulationIndex(Simulation simulation) {
173 return simulations.indexOf(simulation);
176 public void addSimulation(Simulation simulation) {
177 simulations.add(simulation);
178 fireDocumentChangeEvent(new SimulationChangeEvent(simulation));
181 public void addSimulation(Simulation simulation, int n) {
182 simulations.add(n, simulation);
183 fireDocumentChangeEvent(new SimulationChangeEvent(simulation));
186 public void removeSimulation(Simulation simulation) {
187 simulations.remove(simulation);
188 fireDocumentChangeEvent(new SimulationChangeEvent(simulation));
191 public Simulation removeSimulation(int n) {
192 Simulation simulation = simulations.remove(n);
193 fireDocumentChangeEvent(new SimulationChangeEvent(simulation));
198 * Return a unique name suitable for the next simulation. The name begins
199 * with {@link #SIMULATION_NAME_PREFIX} and has a unique number larger than any
200 * previous simulation.
202 * @return the new name.
204 public String getNextSimulationName() {
205 // Generate unique name for the simulation
207 for (Simulation s : simulations) {
208 String name = s.getName();
209 if (name.startsWith(SIMULATION_NAME_PREFIX)) {
211 maxValue = Math.max(maxValue,
212 Integer.parseInt(name.substring(SIMULATION_NAME_PREFIX.length())));
213 } catch (NumberFormatException ignore) {
217 return SIMULATION_NAME_PREFIX + (maxValue + 1);
222 * Adds an undo point at this position. This method should be called *before* any
223 * action that is to be undoable. All actions after the call will be undone by a
224 * single "Undo" action.
226 * The description should be a short, descriptive string of the actions that will
227 * follow. This is shown to the user e.g. in the Edit-menu, for example
228 * "Undo (Modify body tube)". If the actions are not known (in general should not
229 * be the case!) description may be null.
231 * If this method is called successively without any change events occurring between the
232 * calls, only the last call will have any effect.
234 * @param description A short description of the following actions.
236 public void addUndoPosition(String description) {
238 if (storedDescription != null) {
239 logUndoError("addUndoPosition called while storedDescription=" + storedDescription +
240 " description=" + description);
243 // Check whether modifications have been done since last call
244 if (isCleanState()) {
246 log.info("Adding undo position '" + description + "' to " + this + ", document was in clean state");
247 nextDescription = description;
251 log.info("Adding undo position '" + description + "' to " + this + ", document is in unclean state");
254 * Modifications have been made to the rocket. We should be at the end of the
255 * undo history, but check for consistency and try to recover.
257 if (undoPosition != undoHistory.size() - 1) {
258 logUndoError("undo position inconsistency");
260 while (undoPosition < undoHistory.size() - 1) {
261 undoHistory.removeLast();
262 undoDescription.removeLast();
266 // Add the current state to the undo history
267 undoHistory.add(rocket.copyWithOriginalID());
268 undoDescription.add(null);
269 nextDescription = description;
273 // Maintain maximum undo size
274 if (undoHistory.size() > UNDO_LEVELS + UNDO_MARGIN && undoPosition > UNDO_MARGIN) {
275 for (int i = 0; i < UNDO_MARGIN; i++) {
276 undoHistory.removeFirst();
277 undoDescription.removeFirst();
285 * Start a time-limited undoable operation. After the operation {@link #stopUndo()}
286 * must be called, which will restore the previous undo description into effect.
287 * Only one level of start-stop undo descriptions is supported, i.e. start-stop
288 * undo cannot be nested, and no other undo operations may be called between
289 * the start and stop calls.
291 * @param description Description of the following undoable operations.
293 public void startUndo(String description) {
294 if (storedDescription != null) {
295 logUndoError("startUndo called while storedDescription=" + storedDescription +
296 " description=" + description);
298 log.info("Starting time-limited undoable operation '" + description + "' for " + this);
299 String store = nextDescription;
300 addUndoPosition(description);
301 storedDescription = store;
305 * End the previous time-limited undoable operation. This must be called after
306 * {@link #startUndo(String)} has been called before any other undo operations are
309 public void stopUndo() {
310 log.info("Ending time-limited undoable operation for " + this + " nextDescription=" +
311 nextDescription + " storedDescription=" + storedDescription);
312 String stored = storedDescription;
313 storedDescription = null;
314 addUndoPosition(stored);
318 public Action getUndoAction() {
323 public Action getRedoAction() {
329 * Clear the undo history.
331 public void clearUndo() {
332 log.info("Clearing undo history of " + this);
334 undoDescription.clear();
336 undoHistory.add(rocket.copyWithOriginalID());
337 undoDescription.add(null);
340 if (undoAction != null)
341 undoAction.setAllValues();
342 if (redoAction != null)
343 redoAction.setAllValues();
348 public void componentChanged(ComponentChangeEvent e) {
350 if (!e.isUndoChange()) {
351 if (undoPosition < undoHistory.size() - 1) {
352 log.info("Rocket changed while in undo history, removing redo information for " + this +
353 " undoPosition=" + undoPosition + " undoHistory.size=" + undoHistory.size() +
354 " isClean=" + isCleanState());
356 // Remove any redo information if available
357 while (undoPosition < undoHistory.size() - 1) {
358 undoHistory.removeLast();
359 undoDescription.removeLast();
362 // Set the latest description
363 undoDescription.set(undoPosition, nextDescription);
366 undoAction.setAllValues();
367 redoAction.setAllValues();
372 * Return whether undo action is available.
373 * @return <code>true</code> if undo can be performed
375 public boolean isUndoAvailable() {
376 if (undoPosition > 0)
379 return !isCleanState();
383 * Return the description of what action would be undone if undo is called.
384 * @return the description what would be undone, or <code>null</code> if description unavailable.
386 public String getUndoDescription() {
387 if (!isUndoAvailable())
390 if (isCleanState()) {
391 return undoDescription.get(undoPosition - 1);
393 return undoDescription.get(undoPosition);
399 * Return whether redo action is available.
400 * @return <code>true</code> if redo can be performed
402 public boolean isRedoAvailable() {
403 return undoPosition < undoHistory.size() - 1;
407 * Return the description of what action would be redone if redo is called.
408 * @return the description of what would be redone, or <code>null</code> if description unavailable.
410 public String getRedoDescription() {
411 if (!isRedoAvailable())
414 return undoDescription.get(undoPosition);
419 * Perform undo operation on the rocket.
422 log.info("Performing undo for " + this + " undoPosition=" + undoPosition +
423 " undoHistory.size=" + undoHistory.size() + " isClean=" + isCleanState());
424 if (!isUndoAvailable()) {
425 logUndoError("Undo not available");
426 undoAction.setAllValues();
427 redoAction.setAllValues();
430 if (storedDescription != null) {
431 logUndoError("undo() called with storedDescription=" + storedDescription);
434 // Update history position
436 if (isCleanState()) {
437 // We are in a clean state, simply move backwards in history
440 if (undoPosition != undoHistory.size() - 1) {
441 logUndoError("undo position inconsistency");
443 // Modifications have been made, save the state and restore previous state
444 undoHistory.add(rocket.copyWithOriginalID());
445 undoDescription.add(null);
448 rocket.checkComponentStructure();
449 undoHistory.get(undoPosition).checkComponentStructure();
450 undoHistory.get(undoPosition).copyWithOriginalID().checkComponentStructure();
451 rocket.loadFrom(undoHistory.get(undoPosition).copyWithOriginalID());
452 rocket.checkComponentStructure();
457 * Perform redo operation on the rocket.
460 log.info("Performing redo for " + this + " undoPosition=" + undoPosition +
461 " undoHistory.size=" + undoHistory.size() + " isClean=" + isCleanState());
462 if (!isRedoAvailable()) {
463 logUndoError("Redo not available");
464 undoAction.setAllValues();
465 redoAction.setAllValues();
468 if (storedDescription != null) {
469 logUndoError("redo() called with storedDescription=" + storedDescription);
474 rocket.loadFrom(undoHistory.get(undoPosition).copyWithOriginalID());
478 private boolean isCleanState() {
479 return rocket.getModID() == undoHistory.get(undoPosition).getModID();
484 * Log a non-fatal undo/redo error or inconsistency. Reports it to the user the first
485 * time it occurs, but not on subsequent times. Logs automatically the undo system state.
487 private void logUndoError(String error) {
488 log.error(1, error + ": this=" + this + " undoPosition=" + undoPosition +
489 " undoHistory.size=" + undoHistory.size() + " isClean=" + isCleanState() +
490 " nextDescription=" + nextDescription + " storedDescription=" + storedDescription,
491 new TraceException());
493 if (!undoErrorReported) {
494 undoErrorReported = true;
495 ExceptionHandler.handleErrorCondition("Undo/Redo error: " + error);
501 public void addDocumentChangeListener(DocumentChangeListener listener) {
502 listeners.add(listener);
505 public void removeDocumentChangeListener(DocumentChangeListener listener) {
506 listeners.remove(listener);
509 protected void fireDocumentChangeEvent(DocumentChangeEvent event) {
510 DocumentChangeListener[] array = listeners.toArray(new DocumentChangeListener[0]);
511 for (DocumentChangeListener l : array) {
512 l.documentChanged(event);
520 * Inner class to implement undo/redo actions.
522 private class UndoRedoAction extends AbstractAction {
523 public static final int UNDO = 1;
524 public static final int REDO = 2;
526 private final int type;
529 public UndoRedoAction(int type) {
530 if (type != UNDO && type != REDO) {
531 throw new IllegalArgumentException("Unknown type = " + type);
538 // Actual action to make
540 public void actionPerformed(ActionEvent e) {
543 log.user("Performing undo, event=" + e);
548 log.user("Performing redo, event=" + e);
555 // Set all the values correctly (name and enabled/disabled status)
556 public void setAllValues() {
558 boolean actionEnabled;
563 name = trans.get("OpenRocketDocument.Undo");
564 desc = getUndoDescription();
565 actionEnabled = isUndoAvailable();
566 this.putValue(SMALL_ICON, Icons.EDIT_UNDO);
571 name = trans.get("OpenRocketDocument.Redo");
572 desc = getRedoDescription();
573 actionEnabled = isRedoAvailable();
574 this.putValue(SMALL_ICON, Icons.EDIT_REDO);
578 throw new BugException("illegal type=" + type);
582 name = name + " (" + desc + ")";
584 putValue(NAME, name);
585 setEnabled(actionEnabled);