updates
[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.Icons;
21
22
23 public class OpenRocketDocument implements ComponentChangeListener {
24         /**
25          * The minimum number of undo levels that are stored.
26          */
27         public static final int UNDO_LEVELS = 50;
28         /**
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.
31          */
32         public static final int UNDO_MARGIN = 10;
33
34         
35         public static final String SIMULATION_NAME_PREFIX = "Simulation ";
36         
37         private final Rocket rocket;
38         private final Configuration configuration;
39
40         private final ArrayList<Simulation> simulations = new ArrayList<Simulation>();
41         
42         
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>();
46         
47         private String nextDescription = null;
48         
49         
50         private File file = null;
51         private int savedID = -1;
52         
53         private final StorageOptions storageOptions = new StorageOptions();
54         
55         
56         private final List<DocumentChangeListener> listeners = 
57                 new ArrayList<DocumentChangeListener>();
58         
59         /* These must be initialized after undo history is set up. */
60         private final UndoRedoAction undoAction;
61         private final UndoRedoAction redoAction;
62                 
63         
64         public OpenRocketDocument(Rocket rocket) {
65                 this(rocket.getDefaultConfiguration());
66         }
67         
68
69         private OpenRocketDocument(Configuration configuration) {
70                 this.configuration = configuration;
71                 this.rocket = configuration.getRocket();
72                 
73                 undoHistory.add(rocket.copy());
74                 undoDescription.add(null);
75                 undoPosition = 0;
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         public Action getUndoAction() {
238                 return undoAction;
239         }
240         
241         
242         public Action getRedoAction() {
243                 return redoAction;
244         }
245         
246         
247         @Override
248         public void componentChanged(ComponentChangeEvent e) {
249                 
250                 if (!e.isUndoChange()) {
251                         // Remove any redo information if available
252                         while (undoPosition < undoHistory.size()-1) {
253                                 undoHistory.removeLast();
254                                 undoDescription.removeLast();
255                         }
256                         
257                         // Set the latest description
258                         undoDescription.set(undoPosition, nextDescription);
259                 }
260                 
261                 undoAction.setAllValues();
262                 redoAction.setAllValues();
263         }
264
265         
266         public boolean isUndoAvailable() {
267                 if (undoPosition > 0)
268                         return true;
269                 
270                 return !isCleanState();
271         }
272         
273         public String getUndoDescription() {
274                 if (!isUndoAvailable())
275                         return null;
276                 
277                 if (isCleanState()) {
278                         return undoDescription.get(undoPosition-1);
279                 } else {
280                         return undoDescription.get(undoPosition);
281                 }
282         }
283
284         
285         public boolean isRedoAvailable() {
286                 return undoPosition < undoHistory.size()-1;
287         }
288         
289         public String getRedoDescription() {
290                 if (!isRedoAvailable())
291                         return null;
292                 
293                 return undoDescription.get(undoPosition);
294         }
295         
296         
297         
298         public void undo() {
299                 if (!isUndoAvailable()) {
300                         throw new IllegalStateException("Undo not available.");
301                 }
302
303                 // Update history position
304                 
305                 if (isCleanState()) {
306                         // We are in a clean state, simply move backwards in history
307                         undoPosition--;
308                 } else {
309                         // Modifications have been made, save the state and restore previous state
310                         undoHistory.add(rocket.copy());
311                         undoDescription.add(null);
312                 }
313                 
314                 rocket.loadFrom(undoHistory.get(undoPosition).copy());
315         }
316         
317         
318         public void redo() {
319                 if (!isRedoAvailable()) {
320                         throw new IllegalStateException("Redo not available.");
321                 }
322                 
323                 undoPosition++;
324                 
325                 rocket.loadFrom(undoHistory.get(undoPosition).copy());
326         }
327         
328         
329         private boolean isCleanState() {
330                 return rocket.getModID() == undoHistory.get(undoPosition).getModID();
331         }
332         
333         
334         
335         
336         ///////  Listeners
337         
338         public void addDocumentChangeListener(DocumentChangeListener listener) {
339                 listeners.add(listener);
340         }
341         
342         public void removeDocumentChangeListener(DocumentChangeListener listener) {
343                 listeners.remove(listener);
344         }
345         
346         protected void fireDocumentChangeEvent(DocumentChangeEvent event) {
347                 DocumentChangeListener[] array = listeners.toArray(new DocumentChangeListener[0]);
348                 for (DocumentChangeListener l: array) {
349                         l.documentChanged(event);
350                 }
351         }
352         
353         
354         
355         
356         /**
357          * Inner class to implement undo/redo actions.
358          */
359         private class UndoRedoAction extends AbstractAction {
360                 public static final int UNDO = 1;
361                 public static final int REDO = 2;
362                 
363                 private final int type;
364                 
365                 // Sole constructor
366                 public UndoRedoAction(int type) {
367                         if (type != UNDO && type != REDO) {
368                                 throw new IllegalArgumentException("Unknown type = "+type);
369                         }
370                         this.type = type;
371                         setAllValues();
372                 }
373
374                 
375                 // Actual action to make
376                 public void actionPerformed(ActionEvent e) {
377                         switch (type) {
378                         case UNDO:
379                                 undo();
380                                 break;
381                                 
382                         case REDO:
383                                 redo();
384                                 break;
385                         }
386                 }
387
388                 
389                 // Set all the values correctly (name and enabled/disabled status)
390                 public void setAllValues() {
391                         String name,desc;
392                         boolean enabled;
393                         
394                         switch (type) {
395                         case UNDO:
396                                 name = "Undo";
397                                 desc = getUndoDescription();
398                                 enabled = isUndoAvailable();
399                                 this.putValue(SMALL_ICON, Icons.EDIT_UNDO);
400                                 break;
401                                 
402                         case REDO:
403                                 name = "Redo";
404                                 desc = getRedoDescription();
405                                 enabled = isRedoAvailable();
406                                 this.putValue(SMALL_ICON, Icons.EDIT_REDO);
407                                 break;
408                                 
409                         default:
410                                 throw new RuntimeException("EEEK!");
411                         }
412                         
413                         if (desc != null)
414                                 name = name + " ("+desc+")";
415                         
416                         putValue(NAME, name);
417                         setEnabled(enabled);
418                 }
419         }
420 }