committed Doug's Rocksim loader
[debian/openrocket] / src / net / sf / openrocket / gui / main / BasicFrame.java
index 4bd73ca1d3a51c170e3a0d1c59bad2bb32079fef..a2a18e14a369bf739a6bc116bb2de2c2420d1a46 100644 (file)
@@ -3,6 +3,7 @@ package net.sf.openrocket.gui.main;
 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;
@@ -14,9 +15,17 @@ import java.awt.event.MouseListener;
 import java.awt.event.WindowAdapter;
 import java.awt.event.WindowEvent;
 import java.io.File;
+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.Iterator;
+import java.util.concurrent.ExecutionException;
 
 import javax.swing.Action;
 import javax.swing.InputMap;
@@ -33,12 +42,13 @@ import javax.swing.JScrollPane;
 import javax.swing.JSeparator;
 import javax.swing.JSplitPane;
 import javax.swing.JTabbedPane;
+import javax.swing.JTextField;
 import javax.swing.KeyStroke;
-import javax.swing.LookAndFeel;
+import javax.swing.ListSelectionModel;
 import javax.swing.ScrollPaneConstants;
 import javax.swing.SwingUtilities;
+import javax.swing.Timer;
 import javax.swing.ToolTipManager;
-import javax.swing.UIManager;
 import javax.swing.border.TitledBorder;
 import javax.swing.event.TreeSelectionEvent;
 import javax.swing.event.TreeSelectionListener;
@@ -48,25 +58,41 @@ import javax.swing.tree.TreePath;
 import javax.swing.tree.TreeSelectionModel;
 
 import net.miginfocom.swing.MigLayout;
-import net.sf.openrocket.aerodynamics.Warning;
+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.OpenRocketSaver;
 import net.sf.openrocket.file.RocketLoadException;
 import net.sf.openrocket.file.RocketLoader;
 import net.sf.openrocket.file.RocketSaver;
-import net.sf.openrocket.gui.ComponentAnalysisDialog;
-import net.sf.openrocket.gui.PreferencesDialog;
+import net.sf.openrocket.file.openrocket.OpenRocketSaver;
 import net.sf.openrocket.gui.StorageOptionChooser;
 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.ExampleDesignDialog;
+import net.sf.openrocket.gui.dialogs.LicenseDialog;
+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.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.BugException;
+import net.sf.openrocket.util.GUIUtil;
 import net.sf.openrocket.util.Icons;
+import net.sf.openrocket.util.OpenFileWorker;
 import net.sf.openrocket.util.Prefs;
+import net.sf.openrocket.util.Reflection;
+import net.sf.openrocket.util.SaveFileWorker;
+import net.sf.openrocket.util.TestRockets;
 
 public class BasicFrame extends JFrame {
        private static final long serialVersionUID = 1L;
@@ -75,23 +101,26 @@ public class BasicFrame extends JFrame {
         * The RocketLoader instance used for loading all rocket designs.
         */
        private static final RocketLoader ROCKET_LOADER = new GeneralRocketLoader();
+       
+       private static final RocketSaver ROCKET_SAVER = new OpenRocketSaver();
 
        
-       /**
-        * File filter for filtering only rocket designs.
-        */
-       private static final FileFilter ROCKET_DESIGN_FILTER = new FileFilter() {
-               @Override
-               public String getDescription() {
-                       return "OpenRocket designs (*.ork)";
-               }
-               @Override
-               public boolean accept(File f) {
-                       String name = f.getName().toLowerCase();
-                       return name.endsWith(".ork") || name.endsWith(".ork.gz");
-               }
-    };
+       // FileFilters for different types of rocket design files
+       private static final FileFilter ALL_DESIGNS_FILTER =
+               new SimpleFileFilter("All rocket designs (*.ork; *.rkt)", 
+                               ".ork", ".ork.gz", ".rkt", ".rkt.gz");
+       
+       private static final FileFilter OPENROCKET_DESIGN_FILTER = 
+               new SimpleFileFilter("OpenRocket designs (*.ork)", ".ork", ".ork.gz");
+       
+       private static final FileFilter ROCKSIM_DESIGN_FILTER = 
+               new SimpleFileFilter("RockSim designs (*.rkt)", ".rkt", ".rkt.gz");
+               
+       
+       
     
+    public static final int COMPONENT_TAB = 0;
+    public static final int SIMULATION_TAB = 1;
     
 
        /**
@@ -115,9 +144,13 @@ public class BasicFrame extends JFrame {
        private final OpenRocketDocument document;
        private final Rocket rocket;
        
+       private JTabbedPane tabbedPane;
        private RocketPanel rocketpanel;
        private ComponentTree tree = null;
-       private final TreeSelectionModel selectionModel;
+       
+       private final DocumentSelectionModel selectionModel;
+       private final TreeSelectionModel componentSelectionModel;
+       private final ListSelectionModel simulationSelectionModel;
        
        /** Actions available for rocket modifications */
        private final RocketActions actions;
@@ -146,9 +179,19 @@ public class BasicFrame extends JFrame {
                });
                
                
-               // Create the selection model that will be used
-               selectionModel = new DefaultTreeSelectionModel();
-               selectionModel.setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
+               // Create the component tree selection model that will be used
+               componentSelectionModel = new DefaultTreeSelectionModel();
+               componentSelectionModel.setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
+               
+               // Obtain the simulation selection model that will be used
+               SimulationPanel simulationPanel = new SimulationPanel(document);
+               simulationSelectionModel = simulationPanel.getSimulationListSelectionModel();
+               
+               // Combine into a DocumentSelectionModel
+               selectionModel = new DocumentSelectionModel(document);
+               selectionModel.attachComponentTreeSelectionModel(componentSelectionModel);
+               selectionModel.attachSimulationListSelectionModel(simulationSelectionModel);
+               
                
                actions = new RocketActions(document, selectionModel, this);
                
@@ -160,11 +203,11 @@ public class BasicFrame extends JFrame {
 
                
                // The top tabbed pane
-               JTabbedPane tabbed = new JTabbedPane();
-               tabbed.addTab("Rocket design", null, designTab());
-               tabbed.addTab("Flight simulations", null, simulationsTab());
+               tabbedPane = new JTabbedPane();
+               tabbedPane.addTab("Rocket design", null, designTab());
+               tabbedPane.addTab("Flight simulations", null, simulationPanel);
                
-               vertical.setTopComponent(tabbed);
+               vertical.setTopComponent(tabbedPane);
                
 
 
@@ -202,7 +245,9 @@ public class BasicFrame extends JFrame {
                        }
                });
                this.setLocationByPlatform(true);
-                               
+
+               GUIUtil.setWindowIcons(this);
+               
                this.validate();
                vertical.setDividerLocation(0.4);
                setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
@@ -213,7 +258,6 @@ public class BasicFrame extends JFrame {
                        }
                });
                frames.add(this);
-               
        }
        
        
@@ -232,7 +276,7 @@ public class BasicFrame extends JFrame {
                JPanel panel = new JPanel(new MigLayout("fill, flowy","","[grow]"));
 
                tree = new ComponentTree(rocket);
-               tree.setSelectionModel(selectionModel);
+               tree.setSelectionModel(componentSelectionModel);
 
                // Remove JTree key events that interfere with menu accelerators
                InputMap im = SwingUtilities.getUIInputMap(tree, JComponent.WHEN_FOCUSED);
@@ -253,7 +297,7 @@ public class BasicFrame extends JFrame {
                                int selRow = tree.getRowForLocation(e.getX(), e.getY());
                                TreePath selPath = tree.getPathForLocation(e.getX(), e.getY());
                                if(selRow != -1) {
-                                       if(e.getClickCount() == 2) {
+                                       if((e.getClickCount() == 2) && !ComponentConfigDialog.isDialogVisible()) {
                                                // Double-click
                                                RocketComponent c = (RocketComponent)selPath.getLastPathComponent();
                                                ComponentConfigDialog.showDialog(BasicFrame.this, 
@@ -265,10 +309,10 @@ public class BasicFrame extends JFrame {
                tree.addMouseListener(ml);
 
                // Update dialog when selection is changed
-               selectionModel.addTreeSelectionListener(new TreeSelectionListener() {
+               componentSelectionModel.addTreeSelectionListener(new TreeSelectionListener() {
                        public void valueChanged(TreeSelectionEvent e) {
                                // Scroll tree to the selected item
-                               TreePath path = selectionModel.getSelectionPath();
+                               TreePath path = componentSelectionModel.getSelectionPath();
                                if (path == null)
                                        return;
                                tree.scrollPathToVisible(path);
@@ -313,7 +357,7 @@ public class BasicFrame extends JFrame {
 
                scroll = new JScrollPane(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED,
                                ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
-               scroll.setViewportView(new ComponentAddButtons(document, selectionModel,
+               scroll.setViewportView(new ComponentAddButtons(document, componentSelectionModel,
                                scroll.getViewport()));
                scroll.setBorder(null);
                scroll.setViewportBorder(null);
@@ -330,15 +374,6 @@ public class BasicFrame extends JFrame {
        }
        
        
-       /**
-        * Construct the "Flight simulations" tab.
-        * @return
-        */
-       private JComponent simulationsTab() {
-               return new SimulationPanel(document);
-       }
-       
-       
        
        /**
         * Creates the menu for the window.
@@ -379,6 +414,23 @@ public class BasicFrame extends JFrame {
                });
                menu.add(item);
                
+               item = new JMenuItem("Open example...");
+               item.getAccessibleContext().setAccessibleDescription("Open an example rocket design");
+               item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, 
+                               ActionEvent.CTRL_MASK | ActionEvent.SHIFT_MASK));
+               item.setIcon(Icons.FILE_OPEN_EXAMPLE);
+               item.addActionListener(new ActionListener() {
+                       public void actionPerformed(ActionEvent e) {
+                               URL[] urls = ExampleDesignDialog.selectExampleDesigns(BasicFrame.this);
+                               if (urls != null) {
+                                       for (URL u: urls) {
+                                               open(u, BasicFrame.this);
+                                       }
+                               }
+                       }
+               });
+               menu.add(item);
+               
                menu.addSeparator();
                
                item = new JMenuItem("Save",KeyEvent.VK_S);
@@ -505,6 +557,13 @@ public class BasicFrame extends JFrame {
                menu.add(item);
                
                
+               ////  Debug
+               // (shown if openrocket.debug.menu is defined)
+               if (System.getProperty("openrocket.debug.menu") != null) {
+                       menubar.add(makeDebugMenu());
+               }
+
+               
                
                ////  Help
                
@@ -513,6 +572,8 @@ public class BasicFrame extends JFrame {
                menu.getAccessibleContext().setAccessibleDescription("Information about OpenRocket");
                menubar.add(menu);
                
+               
+               
                item = new JMenuItem("License",KeyEvent.VK_L);
                item.getAccessibleContext().setAccessibleDescription("OpenRocket license information");
                item.addActionListener(new ActionListener() {
@@ -522,6 +583,17 @@ public class BasicFrame extends JFrame {
                });
                menu.add(item);
                
+               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);
+                               BugReportDialog.showBugReportDialog(BasicFrame.this);
+                       }
+               });
+               menu.add(item);
+               
                item = new JMenuItem("About",KeyEvent.VK_A);
                item.getAccessibleContext().setAccessibleDescription("About OpenRocket");
                item.addActionListener(new ActionListener() {
@@ -536,65 +608,327 @@ public class BasicFrame extends JFrame {
        }
        
        
+       private JMenu makeDebugMenu() {
+               JMenu menu;
+               JMenuItem item;
+               
+               ////  Debug menu
+               menu = new JMenu("Debug");
+               menu.getAccessibleContext().setAccessibleDescription("OpenRocket debugging tasks");
+               
+               item = new JMenuItem("What is this menu?");
+               item.addActionListener(new ActionListener() {
+                       public void actionPerformed(ActionEvent e) {
+                               JOptionPane.showMessageDialog(BasicFrame.this,
+                                               new Object[] {
+                                               "The 'Debug' menu includes actions for testing and debugging " +
+                                               "OpenRocket.", " ",
+                                               "The menu is made visible by defining the system property " +
+                                               "'openrocket.debug.menu' when starting OpenRocket.",
+                                               "It should not be visible by default." },
+                                               "Debug menu", JOptionPane.INFORMATION_MESSAGE);
+                       }
+               });
+               menu.add(item);
+               
+               menu.addSeparator();
+               
+               item = new JMenuItem("Create test rocket");
+               item.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               JTextField field = new JTextField();
+                               int sel = JOptionPane.showOptionDialog(BasicFrame.this, new Object[] {
+                                               "Input text key to generate random rocket:",
+                                               field
+                                       }, "Generate random test rocket", JOptionPane.DEFAULT_OPTION, 
+                                       JOptionPane.QUESTION_MESSAGE, null, new Object[] {
+                                               "Random", "OK"
+                               }, "OK");
+                               
+                               Rocket r;
+                               if (sel == 0) {
+                                       r = new TestRockets(null).makeTestRocket();
+                               } else if (sel == 1) {
+                                       r = new TestRockets(field.getText()).makeTestRocket();
+                               } else {
+                                       return;
+                               }
+                               
+                               OpenRocketDocument doc = new OpenRocketDocument(r);
+                               doc.setSaved(true);
+                               BasicFrame frame = new BasicFrame(doc);
+                               frame.setVisible(true);
+                       }
+               });
+               menu.add(item);
+               
+               
+
+               item = new JMenuItem("Create 'Iso-Haisu'");
+               item.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               Rocket r = TestRockets.makeIsoHaisu();
+                               OpenRocketDocument doc = new OpenRocketDocument(r);
+                               doc.setSaved(true);
+                               BasicFrame frame = new BasicFrame(doc);
+                               frame.setVisible(true);
+                       }
+               });
+               menu.add(item);
+               
+
+               item = new JMenuItem("Create 'Big Blue'");
+               item.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               Rocket r = TestRockets.makeBigBlue();
+                               OpenRocketDocument doc = new OpenRocketDocument(r);
+                               doc.setSaved(true);
+                               BasicFrame frame = new BasicFrame(doc);
+                               frame.setVisible(true);
+                       }
+               });
+               menu.add(item);
+               
+               
+               
+               menu.addSeparator();
+               
+               item = new JMenuItem("Exception here");
+               item.addActionListener(new ActionListener() {
+                       public void actionPerformed(ActionEvent e) {
+                               throw new RuntimeException("Testing exception from menu action listener");
+                       }
+               });
+               menu.add(item);
+               
+               item = new JMenuItem("Exception from EDT");
+               item.addActionListener(new ActionListener() {
+                       public void actionPerformed(ActionEvent e) {
+                               SwingUtilities.invokeLater(new Runnable() {
+                                       @Override
+                                       public void run() {
+                                               throw new RuntimeException("Testing exception from " +
+                                                               "later invoked EDT thread");
+                                       }
+                               });
+                       }
+               });
+               menu.add(item);
+               
+               item = new JMenuItem("Exception from other thread");
+               item.addActionListener(new ActionListener() {
+                       public void actionPerformed(ActionEvent e) {
+                               new Thread() {
+                                       @Override
+                                       public void run() {
+                                               throw new RuntimeException("Testing exception from " +
+                                                               "newly created thread");
+                                       }
+                               }.start();
+                       }
+               });
+               menu.add(item);
+               
+               
+               
+               return menu;
+       }
+       
+       
+       
+       /**
+        * Select the tab on the main pane.
+        * 
+        * @param tab   one of {@link #COMPONENT_TAB} or {@link #SIMULATION_TAB}.
+        */
+       public void selectTab(int tab) {
+               tabbedPane.setSelectedIndex(tab);
+       }
+
        
-       // TODO: HIGH: Remember last directory on open/save
        
        private void openAction() {
            JFileChooser chooser = new JFileChooser();
-           chooser.setFileFilter(ROCKET_DESIGN_FILTER);
+           
+           chooser.addChoosableFileFilter(ALL_DESIGNS_FILTER);
+           chooser.addChoosableFileFilter(OPENROCKET_DESIGN_FILTER);
+           chooser.addChoosableFileFilter(ROCKSIM_DESIGN_FILTER);
+           chooser.setFileFilter(ALL_DESIGNS_FILTER);
+
+           chooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
            chooser.setMultiSelectionEnabled(true);
            chooser.setCurrentDirectory(Prefs.getDefaultDirectory());
-           if (chooser.showOpenDialog(BasicFrame.this) != JFileChooser.APPROVE_OPTION)
+           if (chooser.showOpenDialog(this) != JFileChooser.APPROVE_OPTION)
                return;
            
            Prefs.setDefaultDirectory(chooser.getCurrentDirectory());
 
            File[] files = chooser.getSelectedFiles();
-           boolean opened = false;
            
            for (File file: files) {
                System.out.println("Opening file: " + file);
-               if (open(file)) {
-                       opened = true;
+               if (open(file, this)) {
+                       
+                       // Close previous window if replacing
+                       if (replaceable && document.isSaved()) {
+                               closeAction();
+                               replaceable = false;
+                       }
                }
            }
+       }
+       
+       
+       
+       private static boolean open(URL url, BasicFrame parent) {
+               String filename = null;
+               
+               // Try using URI.getPath();
+               try {
+                       URI uri = url.toURI();
+                       filename = uri.getPath();
+               } catch (URISyntaxException ignore) { }
 
-           // Close this frame if replaceable and file opened successfully
-               if (replaceable && opened) {
-                       closeAction();
+               // Try URL-decoding the URL
+               if (filename == null) {
+                       try {
+                               filename = URLDecoder.decode(url.toString(), "UTF-8");
+                       } catch (UnsupportedEncodingException ignore) { }
+               }
+               
+               // Last resort
+               if (filename == null) {
+                       filename = "";
+               }
+               
+               // Remove path from filename
+               if (filename.lastIndexOf('/') >= 0) {
+                       filename = filename.substring(filename.lastIndexOf('/')+1);
+               }
+               
+               try {
+                       InputStream is = url.openStream();
+                       if (open(is, filename, parent)) {
+                       // Close previous window if replacing
+                       if (parent.replaceable && parent.document.isSaved()) {
+                               parent.closeAction();
+                               parent.replaceable = false;
+                       }
+                       }
+               } catch (IOException e) {
+                       JOptionPane.showMessageDialog(parent, 
+                                       "An error occurred while opening the file " + filename,
+                                       "Error loading file", JOptionPane.ERROR_MESSAGE);
                }
+               
+               return false;
        }
        
        
        /**
-        * Open the specified file in a new design frame.  If an error occurs, an error dialog
-        * is shown and <code>false</code> is returned.
+        * Open the specified file from an InputStream in a new design frame.  If an error
+        * occurs, an error dialog is shown and <code>false</code> is returned.
         * 
-        * @param file  the file to open.
-        * @return              whether the file was successfully loaded and opened.
+        * @param stream        the stream to load from.
+        * @param filename      the file name to display in dialogs (not set to the document).
+        * @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) {
-           OpenRocketDocument doc = null;
-               try {
-                       doc = ROCKET_LOADER.load(file);
-               } 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(InputStream stream, String filename, Window parent) {
+               OpenFileWorker worker = new OpenFileWorker(stream, ROCKET_LOADER);
+               return open(worker, filename, null, parent);
+       }
+       
+
+       /**
+        * Open the specified file in a new design frame.  If an error occurs, an error
+        * dialog is shown and <code>false</code> is returned.
+        * 
+        * @param file          the file to open.
+        * @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, Window parent) {
+               OpenFileWorker worker = new OpenFileWorker(file, ROCKET_LOADER);
+               return open(worker, file.getName(), file, parent);
+       }
+       
+
+       /**
+        * Open the specified file using the provided worker.
+        * 
+        * @param worker        the OpenFileWorker that loads the file.
+        * @param filename      the file name to display in dialogs.
+        * @param file          the File to set the document to (may be null).
+        * @param parent
+        * @return
+        */
+       private static boolean open(OpenFileWorker worker, String filename, File file, 
+                       Window parent) {
+
+               // Open the file in a Swing worker thread
+               if (!SwingWorkerDialog.runWorker(parent, "Opening file", 
+                               "Reading " + filename + "...", 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: " + filename,
+                                               "Error opening file", JOptionPane.ERROR_MESSAGE);
+                               return false;
+
+                       } else if (cause instanceof RocketLoadException) {
+
+                               JOptionPane.showMessageDialog(parent, 
+                                               "Unable to open file '" + filename +"': " 
+                                               + cause.getMessage(),
+                                               "Error opening file", JOptionPane.ERROR_MESSAGE);
+                               return false;
+
+                       } else {
+
+                               throw new BugException("Unknown error when opening file", e);
+
+                       }
+
+               } catch (InterruptedException e) {
+                       throw new BugException("EDT was interrupted", e);
+               }
+               
+               if (doc == null) {
+                       throw new BugException("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:");
-           while (warns.hasNext()) {
-               System.out.println("  "+warns.next());
-               // TODO: HIGH: dialog
-           }
+               WarningSet warnings = worker.getRocketLoader().getWarnings();
+               if (!warnings.isEmpty()) {
+                       WarningDialog.showWarnings(parent,
+                                       new Object[] {
+                                       "The following problems were encountered while opening " + filename + ".",
+                                       "Some design features may not have been loaded correctly."
+                                       },
+                                       "Warnings while opening file", warnings);
+               }
+               
            
            // Set document state
            doc.setFile(file);
@@ -609,22 +943,45 @@ public class BasicFrame extends JFrame {
        
        
        
+       
+       
+       
+       
+       
+       
        private boolean saveAction() {
                File file = document.getFile();
                if (file==null) {
                        return saveAsAction();
-               } else {
-                       return saveAs(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"));
+
+                       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)
+                               return false;
+                       
+                       document.setFile(file);
+        }
+               return saveAs(file);
        }
        
+       
        private boolean saveAsAction() {
                File file = null;
                while (file == null) {
+                       // TODO: HIGH: what if *.rkt chosen?
                        StorageOptionChooser storageChooser = 
-                               new StorageOptionChooser(document.getDefaultStorageOptions());
+                               new StorageOptionChooser(document, document.getDefaultStorageOptions());
                        JFileChooser chooser = new JFileChooser();
-                       chooser.setFileFilter(ROCKET_DESIGN_FILTER);
+                       chooser.setFileFilter(OPENROCKET_DESIGN_FILTER);
                        chooser.setCurrentDirectory(Prefs.getDefaultDirectory());
                        chooser.setAccessory(storageChooser);
                        if (document.getFile() != null)
@@ -668,18 +1025,40 @@ 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 {
+                               Reflection.handleWrappedException(e);
+                       }
+                       
+               } catch (InterruptedException e) {
+                       throw new BugException("EDT was interrupted", e);
+               }
+           
            return saved;
        }
        
@@ -717,6 +1096,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.
         */
@@ -770,71 +1159,164 @@ public class BasicFrame extends JFrame {
        
        
        
+       /**
+        * Find a currently open BasicFrame containing the specified rocket.  This method
+        * can be used to map a Rocket to a BasicFrame from GUI methods.
+        * 
+        * @param rocket the Rocket.
+        * @return               the corresponding BasicFrame, or <code>null</code> if none found.
+        */
+       public static BasicFrame findFrame(Rocket rocket) {
+               for (BasicFrame f: frames) {
+                       if (f.rocket == rocket)
+                               return f;
+               }
+               return null;
+       }
        
+       /**
+        * Find a currently open document by the rocket object.  This method can be used
+        * to map a Rocket to OpenRocketDocument from GUI methods.
+        * 
+        * @param rocket the Rocket.
+        * @return               the corresponding OpenRocketDocument, or <code>null</code> 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(String[] args) {
+       public static void main(final String[] args) {
                
-               /*
-                * Set the look-and-feel.  On Linux, Motif/Metal is sometimes incorrectly used 
-                * which is butt-ugly, so if the system l&f is Motif/Metal, we search for a few
-                * other alternatives.
-                */
+               // Run the actual startup method in the EDT since it can use progress dialogs etc.
                try {
-                       UIManager.LookAndFeelInfo[] info = UIManager.getInstalledLookAndFeels();
-//                     System.out.println("Available look-and-feels:");
-//                     for (int i=0; i<info.length; i++) {
-//                             System.out.println("  "+info[i]);
-//                     }
-
-                       // Set system L&F
-                       UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
-                       
-                       // Check whether we have an ugly L&F
-                       LookAndFeel laf = UIManager.getLookAndFeel();
-                       if (laf == null ||
-                                       laf.getName().matches(".*[mM][oO][tT][iI][fF].*") ||
-                                       laf.getName().matches(".*[mM][eE][tT][aA][lL].*")) {
-                               
-                               // Search for better LAF
-                               for (UIManager.LookAndFeelInfo l: info) {
-                                       if (l.getName().matches(".*[gG][tT][kK].*")) {
-                                               UIManager.setLookAndFeel(l.getClassName());
-                                               break;
-                                       }
-                                       if (l.getName().contains(".*[wW][iI][nN].*")) {
-                                               UIManager.setLookAndFeel(l.getClassName());
-                                               break;
-                                       }
-                                       if (l.getName().contains(".*[mM][aA][cC].*")) {
-                                               UIManager.setLookAndFeel(l.getClassName());
-                                               break;
-                                       }
+                       SwingUtilities.invokeAndWait(new Runnable() {
+                               @Override
+                               public void run() {
+                                       runMain(args);
                                }
-                       }
-               } catch (Exception e) {
-                       System.err.println("Error setting LAF: " + e);
+                       });
+               } 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();
+               } else {
+                       updateInfo = 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))) {
+                       if (open(new File(file), null)) {
                                opened = true;
                        }
                }
-               
-               if (!opened) {
-                       newAction();
-               }
+               return opened;
        }
 
 }