From: plaa Date: Sun, 3 Oct 2010 14:37:46 +0000 (+0000) Subject: Undo/redo system enhancements, DnD for component tree, bug fixes X-Git-Tag: upstream/1.1.3^2~2 X-Git-Url: https://git.gag.com/?a=commitdiff_plain;h=07813d11b802133d179044ea9c382d23821a57d4;p=debian%2Fopenrocket Undo/redo system enhancements, DnD for component tree, bug fixes git-svn-id: https://openrocket.svn.sourceforge.net/svnroot/openrocket/trunk@86 180e2498-e6e9-4542-8430-84ac67f01cd8 --- diff --git a/ChangeLog b/ChangeLog index a8ea7fac..fa9cde2e 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,16 @@ +2010-10-03 Sampo Niskanen + + * Added VBOSE logging level + +2010-10-02 Sampo Niskanen + + * [BUG] Exception when undoing changes + +2010-09-27 Sampo Niskanen + + * Implemented DnD for component tree + * Documented undo/redo functionality + 2010-09-07 Sampo Niskanen * Released version 1.1.2 diff --git a/doc/techdoc/techdoc.pdf b/doc/techdoc/techdoc.pdf index 90a824cc..2377e85c 100644 Binary files a/doc/techdoc/techdoc.pdf and b/doc/techdoc/techdoc.pdf differ diff --git a/doc/undo-redo-flow.uxf b/doc/undo-redo-flow.uxf new file mode 100644 index 00000000..4aa4b77f --- /dev/null +++ b/doc/undo-redo-flow.uxf @@ -0,0 +1,107 @@ +// Uncomment the following line to change the fontsize: +// fontsize=14 + + +////////////////////////////////////////////////////////////////////////////////////////////// +// Welcome to UMLet! +// +// Double-click on UML elements to add them to the diagram, or to copy them +// Edit elements by modifying the text in this panel +// Hold Ctrl to select multiple elements +// Use Ctrl+mouse to select via lasso +// +// Use ± or Ctrl+mouse wheel to zoom +// Drag a whole relation at its central square icon +// +// Press Ctrl+C to copy the whole diagram to the system clipboard (then just paste it to, eg, Word) +// Edit the files in the "palettes" directory to create your own element palettes +// +// Select "Custom Elements > New..." to create new element types +////////////////////////////////////////////////////////////////////////////////////////////// + + +// This text will be stored with each diagram; use it for notes.10com.umlet.element.custom.State670540280140Clean, at end of history +-- +undoHistory.size = n +undoPosition = n-1 +clean +-. +redo() not availablecom.umlet.element.custom.InitialState3401402020com.umlet.element.base.Relation32012050160lt=<-30;140;30;30com.umlet.element.custom.State200540280140Dirty, at end of history +-- +undoHistory.size = n +undoPosition = n-1 +dirty +-. +redo() not availablecom.umlet.element.base.Relation230370214190lt=<- +Modify rocket: +nextDescription stored +in undoDescription110;170;110;30com.umlet.element.custom.State200260280140Clean, no history +-- +undoHistory.size = 1 +undoPosition = 0 +clean +-. +undo(), redo() not availablecom.umlet.element.base.Relation60350210124lt=<- +addUndoPosition: +nextDescription set140;30;100;30;100;80;190;80;190;50com.umlet.element.base.Relation450560240146lt=<- +addUndoPosition(): +rocket copied to +end of history, +nextDescription set, +undoPosition increased220;50;30;50com.umlet.element.base.Relation310410510150lt=<- +30;130;30;30;490;30;490;130com.umlet.element.custom.State190850280140Clean, at midpoint of history +-- +undoHistory.size = n +undoPosition = m < n-1 +clean +-. +com.umlet.element.base.Relation190650282220lt=<- +undo(): +rocket copied to end of history, +rocket loaded from undoPosition +150;200;150;30com.umlet.element.base.Relation0590238350lt=<- +Modify rocket: +redo information removed, +nextDescription stored +in undoDescription200;30;120;30;120;330;190;330com.umlet.element.base.Relation0940286190lt=<- +undo(): +undoPosition decreased, +Rocket loaded from undoPosition +- +addUndoPosition(): +nextDescription set190;30;150;30;150;80;240;80;240;50com.umlet.element.base.Relation390910282178lt=<- +redo(): +undoPosition increased, +rocket loaded from undoPosition30;80;30;120;150;120;150;30;80;30com.umlet.element.base.Relation510650412348lt=<- +redo(): +undoPosition increased to n-1, +rocket loaded from undoPosition280;30;280;290;30;290com.umlet.element.base.Relation440650422288lt=<- +undo(): +undoPosition decreased, +rocket loaded from undoPosition30;230;290;230;290;30com.umlet.element.base.Relation70450200160lt=<- +Modify rocket +180;90;180;50;80;50;80;140;130;140com.umlet.element.base.Relation860460216160lt=<- +addUndoPosition()30;80;30;50;130;50;130;140;90;140com.umlet.element.custom.State54050400350Variable description +-- +undoHistory: a list of the undo positions. Each position +is a full copy of the rocket structure. Always contains at +least one entry. +-. +undoPosition: current location within the undoHistory. +If at the last entry, the rocket can be "clean" (in the +state of undoHistory.get(undoPosition)) or "dirty" (in +a modified state). +-. +undoDescription: the value at undoDescription.get( +undoPosition) describes the action that would be +undone if undo() was called at that moment. The value +at undoDescription.get(undoPosition+1) describes the +action that would be redone if redo() were be called. +-. +nextDescription: the description of the actions that +will be done next (or are already being done). +-. +storedDescription: temporary storage for +nextDescription during a startUndo() ... stopUndo() +action. +fg=blue \ No newline at end of file diff --git a/src/net/sf/openrocket/aerodynamics/FlightConditions.java b/src/net/sf/openrocket/aerodynamics/FlightConditions.java index 99df61f1..64e81455 100644 --- a/src/net/sf/openrocket/aerodynamics/FlightConditions.java +++ b/src/net/sf/openrocket/aerodynamics/FlightConditions.java @@ -413,7 +413,7 @@ public class FlightConditions implements Cloneable, ChangeSource, Monitorable { cond.atmosphericConditions = atmosphericConditions.clone(); return cond; } catch (CloneNotSupportedException e) { - throw new BugException("BUG: clone not supported!", e); + throw new BugException("clone not supported!", e); } } diff --git a/src/net/sf/openrocket/aerodynamics/barrowman/SymmetricComponentCalc.java b/src/net/sf/openrocket/aerodynamics/barrowman/SymmetricComponentCalc.java index 446b97ea..4f74bf1e 100644 --- a/src/net/sf/openrocket/aerodynamics/barrowman/SymmetricComponentCalc.java +++ b/src/net/sf/openrocket/aerodynamics/barrowman/SymmetricComponentCalc.java @@ -203,9 +203,6 @@ public class SymmetricComponentCalc extends RocketComponentCalc { } - assert (r1 < r2); // Tube and boattail have been checked already - - // All nose cones and shoulders from pre-calculated and interpolating if (interpolator == null) { calculateNoseInterpolator(); @@ -355,8 +352,9 @@ public class SymmetricComponentCalc extends RocketComponentCalc { throw new UnsupportedOperationException("Unknown transition shape: " + shape); } - assert (p >= 0); - assert (p <= 1.001); + if (p < 0 || p > 1.00001) { + throw new BugException("Inconsistent parameter value p=" + p + " shape=" + shape); + } // Check for parameterized shape and interpolate if necessary diff --git a/src/net/sf/openrocket/communication/UpdateInfoRetriever.java b/src/net/sf/openrocket/communication/UpdateInfoRetriever.java index 772bc90c..6cd4e113 100644 --- a/src/net/sf/openrocket/communication/UpdateInfoRetriever.java +++ b/src/net/sf/openrocket/communication/UpdateInfoRetriever.java @@ -7,6 +7,7 @@ import java.io.InputStreamReader; import java.io.Reader; import java.net.HttpURLConnection; import java.util.ArrayList; +import java.util.Locale; import net.sf.openrocket.logging.LogHelper; import net.sf.openrocket.startup.Application; @@ -157,6 +158,8 @@ public class UpdateInfoRetriever { connection.setRequestProperty("X-OpenRocket-Country", Communicator.encode(System.getProperty("user.country") + " " + System.getProperty("user.timezone"))); + connection.setRequestProperty("X-OpenRocket-Locale", + Communicator.encode(Locale.getDefault().toString())); connection.setRequestProperty("X-OpenRocket-CPUs", "" + Runtime.getRuntime().availableProcessors()); InputStream is = null; diff --git a/src/net/sf/openrocket/document/OpenRocketDocument.java b/src/net/sf/openrocket/document/OpenRocketDocument.java index 83c4b0b7..02a91e82 100644 --- a/src/net/sf/openrocket/document/OpenRocketDocument.java +++ b/src/net/sf/openrocket/document/OpenRocketDocument.java @@ -1,5 +1,4 @@ package net.sf.openrocket.document; -//TODO: LOW: move class somewhere else? import java.awt.event.ActionEvent; import java.io.File; @@ -13,15 +12,26 @@ import javax.swing.Action; import net.sf.openrocket.document.events.DocumentChangeEvent; import net.sf.openrocket.document.events.DocumentChangeListener; import net.sf.openrocket.document.events.SimulationChangeEvent; +import net.sf.openrocket.gui.main.ExceptionHandler; +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.logging.TraceException; import net.sf.openrocket.rocketcomponent.ComponentChangeEvent; import net.sf.openrocket.rocketcomponent.ComponentChangeListener; import net.sf.openrocket.rocketcomponent.Configuration; import net.sf.openrocket.rocketcomponent.Rocket; +import net.sf.openrocket.startup.Application; import net.sf.openrocket.util.BugException; import net.sf.openrocket.util.Icons; - +/** + * Class describing an entire OpenRocket document, including a rocket and + * simulations. This class also handles undo/redo operations for the rocket structure. + * + * @author Sampo Niskanen + */ public class OpenRocketDocument implements ComponentChangeListener { + private static final LogHelper log = Application.getLogger(); + /** * The minimum number of undo levels that are stored. */ @@ -31,43 +41,62 @@ public class OpenRocketDocument implements ComponentChangeListener { * UNDO_LEVELS by this amount the undo is purged to that length. */ public static final int UNDO_MARGIN = 10; - public static final String SIMULATION_NAME_PREFIX = "Simulation "; + /** Whether an undo error has already been reported to the user */ + private static boolean undoErrorReported = false; + private final Rocket rocket; private final Configuration configuration; - - private final ArrayList simulations = new ArrayList(); + private final ArrayList simulations = new ArrayList(); - private int undoPosition = -1; // Illegal position, init in constructor + + /* + * The undo/redo variables and mechanism are documented in doc/undo-redo-flow.* + */ + + /** + * The undo history of the rocket. Whenever a new undo position is created while the + * rocket is in "dirty" state, the rocket is copied here. + */ private LinkedList undoHistory = new LinkedList(); private LinkedList undoDescription = new LinkedList(); + /** + * The position in the undoHistory we are currently at. If modifications have been + * made to the rocket, the rocket is in "dirty" state and this points to the previous + * "clean" state. + */ + private int undoPosition = -1; // Illegal position, init in constructor + + /** + * The description of the next action that modifies this rocket. + */ private String nextDescription = null; private String storedDescription = null; - + private File file = null; private int savedID = -1; private final StorageOptions storageOptions = new StorageOptions(); - - private final List listeners = - new ArrayList(); + + private final List listeners = + new ArrayList(); /* These must be initialized after undo history is set up. */ private final UndoRedoAction undoAction; private final UndoRedoAction redoAction; - + public OpenRocketDocument(Rocket rocket) { this(rocket.getDefaultConfiguration()); } - + private OpenRocketDocument(Configuration configuration) { this.configuration = configuration; this.rocket = configuration.getRocket(); @@ -81,31 +110,31 @@ public class OpenRocketDocument implements ComponentChangeListener { } - - + + public Rocket getRocket() { return rocket; } - + public Configuration getDefaultConfiguration() { return configuration; } - - + + public File getFile() { return file; } - + public void setFile(File file) { this.file = file; } - + public boolean isSaved() { return rocket.getModID() == savedID; } - + public void setSaved(boolean saved) { if (saved == false) this.savedID = -1; @@ -123,34 +152,41 @@ public class OpenRocketDocument implements ComponentChangeListener { } - - - + + + @SuppressWarnings("unchecked") public List getSimulations() { - return (ArrayList)simulations.clone(); + return (ArrayList) simulations.clone(); } + public int getSimulationCount() { return simulations.size(); } + public Simulation getSimulation(int n) { return simulations.get(n); } + public int getSimulationIndex(Simulation simulation) { return simulations.indexOf(simulation); } + public void addSimulation(Simulation simulation) { simulations.add(simulation); fireDocumentChangeEvent(new SimulationChangeEvent(simulation)); } + public void addSimulation(Simulation simulation, int n) { simulations.add(n, simulation); fireDocumentChangeEvent(new SimulationChangeEvent(simulation)); } + public void removeSimulation(Simulation simulation) { simulations.remove(simulation); fireDocumentChangeEvent(new SimulationChangeEvent(simulation)); } + public Simulation removeSimulation(int n) { Simulation simulation = simulations.remove(n); fireDocumentChangeEvent(new SimulationChangeEvent(simulation)); @@ -167,16 +203,17 @@ public class OpenRocketDocument implements ComponentChangeListener { public String getNextSimulationName() { // Generate unique name for the simulation int maxValue = 0; - for (Simulation s: simulations) { + for (Simulation s : simulations) { String name = s.getName(); if (name.startsWith(SIMULATION_NAME_PREFIX)) { try { - maxValue = Math.max(maxValue, + maxValue = Math.max(maxValue, Integer.parseInt(name.substring(SIMULATION_NAME_PREFIX.length()))); - } catch (NumberFormatException ignore) { } + } catch (NumberFormatException ignore) { + } } } - return SIMULATION_NAME_PREFIX + (maxValue+1); + return SIMULATION_NAME_PREFIX + (maxValue + 1); } @@ -196,43 +233,52 @@ public class OpenRocketDocument implements ComponentChangeListener { * @param description A short description of the following actions. */ public void addUndoPosition(String description) { - + + if (storedDescription != null) { + logUndoError("addUndoPosition called while storedDescription=" + storedDescription + + " description=" + description); + } + // Check whether modifications have been done since last call if (isCleanState()) { // No modifications + log.info("Adding undo position '" + description + "' to " + this + ", document was in clean state"); nextDescription = description; return; } - + + log.info("Adding undo position '" + description + "' to " + this + ", document is in unclean state"); /* * Modifications have been made to the rocket. We should be at the end of the - * undo history, but check for consistency. + * undo history, but check for consistency and try to recover. */ - assert(undoPosition == undoHistory.size()-1): "Undo inconsistency, report bug!"; - while (undoPosition < undoHistory.size()-1) { + if (undoPosition != undoHistory.size() - 1) { + logUndoError("undo position inconsistency"); + } + while (undoPosition < undoHistory.size() - 1) { undoHistory.removeLast(); undoDescription.removeLast(); } - + // Add the current state to the undo history - undoHistory.add(rocket.copy()); - undoDescription.add(description); + undoHistory.add(rocket.copyWithOriginalID()); + undoDescription.add(null); nextDescription = description; undoPosition++; - + // Maintain maximum undo size - if (undoHistory.size() > UNDO_LEVELS + UNDO_MARGIN) { - for (int i=0; i < UNDO_MARGIN+1; i++) { + if (undoHistory.size() > UNDO_LEVELS + UNDO_MARGIN && undoPosition > UNDO_MARGIN) { + for (int i = 0; i < UNDO_MARGIN; i++) { undoHistory.removeFirst(); undoDescription.removeFirst(); undoPosition--; } } } - + /** * Start a time-limited undoable operation. After the operation {@link #stopUndo()} @@ -244,8 +290,14 @@ public class OpenRocketDocument implements ComponentChangeListener { * @param description Description of the following undoable operations. */ public void startUndo(String description) { - storedDescription = nextDescription; + if (storedDescription != null) { + logUndoError("startUndo called while storedDescription=" + storedDescription + + " description=" + description); + } + log.info("Starting time-limited undoable operation '" + description + "' for " + this); + String store = nextDescription; addUndoPosition(description); + storedDescription = store; } /** @@ -254,8 +306,11 @@ public class OpenRocketDocument implements ComponentChangeListener { * performed. */ public void stopUndo() { - addUndoPosition(storedDescription); + log.info("Ending time-limited undoable operation for " + this + " nextDescription=" + + nextDescription + " storedDescription=" + storedDescription); + String stored = storedDescription; storedDescription = null; + addUndoPosition(stored); } @@ -273,10 +328,11 @@ public class OpenRocketDocument implements ComponentChangeListener { * Clear the undo history. */ public void clearUndo() { + log.info("Clearing undo history of " + this); undoHistory.clear(); undoDescription.clear(); - undoHistory.add(rocket.copy()); + undoHistory.add(rocket.copyWithOriginalID()); undoDescription.add(null); undoPosition = 0; @@ -291,8 +347,13 @@ public class OpenRocketDocument implements ComponentChangeListener { public void componentChanged(ComponentChangeEvent e) { if (!e.isUndoChange()) { + if (undoPosition < undoHistory.size() - 1) { + log.info("Rocket changed while in undo history, removing redo information for " + this + + " undoPosition=" + undoPosition + " undoHistory.size=" + undoHistory.size() + + " isClean=" + isCleanState()); + } // Remove any redo information if available - while (undoPosition < undoHistory.size()-1) { + while (undoPosition < undoHistory.size() - 1) { undoHistory.removeLast(); undoDescription.removeLast(); } @@ -304,8 +365,12 @@ public class OpenRocketDocument implements ComponentChangeListener { undoAction.setAllValues(); redoAction.setAllValues(); } - + + /** + * Return whether undo action is available. + * @return true if undo can be performed + */ public boolean isUndoAvailable() { if (undoPosition > 0) return true; @@ -313,22 +378,34 @@ public class OpenRocketDocument implements ComponentChangeListener { return !isCleanState(); } + /** + * Return the description of what action would be undone if undo is called. + * @return the description what would be undone, or null if description unavailable. + */ public String getUndoDescription() { if (!isUndoAvailable()) return null; if (isCleanState()) { - return undoDescription.get(undoPosition-1); + return undoDescription.get(undoPosition - 1); } else { return undoDescription.get(undoPosition); } } - + + /** + * Return whether redo action is available. + * @return true if redo can be performed + */ public boolean isRedoAvailable() { - return undoPosition < undoHistory.size()-1; + return undoPosition < undoHistory.size() - 1; } + /** + * Return the description of what action would be redone if redo is called. + * @return the description of what would be redone, or null if description unavailable. + */ public String getRedoDescription() { if (!isRedoAvailable()) return null; @@ -337,35 +414,59 @@ public class OpenRocketDocument implements ComponentChangeListener { } - + /** + * Perform undo operation on the rocket. + */ public void undo() { + log.info("Performing undo for " + this + " undoPosition=" + undoPosition + + " undoHistory.size=" + undoHistory.size() + " isClean=" + isCleanState()); if (!isUndoAvailable()) { - throw new IllegalStateException("Undo not available."); + logUndoError("Undo not available"); + undoAction.setAllValues(); + redoAction.setAllValues(); + return; } - + if (storedDescription != null) { + logUndoError("undo() called with storedDescription=" + storedDescription); + } + // Update history position if (isCleanState()) { // We are in a clean state, simply move backwards in history undoPosition--; } else { + if (undoPosition != undoHistory.size() - 1) { + logUndoError("undo position inconsistency"); + } // Modifications have been made, save the state and restore previous state - undoHistory.add(rocket.copy()); + undoHistory.add(rocket.copyWithOriginalID()); undoDescription.add(null); } - rocket.loadFrom(undoHistory.get(undoPosition).copy()); + rocket.loadFrom(undoHistory.get(undoPosition).copyWithOriginalID()); } + /** + * Perform redo operation on the rocket. + */ public void redo() { + log.info("Performing redo for " + this + " undoPosition=" + undoPosition + + " undoHistory.size=" + undoHistory.size() + " isClean=" + isCleanState()); if (!isRedoAvailable()) { - throw new IllegalStateException("Redo not available."); + logUndoError("Redo not available"); + undoAction.setAllValues(); + redoAction.setAllValues(); + return; + } + if (storedDescription != null) { + logUndoError("redo() called with storedDescription=" + storedDescription); } undoPosition++; - rocket.loadFrom(undoHistory.get(undoPosition).copy()); + rocket.loadFrom(undoHistory.get(undoPosition).copyWithOriginalID()); } @@ -374,7 +475,21 @@ public class OpenRocketDocument implements ComponentChangeListener { } - + /** + * Log a non-fatal undo/redo error or inconsistency. Reports it to the user the first + * time it occurs, but not on subsequent times. Logs automatically the undo system state. + */ + private void logUndoError(String error) { + log.error(1, error + ": this=" + this + " undoPosition=" + undoPosition + + " undoHistory.size=" + undoHistory.size() + " isClean=" + isCleanState() + + " nextDescription=" + nextDescription + " storedDescription=" + storedDescription, + new TraceException()); + + if (!undoErrorReported) { + undoErrorReported = true; + ExceptionHandler.handleErrorCondition("Undo/Redo error: " + error); + } + } /////// Listeners @@ -388,14 +503,14 @@ public class OpenRocketDocument implements ComponentChangeListener { protected void fireDocumentChangeEvent(DocumentChangeEvent event) { DocumentChangeListener[] array = listeners.toArray(new DocumentChangeListener[0]); - for (DocumentChangeListener l: array) { + for (DocumentChangeListener l : array) { l.documentChanged(event); } } - - + + /** * Inner class to implement undo/redo actions. */ @@ -408,56 +523,58 @@ public class OpenRocketDocument implements ComponentChangeListener { // Sole constructor public UndoRedoAction(int type) { if (type != UNDO && type != REDO) { - throw new IllegalArgumentException("Unknown type = "+type); + throw new IllegalArgumentException("Unknown type = " + type); } this.type = type; setAllValues(); } - + // Actual action to make public void actionPerformed(ActionEvent e) { switch (type) { case UNDO: + log.user("Performing undo, event=" + e); undo(); break; - + case REDO: + log.user("Performing redo, event=" + e); redo(); break; } } - + // Set all the values correctly (name and enabled/disabled status) public void setAllValues() { - String name,desc; - boolean enabled; + String name, desc; + boolean actionEnabled; switch (type) { case UNDO: name = "Undo"; desc = getUndoDescription(); - enabled = isUndoAvailable(); + actionEnabled = isUndoAvailable(); this.putValue(SMALL_ICON, Icons.EDIT_UNDO); break; - + case REDO: name = "Redo"; desc = getRedoDescription(); - enabled = isRedoAvailable(); + actionEnabled = isRedoAvailable(); this.putValue(SMALL_ICON, Icons.EDIT_REDO); break; - + default: - throw new BugException("illegal type="+type); + throw new BugException("illegal type=" + type); } if (desc != null) - name = name + " ("+desc+")"; + name = name + " (" + desc + ")"; putValue(NAME, name); - setEnabled(enabled); + setEnabled(actionEnabled); } } } diff --git a/src/net/sf/openrocket/document/Simulation.java b/src/net/sf/openrocket/document/Simulation.java index 3d0556b1..d7afa7c8 100644 --- a/src/net/sf/openrocket/document/Simulation.java +++ b/src/net/sf/openrocket/document/Simulation.java @@ -137,8 +137,16 @@ public class Simulation implements ChangeSource, Cloneable { } - - + /** + * Return the rocket associated with this simulation. + * + * @return the rocket. + */ + public Rocket getRocket() { + return rocket; + } + + /** * Return a newly created Configuration for this simulation. The configuration * has the motor ID set and all stages active. diff --git a/src/net/sf/openrocket/file/RocketLoader.java b/src/net/sf/openrocket/file/RocketLoader.java index 113bc690..4fc8dada 100644 --- a/src/net/sf/openrocket/file/RocketLoader.java +++ b/src/net/sf/openrocket/file/RocketLoader.java @@ -13,7 +13,7 @@ import net.sf.openrocket.document.OpenRocketDocument; public abstract class RocketLoader { protected final WarningSet warnings = new WarningSet(); - + /** * Loads a rocket from the specified File object. @@ -21,7 +21,7 @@ public abstract class RocketLoader { public final OpenRocketDocument load(File source) throws RocketLoadException { warnings.clear(); InputStream stream = null; - + try { stream = new BufferedInputStream(new FileInputStream(source)); @@ -39,24 +39,24 @@ public abstract class RocketLoader { } } } - + /** * Loads a rocket from the specified InputStream. */ public final OpenRocketDocument load(InputStream source) throws RocketLoadException { warnings.clear(); - + try { return loadFromStream(source); } catch (RocketLoadException e) { throw e; } catch (IOException e) { - throw new RocketLoadException("I/O error: " + e.getMessage()); + throw new RocketLoadException("I/O error: " + e.getMessage(), e); } } - + /** * This method is called by the default implementations of {@link #load(File)} * and {@link #load(InputStream)} to load the rocket. @@ -65,8 +65,8 @@ public abstract class RocketLoader { */ protected abstract OpenRocketDocument loadFromStream(InputStream source) throws IOException, RocketLoadException; - - + + public final WarningSet getWarnings() { return warnings; diff --git a/src/net/sf/openrocket/file/rocksim/RocksimLoader.java b/src/net/sf/openrocket/file/rocksim/RocksimLoader.java index 70953415..a2258aa3 100644 --- a/src/net/sf/openrocket/file/rocksim/RocksimLoader.java +++ b/src/net/sf/openrocket/file/rocksim/RocksimLoader.java @@ -3,13 +3,13 @@ */ package net.sf.openrocket.file.rocksim; -import net.sf.openrocket.file.RocketLoader; +import java.io.IOException; +import java.io.InputStream; + +import net.sf.openrocket.document.OpenRocketDocument; import net.sf.openrocket.file.RocketLoadException; +import net.sf.openrocket.file.RocketLoader; import net.sf.openrocket.file.simplesax.SimpleSAX; -import net.sf.openrocket.document.OpenRocketDocument; - -import java.io.InputStream; -import java.io.IOException; import org.xml.sax.InputSource; import org.xml.sax.SAXException; @@ -30,28 +30,29 @@ import org.xml.sax.SAXException; * getMaterial */ public class RocksimLoader extends RocketLoader { - /** - * This method is called by the default implementations of {@link #load(java.io.File)} - * and {@link #load(java.io.InputStream)} to load the rocket. - * - * @throws net.sf.openrocket.file.RocketLoadException - * if an error occurs during loading. - */ - @Override - protected OpenRocketDocument loadFromStream(InputStream source) throws IOException, RocketLoadException { - - InputSource xmlSource = new InputSource(source); - - RocksimHandler handler = new RocksimHandler(); + /** + * This method is called by the default implementations of {@link #load(java.io.File)} + * and {@link #load(java.io.InputStream)} to load the rocket. + * + * @throws net.sf.openrocket.file.RocketLoadException + * if an error occurs during loading. + */ + @Override + protected OpenRocketDocument loadFromStream(InputStream source) throws IOException, RocketLoadException { - try { - SimpleSAX.readXML(xmlSource, handler, warnings); - } catch (SAXException e) { - throw new RocketLoadException("Malformed XML in input.", e); - } - - final OpenRocketDocument document = handler.getDocument(); - document.setFile(null); - return document; - } + InputSource xmlSource = new InputSource(source); + + RocksimHandler handler = new RocksimHandler(); + + try { + SimpleSAX.readXML(xmlSource, handler, warnings); + } catch (SAXException e) { + throw new RocketLoadException("Malformed XML in input.", e); + } + + final OpenRocketDocument document = handler.getDocument(); + document.setFile(null); + document.clearUndo(); + return document; + } } diff --git a/src/net/sf/openrocket/gui/adaptors/Column.java b/src/net/sf/openrocket/gui/adaptors/Column.java index 73341367..87997c48 100644 --- a/src/net/sf/openrocket/gui/adaptors/Column.java +++ b/src/net/sf/openrocket/gui/adaptors/Column.java @@ -14,7 +14,7 @@ public abstract class Column { public Column(String name) { this.name = name; } - + /** * Return the caption of the column. */ @@ -55,7 +55,7 @@ public abstract class Column { public Class getColumnClass() { return Object.class; } - + /** * Return the value in this column at the specified row. * @@ -63,4 +63,5 @@ public abstract class Column { * @return the value at the specified position. */ public abstract Object getValueAt(int row); + } diff --git a/src/net/sf/openrocket/gui/adaptors/ColumnTableModel.java b/src/net/sf/openrocket/gui/adaptors/ColumnTableModel.java index 53011f37..2e010f6e 100644 --- a/src/net/sf/openrocket/gui/adaptors/ColumnTableModel.java +++ b/src/net/sf/openrocket/gui/adaptors/ColumnTableModel.java @@ -14,7 +14,7 @@ public abstract class ColumnTableModel extends AbstractTableModel { } public void setColumnWidths(TableColumnModel model) { - for (int i=0; i < columns.length; i++) { + for (int i = 0; i < columns.length; i++) { if (columns[i].getExactWidth() > 0) { TableColumn col = model.getColumn(i); int w = columns[i].getExactWidth(); @@ -27,7 +27,7 @@ public abstract class ColumnTableModel extends AbstractTableModel { } } } - + @Override public int getColumnCount() { return columns.length; @@ -42,16 +42,15 @@ public abstract class ColumnTableModel extends AbstractTableModel { public Class getColumnClass(int col) { return columns[col].getColumnClass(); } - + @Override public Object getValueAt(int row, int col) { if ((row < 0) || (row >= getRowCount()) || (col < 0) || (col >= columns.length)) { - ExceptionHandler.handleErrorCondition("Error: Requested illegal column/row, " + - "col="+col+" row="+row); - assert(false); + ExceptionHandler.handleErrorCondition("Error: Requested illegal column/row, col=" + col + " row=" + row); return null; } return columns[col].getValueAt(row); } + } diff --git a/src/net/sf/openrocket/gui/adaptors/DoubleModel.java b/src/net/sf/openrocket/gui/adaptors/DoubleModel.java index 2f1946a4..20b2ad94 100644 --- a/src/net/sf/openrocket/gui/adaptors/DoubleModel.java +++ b/src/net/sf/openrocket/gui/adaptors/DoubleModel.java @@ -591,9 +591,9 @@ public class DoubleModel implements ChangeListener, ChangeSource { try { return (Double)getMethod.invoke(source)*multiplier; } catch (IllegalArgumentException e) { - throw new BugException("BUG: Unable to invoke getMethod of "+this, e); + throw new BugException("Unable to invoke getMethod of "+this, e); } catch (IllegalAccessException e) { - throw new BugException("BUG: Unable to invoke getMethod of "+this, e); + throw new BugException("Unable to invoke getMethod of "+this, e); } catch (InvocationTargetException e) { throw Reflection.handleWrappedException(e); } @@ -617,9 +617,9 @@ public class DoubleModel implements ChangeListener, ChangeSource { try { setMethod.invoke(source, v/multiplier); } catch (IllegalArgumentException e) { - throw new BugException("BUG: Unable to invoke setMethod of "+this, e); + throw new BugException("Unable to invoke setMethod of "+this, e); } catch (IllegalAccessException e) { - throw new BugException("BUG: Unable to invoke setMethod of "+this, e); + throw new BugException("Unable to invoke setMethod of "+this, e); } catch (InvocationTargetException e) { throw Reflection.handleWrappedException(e); } diff --git a/src/net/sf/openrocket/gui/components/URLLabel.java b/src/net/sf/openrocket/gui/components/URLLabel.java index 52747d1b..b70cd4b1 100644 --- a/src/net/sf/openrocket/gui/components/URLLabel.java +++ b/src/net/sf/openrocket/gui/components/URLLabel.java @@ -63,7 +63,7 @@ public class URLLabel extends SelectableLabel { try { d.browse(new URI(url)); } catch (URISyntaxException e1) { - throw new BugException("BUG: Illegal URL: " + url, e1); + throw new BugException("Illegal URL: " + url, e1); } catch (IOException e1) { log.error("Unable to launch browser: " + e1.getMessage(), e1); } diff --git a/src/net/sf/openrocket/gui/dialogs/DebugLogDialog.java b/src/net/sf/openrocket/gui/dialogs/DebugLogDialog.java new file mode 100644 index 00000000..c67568fc --- /dev/null +++ b/src/net/sf/openrocket/gui/dialogs/DebugLogDialog.java @@ -0,0 +1,518 @@ +package net.sf.openrocket.gui.dialogs; + +import java.awt.Color; +import java.awt.Component; +import java.awt.Rectangle; +import java.awt.Toolkit; +import java.awt.Window; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.EnumMap; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JSplitPane; +import javax.swing.JTable; +import javax.swing.JTextArea; +import javax.swing.ListSelectionModel; +import javax.swing.RowFilter; +import javax.swing.SwingUtilities; +import javax.swing.Timer; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; +import javax.swing.table.TableCellRenderer; +import javax.swing.table.TableModel; +import javax.swing.table.TableRowSorter; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.gui.adaptors.Column; +import net.sf.openrocket.gui.adaptors.ColumnTableModel; +import net.sf.openrocket.gui.components.SelectableLabel; +import net.sf.openrocket.logging.DelegatorLogger; +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.logging.LogLevel; +import net.sf.openrocket.logging.LogLevelBufferLogger; +import net.sf.openrocket.logging.LogLine; +import net.sf.openrocket.logging.StackTraceWriter; +import net.sf.openrocket.logging.TraceException; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.util.GUIUtil; +import net.sf.openrocket.util.NumericComparator; + +public class DebugLogDialog extends JDialog { + private static final LogHelper log = Application.getLogger(); + + private static final int POLL_TIME = 250; + private static final String STACK_TRACE_MARK = "\uFF01"; + + private static final EnumMap backgroundColors = new EnumMap(LogLevel.class); + static { + for (LogLevel l : LogLevel.values()) { + // Just to ensure every level has a bg color + backgroundColors.put(l, Color.ORANGE); + } + final int hi = 255; + final int lo = 150; + backgroundColors.put(LogLevel.ERROR, new Color(hi, lo, lo)); + backgroundColors.put(LogLevel.WARN, new Color(hi, (hi + lo) / 2, lo)); + backgroundColors.put(LogLevel.USER, new Color(lo, lo, hi)); + backgroundColors.put(LogLevel.INFO, new Color(hi, hi, lo)); + backgroundColors.put(LogLevel.DEBUG, new Color(lo, hi, lo)); + backgroundColors.put(LogLevel.VBOSE, new Color(lo, hi, (hi + lo) / 2)); + } + + /** Buffer containing the log lines displayed */ + private final List buffer = new ArrayList(); + + /** Queue of log lines to be added to the displayed buffer */ + private final Queue queue = new ConcurrentLinkedQueue(); + + private final DelegatorLogger delegator; + private final LogListener logListener; + + private final EnumMap filterButtons = new EnumMap(LogLevel.class); + private final JCheckBox followBox; + private final Timer timer; + + + private final JTable table; + private final ColumnTableModel model; + private final TableRowSorter sorter; + + private final SelectableLabel numberLabel; + private final SelectableLabel timeLabel; + private final SelectableLabel levelLabel; + private final SelectableLabel locationLabel; + private final SelectableLabel messageLabel; + private final JTextArea stackTraceLabel; + + public DebugLogDialog(Window parent) { + super(parent, "OpenRocket debug log"); + + // Start listening to log lines + LogHelper applicationLog = Application.getLogger(); + if (applicationLog instanceof DelegatorLogger) { + log.info("Adding log listener"); + delegator = (DelegatorLogger) applicationLog; + logListener = new LogListener(); + delegator.addLogger(logListener); + } else { + log.warn("Application log is not a DelegatorLogger"); + delegator = null; + logListener = null; + } + + // Fetch old log lines + LogLevelBufferLogger bufferLogger = Application.getLogBuffer(); + if (bufferLogger != null) { + buffer.addAll(bufferLogger.getLogs()); + } else { + log.warn("Application does not have a log buffer"); + } + + + // Create the UI + JPanel mainPanel = new JPanel(new MigLayout("fill")); + this.add(mainPanel); + + + JSplitPane split = new JSplitPane(JSplitPane.VERTICAL_SPLIT); + split.setDividerLocation(0.7); + mainPanel.add(split, "grow"); + + // Top panel + JPanel panel = new JPanel(new MigLayout("fill")); + split.add(panel); + + panel.add(new JLabel("Display log lines:"), "gapright para, split"); + for (LogLevel l : LogLevel.values()) { + JCheckBox box = new JCheckBox(l.toString()); + box.setSelected(true); + box.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + sorter.setRowFilter(new LogFilter()); + } + }); + panel.add(box, "gapright unrel"); + filterButtons.put(l, box); + } + + followBox = new JCheckBox("Follow"); + followBox.setSelected(true); + panel.add(followBox, "skip, gapright para, right"); + + JButton clear = new JButton("Clear"); + clear.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + log.user("Clearing log buffer"); + buffer.clear(); + queue.clear(); + model.fireTableDataChanged(); + } + }); + panel.add(clear, "right, wrap"); + + + + // Create the table model + model = new ColumnTableModel( + + new Column("#") { + @Override + public Object getValueAt(int row) { + return buffer.get(row).getLogCount(); + } + + @Override + public int getDefaultWidth() { + return 60; + } + }, + new Column("Time") { + @Override + public Object getValueAt(int row) { + return String.format("%.3f", buffer.get(row).getTimestamp() / 1000.0); + } + + @Override + public int getDefaultWidth() { + return 60; + } + }, + new Column("Level") { + @Override + public Object getValueAt(int row) { + return buffer.get(row).getLevel(); + } + + @Override + public int getDefaultWidth() { + return 60; + } + }, + new Column("") { + @Override + public Object getValueAt(int row) { + if (buffer.get(row).getCause() != null) { + return STACK_TRACE_MARK; + } else { + return ""; + } + } + + @Override + public int getExactWidth() { + return 16; + } + }, + new Column("Location") { + @Override + public Object getValueAt(int row) { + TraceException e = buffer.get(row).getTrace(); + if (e != null) { + return e.getMessage(); + } else { + return ""; + } + } + + @Override + public int getDefaultWidth() { + return 200; + } + }, + new Column("Message") { + @Override + public Object getValueAt(int row) { + return buffer.get(row).getMessage(); + } + + @Override + public int getDefaultWidth() { + return 580; + } + } + + ) { + @Override + public int getRowCount() { + return buffer.size(); + } + }; + + table = new JTable(model); + table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + table.setSelectionBackground(Color.LIGHT_GRAY); + table.setSelectionForeground(Color.BLACK); + model.setColumnWidths(table.getColumnModel()); + table.setDefaultRenderer(Object.class, new Renderer()); + + table.getSelectionModel().addListSelectionListener(new ListSelectionListener() { + @Override + public void valueChanged(ListSelectionEvent e) { + int row = table.getSelectedRow(); + if (row >= 0) { + row = sorter.convertRowIndexToModel(row); + } + updateSelected(row); + } + }); + + sorter = new TableRowSorter(model); + sorter.setComparator(0, NumericComparator.INSTANCE); + sorter.setComparator(1, NumericComparator.INSTANCE); + sorter.setComparator(4, new LocationComparator()); + table.setRowSorter(sorter); + + + panel.add(new JScrollPane(table), "span, grow, width " + + (Toolkit.getDefaultToolkit().getScreenSize().width * 8 / 10) + + "px, height 400px"); + + + panel = new JPanel(new MigLayout("fill")); + split.add(panel); + + panel.add(new JLabel("Log line number:"), "split, gapright rel"); + numberLabel = new SelectableLabel(); + panel.add(numberLabel, "width 70lp, gapright para"); + + panel.add(new JLabel("Time:"), "split, gapright rel"); + timeLabel = new SelectableLabel(); + panel.add(timeLabel, "width 70lp, gapright para"); + + panel.add(new JLabel("Level:"), "split, gapright rel"); + levelLabel = new SelectableLabel(); + panel.add(levelLabel, "width 70lp, gapright para"); + + panel.add(new JLabel("Location:"), "split, gapright rel"); + locationLabel = new SelectableLabel(); + panel.add(locationLabel, "growx, wrap unrel"); + + panel.add(new JLabel("Log message:"), "split, gapright rel"); + messageLabel = new SelectableLabel(); + panel.add(messageLabel, "growx, wrap para"); + + panel.add(new JLabel("Stack trace:"), "wrap rel"); + stackTraceLabel = new JTextArea(8, 80); + stackTraceLabel.setEditable(false); + GUIUtil.changeFontSize(stackTraceLabel, -2); + panel.add(new JScrollPane(stackTraceLabel), "grow"); + + + JButton close = new JButton("Close"); + close.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + DebugLogDialog.this.dispose(); + } + }); + mainPanel.add(close, "newline para, right, tag ok"); + + + // Use timer to purge the queue so as not to overwhelm the EDT with events + timer = new Timer(POLL_TIME, new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + purgeQueue(); + } + }); + timer.setRepeats(true); + timer.start(); + + this.addWindowListener(new WindowAdapter() { + @Override + public void windowClosed(WindowEvent e) { + log.user("Closing debug log dialog"); + timer.stop(); + if (delegator != null) { + log.info("Removing log listener"); + delegator.removeLogger(logListener); + } + } + }); + + GUIUtil.setDisposableDialogOptions(this, close); + followBox.requestFocus(); + } + + + + private void updateSelected(int row) { + if (row < 0) { + + numberLabel.setText(""); + timeLabel.setText(""); + levelLabel.setText(""); + locationLabel.setText(""); + messageLabel.setText(""); + stackTraceLabel.setText(""); + + } else { + + LogLine line = buffer.get(row); + numberLabel.setText("" + line.getLogCount()); + timeLabel.setText(String.format("%.3f s", line.getTimestamp() / 1000.0)); + levelLabel.setText(line.getLevel().toString()); + TraceException e = line.getTrace(); + if (e != null) { + locationLabel.setText(e.getMessage()); + } else { + locationLabel.setText("-"); + } + messageLabel.setText(line.getMessage()); + Throwable t = line.getCause(); + if (t != null) { + StackTraceWriter stw = new StackTraceWriter(); + PrintWriter pw = new PrintWriter(stw); + t.printStackTrace(pw); + pw.flush(); + stackTraceLabel.setText(stw.toString()); + stackTraceLabel.setCaretPosition(0); + } else { + stackTraceLabel.setText(""); + } + + } + } + + + /** + * Check whether a row signifies a number of missing rows. This check is "heuristic" + * and checks whether the timestamp is zero and the message starts with "---". + */ + private boolean isExcludedRow(int row) { + LogLine line = buffer.get(row); + return (line.getTimestamp() == 0) && (line.getMessage().startsWith("---")); + } + + + /** + * Purge the queue of incoming log lines. This is called periodically from the EDT, and + * it adds any lines in the queue to the buffer, and fires a table event. + */ + private void purgeQueue() { + int start = buffer.size(); + + LogLine line; + while ((line = queue.poll()) != null) { + buffer.add(line); + } + + int end = buffer.size() - 1; + if (end >= start) { + model.fireTableRowsInserted(start, end); + if (followBox.isSelected()) { + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + Rectangle rect = table.getCellRect(1000000000, 1, true); + table.scrollRectToVisible(rect); + } + }); + } + } + } + + + /** + * A logger that adds log lines to the queue. This method may be called from any + * thread, and therefore must be thread-safe. + */ + private class LogListener extends LogHelper { + @Override + public void log(LogLine line) { + queue.add(line); + } + } + + private class LogFilter extends RowFilter { + + @Override + public boolean include(RowFilter.Entry entry) { + int index = entry.getIdentifier(); + LogLine line = buffer.get(index); + return filterButtons.get(line.getLevel()).isSelected(); + } + + } + + + private class Renderer extends JLabel implements TableCellRenderer { + @Override + public Component getTableCellRendererComponent(JTable table1, Object value, boolean isSelected, boolean hasFocus, + int row, int column) { + Color fg, bg; + + row = sorter.convertRowIndexToModel(row); + + if (STACK_TRACE_MARK.equals(value)) { + fg = Color.RED; + } else { + fg = table1.getForeground(); + } + bg = backgroundColors.get(buffer.get(row).getLevel()); + + if (isSelected) { + bg = bg.darker(); + } else if (isExcludedRow(row)) { + bg = bg.brighter(); + } + + this.setForeground(fg); + this.setBackground(bg); + + this.setOpaque(true); + this.setText(value.toString()); + + return this; + } + } + + + private class LocationComparator implements Comparator { + private final Pattern splitPattern = Pattern.compile("^\\(([^:]*+):([0-9]++).*\\)$"); + + @Override + public int compare(Object o1, Object o2) { + String s1 = o1.toString(); + String s2 = o2.toString(); + + Matcher m1 = splitPattern.matcher(s1); + Matcher m2 = splitPattern.matcher(s2); + + if (m1.matches() && m2.matches()) { + String class1 = m1.group(1); + String pos1 = m1.group(2); + String class2 = m2.group(1); + String pos2 = m2.group(2); + + if (class1.equals(class2)) { + return NumericComparator.INSTANCE.compare(pos1, pos2); + } else { + return class1.compareTo(class2); + } + } + + return s1.compareTo(s2); + } + + } + +} diff --git a/src/net/sf/openrocket/gui/dialogs/SwingWorkerDialog.java b/src/net/sf/openrocket/gui/dialogs/SwingWorkerDialog.java index 70730ba8..939514b0 100644 --- a/src/net/sf/openrocket/gui/dialogs/SwingWorkerDialog.java +++ b/src/net/sf/openrocket/gui/dialogs/SwingWorkerDialog.java @@ -15,6 +15,8 @@ import javax.swing.JProgressBar; import javax.swing.SwingWorker; import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.startup.Application; import net.sf.openrocket.util.MathUtil; @@ -27,7 +29,8 @@ import net.sf.openrocket.util.MathUtil; * @author Sampo Niskanen */ public class SwingWorkerDialog extends JDialog implements PropertyChangeListener { - + private static final LogHelper log = Application.getLogger(); + /** Number of milliseconds to wait at a time between checking worker status */ private static final int DELAY = 100; @@ -39,21 +42,20 @@ public class SwingWorkerDialog extends JDialog implements PropertyChangeListener /** Open the dialog if estimated total time is longed than this */ private static final int TOTAL_TIME_FOR_DIALOG = 2000; - - private final SwingWorker worker; + + private final SwingWorker worker; private final JProgressBar progressBar; private boolean cancelled = false; - private SwingWorkerDialog(Window parent, String title, String label, - SwingWorker w) { + private SwingWorkerDialog(Window parent, String title, String label, SwingWorker w) { super(parent, title, ModalityType.APPLICATION_MODAL); this.worker = w; w.addPropertyChangeListener(this); - + JPanel panel = new JPanel(new MigLayout("fill")); if (label != null) { @@ -67,18 +69,19 @@ public class SwingWorkerDialog extends JDialog implements PropertyChangeListener cancel.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { + log.user("User cancelled SwingWorker operation"); cancel(); } }); panel.add(cancel, "right"); this.add(panel); - this.setMinimumSize(new Dimension(250,100)); + this.setMinimumSize(new Dimension(250, 100)); this.pack(); this.setLocationRelativeTo(parent); this.setDefaultCloseOperation(DO_NOTHING_ON_CLOSE); } - + @Override public void propertyChange(PropertyChangeEvent evt) { if (worker.getState() == SwingWorker.StateValue.DONE) { @@ -118,12 +121,14 @@ public class SwingWorkerDialog extends JDialog implements PropertyChangeListener * false if the user cancelled the operation */ public static boolean runWorker(Window parent, String title, String label, - SwingWorker worker) { + SwingWorker worker) { + + log.info("Running SwingWorker " + worker); // Start timing the worker final long startTime = System.currentTimeMillis(); worker.execute(); - + // Monitor worker thread before opening the dialog while (true) { @@ -131,11 +136,12 @@ public class SwingWorkerDialog extends JDialog implements PropertyChangeListener Thread.sleep(DELAY); } catch (InterruptedException e) { // Should never occur - e.printStackTrace(); + log.error("EDT was interrupted", e); } if (worker.isDone()) { // Worker has completed within time limits + log.info("Worker completed before opening dialog"); return true; } @@ -144,24 +150,28 @@ public class SwingWorkerDialog extends JDialog implements PropertyChangeListener if (elapsed < ESTIMATION_DELAY) continue; - + // Calculate and check estimated remaining time int progress = MathUtil.clamp(worker.getProgress(), 1, 100); // Avoid div-by-zero long estimate = elapsed * 100 / progress; long remaining = estimate - elapsed; + log.debug("Estimated run time, estimate=" + estimate + " remaining=" + remaining); + if (estimate >= TOTAL_TIME_FOR_DIALOG) break; if (remaining >= REMAINING_TIME_FOR_DIALOG) break; } - + // Dialog is required + log.info("Opening dialog for SwingWorker " + worker); SwingWorkerDialog dialog = new SwingWorkerDialog(parent, title, label, worker); dialog.setVisible(true); + log.info("Worker done, cancelled=" + dialog.cancelled); return !dialog.cancelled; } diff --git a/src/net/sf/openrocket/gui/dialogs/motor/thrustcurve/ThrustCurveMotorSelectionPanel.java b/src/net/sf/openrocket/gui/dialogs/motor/thrustcurve/ThrustCurveMotorSelectionPanel.java index 94f38a97..d3d5e57f 100644 --- a/src/net/sf/openrocket/gui/dialogs/motor/thrustcurve/ThrustCurveMotorSelectionPanel.java +++ b/src/net/sf/openrocket/gui/dialogs/motor/thrustcurve/ThrustCurveMotorSelectionPanel.java @@ -205,23 +205,23 @@ public class ThrustCurveMotorSelectionPanel extends JPanel implements MotorSelec sel = SHOW_ALL; switch (sel) { case SHOW_ALL: - sorter.setRowFilter(new MotorRowFilterAll()); - break; - - case SHOW_SMALLER: - sorter.setRowFilter(new MotorRowFilterSmaller()); - break; - - case SHOW_EXACT: - sorter.setRowFilter(new MotorRowFilterExact()); - break; - - default: - assert (false) : "Should not occur."; - } - Prefs.putChoise("MotorDiameterMatch", sel); - scrollSelectionVisible(); - } + sorter.setRowFilter(new MotorRowFilterAll()); + break; + + case SHOW_SMALLER: + sorter.setRowFilter(new MotorRowFilterSmaller()); + break; + + case SHOW_EXACT: + sorter.setRowFilter(new MotorRowFilterExact()); + break; + + default: + throw new BugException("Invalid selection mode sel=" + sel); + } + Prefs.putChoise("MotorDiameterMatch", sel); + scrollSelectionVisible(); + } }); panel.add(filterComboBox, "spanx, growx, wrap rel"); diff --git a/src/net/sf/openrocket/gui/main/BareComponentTreeModel.java b/src/net/sf/openrocket/gui/main/BareComponentTreeModel.java deleted file mode 100644 index 5c36178a..00000000 --- a/src/net/sf/openrocket/gui/main/BareComponentTreeModel.java +++ /dev/null @@ -1,188 +0,0 @@ -package net.sf.openrocket.gui.main; - -import java.util.ArrayList; -import java.util.Enumeration; -import java.util.Iterator; - -import javax.swing.JTree; -import javax.swing.event.TreeModelEvent; -import javax.swing.event.TreeModelListener; -import javax.swing.tree.TreeModel; -import javax.swing.tree.TreePath; - -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.rocketcomponent.ComponentChangeEvent; -import net.sf.openrocket.rocketcomponent.ComponentChangeListener; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.startup.Application; - - -/** - * A TreeModel that implements viewing of the rocket tree structure. - * This class shows the internal structure of the tree, as opposed to the regular - * user-side view. - * - * @author Sampo Niskanen - */ - -public class BareComponentTreeModel implements TreeModel, ComponentChangeListener { - private static final LogHelper log = Application.getLogger(); - - private ArrayList listeners = new ArrayList(); - - private final RocketComponent root; - private final JTree tree; - - public BareComponentTreeModel(RocketComponent root, JTree tree) { - this.root = root; - this.tree = tree; - root.addComponentChangeListener(this); - } - - - public Object getChild(Object parent, int index) { - RocketComponent component = (RocketComponent)parent; - try { - return component.getChild(index); - } catch (IndexOutOfBoundsException e) { - return null; - } - } - - public int getChildCount(Object parent) { - return ((RocketComponent)parent).getChildCount(); - } - - public int getIndexOfChild(Object parent, Object child) { - RocketComponent p = (RocketComponent)parent; - RocketComponent c = (RocketComponent)child; - return p.getChildPosition(c); - } - - public Object getRoot() { - return root; - } - - public boolean isLeaf(Object node) { - RocketComponent c = (RocketComponent)node; - return (c.getChildCount()==0); - } - - public void addTreeModelListener(TreeModelListener l) { - listeners.add(l); - } - - public void removeTreeModelListener(TreeModelListener l) { - listeners.remove(l); - } - - private void fireTreeNodesChanged() { - Object[] path = { root }; - TreeModelEvent e = new TreeModelEvent(this,path); - Object[] l = listeners.toArray(); - for (int i=0; i enumer = tree.getExpandedDescendants(new TreePath(path)); - ArrayList expanded = new ArrayList(); - if (enumer != null) { - while (enumer.hasMoreElements()) { - TreePath p = enumer.nextElement(); - expanded.add(((RocketComponent)p.getLastPathComponent()).getID()); - } - } - - // Send structure change event - TreeModelEvent e = new TreeModelEvent(this,path); - Object[] l = listeners.toArray(); - for (int i=0; i iter = expanded.iterator(); - while (iter.hasNext()) { - RocketComponent c = root.findComponent(iter.next()); - if (c==null) - continue; - tree.expandPath(makeTreePath(c)); - } - if (source != null) { - TreePath p = makeTreePath(source); - tree.makeVisible(p); - tree.expandPath(p); - } - } - - public void valueForPathChanged(TreePath path, Object newValue) { - log.error("ERROR: valueForPathChanged called?!"); - } - - - public void componentChanged(ComponentChangeEvent e) { - if (e.isTreeChange() || e.isUndoChange()) { - // Tree must be fully updated also in case of an undo change - fireTreeStructureChanged((RocketComponent)e.getSource()); - if (e.isTreeChange() && e.isUndoChange()) { - // If the undo has changed the tree structure, some elements may be hidden unnecessarily - // TODO: LOW: Could this be performed better? - expandAll(); - } - } else if (e.isOtherChange()) { - fireTreeNodesChanged(); - } - } - - public void expandAll() { - Iterator iterator = root.deepIterator(); - while (iterator.hasNext()) { - tree.makeVisible(makeTreePath(iterator.next())); - } - } - - public static TreePath makeTreePath(RocketComponent component) { - int count = 0; - RocketComponent c = component; - - while (c != null) { - count++; - c = c.getParent(); - } - - Object[] list = new Object[count]; - - count--; - c=component; - while (c!=null) { - list[count] = c; - count--; - c = c.getParent(); - } - - return new TreePath(list); - } - -} diff --git a/src/net/sf/openrocket/gui/main/BasicFrame.java b/src/net/sf/openrocket/gui/main/BasicFrame.java index aba53986..4031be8a 100644 --- a/src/net/sf/openrocket/gui/main/BasicFrame.java +++ b/src/net/sf/openrocket/gui/main/BasicFrame.java @@ -19,12 +19,12 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; -import java.lang.reflect.InvocationTargetException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.net.URLDecoder; import java.util.ArrayList; +import java.util.Arrays; import java.util.concurrent.ExecutionException; import javax.swing.Action; @@ -47,8 +47,6 @@ import javax.swing.KeyStroke; import javax.swing.ListSelectionModel; import javax.swing.ScrollPaneConstants; import javax.swing.SwingUtilities; -import javax.swing.Timer; -import javax.swing.ToolTipManager; import javax.swing.border.TitledBorder; import javax.swing.event.TreeSelectionEvent; import javax.swing.event.TreeSelectionListener; @@ -59,9 +57,6 @@ import javax.swing.tree.TreeSelectionModel; import net.miginfocom.swing.MigLayout; import net.sf.openrocket.aerodynamics.WarningSet; -import net.sf.openrocket.communication.UpdateInfo; -import net.sf.openrocket.communication.UpdateInfoRetriever; -import net.sf.openrocket.database.Databases; import net.sf.openrocket.document.OpenRocketDocument; import net.sf.openrocket.file.GeneralRocketLoader; import net.sf.openrocket.file.RocketLoadException; @@ -73,13 +68,14 @@ import net.sf.openrocket.gui.configdialog.ComponentConfigDialog; import net.sf.openrocket.gui.dialogs.AboutDialog; import net.sf.openrocket.gui.dialogs.BugReportDialog; import net.sf.openrocket.gui.dialogs.ComponentAnalysisDialog; +import net.sf.openrocket.gui.dialogs.DebugLogDialog; import net.sf.openrocket.gui.dialogs.ExampleDesignDialog; import net.sf.openrocket.gui.dialogs.LicenseDialog; import net.sf.openrocket.gui.dialogs.MotorDatabaseLoadingDialog; import net.sf.openrocket.gui.dialogs.SwingWorkerDialog; -import net.sf.openrocket.gui.dialogs.UpdateInfoDialog; import net.sf.openrocket.gui.dialogs.WarningDialog; import net.sf.openrocket.gui.dialogs.preferences.PreferencesDialog; +import net.sf.openrocket.gui.main.componenttree.ComponentTree; import net.sf.openrocket.gui.scalefigure.RocketPanel; import net.sf.openrocket.logging.LogHelper; import net.sf.openrocket.rocketcomponent.ComponentChangeEvent; @@ -283,7 +279,7 @@ public class BasicFrame extends JFrame { JPanel panel = new JPanel(new MigLayout("fill, flowy", "", "[grow]")); - tree = new ComponentTree(rocket); + tree = new ComponentTree(document); tree.setSelectionModel(componentSelectionModel); // Remove JTree key events that interfere with menu accelerators @@ -404,9 +400,12 @@ public class BasicFrame extends JFrame { item.setIcon(Icons.FILE_NEW); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { + log.user("New... selected"); newAction(); - if (replaceable) + if (replaceable) { + log.info("Closing previous window"); closeAction(); + } } }); menu.add(item); @@ -417,6 +416,7 @@ public class BasicFrame extends JFrame { item.setIcon(Icons.FILE_OPEN); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { + log.user("Open... selected"); openAction(); } }); @@ -429,9 +429,11 @@ public class BasicFrame extends JFrame { item.setIcon(Icons.FILE_OPEN_EXAMPLE); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { + log.user("Open example... selected"); URL[] urls = ExampleDesignDialog.selectExampleDesigns(BasicFrame.this); if (urls != null) { for (URL u : urls) { + log.user("Opening example " + u); open(u, BasicFrame.this); } } @@ -447,6 +449,7 @@ public class BasicFrame extends JFrame { item.setIcon(Icons.FILE_SAVE); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { + log.user("Save selected"); saveAction(); } }); @@ -460,6 +463,7 @@ public class BasicFrame extends JFrame { item.setIcon(Icons.FILE_SAVE_AS); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { + log.user("Save as... selected"); saveAsAction(); } }); @@ -474,6 +478,7 @@ public class BasicFrame extends JFrame { item.setIcon(Icons.FILE_CLOSE); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { + log.user("Close selected"); closeAction(); } }); @@ -487,6 +492,7 @@ public class BasicFrame extends JFrame { item.setIcon(Icons.FILE_QUIT); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { + log.user("Quit selected"); quitAction(); } }); @@ -540,6 +546,7 @@ public class BasicFrame extends JFrame { "preferences"); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { + log.user("Preferences selected"); PreferencesDialog.showPreferences(); } }); @@ -559,6 +566,7 @@ public class BasicFrame extends JFrame { "separately"); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { + log.user("Component analysis selected"); ComponentAnalysisDialog.showDialog(rocketpanel); } }); @@ -586,26 +594,42 @@ public class BasicFrame extends JFrame { item.getAccessibleContext().setAccessibleDescription("OpenRocket license information"); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { + log.user("License selected"); new LicenseDialog(BasicFrame.this).setVisible(true); } }); menu.add(item); + menu.addSeparator(); + item = new JMenuItem("Bug report", KeyEvent.VK_B); item.getAccessibleContext().setAccessibleDescription("Information about reporting " + "bugs in OpenRocket"); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { - // new BugDialog(BasicFrame.this).setVisible(true); + log.user("Bug report selected"); BugReportDialog.showBugReportDialog(BasicFrame.this); } }); menu.add(item); + item = new JMenuItem("Debug log"); + item.getAccessibleContext().setAccessibleDescription("View the OpenRocket debug log"); + item.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + log.user("Debug log selected"); + new DebugLogDialog(BasicFrame.this).setVisible(true); + } + }); + menu.add(item); + + menu.addSeparator(); + item = new JMenuItem("About", KeyEvent.VK_A); item.getAccessibleContext().setAccessibleDescription("About OpenRocket"); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { + log.user("About selected"); new AboutDialog(BasicFrame.this).setVisible(true); } }); @@ -627,6 +651,7 @@ public class BasicFrame extends JFrame { item = new JMenuItem("What is this menu?"); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { + log.user("What is this menu? selected"); JOptionPane.showMessageDialog(BasicFrame.this, new Object[] { "The 'Debug' menu includes actions for testing and debugging " + @@ -645,6 +670,7 @@ public class BasicFrame extends JFrame { item.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { + log.user("Create test rocket selected"); JTextField field = new JTextField(); int sel = JOptionPane.showOptionDialog(BasicFrame.this, new Object[] { "Input text key to generate random rocket:", @@ -677,6 +703,7 @@ public class BasicFrame extends JFrame { item.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { + log.user("Create Iso-Haisu selected"); Rocket r = TestRockets.makeIsoHaisu(); OpenRocketDocument doc = new OpenRocketDocument(r); doc.setSaved(true); @@ -691,6 +718,7 @@ public class BasicFrame extends JFrame { item.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { + log.user("Create Big Blue selected"); Rocket r = TestRockets.makeBigBlue(); OpenRocketDocument doc = new OpenRocketDocument(r); doc.setSaved(true); @@ -700,13 +728,12 @@ public class BasicFrame extends JFrame { }); menu.add(item); - - menu.addSeparator(); item = new JMenuItem("Exception here"); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { + log.user("Exception here selected"); throw new RuntimeException("Testing exception from menu action listener"); } }); @@ -715,6 +742,7 @@ public class BasicFrame extends JFrame { item = new JMenuItem("Exception from EDT"); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { + log.user("Exception from EDT selected"); SwingUtilities.invokeLater(new Runnable() { @Override public void run() { @@ -729,6 +757,7 @@ public class BasicFrame extends JFrame { item = new JMenuItem("Exception from other thread"); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { + log.user("Exception from other thread selected"); new Thread() { @Override public void run() { @@ -769,19 +798,24 @@ public class BasicFrame extends JFrame { chooser.setFileSelectionMode(JFileChooser.FILES_ONLY); chooser.setMultiSelectionEnabled(true); chooser.setCurrentDirectory(Prefs.getDefaultDirectory()); - if (chooser.showOpenDialog(this) != JFileChooser.APPROVE_OPTION) + int option = chooser.showOpenDialog(this); + if (option != JFileChooser.APPROVE_OPTION) { + log.user("Decided not to open files, option=" + option); return; + } Prefs.setDefaultDirectory(chooser.getCurrentDirectory()); File[] files = chooser.getSelectedFiles(); + log.user("Opening files " + Arrays.toString(files)); for (File file : files) { - System.out.println("Opening file: " + file); + log.info("Opening file: " + file); if (open(file, this)) { // Close previous window if replacing if (replaceable && document.isSaved()) { + log.info("Closing window because it is replaceable"); closeAction(); replaceable = false; } @@ -790,10 +824,17 @@ public class BasicFrame extends JFrame { } - + /** + * Open a file based on a URL. + * @param url the file to open. + * @param parent the parent window for dialogs. + * @return true if opened successfully. + */ private static boolean open(URL url, BasicFrame parent) { String filename = null; + // First figure out the file name from the URL + // Try using URI.getPath(); try { URI uri = url.toURI(); @@ -819,6 +860,9 @@ public class BasicFrame extends JFrame { filename = filename.substring(filename.lastIndexOf('/') + 1); } + + // Open the file + log.info("Opening file from url=" + url + " filename=" + filename); try { InputStream is = url.openStream(); if (open(is, filename, parent)) { @@ -829,6 +873,7 @@ public class BasicFrame extends JFrame { } } } catch (IOException e) { + log.warn("Error opening file" + e); JOptionPane.showMessageDialog(parent, "An error occurred while opening the file " + filename, "Error loading file", JOptionPane.ERROR_MESSAGE); @@ -876,16 +921,15 @@ public class BasicFrame extends JFrame { * @param parent * @return */ - private static boolean open(OpenFileWorker worker, String filename, File file, - Window parent) { + private static boolean open(OpenFileWorker worker, String filename, File file, Window parent) { - MotorDatabaseLoadingDialog.check(null); + MotorDatabaseLoadingDialog.check(parent); // Open the file in a Swing worker thread - if (!SwingWorkerDialog.runWorker(parent, "Opening file", - "Reading " + filename + "...", worker)) { - + log.info("Starting OpenFileWorker"); + if (!SwingWorkerDialog.runWorker(parent, "Opening file", "Reading " + filename + "...", worker)) { // User cancelled the operation + log.info("User cancelled the OpenFileWorker"); return false; } @@ -902,6 +946,7 @@ public class BasicFrame extends JFrame { if (cause instanceof FileNotFoundException) { + log.warn("File not found", cause); JOptionPane.showMessageDialog(parent, "File not found: " + filename, "Error opening file", JOptionPane.ERROR_MESSAGE); @@ -909,6 +954,7 @@ public class BasicFrame extends JFrame { } else if (cause instanceof RocketLoadException) { + log.warn("Error loading the file", cause); JOptionPane.showMessageDialog(parent, "Unable to open file '" + filename + "': " + cause.getMessage(), @@ -926,13 +972,14 @@ public class BasicFrame extends JFrame { } if (doc == null) { - throw new BugException("BUG: Document loader returned null"); + throw new BugException("Document loader returned null"); } // Show warnings WarningSet warnings = worker.getRocketLoader().getWarnings(); if (!warnings.isEmpty()) { + log.info("Warnings while reading file: " + warnings); WarningDialog.showWarnings(parent, new Object[] { "The following problems were encountered while opening " + filename + ".", @@ -947,6 +994,7 @@ public class BasicFrame extends JFrame { doc.setSaved(true); // Open the frame + log.debug("Opening new frame with the document"); BasicFrame frame = new BasicFrame(doc); frame.setVisible(true); @@ -960,21 +1008,26 @@ public class BasicFrame extends JFrame { private boolean saveAction() { File file = document.getFile(); if (file == null) { + log.info("Document does not contain file, opening save as dialog instead"); return saveAsAction(); } + log.info("Saving document to " + file); // Saving RockSim designs is not supported if (ROCKSIM_DESIGN_FILTER.accept(file)) { file = new File(file.getAbsolutePath().replaceAll(".[rR][kK][tT](.[gG][zZ])?$", ".ork")); + log.info("Attempting to save in RockSim format, renaming to " + file); int option = JOptionPane.showConfirmDialog(this, new Object[] { "Saving designs in RockSim format is not supported.", "Save in OpenRocket format instead (" + file.getName() + ")?" }, "Save " + file.getName(), JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, null); - if (option != JOptionPane.YES_OPTION) + if (option != JOptionPane.YES_OPTION) { + log.user("User chose not to save"); return false; + } document.setFile(file); } @@ -984,52 +1037,60 @@ public class BasicFrame extends JFrame { private boolean saveAsAction() { File file = null; - while (file == null) { - // TODO: HIGH: what if *.rkt chosen? - StorageOptionChooser storageChooser = - new StorageOptionChooser(document, document.getDefaultStorageOptions()); - JFileChooser chooser = new JFileChooser(); - chooser.setFileFilter(OPENROCKET_DESIGN_FILTER); - chooser.setCurrentDirectory(Prefs.getDefaultDirectory()); - chooser.setAccessory(storageChooser); - if (document.getFile() != null) - chooser.setSelectedFile(document.getFile()); - - if (chooser.showSaveDialog(BasicFrame.this) != JFileChooser.APPROVE_OPTION) - return false; - - file = chooser.getSelectedFile(); - if (file == null) + + // TODO: HIGH: what if *.rkt chosen? + StorageOptionChooser storageChooser = + new StorageOptionChooser(document, document.getDefaultStorageOptions()); + JFileChooser chooser = new JFileChooser(); + chooser.setFileFilter(OPENROCKET_DESIGN_FILTER); + chooser.setCurrentDirectory(Prefs.getDefaultDirectory()); + chooser.setAccessory(storageChooser); + if (document.getFile() != null) + chooser.setSelectedFile(document.getFile()); + + int option = chooser.showSaveDialog(BasicFrame.this); + if (option != JFileChooser.APPROVE_OPTION) { + log.user("User decided not to save, option=" + option); + return false; + } + + file = chooser.getSelectedFile(); + if (file == null) { + log.user("User did not select a file"); + return false; + } + + Prefs.setDefaultDirectory(chooser.getCurrentDirectory()); + storageChooser.storeOptions(document.getDefaultStorageOptions()); + + if (file.getName().indexOf('.') < 0) { + log.debug("File name does not contain extension, adding .ork"); + String name = file.getAbsolutePath(); + name = name + ".ork"; + file = new File(name); + } + + if (file.exists()) { + log.info("File " + file + " exists, confirming overwrite from user"); + int result = JOptionPane.showConfirmDialog(this, + "File '" + file.getName() + "' exists. Do you want to overwrite it?", + "File exists", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE); + if (result != JOptionPane.YES_OPTION) { + log.user("User decided not to overwrite the file"); return false; - - Prefs.setDefaultDirectory(chooser.getCurrentDirectory()); - storageChooser.storeOptions(document.getDefaultStorageOptions()); - - if (file.getName().indexOf('.') < 0) { - String name = file.getAbsolutePath(); - name = name + ".ork"; - file = new File(name); - } - - if (file.exists()) { - int result = JOptionPane.showConfirmDialog(this, - "File '" + file.getName() + "' exists. Do you want to overwrite it?", - "File exists", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE); - if (result != JOptionPane.YES_OPTION) - return false; } } - saveAs(file); - return true; + + return saveAs(file); } - private boolean saveAs(File file) { - System.out.println("Saving to file: " + file.getName()); + log.info("Saving document as " + file); boolean saved = false; if (!StorageOptionChooser.verifyStorageOptions(document, this)) { // User cancelled the dialog + log.user("User cancelled saving in storage options dialog"); return false; } @@ -1040,6 +1101,7 @@ public class BasicFrame extends JFrame { "Writing " + file.getName() + "...", worker)) { // User cancelled the save + log.user("User cancelled the save, deleting the file"); file.delete(); return false; } @@ -1055,6 +1117,7 @@ public class BasicFrame extends JFrame { Throwable cause = e.getCause(); if (cause instanceof IOException) { + log.warn("An I/O error occurred while saving " + file, cause); JOptionPane.showMessageDialog(this, new String[] { "An I/O error occurred while saving:", e.getMessage() }, "Saving failed", JOptionPane.ERROR_MESSAGE); @@ -1073,6 +1136,7 @@ public class BasicFrame extends JFrame { private boolean closeAction() { if (!document.isSaved()) { + log.info("Confirming whether to save the design"); ComponentConfigDialog.hideDialog(); int result = JOptionPane.showConfirmDialog(this, "Design '" + rocket.getName() + "' has not been saved. " + @@ -1081,44 +1145,44 @@ public class BasicFrame extends JFrame { JOptionPane.QUESTION_MESSAGE); if (result == JOptionPane.YES_OPTION) { // Save - if (!saveAction()) - return false; // If save was interrupted + log.user("User requested file save"); + if (!saveAction()) { + log.info("File save was interrupted, not closing"); + return false; + } } else if (result == JOptionPane.NO_OPTION) { // Don't save: No-op + log.user("User requested to discard design"); } else { // Cancel or close + log.user("User cancelled closing, result=" + result); return false; } } // Rocket has been saved or discarded + log.debug("Disposing window"); this.dispose(); - // TODO: LOW: Close only dialogs that have this frame as their parent ComponentConfigDialog.hideDialog(); ComponentAnalysisDialog.hideDialog(); frames.remove(this); - if (frames.isEmpty()) + if (frames.isEmpty()) { + log.info("Last frame closed, exiting"); System.exit(0); + } return true; } - /** - * Closes this frame if it is replaceable. - */ - public void closeIfReplaceable() { - if (this.replaceable && document.isSaved()) { - closeAction(); - } - } - + /** * Open a new design window with a basic rocket+stage. */ public static void newAction() { - log.debug("New action initiated"); + log.info("New action initiated"); + Rocket rocket = new Rocket(); Stage stage = new Stage(); stage.setName("Sustainer"); @@ -1136,13 +1200,17 @@ public class BasicFrame extends JFrame { * Quit the application. Confirms saving unsaved designs. The action of File->Quit. */ public static void quitAction() { + log.info("Quit action initiated"); for (int i = frames.size() - 1; i >= 0; i--) { + log.debug("Closing frame " + frames.get(i)); if (!frames.get(i).closeAction()) { // Close canceled + log.info("Quit was cancelled"); return; } } // Should not be reached, but just in case + log.error("Should already have exited application"); System.exit(0); } @@ -1177,9 +1245,12 @@ public class BasicFrame extends JFrame { */ public static BasicFrame findFrame(Rocket rocket) { for (BasicFrame f : frames) { - if (f.rocket == rocket) + if (f.rocket == rocket) { + log.debug("Found frame " + f + " for rocket " + rocket); return f; + } } + log.debug("Could not find frame for rocket " + rocket); return null; } @@ -1191,142 +1262,11 @@ public class BasicFrame extends JFrame { * @return the corresponding OpenRocketDocument, or null if not found. */ public static OpenRocketDocument findDocument(Rocket rocket) { - for (BasicFrame f : frames) { - if (f.rocket == rocket) - return f.document; - } - return null; - } - - - public static void main(final String[] args) { - - // Run the actual startup method in the EDT since it can use progress dialogs etc. - try { - SwingUtilities.invokeAndWait(new Runnable() { - @Override - public void run() { - runMain(args); - } - }); - } catch (InterruptedException e) { - e.printStackTrace(); - } catch (InvocationTargetException e) { - e.printStackTrace(); - } - - } - - - private static void runMain(String[] args) { - - // Initialize the splash screen with version info - Splash.init(); - - - // Start update info fetching - final UpdateInfoRetriever updateInfo; - if (Prefs.getCheckUpdates()) { - updateInfo = new UpdateInfoRetriever(); - updateInfo.start(); + BasicFrame frame = findFrame(rocket); + if (frame != null) { + return frame.document; } else { - updateInfo = null; + return null; } - - - // Set the best available look-and-feel - GUIUtil.setBestLAF(); - - // Set tooltip delay time. Tooltips are used in MotorChooserDialog extensively. - ToolTipManager.sharedInstance().setDismissDelay(30000); - - - // Setup the uncaught exception handler - ExceptionHandler.registerExceptionHandler(); - - - // Load defaults - Prefs.loadDefaultUnits(); - - - // Load motors etc. - Databases.fakeMethod(); - - // Starting action (load files or open new document) - if (!handleCommandLine(args)) { - newAction(); - } - - - // Check whether update info has been fetched or whether it needs more time - checkUpdateStatus(updateInfo); - } - - - private static void checkUpdateStatus(final UpdateInfoRetriever updateInfo) { - if (updateInfo == null) - return; - - int delay = 1000; - if (!updateInfo.isRunning()) - delay = 100; - - final Timer timer = new Timer(delay, null); - - ActionListener listener = new ActionListener() { - private int count = 5; - - @Override - public void actionPerformed(ActionEvent e) { - if (!updateInfo.isRunning()) { - timer.stop(); - - String current = Prefs.getVersion(); - String last = Prefs.getString(Prefs.LAST_UPDATE, ""); - - UpdateInfo info = updateInfo.getUpdateInfo(); - if (info != null && info.getLatestVersion() != null && - !current.equals(info.getLatestVersion()) && - !last.equals(info.getLatestVersion())) { - - UpdateInfoDialog infoDialog = new UpdateInfoDialog(info); - infoDialog.setVisible(true); - if (infoDialog.isReminderSelected()) { - Prefs.putString(Prefs.LAST_UPDATE, ""); - } else { - Prefs.putString(Prefs.LAST_UPDATE, info.getLatestVersion()); - } - } - } - count--; - if (count <= 0) - timer.stop(); - } - }; - timer.addActionListener(listener); - timer.start(); - } - - - /** - * Handles arguments passed from the command line. This may be used either - * when starting the first instance of OpenRocket or later when OpenRocket is - * executed again while running. - * - * @param args the command-line arguments. - * @return whether a new frame was opened or similar user desired action was - * performed as a result. - */ - public static boolean handleCommandLine(String[] args) { - - // Check command-line for files - boolean opened = false; - for (String file : args) { - if (open(new File(file), null)) { - opened = true; - } - } - return opened; } - } diff --git a/src/net/sf/openrocket/gui/main/ComponentAddButtons.java b/src/net/sf/openrocket/gui/main/ComponentAddButtons.java index 3fe59267..af1d1bb1 100644 --- a/src/net/sf/openrocket/gui/main/ComponentAddButtons.java +++ b/src/net/sf/openrocket/gui/main/ComponentAddButtons.java @@ -29,6 +29,7 @@ import net.miginfocom.swing.MigLayout; import net.sf.openrocket.document.OpenRocketDocument; import net.sf.openrocket.gui.components.StyledLabel; import net.sf.openrocket.gui.configdialog.ComponentConfigDialog; +import net.sf.openrocket.gui.main.componenttree.ComponentTreeModel; import net.sf.openrocket.rocketcomponent.BodyComponent; import net.sf.openrocket.rocketcomponent.BodyTube; import net.sf.openrocket.rocketcomponent.Bulkhead; diff --git a/src/net/sf/openrocket/gui/main/ComponentTree.java b/src/net/sf/openrocket/gui/main/ComponentTree.java deleted file mode 100644 index 41646663..00000000 --- a/src/net/sf/openrocket/gui/main/ComponentTree.java +++ /dev/null @@ -1,91 +0,0 @@ -package net.sf.openrocket.gui.main; - -import java.awt.BasicStroke; -import java.awt.Color; -import java.awt.Component; -import java.awt.Graphics; -import java.awt.Graphics2D; - -import javax.swing.Icon; -import javax.swing.JTree; - -import net.sf.openrocket.rocketcomponent.RocketComponent; - - -public class ComponentTree extends JTree { - private static final long serialVersionUID = 1L; - - public ComponentTree(RocketComponent root) { - super(); - this.setModel(new ComponentTreeModel(root,this)); - this.setToggleClickCount(0); - - javax.swing.plaf.basic.BasicTreeUI ui = new javax.swing.plaf.basic.BasicTreeUI(); - this.setUI(ui); - - ui.setExpandedIcon(TreeIcon.MINUS); - ui.setCollapsedIcon(TreeIcon.PLUS); - - ui.setLeftChildIndent(15); - - - setBackground(Color.WHITE); - setShowsRootHandles(false); - - setCellRenderer(new ComponentTreeRenderer()); - - // Expand whole tree by default - expandTree(); - } - - - public void expandTree() { - for (int i=0; i - */ - -public class ComponentTreeModel implements TreeModel, ComponentChangeListener { - ArrayList listeners = new ArrayList(); - - private final RocketComponent root; - private final JTree tree; - - public ComponentTreeModel(RocketComponent root, JTree tree) { - this.root = root; - this.tree = tree; - root.addComponentChangeListener(this); - } - - - public Object getChild(Object parent, int index) { - RocketComponent component = (RocketComponent)parent; - - try { - return component.getChild(index); - } catch (IndexOutOfBoundsException e) { - return null; - } - } - - - public int getChildCount(Object parent) { - RocketComponent c = (RocketComponent)parent; - - return c.getChildCount(); - } - - - public int getIndexOfChild(Object parent, Object child) { - if (parent==null || child==null) - return -1; - - RocketComponent p = (RocketComponent)parent; - RocketComponent c = (RocketComponent)child; - - return p.getChildPosition(c); - } - - public Object getRoot() { - return root; - } - - public boolean isLeaf(Object node) { - RocketComponent c = (RocketComponent)node; - - return (c.getChildCount() == 0); - } - - public void addTreeModelListener(TreeModelListener l) { - listeners.add(l); - } - - public void removeTreeModelListener(TreeModelListener l) { - listeners.remove(l); - } - - private void fireTreeNodeChanged(RocketComponent node) { - TreeModelEvent e = new TreeModelEvent(this, makeTreePath(node), null, null); - Object[] l = listeners.toArray(); - for (int i=0; i enumer = tree.getExpandedDescendants(new TreePath(path)); - ArrayList expanded = new ArrayList(); - if (enumer != null) { - while (enumer.hasMoreElements()) { - TreePath p = enumer.nextElement(); - expanded.add(((RocketComponent)p.getLastPathComponent()).getID()); - } - } - - // Send structure change event - TreeModelEvent e = new TreeModelEvent(this,path); - Object[] l = listeners.toArray(); - for (int i=0; i