Updates
authorplaa <plaa@180e2498-e6e9-4542-8430-84ac67f01cd8>
Tue, 30 Jun 2009 15:08:46 +0000 (15:08 +0000)
committerplaa <plaa@180e2498-e6e9-4542-8430-84ac67f01cd8>
Tue, 30 Jun 2009 15:08:46 +0000 (15:08 +0000)
git-svn-id: https://openrocket.svn.sourceforge.net/svnroot/openrocket/trunk@11 180e2498-e6e9-4542-8430-84ac67f01cd8

12 files changed:
ChangeLog
TODO
src/net/sf/openrocket/file/OpenRocketSaver.java
src/net/sf/openrocket/file/RocketLoader.java
src/net/sf/openrocket/file/RocketSaver.java
src/net/sf/openrocket/gui/StorageOptionChooser.java
src/net/sf/openrocket/gui/dialogs/SwingWorkerDialog.java
src/net/sf/openrocket/gui/main/BasicFrame.java
src/net/sf/openrocket/util/ConcurrentProgressMonitor.java
src/net/sf/openrocket/util/ConcurrentProgressMonitorInputStream.java
src/net/sf/openrocket/util/OpenFileWorker.java [new file with mode: 0644]
src/net/sf/openrocket/util/SaveFileWorker.java [new file with mode: 0644]

index f54156a778039ae172f84e2c8dbf9744fcefa0fd..c70e5c230433542c59df414efd62f4d6dbf308b3 100644 (file)
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,8 @@
+2009-06-26  Sampo Niskanen
+
+       * Progress dialogs for file open/save
+       * File size estimate in save dialog
+
 2009-06-20  Sampo Niskanen
 
        * New edit motor configurations dialog
diff --git a/TODO b/TODO
index e05d6f848e158050a64262510cd1c4d4fe4b0c70..a44b0c6b067c968fe9d6931343cba3a3ca836f34 100644 (file)
--- a/TODO
+++ b/TODO
@@ -9,7 +9,7 @@ Must-have:
 - Read more thrust curve formats / go through thrust curves and correct errors
 - Create application icon and take into use
 - Fix engine block icons
-- Progress and error dialogs when reading/writing files
+- Better error/warning dialogs when reading/writing files
 
 
 Maybe:
index 970aa2a82c86dac3fdf53be4d358378b771ec861..d7a7eba1df52a87e2692ab8d2962c5bf9e20e7e9 100644 (file)
@@ -6,6 +6,7 @@ import java.io.OutputStream;
 import java.io.OutputStreamWriter;
 import java.io.Writer;
 import java.util.ArrayList;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Locale;
 import java.util.zip.GZIPOutputStream;
@@ -14,6 +15,7 @@ import net.sf.openrocket.aerodynamics.Warning;
 import net.sf.openrocket.document.OpenRocketDocument;
 import net.sf.openrocket.document.Simulation;
 import net.sf.openrocket.document.StorageOptions;
+import net.sf.openrocket.rocketcomponent.Rocket;
 import net.sf.openrocket.rocketcomponent.RocketComponent;
 import net.sf.openrocket.simulation.FlightData;
 import net.sf.openrocket.simulation.FlightDataBranch;
@@ -34,6 +36,17 @@ public class OpenRocketSaver extends RocketSaver {
        private static final String METHOD_PACKAGE = "net.sf.openrocket.file.openrocket";
        private static final String METHOD_SUFFIX = "Saver";
        
+       
+       // Estimated storage used by different portions
+       // These have been hand-estimated from saved files
+       private static final int BYTES_PER_COMPONENT_UNCOMPRESSED = 590;
+       private static final int BYTES_PER_COMPONENT_COMPRESSED = 80;
+       private static final int BYTES_PER_SIMULATION_UNCOMPRESSED = 1000;
+       private static final int BYTES_PER_SIMULATION_COMPRESSED = 100;
+       private static final int BYTES_PER_DATAPOINT_UNCOMPRESSED = 350;
+       private static final int BYTES_PER_DATAPOINT_COMPRESSED = 100;
+       
+       
        private int indent;
        private Writer dest;
        
@@ -83,6 +96,55 @@ public class OpenRocketSaver extends RocketSaver {
                        ((GZIPOutputStream)output).finish();
        }
        
+       
+       
+       @Override
+       public long estimateFileSize(OpenRocketDocument doc, StorageOptions options) {
+               
+               long size = 0;
+               
+               // Size per component
+               int componentCount = 0;
+               Rocket rocket = doc.getRocket();
+               Iterator<RocketComponent> iterator = rocket.deepIterator(true);
+               while (iterator.hasNext()) {
+                       iterator.next();
+                       componentCount++;
+               }
+               
+               if (options.isCompressionEnabled())
+                       size += componentCount * BYTES_PER_COMPONENT_COMPRESSED;
+               else
+                       size += componentCount * BYTES_PER_COMPONENT_UNCOMPRESSED;
+               
+               
+               // Size per simulation
+               if (options.isCompressionEnabled())
+                       size += doc.getSimulationCount() * BYTES_PER_SIMULATION_COMPRESSED;
+               else
+                       size += doc.getSimulationCount() * BYTES_PER_SIMULATION_UNCOMPRESSED;
+               
+               
+               // Size per flight data point
+               int pointCount = 0;
+               double timeSkip = options.getSimulationTimeSkip();
+               if (timeSkip != StorageOptions.SIMULATION_DATA_NONE) {
+                       for (Simulation s: doc.getSimulations()) {
+                               FlightData data = s.getSimulatedData();
+                               for (int i=0; i < data.getBranchCount(); i++) {
+                                       pointCount += countFlightDataBranchPoints(data.getBranch(i), timeSkip);
+                               }
+                       }
+               }
+               
+               if (options.isCompressionEnabled())
+                       size += pointCount * BYTES_PER_DATAPOINT_COMPRESSED;
+               else
+                       size += pointCount * BYTES_PER_DATAPOINT_UNCOMPRESSED;
+               
+               return size;
+       }
+       
 
        
        @SuppressWarnings("unchecked")
@@ -231,8 +293,9 @@ public class OpenRocketSaver extends RocketSaver {
        
        
        
-       private void saveFlightDataBranch(FlightDataBranch branch, double timeSkip) throws IOException {
-               double previousTime = -100;
+       private void saveFlightDataBranch(FlightDataBranch branch, double timeSkip) 
+       throws IOException {
+               double previousTime = -100000;
                
                if (branch == null)
                        return;
@@ -297,6 +360,53 @@ public class OpenRocketSaver extends RocketSaver {
                writeln("</databranch>");
        }
        
+       
+       
+       /* TODO: LOW: This is largely duplicated from above! */
+       private int countFlightDataBranchPoints(FlightDataBranch branch, double timeSkip) {
+               int count = 0;
+
+               double previousTime = -100000;
+               
+               if (branch == null)
+                       return 0;
+               
+               // Retrieve the types from the branch
+               FlightDataBranch.Type[] types = branch.getTypes();
+               
+               if (types.length == 0)
+                       return 0;
+               
+               List<Double> timeData = branch.get(FlightDataBranch.TYPE_TIME);
+               if (timeData == null) {
+                       // TODO: MEDIUM: External data may not have time data
+                       throw new IllegalArgumentException("Data did not contain time data");
+               }
+               
+               // Write the data
+               int length = branch.getLength();
+               if (length > 0) {
+                       count++;
+                       previousTime = timeData.get(0);
+               }
+               
+               for (int i=1; i < length-1; i++) {
+                       if (Math.abs(timeData.get(i) - previousTime - timeSkip) < 
+                                       Math.abs(timeData.get(i+1) - previousTime - timeSkip)) {
+                               count++;
+                               previousTime = timeData.get(i);
+                       }
+               }
+               
+               if (length > 1) {
+                       count++;
+               }
+
+               return count;
+       }
+       
+       
+       
        private void writeDataPointString(List<List<Double>> data, int index, StringBuilder sb)
        throws IOException {
                sb.setLength(0);
index 0e8e051e6bfed7a4bf1c054def8df3af5e712c41..ee2f94106d5271e01515c59a04dd79103cf1788e 100644 (file)
@@ -1,6 +1,5 @@
 package net.sf.openrocket.file;
 
-import java.awt.Component;
 import java.io.BufferedInputStream;
 import java.io.File;
 import java.io.FileInputStream;
@@ -8,8 +7,6 @@ import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
 
-import javax.swing.ProgressMonitorInputStream;
-
 import net.sf.openrocket.aerodynamics.WarningSet;
 import net.sf.openrocket.document.OpenRocketDocument;
 
@@ -18,29 +15,28 @@ public abstract class RocketLoader {
        protected final WarningSet warnings = new WarningSet();
 
        
-       public final OpenRocketDocument load(File source, Component parent) 
-       throws RocketLoadException {
-               warnings.clear();
-               
-               try {
-                       return load(new BufferedInputStream(new ProgressMonitorInputStream(
-                                       parent, "Loading " + source.getName(),
-                                       new FileInputStream(source))));
-               } catch (FileNotFoundException e) {
-                       throw new RocketLoadException("File not found: " + source);
-               }
-       }
-
        /**
         * Loads a rocket from the specified File object.
         */
        public final OpenRocketDocument load(File source) throws RocketLoadException {
                warnings.clear();
+               InputStream stream = null;
 
                try {
-                       return load(new BufferedInputStream(new FileInputStream(source)));
+                       
+                       stream = new BufferedInputStream(new FileInputStream(source));
+                       return load(stream);
+                       
                } catch (FileNotFoundException e) {
                        throw new RocketLoadException("File not found: " + source);
+               } finally {
+                       if (stream != null) {
+                               try {
+                                       stream.close();
+                               } catch (IOException e) {
+                                       e.printStackTrace();
+                               }
+                       }
                }
        }
 
index b6f8b607c15d999a7f12f7d2087319013d3a2a28..5c18bb8de53230e6912dcbb61ed2134f57d18f77 100644 (file)
@@ -68,6 +68,16 @@ public abstract class RocketSaver {
        
        
        
+       /**
+        * Provide an estimate of the file size when saving the document with the
+        * specified options.  This is used as an indication to the user and when estimating
+        * file save progress.
+        * 
+        * @param doc           the document.
+        * @param options       the save options, compression must be taken into account.
+        * @return                      the estimated number of bytes the storage would take.
+        */
+       public abstract long estimateFileSize(OpenRocketDocument doc, StorageOptions options);
        
        
        
index 9b8f1aa46190899fdf023f69809eb404b779837a..7e4b956c863ed85348673c1f79a8f4719f3d3398 100644 (file)
@@ -1,5 +1,8 @@
 package net.sf.openrocket.gui;
 
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
 import javax.swing.BorderFactory;
 import javax.swing.ButtonGroup;
 import javax.swing.JCheckBox;
@@ -17,6 +20,8 @@ import net.miginfocom.swing.MigLayout;
 import net.sf.openrocket.document.OpenRocketDocument;
 import net.sf.openrocket.document.Simulation;
 import net.sf.openrocket.document.StorageOptions;
+import net.sf.openrocket.file.OpenRocketSaver;
+import net.sf.openrocket.file.RocketSaver;
 import net.sf.openrocket.simulation.FlightData;
 import net.sf.openrocket.simulation.FlightDataBranch;
 
@@ -24,6 +29,8 @@ public class StorageOptionChooser extends JPanel {
        
        public static final double DEFAULT_SAVE_TIME_SKIP = 0.20;
 
+       private final OpenRocketDocument document;
+       
        private JRadioButton allButton;
        private JRadioButton someButton;
        private JRadioButton noneButton;
@@ -32,11 +39,30 @@ public class StorageOptionChooser extends JPanel {
        
        private JCheckBox compressButton;
        
+       private JLabel estimateLabel;
+       
        
        private boolean artificialEvent = false;
        
-       public StorageOptionChooser(StorageOptions opts) {
+       public StorageOptionChooser(OpenRocketDocument doc, StorageOptions opts) {
                super(new MigLayout());
+               
+               this.document = doc;
+               
+               
+               ChangeListener changeUpdater = new ChangeListener() {
+                       @Override
+                       public void stateChanged(ChangeEvent e) {
+                               updateEstimate();
+                       }
+               };
+               ActionListener actionUpdater = new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               updateEstimate();
+                       }
+               };
+               
 
                ButtonGroup buttonGroup = new ButtonGroup();
                String tip;
@@ -47,6 +73,7 @@ public class StorageOptionChooser extends JPanel {
                allButton.setToolTipText("<html>Store all simulated data.<br>" +
                                "This can result in very large files!");
                buttonGroup.add(allButton);
+               allButton.addActionListener(actionUpdater);
                this.add(allButton, "spanx, wrap rel");
                
                
@@ -55,6 +82,7 @@ public class StorageOptionChooser extends JPanel {
                                "Larger values result in smaller files.";
                someButton.setToolTipText(tip);
                buttonGroup.add(someButton);
+               someButton.addActionListener(actionUpdater);
                this.add(someButton, "");
                
                timeSpinner = new JSpinner(new SpinnerNumberModel(0.0, 0.0, 5.0, 0.1));
@@ -68,6 +96,7 @@ public class StorageOptionChooser extends JPanel {
                        }
                });
                this.add(timeSpinner, "wmin 55lp");
+               timeSpinner.addChangeListener(changeUpdater);
                
                JLabel label = new JLabel("seconds");
                label.setToolTipText(tip);
@@ -78,13 +107,22 @@ public class StorageOptionChooser extends JPanel {
                noneButton.setToolTipText("<html>Store only the values shown in the summary table.<br>" +
                                "This results in the smallest files.");
                buttonGroup.add(noneButton);
-
+               noneButton.addActionListener(actionUpdater);
                this.add(noneButton, "spanx, wrap 20lp");
                
                
+               
                compressButton = new JCheckBox("Compress file");
                compressButton.setToolTipText("Using compression reduces the file size significantly.");
-               this.add(compressButton, "spanx");
+               compressButton.addActionListener(actionUpdater);
+               this.add(compressButton, "spanx, wrap para");
+               
+               
+               // Estimate is updated in loadOptions(opts)
+               estimateLabel = new JLabel("");
+               estimateLabel.setToolTipText("An estimate on how large the resulting file would " +
+                               "be with the present options.");
+               this.add(estimateLabel, "spanx");
                
                
                this.setBorder(BorderFactory.createCompoundBorder(
@@ -117,6 +155,8 @@ public class StorageOptionChooser extends JPanel {
                
                // Compression checkbox
                compressButton.setSelected(opts.isCompressionEnabled());
+               
+               updateEstimate();
        }
        
        
@@ -140,6 +180,33 @@ public class StorageOptionChooser extends JPanel {
        
        
        
+       // TODO: MEDIUM: The estimation method always uses OpenRocketSaver!
+       private static final RocketSaver ROCKET_SAVER = new OpenRocketSaver();
+       
+       private void updateEstimate() {
+               StorageOptions opts = new StorageOptions();
+               
+               storeOptions(opts);
+               long size = ROCKET_SAVER.estimateFileSize(document, opts);
+               size = Math.max((size+512)/1024, 1);
+
+               String formatted;
+               
+               if (size >= 10000) {
+                       formatted = (size/1000) + " MB";
+               } else if (size >= 1000){
+                       formatted = (size/1000) + "." + ((size/100)%10) + " MB";
+               } else if (size >= 100) {
+                       formatted = ((size/10)*10) + " kB";
+               } else {
+                       formatted = size + " kB";
+               }
+
+               estimateLabel.setText("Estimated file size: " + formatted);
+       }
+       
+       
+       
        /**
         * Asks the user the storage options using a modal dialog window if the document
         * contains simulated data and the user has not explicitly set how to store the data.
@@ -187,7 +254,7 @@ public class StorageOptionChooser extends JPanel {
                }
                
                
-               StorageOptionChooser chooser = new StorageOptionChooser(options);
+               StorageOptionChooser chooser = new StorageOptionChooser(document, options);
                
                if (JOptionPane.showConfirmDialog(parent, chooser, "Save options", 
                                JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE) !=
@@ -200,5 +267,4 @@ public class StorageOptionChooser extends JPanel {
                return true;
        }
        
-       
 }
index 6670a9317328c835143088d9d867d42f7dfe5efa..2cb73bae5f780e72b1340fbb693f86aea1576934 100644 (file)
@@ -1,6 +1,9 @@
 package net.sf.openrocket.gui.dialogs;
 
+import java.awt.Dimension;
 import java.awt.Window;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
 import java.beans.PropertyChangeEvent;
 import java.beans.PropertyChangeListener;
 
@@ -12,7 +15,7 @@ import javax.swing.JProgressBar;
 import javax.swing.SwingWorker;
 
 import net.miginfocom.swing.MigLayout;
-import net.sf.openrocket.util.Pair;
+import net.sf.openrocket.util.MathUtil;
 
 
 /**
@@ -25,94 +28,135 @@ import net.sf.openrocket.util.Pair;
  */
 public class SwingWorkerDialog extends JDialog implements PropertyChangeListener {
 
-       private final JLabel label;
-       private final JProgressBar progressBar;
+       /** Number of milliseconds to wait at a time between checking worker status */
+       private static final int DELAY = 100;
+       
+       /** Minimum number of milliseconds to wait before estimating work length */
+       private static final int ESTIMATION_DELAY = 190;
+       
+       /** Open the dialog if estimated remaining time is longer than this */
+       private static final int REMAINING_TIME_FOR_DIALOG = 1000;
        
-       private int position;
-       private Pair<String, SwingWorker<?,?>>[] workers;
+       /** 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 JProgressBar progressBar;
        
        private boolean cancelled = false;
        
-       public SwingWorkerDialog(Window parent, String title) {
+       
+       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"));
                
-               label = new JLabel("");
-               panel.add(label, "wrap para");
+               if (label != null) {
+                       panel.add(new JLabel(label), "wrap para");
+               }
                
                progressBar = new JProgressBar();
                panel.add(progressBar, "growx, wrap para");
                
                JButton cancel = new JButton("Cancel");
-               // TODO: CRITICAL: Implement cancel
+               cancel.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               cancelled = true;
+                               worker.cancel(true);
+                               close();
+                       }
+               });
                panel.add(cancel, "right");
                
                this.add(panel);
+               this.setMinimumSize(new Dimension(250,100));
                this.pack();
+               this.setLocationRelativeTo(parent);
                this.setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
        }
 
-       
-       /**
-        * Execute the provided workers one after another.  When this call returns
-        * the workers will all have completed.
-        *   
-        * @param workers       pairs of description texts and workers to run.
-        */
-       public void runWorkers(Pair<String, SwingWorker<?,?>> ... workers) {
-               if (workers.length == 0) {
-                       throw new IllegalArgumentException("No workers provided.");
-               }
-               
-               this.workers = workers;
-               position = -1;
-               
-               for (int i=0; i < workers.length; i++) {
-                       workers[i].getV().addPropertyChangeListener(this);
+       @Override
+       public void propertyChange(PropertyChangeEvent evt) {
+               if (worker.getState() == SwingWorker.StateValue.DONE) {
+                       close();
                }
-               
-               nextWorker();
-               this.setVisible(true);  // Waits until all have ended
+               progressBar.setValue(worker.getProgress());
+       }
+       
+       private void close() {
+               worker.removePropertyChangeListener(this);
+               this.setVisible(false);
        }
-
        
        
        /**
-        * Starts the execution of the next worker in the queue.  If the last worker
-        * has completed or the operation has been cancelled, closes the dialog.
+        * Run a SwingWorker and if necessary show a dialog displaying the progress of
+        * the worker.  The progress information is obtained from the SwingWorker's
+        * progress property.  The dialog is shown only if the worker is estimated to
+        * take a notable amount of time.
+        * <p>
+        * The dialog contains a cancel button.  Clicking it will call
+        * <code>worker.cancel(true)</code> and close the dialog immediately.
+        * 
+        * @param parent        the parent window for the dialog, or <code>null</code>.
+        * @param title         the title for the dialog.
+        * @param label         an additional label for the dialog, or <code>null</code>.
+        * @param worker        the SwingWorker to execute.
+        * @return                      <code>true</code> if the worker has completed normally,
+        *                                      <code>false</code> if the user cancelled the operation
         */
-       private void nextWorker() {
-               if ((position >= workers.length-1) || cancelled) {
-                       close();
-                       return;
-               }
-               
-               position++;
+       public static boolean runWorker(Window parent, String title, String label,
+                       SwingWorker<?,?> worker) {
                
-               label.setText(workers[position].getU());
-               workers[position].getV().execute();
-       }
-       
-       
+               // Start timing the worker
+               final long startTime = System.currentTimeMillis();
+               worker.execute();
 
-       @Override
-       public void propertyChange(PropertyChangeEvent evt) {
-               if (workers[position].getV().getState() == SwingWorker.StateValue.DONE) {
-                       nextWorker();
+               // Monitor worker thread before opening the dialog
+               while (true) {
+                       
+                       try {
+                               Thread.sleep(DELAY);
+                       } catch (InterruptedException e) {
+                               // Should never occur
+                               e.printStackTrace();
+                       }
+                       
+                       if (worker.isDone()) {
+                               // Worker has completed within time limits
+                               return true;
+                       }
+                       
+                       // Check whether enough time has gone to get realistic estimate
+                       long elapsed = System.currentTimeMillis() - startTime;
+                       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;
+                       
+                       if (estimate >= TOTAL_TIME_FOR_DIALOG)
+                               break;
+                       
+                       if (remaining >= REMAINING_TIME_FOR_DIALOG)
+                               break;
                }
                
-               int value = workers[position].getV().getProgress();
-               value = (value + position*100 ) / workers.length;
-               progressBar.setValue(value);
-       }
-       
-       
-       
-       private void close() {
-               for (int i=0; i < workers.length; i++) {
-                       workers[i].getV().removePropertyChangeListener(this);
-               }
-               this.setVisible(false);
+               
+               // Dialog is required
+               
+               SwingWorkerDialog dialog = new SwingWorkerDialog(parent, title, label, worker);
+               dialog.setVisible(true);
+               
+               return !dialog.cancelled;
        }
 }
index e7cb2ca5c4c451dd28f2343d0c61e4e5454ba305..fc523d9223dd652d1277f1f80558dfb790c021b6 100644 (file)
@@ -1,9 +1,9 @@
 package net.sf.openrocket.gui.main;
 
-import java.awt.Component;
 import java.awt.Dimension;
 import java.awt.Font;
 import java.awt.Toolkit;
+import java.awt.Window;
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
 import java.awt.event.ComponentAdapter;
@@ -15,10 +15,11 @@ import java.awt.event.MouseListener;
 import java.awt.event.WindowAdapter;
 import java.awt.event.WindowEvent;
 import java.io.File;
-import java.io.FileInputStream;
+import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Iterator;
+import java.util.concurrent.ExecutionException;
 
 import javax.swing.Action;
 import javax.swing.InputMap;
@@ -40,7 +41,6 @@ import javax.swing.ListSelectionModel;
 import javax.swing.LookAndFeel;
 import javax.swing.ScrollPaneConstants;
 import javax.swing.SwingUtilities;
-import javax.swing.SwingWorker;
 import javax.swing.ToolTipManager;
 import javax.swing.UIManager;
 import javax.swing.border.TitledBorder;
@@ -65,16 +65,17 @@ import net.sf.openrocket.gui.dialogs.BugDialog;
 import net.sf.openrocket.gui.dialogs.ComponentAnalysisDialog;
 import net.sf.openrocket.gui.dialogs.LicenseDialog;
 import net.sf.openrocket.gui.dialogs.PreferencesDialog;
+import net.sf.openrocket.gui.dialogs.SwingWorkerDialog;
 import net.sf.openrocket.gui.scalefigure.RocketPanel;
 import net.sf.openrocket.rocketcomponent.ComponentChangeEvent;
 import net.sf.openrocket.rocketcomponent.ComponentChangeListener;
 import net.sf.openrocket.rocketcomponent.Rocket;
 import net.sf.openrocket.rocketcomponent.RocketComponent;
 import net.sf.openrocket.rocketcomponent.Stage;
-import net.sf.openrocket.util.ConcurrentProgressMonitor;
-import net.sf.openrocket.util.ConcurrentProgressMonitorInputStream;
 import net.sf.openrocket.util.Icons;
+import net.sf.openrocket.util.OpenFileWorker;
 import net.sf.openrocket.util.Prefs;
+import net.sf.openrocket.util.SaveFileWorker;
 
 public class BasicFrame extends JFrame {
        private static final long serialVersionUID = 1L;
@@ -83,6 +84,9 @@ public class BasicFrame extends JFrame {
         * The RocketLoader instance used for loading all rocket designs.
         */
        private static final RocketLoader ROCKET_LOADER = new GeneralRocketLoader();
+       
+       // TODO: Always uses OpenRocketSaver
+       private static final RocketSaver ROCKET_SAVER = new OpenRocketSaver();
 
        
        /**
@@ -241,7 +245,6 @@ public class BasicFrame extends JFrame {
                        }
                });
                frames.add(this);
-               
        }
        
        
@@ -590,19 +593,18 @@ public class BasicFrame extends JFrame {
            Prefs.setDefaultDirectory(chooser.getCurrentDirectory());
 
            File[] files = chooser.getSelectedFiles();
-           boolean opened = false;
            
            for (File file: files) {
                System.out.println("Opening file: " + file);
                if (open(file, this)) {
-                       opened = true;
+                       
+                       // Close previous window if replacing
+                       if (replaceable && document.isSaved()) {
+                               closeAction();
+                               replaceable = false;
+                       }
                }
            }
-
-           // Close this frame if replaceable and file opened successfully
-               if (replaceable && opened) {
-                       closeAction();
-               }
        }
        
        
@@ -614,23 +616,58 @@ public class BasicFrame extends JFrame {
         * @param parent        the parent component for which a progress dialog is opened.
         * @return                      whether the file was successfully loaded and opened.
         */
-       private static boolean open(File file, Component parent) {
-           OpenRocketDocument doc = null;
-           
-           
-               try {
-                       doc = ROCKET_LOADER.load(file, parent);
-               } catch (RocketLoadException e) {
-                       JOptionPane.showMessageDialog(null, "Unable to open file '" + file.getName() 
-                                       +"': " + e.getMessage(), "Error opening file", JOptionPane.ERROR_MESSAGE);
-                       e.printStackTrace();
+       private static boolean open(File file, Window parent) {
+
+               // Open the file in a Swing worker thread
+               OpenFileWorker worker = new OpenFileWorker(file);
+               if (!SwingWorkerDialog.runWorker(parent, "Opening file", 
+                               "Reading " + file.getName() + "...", worker)) {
+
+                       // User cancelled the operation
                        return false;
                }
+
+               
+               // Handle the document
+               OpenRocketDocument doc = null;
+               try {
+
+                       doc = worker.get();
+
+               } catch (ExecutionException e) {
+
+                       Throwable cause = e.getCause();
+
+                       if (cause instanceof FileNotFoundException) {
+
+                               JOptionPane.showMessageDialog(parent, 
+                                               "File not found: " + file.getName(), 
+                                               "Error opening file", JOptionPane.ERROR_MESSAGE);
+                               return false;
+
+                       } else if (cause instanceof RocketLoadException) {
+
+                               JOptionPane.showMessageDialog(parent, 
+                                               "Unable to open file '" + file.getName() +"': " 
+                                               + cause.getMessage(),
+                                               "Error opening file", JOptionPane.ERROR_MESSAGE);
+                               return false;
+
+                       } else {
+
+                               throw new RuntimeException("Unknown error when opening file", e);
+
+                       }
+
+               } catch (InterruptedException e) {
+                       throw new RuntimeException("EDT was interrupted", e);
+               }
+               
+               if (doc == null) {
+                       throw new RuntimeException("BUG: Document loader returned null");
+               }
+               
                
-           if (doc == null) {
-               throw new RuntimeException("BUG: Rocket loader returned null");
-           }       
-           
            // Show warnings
            Iterator<Warning> warns = ROCKET_LOADER.getWarnings().iterator();
            System.out.println("Warnings:");
@@ -652,29 +689,8 @@ public class BasicFrame extends JFrame {
        
        
        
-       private static class OpenWorker extends SwingWorker<OpenRocketDocument, Void> {
-               private final File file;
-               private final Component parent;
-               private ConcurrentProgressMonitor monitor = null;
-               
-               public OpenWorker(File file, Component parent) {
-                       this.file = file;
-                       this.parent = parent;
-               }
-               
-               @Override
-               protected OpenRocketDocument doInBackground() throws Exception {
-                       ConcurrentProgressMonitorInputStream is = 
-                               new ConcurrentProgressMonitorInputStream(parent, 
-                                               "Loading " + file.getName(), new FileInputStream(file));
-                       monitor = is.getProgressMonitor();
-                       return ROCKET_LOADER.load(is);
-               }
-               
-               public ConcurrentProgressMonitor getMonitor() {
-                       return monitor;
-               }
-       }
+       
+       
        
        
        
@@ -688,11 +704,12 @@ public class BasicFrame extends JFrame {
                }
        }
        
+       
        private boolean saveAsAction() {
                File file = null;
                while (file == null) {
                        StorageOptionChooser storageChooser = 
-                               new StorageOptionChooser(document.getDefaultStorageOptions());
+                               new StorageOptionChooser(document, document.getDefaultStorageOptions());
                        JFileChooser chooser = new JFileChooser();
                        chooser.setFileFilter(ROCKET_DESIGN_FILTER);
                        chooser.setCurrentDirectory(Prefs.getDefaultDirectory());
@@ -738,18 +755,39 @@ public class BasicFrame extends JFrame {
                return false;
            }
 
-           RocketSaver saver = new OpenRocketSaver();
-           try {
-               saver.save(file, document);
-               document.setFile(file);
-               document.setSaved(true);
-               saved = true;
-           } catch (IOException e) {
-               JOptionPane.showMessageDialog(this, new String[] { 
-                               "An I/O error occurred while saving:",
-                               e.getMessage() }, "Saving failed", JOptionPane.ERROR_MESSAGE);
+
+           SaveFileWorker worker = new SaveFileWorker(document, file, ROCKET_SAVER);
+
+           if (!SwingWorkerDialog.runWorker(this, "Saving file", 
+                       "Writing " + file.getName() + "...", worker)) {
+               
+               // User cancelled the save
+               file.delete();
+               return false;
            }
-           setTitle();
+           
+           try {
+                       worker.get();
+                       document.setFile(file);
+                       document.setSaved(true);
+                       saved = true;
+                   setTitle();
+               } catch (ExecutionException e) {
+                       Throwable cause = e.getCause();
+                       
+                       if (cause instanceof IOException) {
+                       JOptionPane.showMessageDialog(this, new String[] { 
+                                       "An I/O error occurred while saving:",
+                                       e.getMessage() }, "Saving failed", JOptionPane.ERROR_MESSAGE);
+                       return false;
+                       } else {
+                               throw new RuntimeException("Unknown error when saving file", e);
+                       }
+                       
+               } catch (InterruptedException e) {
+                       throw new RuntimeException("EDT was interrupted", e);
+               }
+           
            return saved;
        }
        
@@ -787,6 +825,16 @@ public class BasicFrame extends JFrame {
                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.
         */
index 6d98a5fc2455b116db0ef86aecdc74a4b6f8e23f..21017940176bdb680af1d27111c02b84b9466643 100644 (file)
@@ -36,10 +36,26 @@ public class ConcurrentProgressMonitor extends ProgressMonitor {
                                }
                                
                        });
-                       
                }
        }
        
        
+       @Override
+       public void close() {
+               if (SwingUtilities.isEventDispatchThread()) {
+                       super.close();
+               } else {
+                       
+                       SwingUtilities.invokeLater(new Runnable() {
+
+                               @Override
+                               public void run() {
+                                       ConcurrentProgressMonitor.super.close();
+                               }
+                               
+                       });
+               }
+       }
+
 
 }
index 7fd5a8a91865193ee265604583853a596fd2e9cb..abe83978171a229b4a116a107a83f61cc68602bb 100644 (file)
@@ -130,6 +130,7 @@ public class ConcurrentProgressMonitorInputStream extends FilterInputStream {
        @Override
        public void close() throws IOException {
                in.close();
+        monitor.close();
        }
 
 
diff --git a/src/net/sf/openrocket/util/OpenFileWorker.java b/src/net/sf/openrocket/util/OpenFileWorker.java
new file mode 100644 (file)
index 0000000..2969ae7
--- /dev/null
@@ -0,0 +1,145 @@
+package net.sf.openrocket.util;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InterruptedIOException;
+
+import javax.swing.SwingWorker;
+
+import net.sf.openrocket.document.OpenRocketDocument;
+import net.sf.openrocket.file.GeneralRocketLoader;
+import net.sf.openrocket.file.RocketLoader;
+
+
+/**
+ * A SwingWorker thread that opens a rocket design file.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class OpenFileWorker extends SwingWorker<OpenRocketDocument, Void> {
+
+       private static final RocketLoader ROCKET_LOADER = new GeneralRocketLoader();
+
+       private final File file;
+       
+       public OpenFileWorker(File file) {
+               this.file = file;
+       }
+       
+       
+       @Override
+       protected OpenRocketDocument doInBackground() throws Exception {
+               ProgressInputStream is = new ProgressInputStream(
+                               new BufferedInputStream(new FileInputStream(file)));
+               try {
+                       return ROCKET_LOADER.load(is);
+               } finally {
+                       try {
+                               is.close();
+                       } catch (Exception e) {
+                               System.err.println("Error closing file: ");
+                               e.printStackTrace();
+                       }
+               }
+       }
+
+       
+       
+
+       private class ProgressInputStream extends FilterInputStream {
+
+               private final int size;
+               private int readBytes = 0;
+               private int progress = -1;
+               
+               protected ProgressInputStream(InputStream in) {
+                       super(in);
+                       int s;
+                       try {
+                               s = in.available();
+                       } catch (IOException e) {
+                               System.err.println("ERROR estimating available bytes!");
+                               s = 0;
+                       }
+                       size = s;
+               }
+
+               
+               
+               @Override
+               public int read() throws IOException {
+                       int c = in.read();
+                       if (c >= 0) {
+                               readBytes++;
+                               setProgress();
+                       }
+                       if (isCancelled()) {
+                               throw new InterruptedIOException("OpenFileWorker was cancelled");
+                       }
+                       return c;
+               }
+
+               @Override
+               public int read(byte[] b, int off, int len) throws IOException {
+                       int n = in.read(b, off, len);
+                       if (n > 0) {
+                               readBytes += n;
+                               setProgress();
+                       }
+                       if (isCancelled()) {
+                               throw new InterruptedIOException("OpenFileWorker was cancelled");
+                       }
+                       return n;
+               }
+
+               @Override
+               public int read(byte[] b) throws IOException {
+                       int n = in.read(b);
+                       if (n > 0) {
+                               readBytes += n;
+                               setProgress();
+                       }
+                       if (isCancelled()) {
+                               throw new InterruptedIOException("OpenFileWorker was cancelled");
+                       }
+                       return n;
+               }
+
+               @Override
+               public long skip(long n) throws IOException {
+                       long nr = in.skip(n);
+                       if (nr > 0) {
+                               readBytes += nr;
+                               setProgress();
+                       }
+                       if (isCancelled()) {
+                               throw new InterruptedIOException("OpenFileWorker was cancelled");
+                       }
+                       return nr;
+               }
+               
+               @Override
+               public synchronized void reset() throws IOException {
+                       in.reset();
+                       readBytes = size - in.available();
+                       setProgress();
+                       if (isCancelled()) {
+                               throw new InterruptedIOException("OpenFileWorker was cancelled");
+                       }
+               }
+
+               
+               
+               private void setProgress() {
+                       int p = MathUtil.clamp(readBytes * 100 / size, 0, 100);
+                       if (progress != p) {
+                               progress = p;
+                               OpenFileWorker.this.setProgress(progress);
+                       }
+               }
+       }
+}
diff --git a/src/net/sf/openrocket/util/SaveFileWorker.java b/src/net/sf/openrocket/util/SaveFileWorker.java
new file mode 100644 (file)
index 0000000..1a4ab53
--- /dev/null
@@ -0,0 +1,99 @@
+package net.sf.openrocket.util;
+
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.io.OutputStream;
+
+import javax.swing.SwingWorker;
+
+import net.sf.openrocket.document.OpenRocketDocument;
+import net.sf.openrocket.file.RocketSaver;
+
+public class SaveFileWorker extends SwingWorker<Void, Void> {
+
+       private final OpenRocketDocument document;
+       private final File file;
+       private final RocketSaver saver;
+       
+       public SaveFileWorker(OpenRocketDocument document, File file, RocketSaver saver) {
+               this.document = document;
+               this.file = file;
+               this.saver = saver;
+       }
+       
+       
+       @Override
+       protected Void doInBackground() throws Exception {
+               ProgressOutputStream os = new ProgressOutputStream(
+                               new BufferedOutputStream(new FileOutputStream(file)), 
+                               (int)saver.estimateFileSize(document, document.getDefaultStorageOptions()));
+               
+               try {
+                       saver.save(os, document);
+               } finally {
+                       try {
+                               os.close();
+                       } catch (Exception e) {
+                               System.err.println("Error closing file: ");
+                               e.printStackTrace();
+                       }
+               }
+               return null;
+       }
+       
+       
+       private class ProgressOutputStream extends FilterOutputStream {
+
+               private final int totalBytes;
+               private int writtenBytes = 0;
+               private int progress = -1;
+               
+               public ProgressOutputStream(OutputStream out, int estimate) {
+                       super(out);
+                       this.totalBytes = estimate;
+               }
+
+               @Override
+               public void write(byte[] b, int off, int len) throws IOException {
+                       out.write(b, off, len);
+                       writtenBytes += len;
+                       setProgress();
+                       if (isCancelled()) {
+                               throw new InterruptedIOException("SaveFileWorker was cancelled");
+                       }
+               }
+
+               @Override
+               public void write(byte[] b) throws IOException {
+                       out.write(b);
+                       writtenBytes += b.length;
+                       setProgress();
+                       if (isCancelled()) {
+                               throw new InterruptedIOException("SaveFileWorker was cancelled");
+                       }
+               }
+
+               @Override
+               public void write(int b) throws IOException {
+                       out.write(b);
+                       writtenBytes++;
+                       setProgress();
+                       if (isCancelled()) {
+                               throw new InterruptedIOException("SaveFileWorker was cancelled");
+                       }
+               }
+               
+               
+               private void setProgress() {
+                       int p = MathUtil.clamp(writtenBytes * 100 / totalBytes, 0, 100);
+                       if (progress != p) {
+                               progress = p;
+                               SaveFileWorker.this.setProgress(progress);
+                       }
+               }
+       }
+}