updates for 0.9.3
[debian/openrocket] / src / net / sf / openrocket / gui / main / BasicFrame.java
index 3f12d259fc916fdd7047bd662a52acfce6a54a9c..5357b3290add757453d6fc4dd93ad400c6d08eb7 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;
@@ -49,26 +58,34 @@ 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.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.gui.StorageOptionChooser;
 import net.sf.openrocket.gui.configdialog.ComponentConfigDialog;
-import net.sf.openrocket.gui.dialogs.BugDialog;
+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.PreferencesDialog;
+import net.sf.openrocket.gui.dialogs.SwingWorkerDialog;
+import net.sf.openrocket.gui.dialogs.WarningDialog;
 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.GUIUtil;
 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;
@@ -77,6 +94,8 @@ 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();
 
        
        /**
@@ -224,7 +243,9 @@ public class BasicFrame extends JFrame {
                        }
                });
                this.setLocationByPlatform(true);
-                               
+
+               GUIUtil.setWindowIcons(this);
+               
                this.validate();
                vertical.setDividerLocation(0.4);
                setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
@@ -235,7 +256,6 @@ public class BasicFrame extends JFrame {
                        }
                });
                frames.add(this);
-               
        }
        
        
@@ -392,6 +412,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);
@@ -518,6 +555,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
                
@@ -542,7 +586,8 @@ public class BasicFrame extends JFrame {
                                "bugs in OpenRocket");
                item.addActionListener(new ActionListener() {
                        public void actionPerformed(ActionEvent e) {
-                               new BugDialog(BasicFrame.this).setVisible(true);
+//                             new BugDialog(BasicFrame.this).setVisible(true);
+                               BugReportDialog.showBugReportDialog(BasicFrame.this);
                        }
                });
                menu.add(item);
@@ -561,6 +606,73 @@ 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("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.
@@ -584,51 +696,163 @@ 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)) {
-                       opened = true;
+               if (open(file, this)) {
+                       
+                       // Close previous window if replacing
+                       if (replaceable && document.isSaved()) {
+                               closeAction();
+                               replaceable = false;
+                       }
                }
            }
+       }
+       
+       
+       
+       
+       private static boolean open(URL url, Window 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();
+                       open(is, filename, parent);
+               } 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 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:");
-           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);
@@ -643,6 +867,12 @@ public class BasicFrame extends JFrame {
        
        
        
+       
+       
+       
+       
+       
+       
        private boolean saveAction() {
                File file = document.getFile();
                if (file==null) {
@@ -652,11 +882,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());
@@ -702,18 +933,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;
        }
        
@@ -751,6 +1003,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.
         */
@@ -804,23 +1066,64 @@ 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) {
                
+               // Run the actual startup method in the EDT since it can use progress dialogs etc.
+               try {
+                       SwingUtilities.invokeAndWait(new Runnable() {
+                               @Override
+                               public void run() {
+                                       runMain(args);
+                               }
+                       });
+               } catch (InterruptedException e) {
+                       e.printStackTrace();
+               } catch (InvocationTargetException e) {
+                       e.printStackTrace();
+               }
+               
+       }
+       
+       
+       private static void runMain(String[] args) {
+
                /*
                 * 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.
                 */
                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());
                        
@@ -831,19 +1134,22 @@ public class BasicFrame extends JFrame {
                                        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;
-                                       }
+                               UIManager.LookAndFeelInfo[] info = UIManager.getInstalledLookAndFeels();
+                               String lafNames[] = {
+                                               ".*[gG][tT][kK].*",
+                                               ".*[wW][iI][nN].*",
+                                               ".*[mM][aA][cC].*",
+                                               ".*[aA][qQ][uU][aA].*",
+                                               ".*[nN][iI][mM][bB].*"
+                               };
+                               
+                               lf: for (String lafName: lafNames) {
+                                       for (UIManager.LookAndFeelInfo l: info) {
+                                               if (l.getName().matches(lafName)) {
+                                                       UIManager.setLookAndFeel(l.getClassName());
+                                                       break lf;
+                                               }
+                                       }                                       
                                }
                        }
                } catch (Exception e) {
@@ -854,21 +1160,40 @@ public class BasicFrame extends JFrame {
                ToolTipManager.sharedInstance().setDismissDelay(30000);
                
                
+               // Setup the uncaught exception handler
+               ExceptionHandler.registerExceptionHandler();
+               
+               
                // Load defaults
                Prefs.loadDefaultUnits();
 
                
+               // Starting action
+               if (!handleCommandLine(args)) {
+                       newAction();
+               }
+       }
+       
+       
+       /**
+        * 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;
        }
 
 }