Updates for 0.9.5
[debian/openrocket] / src / net / sf / openrocket / document / OpenRocketDocument.java
1 package net.sf.openrocket.document;
2 //TODO: LOW: move class somewhere else?
3
4 import java.awt.event.ActionEvent;
5 import java.io.File;
6 import java.util.ArrayList;
7 import java.util.LinkedList;
8 import java.util.List;
9
10 import javax.swing.AbstractAction;
11 import javax.swing.Action;
12
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;
22
23
24 public class OpenRocketDocument implements ComponentChangeListener {
25         /**
26          * The minimum number of undo levels that are stored.
27          */
28         public static final int UNDO_LEVELS = 50;
29         /**
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.
32          */
33         public static final int UNDO_MARGIN = 10;
34
35         
36         public static final String SIMULATION_NAME_PREFIX = "Simulation ";
37         
38         private final Rocket rocket;
39         private final Configuration configuration;
40
41         private final ArrayList<Simulation> simulations = new ArrayList<Simulation>();
42         
43         
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>();
47         
48         private String nextDescription = null;
49         private String storedDescription = null;
50         
51         
52         private File file = null;
53         private int savedID = -1;
54         
55         private final StorageOptions storageOptions = new StorageOptions();
56         
57         
58         private final List<DocumentChangeListener> listeners = 
59                 new ArrayList<DocumentChangeListener>();
60         
61         /* These must be initialized after undo history is set up. */
62         private final UndoRedoAction undoAction;
63         private final UndoRedoAction redoAction;
64                 
65         
66         public OpenRocketDocument(Rocket rocket) {
67                 this(rocket.getDefaultConfiguration());
68         }
69         
70
71         private OpenRocketDocument(Configuration configuration) {
72                 this.configuration = configuration;
73                 this.rocket = configuration.getRocket();
74                 
75                 clearUndo();
76                 
77                 undoAction = new UndoRedoAction(UndoRedoAction.UNDO);
78                 redoAction = new UndoRedoAction(UndoRedoAction.REDO);
79                 
80                 rocket.addComponentChangeListener(this);
81         }
82         
83         
84         
85         
86         public Rocket getRocket() {
87                 return rocket;
88         }
89
90         
91         public Configuration getDefaultConfiguration() {
92                 return configuration;
93         }
94
95
96         public File getFile() {
97                 return file;
98         }
99
100         public void setFile(File file) {
101                 this.file = file;
102         }
103         
104
105         public boolean isSaved() {
106                 return rocket.getModID() == savedID;
107         }
108
109         public void setSaved(boolean saved) {
110                 if (saved == false)
111                         this.savedID = -1;
112                 else
113                         this.savedID = rocket.getModID();
114         }
115         
116         /**
117          * Retrieve the default storage options for this document.
118          * 
119          * @return      the storage options.
120          */
121         public StorageOptions getDefaultStorageOptions() {
122                 return storageOptions;
123         }
124         
125         
126         
127         
128         
129         @SuppressWarnings("unchecked")
130         public List<Simulation> getSimulations() {
131                 return (ArrayList<Simulation>)simulations.clone();
132         }
133         public int getSimulationCount() {
134                 return simulations.size();
135         }
136         public Simulation getSimulation(int n) {
137                 return simulations.get(n);
138         }
139         public int getSimulationIndex(Simulation simulation) {
140                 return simulations.indexOf(simulation);
141         }
142         public void addSimulation(Simulation simulation) {
143                 simulations.add(simulation);
144                 fireDocumentChangeEvent(new SimulationChangeEvent(simulation));
145         }
146         public void addSimulation(Simulation simulation, int n) {
147                 simulations.add(n, simulation);
148                 fireDocumentChangeEvent(new SimulationChangeEvent(simulation));
149         }
150         public void removeSimulation(Simulation simulation) {
151                 simulations.remove(simulation);
152                 fireDocumentChangeEvent(new SimulationChangeEvent(simulation));
153         }
154         public Simulation removeSimulation(int n) {
155                 Simulation simulation = simulations.remove(n);
156                 fireDocumentChangeEvent(new SimulationChangeEvent(simulation));
157                 return simulation;
158         }
159         
160         /**
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.
164          * 
165          * @return      the new name.
166          */
167         public String getNextSimulationName() {
168                 // Generate unique name for the simulation
169                 int maxValue = 0;
170                 for (Simulation s: simulations) {
171                         String name = s.getName();
172                         if (name.startsWith(SIMULATION_NAME_PREFIX)) {
173                                 try {
174                                         maxValue = Math.max(maxValue, 
175                                                         Integer.parseInt(name.substring(SIMULATION_NAME_PREFIX.length())));
176                                 } catch (NumberFormatException ignore) { }
177                         }
178                 }
179                 return SIMULATION_NAME_PREFIX + (maxValue+1);
180         }
181         
182         
183         /**
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.
187          * <p>
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.
192          * <p>
193          * If this method is called successively without any change events occurring between the
194          * calls, only the last call will have any effect.
195          * 
196          * @param description A short description of the following actions.
197          */
198         public void addUndoPosition(String description) {
199
200                 // Check whether modifications have been done since last call
201                 if (isCleanState()) {
202                         // No modifications
203                         nextDescription = description;
204                         return;
205                 }
206
207                 
208                 /*
209                  * Modifications have been made to the rocket.  We should be at the end of the
210                  * undo history, but check for consistency.
211                  */
212                 assert(undoPosition == undoHistory.size()-1): "Undo inconsistency, report bug!";
213                 while (undoPosition < undoHistory.size()-1) {
214                         undoHistory.removeLast();
215                         undoDescription.removeLast();
216                 }
217                 
218                 
219                 // Add the current state to the undo history
220                 undoHistory.add(rocket.copy());
221                 undoDescription.add(description);
222                 nextDescription = description;
223                 undoPosition++;
224                 
225                 
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();
231                                 undoPosition--;
232                         }
233                 }
234         }
235
236         
237         /**
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.
243          * 
244          * @param description   Description of the following undoable operations.
245          */
246         public void startUndo(String description) {
247                 storedDescription = nextDescription;
248                 addUndoPosition(description);
249         }
250         
251         /**
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
254          * performed.
255          */
256         public void stopUndo() {
257                 addUndoPosition(storedDescription);
258                 storedDescription = null;
259         }
260         
261         
262         public Action getUndoAction() {
263                 return undoAction;
264         }
265         
266         
267         public Action getRedoAction() {
268                 return redoAction;
269         }
270         
271         
272         /**
273          * Clear the undo history.
274          */
275         public void clearUndo() {
276                 undoHistory.clear();
277                 undoDescription.clear();
278                 
279                 undoHistory.add(rocket.copy());
280                 undoDescription.add(null);
281                 undoPosition = 0;
282                 
283                 if (undoAction != null)
284                         undoAction.setAllValues();
285                 if (redoAction != null)
286                         redoAction.setAllValues();
287         }
288         
289         
290         @Override
291         public void componentChanged(ComponentChangeEvent e) {
292                 
293                 if (!e.isUndoChange()) {
294                         // Remove any redo information if available
295                         while (undoPosition < undoHistory.size()-1) {
296                                 undoHistory.removeLast();
297                                 undoDescription.removeLast();
298                         }
299                         
300                         // Set the latest description
301                         undoDescription.set(undoPosition, nextDescription);
302                 }
303                 
304                 undoAction.setAllValues();
305                 redoAction.setAllValues();
306         }
307
308         
309         public boolean isUndoAvailable() {
310                 if (undoPosition > 0)
311                         return true;
312                 
313                 return !isCleanState();
314         }
315         
316         public String getUndoDescription() {
317                 if (!isUndoAvailable())
318                         return null;
319                 
320                 if (isCleanState()) {
321                         return undoDescription.get(undoPosition-1);
322                 } else {
323                         return undoDescription.get(undoPosition);
324                 }
325         }
326
327         
328         public boolean isRedoAvailable() {
329                 return undoPosition < undoHistory.size()-1;
330         }
331         
332         public String getRedoDescription() {
333                 if (!isRedoAvailable())
334                         return null;
335                 
336                 return undoDescription.get(undoPosition);
337         }
338         
339         
340         
341         public void undo() {
342                 if (!isUndoAvailable()) {
343                         throw new IllegalStateException("Undo not available.");
344                 }
345
346                 // Update history position
347                 
348                 if (isCleanState()) {
349                         // We are in a clean state, simply move backwards in history
350                         undoPosition--;
351                 } else {
352                         // Modifications have been made, save the state and restore previous state
353                         undoHistory.add(rocket.copy());
354                         undoDescription.add(null);
355                 }
356                 
357                 rocket.loadFrom(undoHistory.get(undoPosition).copy());
358         }
359         
360         
361         public void redo() {
362                 if (!isRedoAvailable()) {
363                         throw new IllegalStateException("Redo not available.");
364                 }
365                 
366                 undoPosition++;
367                 
368                 rocket.loadFrom(undoHistory.get(undoPosition).copy());
369         }
370         
371         
372         private boolean isCleanState() {
373                 return rocket.getModID() == undoHistory.get(undoPosition).getModID();
374         }
375         
376         
377         
378         
379         ///////  Listeners
380         
381         public void addDocumentChangeListener(DocumentChangeListener listener) {
382                 listeners.add(listener);
383         }
384         
385         public void removeDocumentChangeListener(DocumentChangeListener listener) {
386                 listeners.remove(listener);
387         }
388         
389         protected void fireDocumentChangeEvent(DocumentChangeEvent event) {
390                 DocumentChangeListener[] array = listeners.toArray(new DocumentChangeListener[0]);
391                 for (DocumentChangeListener l: array) {
392                         l.documentChanged(event);
393                 }
394         }
395         
396         
397         
398         
399         /**
400          * Inner class to implement undo/redo actions.
401          */
402         private class UndoRedoAction extends AbstractAction {
403                 public static final int UNDO = 1;
404                 public static final int REDO = 2;
405                 
406                 private final int type;
407                 
408                 // Sole constructor
409                 public UndoRedoAction(int type) {
410                         if (type != UNDO && type != REDO) {
411                                 throw new IllegalArgumentException("Unknown type = "+type);
412                         }
413                         this.type = type;
414                         setAllValues();
415                 }
416
417                 
418                 // Actual action to make
419                 public void actionPerformed(ActionEvent e) {
420                         switch (type) {
421                         case UNDO:
422                                 undo();
423                                 break;
424                                 
425                         case REDO:
426                                 redo();
427                                 break;
428                         }
429                 }
430
431                 
432                 // Set all the values correctly (name and enabled/disabled status)
433                 public void setAllValues() {
434                         String name,desc;
435                         boolean enabled;
436                         
437                         switch (type) {
438                         case UNDO:
439                                 name = "Undo";
440                                 desc = getUndoDescription();
441                                 enabled = isUndoAvailable();
442                                 this.putValue(SMALL_ICON, Icons.EDIT_UNDO);
443                                 break;
444                                 
445                         case REDO:
446                                 name = "Redo";
447                                 desc = getRedoDescription();
448                                 enabled = isRedoAvailable();
449                                 this.putValue(SMALL_ICON, Icons.EDIT_REDO);
450                                 break;
451                                 
452                         default:
453                                 throw new BugException("illegal type="+type);
454                         }
455                         
456                         if (desc != null)
457                                 name = name + " ("+desc+")";
458                         
459                         putValue(NAME, name);
460                         setEnabled(enabled);
461                 }
462         }
463 }