From 6930dda4b30229cfc3b34f51561e323fd4f9936b Mon Sep 17 00:00:00 2001 From: plaa Date: Tue, 30 Jun 2009 15:08:46 +0000 Subject: [PATCH] Updates git-svn-id: https://openrocket.svn.sourceforge.net/svnroot/openrocket/trunk@11 180e2498-e6e9-4542-8430-84ac67f01cd8 --- ChangeLog | 5 + TODO | 2 +- .../sf/openrocket/file/OpenRocketSaver.java | 114 +++++++++++- src/net/sf/openrocket/file/RocketLoader.java | 30 ++- src/net/sf/openrocket/file/RocketSaver.java | 10 + .../openrocket/gui/StorageOptionChooser.java | 76 +++++++- .../gui/dialogs/SwingWorkerDialog.java | 164 +++++++++++------ .../sf/openrocket/gui/main/BasicFrame.java | 172 +++++++++++------- .../util/ConcurrentProgressMonitor.java | 18 +- .../ConcurrentProgressMonitorInputStream.java | 1 + .../sf/openrocket/util/OpenFileWorker.java | 145 +++++++++++++++ .../sf/openrocket/util/SaveFileWorker.java | 99 ++++++++++ 12 files changed, 688 insertions(+), 148 deletions(-) create mode 100644 src/net/sf/openrocket/util/OpenFileWorker.java create mode 100644 src/net/sf/openrocket/util/SaveFileWorker.java diff --git a/ChangeLog b/ChangeLog index f54156a7..c70e5c23 100644 --- 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 e05d6f84..a44b0c6b 100644 --- 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: diff --git a/src/net/sf/openrocket/file/OpenRocketSaver.java b/src/net/sf/openrocket/file/OpenRocketSaver.java index 970aa2a8..d7a7eba1 100644 --- a/src/net/sf/openrocket/file/OpenRocketSaver.java +++ b/src/net/sf/openrocket/file/OpenRocketSaver.java @@ -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 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(""); } + + + /* 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 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> data, int index, StringBuilder sb) throws IOException { sb.setLength(0); diff --git a/src/net/sf/openrocket/file/RocketLoader.java b/src/net/sf/openrocket/file/RocketLoader.java index 0e8e051e..ee2f9410 100644 --- a/src/net/sf/openrocket/file/RocketLoader.java +++ b/src/net/sf/openrocket/file/RocketLoader.java @@ -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(); + } + } } } diff --git a/src/net/sf/openrocket/file/RocketSaver.java b/src/net/sf/openrocket/file/RocketSaver.java index b6f8b607..5c18bb8d 100644 --- a/src/net/sf/openrocket/file/RocketSaver.java +++ b/src/net/sf/openrocket/file/RocketSaver.java @@ -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); diff --git a/src/net/sf/openrocket/gui/StorageOptionChooser.java b/src/net/sf/openrocket/gui/StorageOptionChooser.java index 9b8f1aa4..7e4b956c 100644 --- a/src/net/sf/openrocket/gui/StorageOptionChooser.java +++ b/src/net/sf/openrocket/gui/StorageOptionChooser.java @@ -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("Store all simulated data.
" + "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("Store only the values shown in the summary table.
" + "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; } - } diff --git a/src/net/sf/openrocket/gui/dialogs/SwingWorkerDialog.java b/src/net/sf/openrocket/gui/dialogs/SwingWorkerDialog.java index 6670a931..2cb73bae 100644 --- a/src/net/sf/openrocket/gui/dialogs/SwingWorkerDialog.java +++ b/src/net/sf/openrocket/gui/dialogs/SwingWorkerDialog.java @@ -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>[] 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> ... 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. + *

+ * The dialog contains a cancel button. Clicking it will call + * worker.cancel(true) and close the dialog immediately. + * + * @param parent the parent window for the dialog, or null. + * @param title the title for the dialog. + * @param label an additional label for the dialog, or null. + * @param worker the SwingWorker to execute. + * @return true if the worker has completed normally, + * false 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; } } diff --git a/src/net/sf/openrocket/gui/main/BasicFrame.java b/src/net/sf/openrocket/gui/main/BasicFrame.java index e7cb2ca5..fc523d92 100644 --- a/src/net/sf/openrocket/gui/main/BasicFrame.java +++ b/src/net/sf/openrocket/gui/main/BasicFrame.java @@ -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 warns = ROCKET_LOADER.getWarnings().iterator(); System.out.println("Warnings:"); @@ -652,29 +689,8 @@ public class BasicFrame extends JFrame { - private static class OpenWorker extends SwingWorker { - 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. */ diff --git a/src/net/sf/openrocket/util/ConcurrentProgressMonitor.java b/src/net/sf/openrocket/util/ConcurrentProgressMonitor.java index 6d98a5fc..21017940 100644 --- a/src/net/sf/openrocket/util/ConcurrentProgressMonitor.java +++ b/src/net/sf/openrocket/util/ConcurrentProgressMonitor.java @@ -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(); + } + + }); + } + } + } diff --git a/src/net/sf/openrocket/util/ConcurrentProgressMonitorInputStream.java b/src/net/sf/openrocket/util/ConcurrentProgressMonitorInputStream.java index 7fd5a8a9..abe83978 100644 --- a/src/net/sf/openrocket/util/ConcurrentProgressMonitorInputStream.java +++ b/src/net/sf/openrocket/util/ConcurrentProgressMonitorInputStream.java @@ -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 index 00000000..2969ae78 --- /dev/null +++ b/src/net/sf/openrocket/util/OpenFileWorker.java @@ -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 + */ +public class OpenFileWorker extends SwingWorker { + + 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 index 00000000..1a4ab534 --- /dev/null +++ b/src/net/sf/openrocket/util/SaveFileWorker.java @@ -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 { + + 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); + } + } + } +} -- 2.30.2