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