1 package net.sf.openrocket.document;
2 //TODO: LOW: move class somewhere else?
4 import java.awt.event.ActionEvent;
6 import java.util.ArrayList;
7 import java.util.LinkedList;
10 import javax.swing.AbstractAction;
11 import javax.swing.Action;
13 import net.sf.openrocket.document.events.DocumentChangeEvent;
14 import net.sf.openrocket.document.events.DocumentChangeListener;
15 import net.sf.openrocket.document.events.SimulationChangeEvent;
16 import net.sf.openrocket.rocketcomponent.ComponentChangeEvent;
17 import net.sf.openrocket.rocketcomponent.ComponentChangeListener;
18 import net.sf.openrocket.rocketcomponent.Configuration;
19 import net.sf.openrocket.rocketcomponent.Rocket;
20 import net.sf.openrocket.util.BugException;
21 import net.sf.openrocket.util.Icons;
24 public class OpenRocketDocument implements ComponentChangeListener {
26 * The minimum number of undo levels that are stored.
28 public static final int UNDO_LEVELS = 50;
30 * The margin of the undo levels. After the number of undo levels exceeds
31 * UNDO_LEVELS by this amount the undo is purged to that length.
33 public static final int UNDO_MARGIN = 10;
36 public static final String SIMULATION_NAME_PREFIX = "Simulation ";
38 private final Rocket rocket;
39 private final Configuration configuration;
41 private final ArrayList<Simulation> simulations = new ArrayList<Simulation>();
44 private int undoPosition = -1; // Illegal position, init in constructor
45 private LinkedList<Rocket> undoHistory = new LinkedList<Rocket>();
46 private LinkedList<String> undoDescription = new LinkedList<String>();
48 private String nextDescription = null;
49 private String storedDescription = null;
52 private File file = null;
53 private int savedID = -1;
55 private final StorageOptions storageOptions = new StorageOptions();
58 private final List<DocumentChangeListener> listeners =
59 new ArrayList<DocumentChangeListener>();
61 /* These must be initialized after undo history is set up. */
62 private final UndoRedoAction undoAction;
63 private final UndoRedoAction redoAction;
66 public OpenRocketDocument(Rocket rocket) {
67 this(rocket.getDefaultConfiguration());
71 private OpenRocketDocument(Configuration configuration) {
72 this.configuration = configuration;
73 this.rocket = configuration.getRocket();
77 undoAction = new UndoRedoAction(UndoRedoAction.UNDO);
78 redoAction = new UndoRedoAction(UndoRedoAction.REDO);
80 rocket.addComponentChangeListener(this);
86 public Rocket getRocket() {
91 public Configuration getDefaultConfiguration() {
96 public File getFile() {
100 public void setFile(File file) {
105 public boolean isSaved() {
106 return rocket.getModID() == savedID;
109 public void setSaved(boolean saved) {
113 this.savedID = rocket.getModID();
117 * Retrieve the default storage options for this document.
119 * @return the storage options.
121 public StorageOptions getDefaultStorageOptions() {
122 return storageOptions;
129 @SuppressWarnings("unchecked")
130 public List<Simulation> getSimulations() {
131 return (ArrayList<Simulation>)simulations.clone();
133 public int getSimulationCount() {
134 return simulations.size();
136 public Simulation getSimulation(int n) {
137 return simulations.get(n);
139 public int getSimulationIndex(Simulation simulation) {
140 return simulations.indexOf(simulation);
142 public void addSimulation(Simulation simulation) {
143 simulations.add(simulation);
144 fireDocumentChangeEvent(new SimulationChangeEvent(simulation));
146 public void addSimulation(Simulation simulation, int n) {
147 simulations.add(n, simulation);
148 fireDocumentChangeEvent(new SimulationChangeEvent(simulation));
150 public void removeSimulation(Simulation simulation) {
151 simulations.remove(simulation);
152 fireDocumentChangeEvent(new SimulationChangeEvent(simulation));
154 public Simulation removeSimulation(int n) {
155 Simulation simulation = simulations.remove(n);
156 fireDocumentChangeEvent(new SimulationChangeEvent(simulation));
161 * Return a unique name suitable for the next simulation. The name begins
162 * with {@link #SIMULATION_NAME_PREFIX} and has a unique number larger than any
163 * previous simulation.
165 * @return the new name.
167 public String getNextSimulationName() {
168 // Generate unique name for the simulation
170 for (Simulation s: simulations) {
171 String name = s.getName();
172 if (name.startsWith(SIMULATION_NAME_PREFIX)) {
174 maxValue = Math.max(maxValue,
175 Integer.parseInt(name.substring(SIMULATION_NAME_PREFIX.length())));
176 } catch (NumberFormatException ignore) { }
179 return SIMULATION_NAME_PREFIX + (maxValue+1);
184 * Adds an undo point at this position. This method should be called *before* any
185 * action that is to be undoable. All actions after the call will be undone by a
186 * single "Undo" action.
188 * The description should be a short, descriptive string of the actions that will
189 * follow. This is shown to the user e.g. in the Edit-menu, for example
190 * "Undo (Modify body tube)". If the actions are not known (in general should not
191 * be the case!) description may be null.
193 * If this method is called successively without any change events occurring between the
194 * calls, only the last call will have any effect.
196 * @param description A short description of the following actions.
198 public void addUndoPosition(String description) {
200 // Check whether modifications have been done since last call
201 if (isCleanState()) {
203 nextDescription = description;
209 * Modifications have been made to the rocket. We should be at the end of the
210 * undo history, but check for consistency.
212 assert(undoPosition == undoHistory.size()-1): "Undo inconsistency, report bug!";
213 while (undoPosition < undoHistory.size()-1) {
214 undoHistory.removeLast();
215 undoDescription.removeLast();
219 // Add the current state to the undo history
220 undoHistory.add(rocket.copy());
221 undoDescription.add(description);
222 nextDescription = description;
226 // Maintain maximum undo size
227 if (undoHistory.size() > UNDO_LEVELS + UNDO_MARGIN) {
228 for (int i=0; i < UNDO_MARGIN+1; i++) {
229 undoHistory.removeFirst();
230 undoDescription.removeFirst();
238 * Start a time-limited undoable operation. After the operation {@link #stopUndo()}
239 * must be called, which will restore the previous undo description into effect.
240 * Only one level of start-stop undo descriptions is supported, i.e. start-stop
241 * undo cannot be nested, and no other undo operations may be called between
242 * the start and stop calls.
244 * @param description Description of the following undoable operations.
246 public void startUndo(String description) {
247 storedDescription = nextDescription;
248 addUndoPosition(description);
252 * End the previous time-limited undoable operation. This must be called after
253 * {@link #startUndo(String)} has been called before any other undo operations are
256 public void stopUndo() {
257 addUndoPosition(storedDescription);
258 storedDescription = null;
262 public Action getUndoAction() {
267 public Action getRedoAction() {
273 * Clear the undo history.
275 public void clearUndo() {
277 undoDescription.clear();
279 undoHistory.add(rocket.copy());
280 undoDescription.add(null);
283 if (undoAction != null)
284 undoAction.setAllValues();
285 if (redoAction != null)
286 redoAction.setAllValues();
291 public void componentChanged(ComponentChangeEvent e) {
293 if (!e.isUndoChange()) {
294 // Remove any redo information if available
295 while (undoPosition < undoHistory.size()-1) {
296 undoHistory.removeLast();
297 undoDescription.removeLast();
300 // Set the latest description
301 undoDescription.set(undoPosition, nextDescription);
304 undoAction.setAllValues();
305 redoAction.setAllValues();
309 public boolean isUndoAvailable() {
310 if (undoPosition > 0)
313 return !isCleanState();
316 public String getUndoDescription() {
317 if (!isUndoAvailable())
320 if (isCleanState()) {
321 return undoDescription.get(undoPosition-1);
323 return undoDescription.get(undoPosition);
328 public boolean isRedoAvailable() {
329 return undoPosition < undoHistory.size()-1;
332 public String getRedoDescription() {
333 if (!isRedoAvailable())
336 return undoDescription.get(undoPosition);
342 if (!isUndoAvailable()) {
343 throw new IllegalStateException("Undo not available.");
346 // Update history position
348 if (isCleanState()) {
349 // We are in a clean state, simply move backwards in history
352 // Modifications have been made, save the state and restore previous state
353 undoHistory.add(rocket.copy());
354 undoDescription.add(null);
357 rocket.loadFrom(undoHistory.get(undoPosition).copy());
362 if (!isRedoAvailable()) {
363 throw new IllegalStateException("Redo not available.");
368 rocket.loadFrom(undoHistory.get(undoPosition).copy());
372 private boolean isCleanState() {
373 return rocket.getModID() == undoHistory.get(undoPosition).getModID();
381 public void addDocumentChangeListener(DocumentChangeListener listener) {
382 listeners.add(listener);
385 public void removeDocumentChangeListener(DocumentChangeListener listener) {
386 listeners.remove(listener);
389 protected void fireDocumentChangeEvent(DocumentChangeEvent event) {
390 DocumentChangeListener[] array = listeners.toArray(new DocumentChangeListener[0]);
391 for (DocumentChangeListener l: array) {
392 l.documentChanged(event);
400 * Inner class to implement undo/redo actions.
402 private class UndoRedoAction extends AbstractAction {
403 public static final int UNDO = 1;
404 public static final int REDO = 2;
406 private final int type;
409 public UndoRedoAction(int type) {
410 if (type != UNDO && type != REDO) {
411 throw new IllegalArgumentException("Unknown type = "+type);
418 // Actual action to make
419 public void actionPerformed(ActionEvent e) {
432 // Set all the values correctly (name and enabled/disabled status)
433 public void setAllValues() {
440 desc = getUndoDescription();
441 enabled = isUndoAvailable();
442 this.putValue(SMALL_ICON, Icons.EDIT_UNDO);
447 desc = getRedoDescription();
448 enabled = isRedoAvailable();
449 this.putValue(SMALL_ICON, Icons.EDIT_REDO);
453 throw new BugException("illegal type="+type);
457 name = name + " ("+desc+")";
459 putValue(NAME, name);