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.Icons;
23 public class OpenRocketDocument implements ComponentChangeListener {
25 * The minimum number of undo levels that are stored.
27 public static final int UNDO_LEVELS = 50;
29 * The margin of the undo levels. After the number of undo levels exceeds
30 * UNDO_LEVELS by this amount the undo is purged to that length.
32 public static final int UNDO_MARGIN = 10;
35 public static final String SIMULATION_NAME_PREFIX = "Simulation ";
37 private final Rocket rocket;
38 private final Configuration configuration;
40 private final ArrayList<Simulation> simulations = new ArrayList<Simulation>();
43 private int undoPosition = -1; // Illegal position, init in constructor
44 private LinkedList<Rocket> undoHistory = new LinkedList<Rocket>();
45 private LinkedList<String> undoDescription = new LinkedList<String>();
47 private String nextDescription = null;
48 private String storedDescription = null;
51 private File file = null;
52 private int savedID = -1;
54 private final StorageOptions storageOptions = new StorageOptions();
57 private final List<DocumentChangeListener> listeners =
58 new ArrayList<DocumentChangeListener>();
60 /* These must be initialized after undo history is set up. */
61 private final UndoRedoAction undoAction;
62 private final UndoRedoAction redoAction;
65 public OpenRocketDocument(Rocket rocket) {
66 this(rocket.getDefaultConfiguration());
70 private OpenRocketDocument(Configuration configuration) {
71 this.configuration = configuration;
72 this.rocket = configuration.getRocket();
76 undoAction = new UndoRedoAction(UndoRedoAction.UNDO);
77 redoAction = new UndoRedoAction(UndoRedoAction.REDO);
79 rocket.addComponentChangeListener(this);
85 public Rocket getRocket() {
90 public Configuration getDefaultConfiguration() {
95 public File getFile() {
99 public void setFile(File file) {
104 public boolean isSaved() {
105 return rocket.getModID() == savedID;
108 public void setSaved(boolean saved) {
112 this.savedID = rocket.getModID();
116 * Retrieve the default storage options for this document.
118 * @return the storage options.
120 public StorageOptions getDefaultStorageOptions() {
121 return storageOptions;
128 @SuppressWarnings("unchecked")
129 public List<Simulation> getSimulations() {
130 return (ArrayList<Simulation>)simulations.clone();
132 public int getSimulationCount() {
133 return simulations.size();
135 public Simulation getSimulation(int n) {
136 return simulations.get(n);
138 public int getSimulationIndex(Simulation simulation) {
139 return simulations.indexOf(simulation);
141 public void addSimulation(Simulation simulation) {
142 simulations.add(simulation);
143 fireDocumentChangeEvent(new SimulationChangeEvent(simulation));
145 public void addSimulation(Simulation simulation, int n) {
146 simulations.add(n, simulation);
147 fireDocumentChangeEvent(new SimulationChangeEvent(simulation));
149 public void removeSimulation(Simulation simulation) {
150 simulations.remove(simulation);
151 fireDocumentChangeEvent(new SimulationChangeEvent(simulation));
153 public Simulation removeSimulation(int n) {
154 Simulation simulation = simulations.remove(n);
155 fireDocumentChangeEvent(new SimulationChangeEvent(simulation));
160 * Return a unique name suitable for the next simulation. The name begins
161 * with {@link #SIMULATION_NAME_PREFIX} and has a unique number larger than any
162 * previous simulation.
164 * @return the new name.
166 public String getNextSimulationName() {
167 // Generate unique name for the simulation
169 for (Simulation s: simulations) {
170 String name = s.getName();
171 if (name.startsWith(SIMULATION_NAME_PREFIX)) {
173 maxValue = Math.max(maxValue,
174 Integer.parseInt(name.substring(SIMULATION_NAME_PREFIX.length())));
175 } catch (NumberFormatException ignore) { }
178 return SIMULATION_NAME_PREFIX + (maxValue+1);
183 * Adds an undo point at this position. This method should be called *before* any
184 * action that is to be undoable. All actions after the call will be undone by a
185 * single "Undo" action.
187 * The description should be a short, descriptive string of the actions that will
188 * follow. This is shown to the user e.g. in the Edit-menu, for example
189 * "Undo (Modify body tube)". If the actions are not known (in general should not
190 * be the case!) description may be null.
192 * If this method is called successively without any change events occurring between the
193 * calls, only the last call will have any effect.
195 * @param description A short description of the following actions.
197 public void addUndoPosition(String description) {
199 // Check whether modifications have been done since last call
200 if (isCleanState()) {
202 nextDescription = description;
208 * Modifications have been made to the rocket. We should be at the end of the
209 * undo history, but check for consistency.
211 assert(undoPosition == undoHistory.size()-1): "Undo inconsistency, report bug!";
212 while (undoPosition < undoHistory.size()-1) {
213 undoHistory.removeLast();
214 undoDescription.removeLast();
218 // Add the current state to the undo history
219 undoHistory.add(rocket.copy());
220 undoDescription.add(description);
221 nextDescription = description;
225 // Maintain maximum undo size
226 if (undoHistory.size() > UNDO_LEVELS + UNDO_MARGIN) {
227 for (int i=0; i < UNDO_MARGIN+1; i++) {
228 undoHistory.removeFirst();
229 undoDescription.removeFirst();
237 * Start a time-limited undoable operation. After the operation {@link #stopUndo()}
238 * must be called, which will restore the previous undo description into effect.
239 * Only one level of start-stop undo descriptions is supported, i.e. start-stop
240 * undo cannot be nested, and no other undo operations may be called between
241 * the start and stop calls.
243 * @param description Description of the following undoable operations.
245 public void startUndo(String description) {
246 storedDescription = nextDescription;
247 addUndoPosition(description);
251 * End the previous time-limited undoable operation. This must be called after
252 * {@link #startUndo(String)} has been called before any other undo operations are
255 public void stopUndo() {
256 addUndoPosition(storedDescription);
257 storedDescription = null;
261 public Action getUndoAction() {
266 public Action getRedoAction() {
272 * Clear the undo history.
274 public void clearUndo() {
276 undoDescription.clear();
278 undoHistory.add(rocket.copy());
279 undoDescription.add(null);
282 if (undoAction != null)
283 undoAction.setAllValues();
284 if (redoAction != null)
285 redoAction.setAllValues();
290 public void componentChanged(ComponentChangeEvent e) {
292 if (!e.isUndoChange()) {
293 // Remove any redo information if available
294 while (undoPosition < undoHistory.size()-1) {
295 undoHistory.removeLast();
296 undoDescription.removeLast();
299 // Set the latest description
300 undoDescription.set(undoPosition, nextDescription);
303 undoAction.setAllValues();
304 redoAction.setAllValues();
308 public boolean isUndoAvailable() {
309 if (undoPosition > 0)
312 return !isCleanState();
315 public String getUndoDescription() {
316 if (!isUndoAvailable())
319 if (isCleanState()) {
320 return undoDescription.get(undoPosition-1);
322 return undoDescription.get(undoPosition);
327 public boolean isRedoAvailable() {
328 return undoPosition < undoHistory.size()-1;
331 public String getRedoDescription() {
332 if (!isRedoAvailable())
335 return undoDescription.get(undoPosition);
341 if (!isUndoAvailable()) {
342 throw new IllegalStateException("Undo not available.");
345 // Update history position
347 if (isCleanState()) {
348 // We are in a clean state, simply move backwards in history
351 // Modifications have been made, save the state and restore previous state
352 undoHistory.add(rocket.copy());
353 undoDescription.add(null);
356 rocket.loadFrom(undoHistory.get(undoPosition).copy());
361 if (!isRedoAvailable()) {
362 throw new IllegalStateException("Redo not available.");
367 rocket.loadFrom(undoHistory.get(undoPosition).copy());
371 private boolean isCleanState() {
372 return rocket.getModID() == undoHistory.get(undoPosition).getModID();
380 public void addDocumentChangeListener(DocumentChangeListener listener) {
381 listeners.add(listener);
384 public void removeDocumentChangeListener(DocumentChangeListener listener) {
385 listeners.remove(listener);
388 protected void fireDocumentChangeEvent(DocumentChangeEvent event) {
389 DocumentChangeListener[] array = listeners.toArray(new DocumentChangeListener[0]);
390 for (DocumentChangeListener l: array) {
391 l.documentChanged(event);
399 * Inner class to implement undo/redo actions.
401 private class UndoRedoAction extends AbstractAction {
402 public static final int UNDO = 1;
403 public static final int REDO = 2;
405 private final int type;
408 public UndoRedoAction(int type) {
409 if (type != UNDO && type != REDO) {
410 throw new IllegalArgumentException("Unknown type = "+type);
417 // Actual action to make
418 public void actionPerformed(ActionEvent e) {
431 // Set all the values correctly (name and enabled/disabled status)
432 public void setAllValues() {
439 desc = getUndoDescription();
440 enabled = isUndoAvailable();
441 this.putValue(SMALL_ICON, Icons.EDIT_UNDO);
446 desc = getRedoDescription();
447 enabled = isRedoAvailable();
448 this.putValue(SMALL_ICON, Icons.EDIT_REDO);
452 throw new RuntimeException("EEEK!");
456 name = name + " ("+desc+")";
458 putValue(NAME, name);