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