25b149e45374eb9ca60f1aa7e4d8a516cc552eeb
[debian/openrocket] / src / net / sf / openrocket / document / OpenRocketDocument.java
1 package net.sf.openrocket.document;
2
3 import java.awt.event.ActionEvent;
4 import java.io.File;
5 import java.util.LinkedList;
6 import java.util.List;
7
8 import javax.swing.AbstractAction;
9 import javax.swing.Action;
10
11 import net.sf.openrocket.document.events.DocumentChangeEvent;
12 import net.sf.openrocket.document.events.DocumentChangeListener;
13 import net.sf.openrocket.document.events.SimulationChangeEvent;
14 import net.sf.openrocket.gui.main.ExceptionHandler;
15 import net.sf.openrocket.logging.LogHelper;
16 import net.sf.openrocket.logging.TraceException;
17 import net.sf.openrocket.rocketcomponent.ComponentChangeEvent;
18 import net.sf.openrocket.rocketcomponent.ComponentChangeListener;
19 import net.sf.openrocket.rocketcomponent.Configuration;
20 import net.sf.openrocket.rocketcomponent.Rocket;
21 import net.sf.openrocket.startup.Application;
22 import net.sf.openrocket.util.ArrayList;
23 import net.sf.openrocket.util.BugException;
24 import net.sf.openrocket.util.Icons;
25
26 /**
27  * Class describing an entire OpenRocket document, including a rocket and
28  * simulations.  This class also handles undo/redo operations for the rocket structure.
29  * 
30  * @author Sampo Niskanen <sampo.niskanen@iki.fi>
31  */
32 public class OpenRocketDocument implements ComponentChangeListener {
33         private static final LogHelper log = Application.getLogger();
34         
35         /**
36          * The minimum number of undo levels that are stored.
37          */
38         public static final int UNDO_LEVELS = 50;
39         /**
40          * The margin of the undo levels.  After the number of undo levels exceeds 
41          * UNDO_LEVELS by this amount the undo is purged to that length.
42          */
43         public static final int UNDO_MARGIN = 10;
44         
45         public static final String SIMULATION_NAME_PREFIX = "Simulation ";
46         
47         /** Whether an undo error has already been reported to the user */
48         private static boolean undoErrorReported = false;
49         
50         private final Rocket rocket;
51         private final Configuration configuration;
52         
53         private final ArrayList<Simulation> simulations = new ArrayList<Simulation>();
54         
55
56         /*
57          * The undo/redo variables and mechanism are documented in doc/undo-redo-flow.*
58          */
59
60         /** 
61          * The undo history of the rocket.   Whenever a new undo position is created while the
62          * rocket is in "dirty" state, the rocket is copied here.
63          */
64         private LinkedList<Rocket> undoHistory = new LinkedList<Rocket>();
65         private LinkedList<String> undoDescription = new LinkedList<String>();
66         
67         /**
68          * The position in the undoHistory we are currently at.  If modifications have been
69          * made to the rocket, the rocket is in "dirty" state and this points to the previous
70          * "clean" state.
71          */
72         private int undoPosition = -1; // Illegal position, init in constructor
73         
74         /**
75          * The description of the next action that modifies this rocket.
76          */
77         private String nextDescription = null;
78         private String storedDescription = null;
79         
80
81         private File file = null;
82         private int savedID = -1;
83         
84         private final StorageOptions storageOptions = new StorageOptions();
85         
86
87         private final List<DocumentChangeListener> listeners =
88                         new ArrayList<DocumentChangeListener>();
89         
90         /* These must be initialized after undo history is set up. */
91         private final UndoRedoAction undoAction;
92         private final UndoRedoAction redoAction;
93         
94         
95         public OpenRocketDocument(Rocket rocket) {
96                 this(rocket.getDefaultConfiguration());
97         }
98         
99         
100         private OpenRocketDocument(Configuration configuration) {
101                 this.configuration = configuration;
102                 this.rocket = configuration.getRocket();
103                 
104                 clearUndo();
105                 
106                 undoAction = new UndoRedoAction(UndoRedoAction.UNDO);
107                 redoAction = new UndoRedoAction(UndoRedoAction.REDO);
108                 
109                 rocket.addComponentChangeListener(this);
110         }
111         
112         
113
114
115         public Rocket getRocket() {
116                 return rocket;
117         }
118         
119         
120         public Configuration getDefaultConfiguration() {
121                 return configuration;
122         }
123         
124         
125         public File getFile() {
126                 return file;
127         }
128         
129         public void setFile(File file) {
130                 this.file = file;
131         }
132         
133         
134         public boolean isSaved() {
135                 return rocket.getModID() == savedID;
136         }
137         
138         public void setSaved(boolean saved) {
139                 if (saved == false)
140                         this.savedID = -1;
141                 else
142                         this.savedID = rocket.getModID();
143         }
144         
145         /**
146          * Retrieve the default storage options for this document.
147          * 
148          * @return      the storage options.
149          */
150         public StorageOptions getDefaultStorageOptions() {
151                 return storageOptions;
152         }
153         
154         
155
156
157
158         public List<Simulation> getSimulations() {
159                 return simulations.clone();
160         }
161         
162         public int getSimulationCount() {
163                 return simulations.size();
164         }
165         
166         public Simulation getSimulation(int n) {
167                 return simulations.get(n);
168         }
169         
170         public int getSimulationIndex(Simulation simulation) {
171                 return simulations.indexOf(simulation);
172         }
173         
174         public void addSimulation(Simulation simulation) {
175                 simulations.add(simulation);
176                 fireDocumentChangeEvent(new SimulationChangeEvent(simulation));
177         }
178         
179         public void addSimulation(Simulation simulation, int n) {
180                 simulations.add(n, simulation);
181                 fireDocumentChangeEvent(new SimulationChangeEvent(simulation));
182         }
183         
184         public void removeSimulation(Simulation simulation) {
185                 simulations.remove(simulation);
186                 fireDocumentChangeEvent(new SimulationChangeEvent(simulation));
187         }
188         
189         public Simulation removeSimulation(int n) {
190                 Simulation simulation = simulations.remove(n);
191                 fireDocumentChangeEvent(new SimulationChangeEvent(simulation));
192                 return simulation;
193         }
194         
195         /**
196          * Return a unique name suitable for the next simulation.  The name begins
197          * with {@link #SIMULATION_NAME_PREFIX} and has a unique number larger than any
198          * previous simulation.
199          * 
200          * @return      the new name.
201          */
202         public String getNextSimulationName() {
203                 // Generate unique name for the simulation
204                 int maxValue = 0;
205                 for (Simulation s : simulations) {
206                         String name = s.getName();
207                         if (name.startsWith(SIMULATION_NAME_PREFIX)) {
208                                 try {
209                                         maxValue = Math.max(maxValue,
210                                                         Integer.parseInt(name.substring(SIMULATION_NAME_PREFIX.length())));
211                                 } catch (NumberFormatException ignore) {
212                                 }
213                         }
214                 }
215                 return SIMULATION_NAME_PREFIX + (maxValue + 1);
216         }
217         
218         
219         /**
220          * Adds an undo point at this position.  This method should be called *before* any
221          * action that is to be undoable.  All actions after the call will be undone by a 
222          * single "Undo" action.
223          * <p>
224          * The description should be a short, descriptive string of the actions that will 
225          * follow.  This is shown to the user e.g. in the Edit-menu, for example 
226          * "Undo (Modify body tube)".  If the actions are not known (in general should not
227          * be the case!) description may be null.
228          * <p>
229          * If this method is called successively without any change events occurring between the
230          * calls, only the last call will have any effect.
231          * 
232          * @param description A short description of the following actions.
233          */
234         public void addUndoPosition(String description) {
235                 
236                 if (storedDescription != null) {
237                         logUndoError("addUndoPosition called while storedDescription=" + storedDescription +
238                                         " description=" + description);
239                 }
240                 
241                 // Check whether modifications have been done since last call
242                 if (isCleanState()) {
243                         // No modifications
244                         log.info("Adding undo position '" + description + "' to " + this + ", document was in clean state");
245                         nextDescription = description;
246                         return;
247                 }
248                 
249                 log.info("Adding undo position '" + description + "' to " + this + ", document is in unclean state");
250                 
251                 /*
252                  * Modifications have been made to the rocket.  We should be at the end of the
253                  * undo history, but check for consistency and try to recover.
254                  */
255                 if (undoPosition != undoHistory.size() - 1) {
256                         logUndoError("undo position inconsistency");
257                 }
258                 while (undoPosition < undoHistory.size() - 1) {
259                         undoHistory.removeLast();
260                         undoDescription.removeLast();
261                 }
262                 
263
264                 // Add the current state to the undo history
265                 undoHistory.add(rocket.copyWithOriginalID());
266                 undoDescription.add(null);
267                 nextDescription = description;
268                 undoPosition++;
269                 
270
271                 // Maintain maximum undo size
272                 if (undoHistory.size() > UNDO_LEVELS + UNDO_MARGIN && undoPosition > UNDO_MARGIN) {
273                         for (int i = 0; i < UNDO_MARGIN; i++) {
274                                 undoHistory.removeFirst();
275                                 undoDescription.removeFirst();
276                                 undoPosition--;
277                         }
278                 }
279         }
280         
281         
282         /**
283          * Start a time-limited undoable operation.  After the operation {@link #stopUndo()}
284          * must be called, which will restore the previous undo description into effect.
285          * Only one level of start-stop undo descriptions is supported, i.e. start-stop
286          * undo cannot be nested, and no other undo operations may be called between
287          * the start and stop calls.
288          * 
289          * @param description   Description of the following undoable operations.
290          */
291         public void startUndo(String description) {
292                 if (storedDescription != null) {
293                         logUndoError("startUndo called while storedDescription=" + storedDescription +
294                                         " description=" + description);
295                 }
296                 log.info("Starting time-limited undoable operation '" + description + "' for " + this);
297                 String store = nextDescription;
298                 addUndoPosition(description);
299                 storedDescription = store;
300         }
301         
302         /**
303          * End the previous time-limited undoable operation.  This must be called after
304          * {@link #startUndo(String)} has been called before any other undo operations are
305          * performed.
306          */
307         public void stopUndo() {
308                 log.info("Ending time-limited undoable operation for " + this + " nextDescription=" +
309                                 nextDescription + "     storedDescription=" + storedDescription);
310                 String stored = storedDescription;
311                 storedDescription = null;
312                 addUndoPosition(stored);
313         }
314         
315         
316         public Action getUndoAction() {
317                 return undoAction;
318         }
319         
320         
321         public Action getRedoAction() {
322                 return redoAction;
323         }
324         
325         
326         /**
327          * Clear the undo history.
328          */
329         public void clearUndo() {
330                 log.info("Clearing undo history of " + this);
331                 undoHistory.clear();
332                 undoDescription.clear();
333                 
334                 undoHistory.add(rocket.copyWithOriginalID());
335                 undoDescription.add(null);
336                 undoPosition = 0;
337                 
338                 if (undoAction != null)
339                         undoAction.setAllValues();
340                 if (redoAction != null)
341                         redoAction.setAllValues();
342         }
343         
344         
345         @Override
346         public void componentChanged(ComponentChangeEvent e) {
347                 
348                 if (!e.isUndoChange()) {
349                         if (undoPosition < undoHistory.size() - 1) {
350                                 log.info("Rocket changed while in undo history, removing redo information for " + this +
351                                                 " undoPosition=" + undoPosition + " undoHistory.size=" + undoHistory.size() +
352                                                 " isClean=" + isCleanState());
353                         }
354                         // Remove any redo information if available
355                         while (undoPosition < undoHistory.size() - 1) {
356                                 undoHistory.removeLast();
357                                 undoDescription.removeLast();
358                         }
359                         
360                         // Set the latest description
361                         undoDescription.set(undoPosition, nextDescription);
362                 }
363                 
364                 undoAction.setAllValues();
365                 redoAction.setAllValues();
366         }
367         
368         
369         /**
370          * Return whether undo action is available.
371          * @return      <code>true</code> if undo can be performed
372          */
373         public boolean isUndoAvailable() {
374                 if (undoPosition > 0)
375                         return true;
376                 
377                 return !isCleanState();
378         }
379         
380         /**
381          * Return the description of what action would be undone if undo is called.
382          * @return      the description what would be undone, or <code>null</code> if description unavailable.
383          */
384         public String getUndoDescription() {
385                 if (!isUndoAvailable())
386                         return null;
387                 
388                 if (isCleanState()) {
389                         return undoDescription.get(undoPosition - 1);
390                 } else {
391                         return undoDescription.get(undoPosition);
392                 }
393         }
394         
395         
396         /**
397          * Return whether redo action is available.
398          * @return      <code>true</code> if redo can be performed
399          */
400         public boolean isRedoAvailable() {
401                 return undoPosition < undoHistory.size() - 1;
402         }
403         
404         /**
405          * Return the description of what action would be redone if redo is called.
406          * @return      the description of what would be redone, or <code>null</code> if description unavailable.
407          */
408         public String getRedoDescription() {
409                 if (!isRedoAvailable())
410                         return null;
411                 
412                 return undoDescription.get(undoPosition);
413         }
414         
415         
416         /**
417          * Perform undo operation on the rocket.
418          */
419         public void undo() {
420                 log.info("Performing undo for " + this + " undoPosition=" + undoPosition +
421                                 " undoHistory.size=" + undoHistory.size() + " isClean=" + isCleanState());
422                 if (!isUndoAvailable()) {
423                         logUndoError("Undo not available");
424                         undoAction.setAllValues();
425                         redoAction.setAllValues();
426                         return;
427                 }
428                 if (storedDescription != null) {
429                         logUndoError("undo() called with storedDescription=" + storedDescription);
430                 }
431                 
432                 // Update history position
433                 
434                 if (isCleanState()) {
435                         // We are in a clean state, simply move backwards in history
436                         undoPosition--;
437                 } else {
438                         if (undoPosition != undoHistory.size() - 1) {
439                                 logUndoError("undo position inconsistency");
440                         }
441                         // Modifications have been made, save the state and restore previous state
442                         undoHistory.add(rocket.copyWithOriginalID());
443                         undoDescription.add(null);
444                 }
445                 
446                 rocket.checkComponentStructure();
447                 undoHistory.get(undoPosition).checkComponentStructure();
448                 undoHistory.get(undoPosition).copyWithOriginalID().checkComponentStructure();
449                 rocket.loadFrom(undoHistory.get(undoPosition).copyWithOriginalID());
450                 rocket.checkComponentStructure();
451         }
452         
453         
454         /**
455          * Perform redo operation on the rocket.
456          */
457         public void redo() {
458                 log.info("Performing redo for " + this + " undoPosition=" + undoPosition +
459                                 " undoHistory.size=" + undoHistory.size() + " isClean=" + isCleanState());
460                 if (!isRedoAvailable()) {
461                         logUndoError("Redo not available");
462                         undoAction.setAllValues();
463                         redoAction.setAllValues();
464                         return;
465                 }
466                 if (storedDescription != null) {
467                         logUndoError("redo() called with storedDescription=" + storedDescription);
468                 }
469                 
470                 undoPosition++;
471                 
472                 rocket.loadFrom(undoHistory.get(undoPosition).copyWithOriginalID());
473         }
474         
475         
476         private boolean isCleanState() {
477                 return rocket.getModID() == undoHistory.get(undoPosition).getModID();
478         }
479         
480         
481         /**
482          * Log a non-fatal undo/redo error or inconsistency.  Reports it to the user the first 
483          * time it occurs, but not on subsequent times.  Logs automatically the undo system state.
484          */
485         private void logUndoError(String error) {
486                 log.error(1, error + ": this=" + this + " undoPosition=" + undoPosition +
487                                 " undoHistory.size=" + undoHistory.size() + " isClean=" + isCleanState() +
488                                 " nextDescription=" + nextDescription + " storedDescription=" + storedDescription,
489                                 new TraceException());
490                 
491                 if (!undoErrorReported) {
492                         undoErrorReported = true;
493                         ExceptionHandler.handleErrorCondition("Undo/Redo error: " + error);
494                 }
495         }
496         
497         ///////  Listeners
498         
499         public void addDocumentChangeListener(DocumentChangeListener listener) {
500                 listeners.add(listener);
501         }
502         
503         public void removeDocumentChangeListener(DocumentChangeListener listener) {
504                 listeners.remove(listener);
505         }
506         
507         protected void fireDocumentChangeEvent(DocumentChangeEvent event) {
508                 DocumentChangeListener[] array = listeners.toArray(new DocumentChangeListener[0]);
509                 for (DocumentChangeListener l : array) {
510                         l.documentChanged(event);
511                 }
512         }
513         
514         
515
516
517         /**
518          * Inner class to implement undo/redo actions.
519          */
520         private class UndoRedoAction extends AbstractAction {
521                 public static final int UNDO = 1;
522                 public static final int REDO = 2;
523                 
524                 private final int type;
525                 
526                 // Sole constructor
527                 public UndoRedoAction(int type) {
528                         if (type != UNDO && type != REDO) {
529                                 throw new IllegalArgumentException("Unknown type = " + type);
530                         }
531                         this.type = type;
532                         setAllValues();
533                 }
534                 
535                 
536                 // Actual action to make
537                 @Override
538                 public void actionPerformed(ActionEvent e) {
539                         switch (type) {
540                         case UNDO:
541                                 log.user("Performing undo, event=" + e);
542                                 undo();
543                                 break;
544                         
545                         case REDO:
546                                 log.user("Performing redo, event=" + e);
547                                 redo();
548                                 break;
549                         }
550                 }
551                 
552                 
553                 // Set all the values correctly (name and enabled/disabled status)
554                 public void setAllValues() {
555                         String name, desc;
556                         boolean actionEnabled;
557                         
558                         switch (type) {
559                         case UNDO:
560                                 name = "Undo";
561                                 desc = getUndoDescription();
562                                 actionEnabled = isUndoAvailable();
563                                 this.putValue(SMALL_ICON, Icons.EDIT_UNDO);
564                                 break;
565                         
566                         case REDO:
567                                 name = "Redo";
568                                 desc = getRedoDescription();
569                                 actionEnabled = isRedoAvailable();
570                                 this.putValue(SMALL_ICON, Icons.EDIT_REDO);
571                                 break;
572                         
573                         default:
574                                 throw new BugException("illegal type=" + type);
575                         }
576                         
577                         if (desc != null)
578                                 name = name + " (" + desc + ")";
579                         
580                         putValue(NAME, name);
581                         setEnabled(actionEnabled);
582                 }
583         }
584 }