updates for 0.9.2
[debian/openrocket] / src / net / sf / openrocket / gui / main / BasicFrame.java
1 package net.sf.openrocket.gui.main;
2
3 import java.awt.Dimension;
4 import java.awt.Font;
5 import java.awt.Toolkit;
6 import java.awt.Window;
7 import java.awt.event.ActionEvent;
8 import java.awt.event.ActionListener;
9 import java.awt.event.ComponentAdapter;
10 import java.awt.event.ComponentEvent;
11 import java.awt.event.KeyEvent;
12 import java.awt.event.MouseAdapter;
13 import java.awt.event.MouseEvent;
14 import java.awt.event.MouseListener;
15 import java.awt.event.WindowAdapter;
16 import java.awt.event.WindowEvent;
17 import java.io.File;
18 import java.io.FileNotFoundException;
19 import java.io.IOException;
20 import java.lang.reflect.InvocationTargetException;
21 import java.util.ArrayList;
22 import java.util.Iterator;
23 import java.util.concurrent.ExecutionException;
24
25 import javax.swing.Action;
26 import javax.swing.InputMap;
27 import javax.swing.JButton;
28 import javax.swing.JComponent;
29 import javax.swing.JFileChooser;
30 import javax.swing.JFrame;
31 import javax.swing.JMenu;
32 import javax.swing.JMenuBar;
33 import javax.swing.JMenuItem;
34 import javax.swing.JOptionPane;
35 import javax.swing.JPanel;
36 import javax.swing.JScrollPane;
37 import javax.swing.JSeparator;
38 import javax.swing.JSplitPane;
39 import javax.swing.JTabbedPane;
40 import javax.swing.KeyStroke;
41 import javax.swing.ListSelectionModel;
42 import javax.swing.LookAndFeel;
43 import javax.swing.ScrollPaneConstants;
44 import javax.swing.SwingUtilities;
45 import javax.swing.ToolTipManager;
46 import javax.swing.UIManager;
47 import javax.swing.border.TitledBorder;
48 import javax.swing.event.TreeSelectionEvent;
49 import javax.swing.event.TreeSelectionListener;
50 import javax.swing.filechooser.FileFilter;
51 import javax.swing.tree.DefaultTreeSelectionModel;
52 import javax.swing.tree.TreePath;
53 import javax.swing.tree.TreeSelectionModel;
54
55 import net.miginfocom.swing.MigLayout;
56 import net.sf.openrocket.aerodynamics.Warning;
57 import net.sf.openrocket.document.OpenRocketDocument;
58 import net.sf.openrocket.file.GeneralRocketLoader;
59 import net.sf.openrocket.file.OpenRocketSaver;
60 import net.sf.openrocket.file.RocketLoadException;
61 import net.sf.openrocket.file.RocketLoader;
62 import net.sf.openrocket.file.RocketSaver;
63 import net.sf.openrocket.gui.StorageOptionChooser;
64 import net.sf.openrocket.gui.configdialog.ComponentConfigDialog;
65 import net.sf.openrocket.gui.dialogs.BugDialog;
66 import net.sf.openrocket.gui.dialogs.ComponentAnalysisDialog;
67 import net.sf.openrocket.gui.dialogs.LicenseDialog;
68 import net.sf.openrocket.gui.dialogs.PreferencesDialog;
69 import net.sf.openrocket.gui.dialogs.SwingWorkerDialog;
70 import net.sf.openrocket.gui.scalefigure.RocketPanel;
71 import net.sf.openrocket.rocketcomponent.ComponentChangeEvent;
72 import net.sf.openrocket.rocketcomponent.ComponentChangeListener;
73 import net.sf.openrocket.rocketcomponent.Rocket;
74 import net.sf.openrocket.rocketcomponent.RocketComponent;
75 import net.sf.openrocket.rocketcomponent.Stage;
76 import net.sf.openrocket.util.Icons;
77 import net.sf.openrocket.util.OpenFileWorker;
78 import net.sf.openrocket.util.Prefs;
79 import net.sf.openrocket.util.SaveFileWorker;
80
81 public class BasicFrame extends JFrame {
82         private static final long serialVersionUID = 1L;
83
84         /**
85          * The RocketLoader instance used for loading all rocket designs.
86          */
87         private static final RocketLoader ROCKET_LOADER = new GeneralRocketLoader();
88         
89         // TODO: Always uses OpenRocketSaver
90         private static final RocketSaver ROCKET_SAVER = new OpenRocketSaver();
91
92         
93         /**
94          * File filter for filtering only rocket designs.
95          */
96         private static final FileFilter ROCKET_DESIGN_FILTER = new FileFilter() {
97                 @Override
98                 public String getDescription() {
99                         return "OpenRocket designs (*.ork)";
100                 }
101                 @Override
102                 public boolean accept(File f) {
103                         if (f.isDirectory())
104                                 return true;
105                         String name = f.getName().toLowerCase();
106                         return name.endsWith(".ork") || name.endsWith(".ork.gz");
107                 }
108     };
109     
110     
111     
112     public static final int COMPONENT_TAB = 0;
113     public static final int SIMULATION_TAB = 1;
114     
115
116         /**
117          * List of currently open frames.  When the list goes empty
118          * it is time to exit the application.
119          */
120         private static final ArrayList<BasicFrame> frames = new ArrayList<BasicFrame>();
121         
122         
123         
124         
125         
126         /**
127          * Whether "New" and "Open" should replace this frame.
128          * Should be set to false on the first rocket modification.
129          */
130         private boolean replaceable = false;
131         
132         
133         
134         private final OpenRocketDocument document;
135         private final Rocket rocket;
136         
137         private JTabbedPane tabbedPane;
138         private RocketPanel rocketpanel;
139         private ComponentTree tree = null;
140         
141         private final DocumentSelectionModel selectionModel;
142         private final TreeSelectionModel componentSelectionModel;
143         private final ListSelectionModel simulationSelectionModel;
144         
145         /** Actions available for rocket modifications */
146         private final RocketActions actions;
147         
148         
149         
150         /**
151          * Sole constructor.  Creates a new frame based on the supplied document
152          * and adds it to the current frames list.
153          * 
154          * @param document      the document to show.
155          */
156         public BasicFrame(OpenRocketDocument document) {
157
158                 this.document = document;
159                 this.rocket = document.getRocket();
160                 this.rocket.getDefaultConfiguration().setAllStages();
161                 
162                 
163                 // Set replaceable flag to false at first modification
164                 rocket.addComponentChangeListener(new ComponentChangeListener() {
165                         public void componentChanged(ComponentChangeEvent e) {
166                                 replaceable = false;
167                                 BasicFrame.this.rocket.removeComponentChangeListener(this);
168                         }
169                 });
170                 
171                 
172                 // Create the component tree selection model that will be used
173                 componentSelectionModel = new DefaultTreeSelectionModel();
174                 componentSelectionModel.setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
175                 
176                 // Obtain the simulation selection model that will be used
177                 SimulationPanel simulationPanel = new SimulationPanel(document);
178                 simulationSelectionModel = simulationPanel.getSimulationListSelectionModel();
179                 
180                 // Combine into a DocumentSelectionModel
181                 selectionModel = new DocumentSelectionModel(document);
182                 selectionModel.attachComponentTreeSelectionModel(componentSelectionModel);
183                 selectionModel.attachSimulationListSelectionModel(simulationSelectionModel);
184                 
185                 
186                 actions = new RocketActions(document, selectionModel, this);
187                 
188                 
189                 // The main vertical split pane         
190                 JSplitPane vertical = new JSplitPane(JSplitPane.VERTICAL_SPLIT, true);
191                 vertical.setResizeWeight(0.5);
192                 this.add(vertical);
193
194                 
195                 // The top tabbed pane
196                 tabbedPane = new JTabbedPane();
197                 tabbedPane.addTab("Rocket design", null, designTab());
198                 tabbedPane.addTab("Flight simulations", null, simulationPanel);
199                 
200                 vertical.setTopComponent(tabbedPane);
201                 
202
203
204                 //  Bottom segment, rocket figure
205                 
206                 rocketpanel = new RocketPanel(document);
207                 vertical.setBottomComponent(rocketpanel);
208
209                 rocketpanel.setSelectionModel(tree.getSelectionModel());
210
211                                         
212                 createMenu();
213                 
214                 
215                 rocket.addComponentChangeListener(new ComponentChangeListener() {
216                         public void componentChanged(ComponentChangeEvent e) {
217                                 setTitle();
218                         }
219                 });
220                 
221                 setTitle();
222                 this.pack();
223
224                 Dimension size = Prefs.getWindowSize(this.getClass());
225                 if (size == null) {
226                         size = Toolkit.getDefaultToolkit().getScreenSize();
227                         size.width = size.width*9/10;
228                         size.height = size.height*9/10;
229                 }
230                 this.setSize(size);
231                 this.addComponentListener(new ComponentAdapter() {
232                         @Override
233                         public void componentResized(ComponentEvent e) {
234                                 Prefs.setWindowSize(BasicFrame.this.getClass(), BasicFrame.this.getSize());
235                         }
236                 });
237                 this.setLocationByPlatform(true);
238                                 
239                 this.validate();
240                 vertical.setDividerLocation(0.4);
241                 setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
242                 addWindowListener(new WindowAdapter() {
243                         @Override
244                         public void windowClosing(WindowEvent e) {
245                                 closeAction();
246                         }
247                 });
248                 frames.add(this);
249         }
250         
251         
252         /**
253          * Construct the "Rocket design" tab.  This contains a horizontal split pane
254          * with the left component the design tree and the right component buttons
255          * for adding components.
256          */
257         private JComponent designTab() {
258                 JSplitPane horizontal = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT,true);
259                 horizontal.setResizeWeight(0.5);
260
261
262                 //  Upper-left segment, component tree
263
264                 JPanel panel = new JPanel(new MigLayout("fill, flowy","","[grow]"));
265
266                 tree = new ComponentTree(rocket);
267                 tree.setSelectionModel(componentSelectionModel);
268
269                 // Remove JTree key events that interfere with menu accelerators
270                 InputMap im = SwingUtilities.getUIInputMap(tree, JComponent.WHEN_FOCUSED);
271                 im.put(KeyStroke.getKeyStroke(KeyEvent.VK_X, ActionEvent.CTRL_MASK), null);
272                 im.put(KeyStroke.getKeyStroke(KeyEvent.VK_C, ActionEvent.CTRL_MASK), null);
273                 im.put(KeyStroke.getKeyStroke(KeyEvent.VK_V, ActionEvent.CTRL_MASK), null);
274                 im.put(KeyStroke.getKeyStroke(KeyEvent.VK_A, ActionEvent.CTRL_MASK), null);
275                 im.put(KeyStroke.getKeyStroke(KeyEvent.VK_S, ActionEvent.CTRL_MASK), null);
276                 im.put(KeyStroke.getKeyStroke(KeyEvent.VK_O, ActionEvent.CTRL_MASK), null);
277                 im.put(KeyStroke.getKeyStroke(KeyEvent.VK_N, ActionEvent.CTRL_MASK), null);
278
279
280                 
281                 // Double-click opens config dialog
282                 MouseListener ml = new MouseAdapter() {
283                         @Override
284                         public void mousePressed(MouseEvent e) {
285                                 int selRow = tree.getRowForLocation(e.getX(), e.getY());
286                                 TreePath selPath = tree.getPathForLocation(e.getX(), e.getY());
287                                 if(selRow != -1) {
288                                         if(e.getClickCount() == 2) {
289                                                 // Double-click
290                                                 RocketComponent c = (RocketComponent)selPath.getLastPathComponent();
291                                                 ComponentConfigDialog.showDialog(BasicFrame.this, 
292                                                                 BasicFrame.this.document, c);
293                                         }
294                                 }
295                         }
296                 };
297                 tree.addMouseListener(ml);
298
299                 // Update dialog when selection is changed
300                 componentSelectionModel.addTreeSelectionListener(new TreeSelectionListener() {
301                         public void valueChanged(TreeSelectionEvent e) {
302                                 // Scroll tree to the selected item
303                                 TreePath path = componentSelectionModel.getSelectionPath();
304                                 if (path == null)
305                                         return;
306                                 tree.scrollPathToVisible(path);
307                                 
308                                 if (!ComponentConfigDialog.isDialogVisible())
309                                         return;
310                                 RocketComponent c = (RocketComponent)path.getLastPathComponent();
311                                 ComponentConfigDialog.showDialog(BasicFrame.this, 
312                                                 BasicFrame.this.document, c);
313                         }
314                 });
315
316                 // Place tree inside scroll pane
317                 JScrollPane scroll = new JScrollPane(tree);
318                 panel.add(scroll,"spany, grow, wrap");
319                 
320                 
321                 // Buttons
322                 JButton button = new JButton(actions.getMoveUpAction());
323                 panel.add(button,"sizegroup buttons, aligny 65%");
324                 
325                 button = new JButton(actions.getMoveDownAction());
326                 panel.add(button,"sizegroup buttons, aligny 0%");
327                 
328                 button = new JButton(actions.getEditAction());
329                 panel.add(button, "sizegroup buttons");
330                 
331                 button = new JButton(actions.getNewStageAction());
332                 panel.add(button,"sizegroup buttons");
333                 
334                 button = new JButton(actions.getDeleteAction());
335                 button.setIcon(null);
336                 button.setMnemonic(0);
337                 panel.add(button,"sizegroup buttons");
338
339                 horizontal.setLeftComponent(panel);
340
341
342                 //  Upper-right segment, component addition buttons
343
344                 panel = new JPanel(new MigLayout("fill, insets 0","[0::]"));
345
346                 scroll = new JScrollPane(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED,
347                                 ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
348                 scroll.setViewportView(new ComponentAddButtons(document, componentSelectionModel,
349                                 scroll.getViewport()));
350                 scroll.setBorder(null);
351                 scroll.setViewportBorder(null);
352
353                 TitledBorder border = new TitledBorder("Add new component");
354                 border.setTitleFont(border.getTitleFont().deriveFont(Font.BOLD));
355                 scroll.setBorder(border);
356
357                 panel.add(scroll,"grow");
358
359                 horizontal.setRightComponent(panel);
360
361                 return horizontal;
362         }
363         
364         
365         
366         /**
367          * Creates the menu for the window.
368          */
369         private void createMenu() {
370                 JMenuBar menubar = new JMenuBar();
371                 JMenu menu;
372                 JMenuItem item;
373                 
374                 ////  File
375                 menu = new JMenu("File");
376                 menu.setMnemonic(KeyEvent.VK_F);
377                 menu.getAccessibleContext().setAccessibleDescription("File-handling related tasks");
378                 menubar.add(menu);
379                 
380                 item = new JMenuItem("New",KeyEvent.VK_N);
381                 item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_N, ActionEvent.CTRL_MASK));
382                 item.setMnemonic(KeyEvent.VK_N);
383                 item.getAccessibleContext().setAccessibleDescription("Create a new rocket design");
384                 item.setIcon(Icons.FILE_NEW);
385                 item.addActionListener(new ActionListener() {
386                         public void actionPerformed(ActionEvent e) {
387                                 newAction();
388                                 if (replaceable)
389                                         closeAction();
390                         }
391                 });
392                 menu.add(item);
393                 
394                 item = new JMenuItem("Open...",KeyEvent.VK_O);
395                 item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, ActionEvent.CTRL_MASK));
396                 item.getAccessibleContext().setAccessibleDescription("Open a rocket design");
397                 item.setIcon(Icons.FILE_OPEN);
398                 item.addActionListener(new ActionListener() {
399                         public void actionPerformed(ActionEvent e) {
400                                 openAction();
401                         }
402                 });
403                 menu.add(item);
404                 
405                 menu.addSeparator();
406                 
407                 item = new JMenuItem("Save",KeyEvent.VK_S);
408                 item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, ActionEvent.CTRL_MASK));
409                 item.getAccessibleContext().setAccessibleDescription("Save the current rocket design");
410                 item.setIcon(Icons.FILE_SAVE);
411                 item.addActionListener(new ActionListener() {
412                         public void actionPerformed(ActionEvent e) {
413                                 saveAction();
414                         }
415                 });
416                 menu.add(item);
417                 
418                 item = new JMenuItem("Save as...",KeyEvent.VK_A);
419                 item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, 
420                                 ActionEvent.CTRL_MASK | ActionEvent.SHIFT_MASK));
421                 item.getAccessibleContext().setAccessibleDescription("Save the current rocket design "+
422                                 "to a new file");
423                 item.setIcon(Icons.FILE_SAVE_AS);
424                 item.addActionListener(new ActionListener() {
425                         public void actionPerformed(ActionEvent e) {
426                                 saveAsAction();
427                         }
428                 });
429                 menu.add(item);
430                 
431 //              menu.addSeparator();
432                 menu.add(new JSeparator());
433                 
434                 item = new JMenuItem("Close",KeyEvent.VK_C);
435                 item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_W, ActionEvent.CTRL_MASK));
436                 item.getAccessibleContext().setAccessibleDescription("Close the current rocket design");
437                 item.setIcon(Icons.FILE_CLOSE);
438                 item.addActionListener(new ActionListener() {
439                         public void actionPerformed(ActionEvent e) {
440                                 closeAction();
441                         }
442                 });
443                 menu.add(item);
444                 
445                 menu.addSeparator();
446
447                 item = new JMenuItem("Quit",KeyEvent.VK_Q);
448                 item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Q, ActionEvent.CTRL_MASK));
449                 item.getAccessibleContext().setAccessibleDescription("Quit the program");
450                 item.setIcon(Icons.FILE_QUIT);
451                 item.addActionListener(new ActionListener() {
452                         public void actionPerformed(ActionEvent e) {
453                                 quitAction();
454                         }
455                 });
456                 menu.add(item);
457                 
458                 
459
460                 ////  Edit
461                 menu = new JMenu("Edit");
462                 menu.setMnemonic(KeyEvent.VK_E);
463                 menu.getAccessibleContext().setAccessibleDescription("Rocket editing");
464                 menubar.add(menu);
465                 
466                 
467                 Action action = document.getUndoAction();
468                 item = new JMenuItem(action);
469                 item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Z, ActionEvent.CTRL_MASK));
470                 item.setMnemonic(KeyEvent.VK_U);
471                 item.getAccessibleContext().setAccessibleDescription("Undo the previous operation");
472
473                 menu.add(item);
474
475                 action = document.getRedoAction();
476                 item = new JMenuItem(action);
477                 item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Y, ActionEvent.CTRL_MASK));
478                 item.setMnemonic(KeyEvent.VK_R);
479                 item.getAccessibleContext().setAccessibleDescription("Redo the previously undone " +
480                                 "operation");
481                 menu.add(item);
482                 
483                 menu.addSeparator();
484                 
485                 
486                 item = new JMenuItem(actions.getCutAction());
487                 menu.add(item);
488         
489                 item = new JMenuItem(actions.getCopyAction());
490                 menu.add(item);
491         
492                 item = new JMenuItem(actions.getPasteAction());
493                 menu.add(item);
494                 
495                 item = new JMenuItem(actions.getDeleteAction());
496                 menu.add(item);
497                 
498                 menu.addSeparator();
499                 
500                 item = new JMenuItem("Preferences");
501                 item.setIcon(Icons.PREFERENCES);
502                 item.getAccessibleContext().setAccessibleDescription("Setup the application "+
503                                 "preferences");
504                 item.addActionListener(new ActionListener() {
505                         public void actionPerformed(ActionEvent e) {
506                                 PreferencesDialog.showPreferences();
507                         }
508                 });
509                 menu.add(item);
510         
511                 
512
513
514                 ////  Analyze
515                 menu = new JMenu("Analyze");
516                 menu.setMnemonic(KeyEvent.VK_A);
517                 menu.getAccessibleContext().setAccessibleDescription("Analyzing the rocket");
518                 menubar.add(menu);
519                 
520                 item = new JMenuItem("Component analysis",KeyEvent.VK_C);
521                 item.getAccessibleContext().setAccessibleDescription("Analyze the rocket components " +
522                                 "separately");
523                 item.addActionListener(new ActionListener() {
524                         public void actionPerformed(ActionEvent e) {
525                                 ComponentAnalysisDialog.showDialog(rocketpanel);
526                         }
527                 });
528                 menu.add(item);
529                 
530                 
531                 
532                 ////  Help
533                 
534                 menu = new JMenu("Help");
535                 menu.setMnemonic(KeyEvent.VK_H);
536                 menu.getAccessibleContext().setAccessibleDescription("Information about OpenRocket");
537                 menubar.add(menu);
538                 
539                 
540                 
541                 item = new JMenuItem("License",KeyEvent.VK_L);
542                 item.getAccessibleContext().setAccessibleDescription("OpenRocket license information");
543                 item.addActionListener(new ActionListener() {
544                         public void actionPerformed(ActionEvent e) {
545                                 new LicenseDialog(BasicFrame.this).setVisible(true);
546                         }
547                 });
548                 menu.add(item);
549                 
550                 item = new JMenuItem("Bug report",KeyEvent.VK_B);
551                 item.getAccessibleContext().setAccessibleDescription("Information about reporting " +
552                                 "bugs in OpenRocket");
553                 item.addActionListener(new ActionListener() {
554                         public void actionPerformed(ActionEvent e) {
555                                 new BugDialog(BasicFrame.this).setVisible(true);
556                         }
557                 });
558                 menu.add(item);
559                 
560                 item = new JMenuItem("About",KeyEvent.VK_A);
561                 item.getAccessibleContext().setAccessibleDescription("About OpenRocket");
562                 item.addActionListener(new ActionListener() {
563                         public void actionPerformed(ActionEvent e) {
564                                 new AboutDialog(BasicFrame.this).setVisible(true);
565                         }
566                 });
567                 menu.add(item);
568                 
569                 
570                 this.setJMenuBar(menubar);
571         }
572         
573         
574         
575         /**
576          * Select the tab on the main pane.
577          * 
578          * @param tab   one of {@link #COMPONENT_TAB} or {@link #SIMULATION_TAB}.
579          */
580         public void selectTab(int tab) {
581                 tabbedPane.setSelectedIndex(tab);
582         }
583
584         
585         
586         private void openAction() {
587             JFileChooser chooser = new JFileChooser();
588             chooser.setFileFilter(ROCKET_DESIGN_FILTER);
589             chooser.setMultiSelectionEnabled(true);
590             chooser.setCurrentDirectory(Prefs.getDefaultDirectory());
591             if (chooser.showOpenDialog(this) != JFileChooser.APPROVE_OPTION)
592                 return;
593             
594             Prefs.setDefaultDirectory(chooser.getCurrentDirectory());
595
596             File[] files = chooser.getSelectedFiles();
597             
598             for (File file: files) {
599                 System.out.println("Opening file: " + file);
600                 if (open(file, this)) {
601                         
602                         // Close previous window if replacing
603                         if (replaceable && document.isSaved()) {
604                                 closeAction();
605                                 replaceable = false;
606                         }
607                 }
608             }
609         }
610         
611         
612         /**
613          * Open the specified file in a new design frame.  If an error occurs, an error dialog
614          * is shown and <code>false</code> is returned.
615          * 
616          * @param file          the file to open.
617          * @param parent        the parent component for which a progress dialog is opened.
618          * @return                      whether the file was successfully loaded and opened.
619          */
620         private static boolean open(File file, Window parent) {
621
622                 // Open the file in a Swing worker thread
623                 OpenFileWorker worker = new OpenFileWorker(file);
624                 if (!SwingWorkerDialog.runWorker(parent, "Opening file", 
625                                 "Reading " + file.getName() + "...", worker)) {
626
627                         // User cancelled the operation
628                         return false;
629                 }
630
631                 
632                 // Handle the document
633                 OpenRocketDocument doc = null;
634                 try {
635
636                         doc = worker.get();
637
638                 } catch (ExecutionException e) {
639
640                         Throwable cause = e.getCause();
641
642                         if (cause instanceof FileNotFoundException) {
643
644                                 JOptionPane.showMessageDialog(parent, 
645                                                 "File not found: " + file.getName(), 
646                                                 "Error opening file", JOptionPane.ERROR_MESSAGE);
647                                 return false;
648
649                         } else if (cause instanceof RocketLoadException) {
650
651                                 JOptionPane.showMessageDialog(parent, 
652                                                 "Unable to open file '" + file.getName() +"': " 
653                                                 + cause.getMessage(),
654                                                 "Error opening file", JOptionPane.ERROR_MESSAGE);
655                                 return false;
656
657                         } else {
658
659                                 throw new RuntimeException("Unknown error when opening file", e);
660
661                         }
662
663                 } catch (InterruptedException e) {
664                         throw new RuntimeException("EDT was interrupted", e);
665                 }
666                 
667                 if (doc == null) {
668                         throw new RuntimeException("BUG: Document loader returned null");
669                 }
670                 
671                 
672             // Show warnings
673             Iterator<Warning> warns = ROCKET_LOADER.getWarnings().iterator();
674             System.out.println("Warnings:");
675             while (warns.hasNext()) {
676                 System.out.println("  "+warns.next());
677                 // TODO: HIGH: dialog
678             }
679             
680             // Set document state
681             doc.setFile(file);
682             doc.setSaved(true);
683
684             // Open the frame
685             BasicFrame frame = new BasicFrame(doc);
686             frame.setVisible(true);
687
688             return true;
689         }
690         
691         
692         
693         
694         
695         
696         
697         
698         
699         private boolean saveAction() {
700                 File file = document.getFile();
701                 if (file==null) {
702                         return saveAsAction();
703                 } else {
704                         return saveAs(file);
705                 }
706         }
707         
708         
709         private boolean saveAsAction() {
710                 File file = null;
711                 while (file == null) {
712                         StorageOptionChooser storageChooser = 
713                                 new StorageOptionChooser(document, document.getDefaultStorageOptions());
714                         JFileChooser chooser = new JFileChooser();
715                         chooser.setFileFilter(ROCKET_DESIGN_FILTER);
716                         chooser.setCurrentDirectory(Prefs.getDefaultDirectory());
717                         chooser.setAccessory(storageChooser);
718                         if (document.getFile() != null)
719                                 chooser.setSelectedFile(document.getFile());
720                         
721                         if (chooser.showSaveDialog(BasicFrame.this) != JFileChooser.APPROVE_OPTION)
722                                 return false;
723                         
724                         file = chooser.getSelectedFile();
725                         if (file == null)
726                                 return false;
727
728                         Prefs.setDefaultDirectory(chooser.getCurrentDirectory());
729                         storageChooser.storeOptions(document.getDefaultStorageOptions());
730                         
731                         if (file.getName().indexOf('.') < 0) {
732                                 String name = file.getAbsolutePath();
733                                 name = name + ".ork";
734                                 file = new File(name);
735                         }
736                         
737                         if (file.exists()) {
738                                 int result = JOptionPane.showConfirmDialog(this, 
739                                                 "File '"+file.getName()+"' exists.  Do you want to overwrite it?", 
740                                                 "File exists", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE);
741                                 if (result != JOptionPane.YES_OPTION)
742                                         return false;
743                         }
744                 }
745             saveAs(file);
746             return true;
747         }
748         
749         
750         private boolean saveAs(File file) {
751             System.out.println("Saving to file: " + file.getName());
752             boolean saved = false;
753             
754             if (!StorageOptionChooser.verifyStorageOptions(document, this)) {
755                 // User cancelled the dialog
756                 return false;
757             }
758
759
760             SaveFileWorker worker = new SaveFileWorker(document, file, ROCKET_SAVER);
761
762             if (!SwingWorkerDialog.runWorker(this, "Saving file", 
763                         "Writing " + file.getName() + "...", worker)) {
764                 
765                 // User cancelled the save
766                 file.delete();
767                 return false;
768             }
769             
770             try {
771                         worker.get();
772                         document.setFile(file);
773                         document.setSaved(true);
774                         saved = true;
775                     setTitle();
776                 } catch (ExecutionException e) {
777                         Throwable cause = e.getCause();
778                         
779                         if (cause instanceof IOException) {
780                         JOptionPane.showMessageDialog(this, new String[] { 
781                                         "An I/O error occurred while saving:",
782                                         e.getMessage() }, "Saving failed", JOptionPane.ERROR_MESSAGE);
783                         return false;
784                         } else {
785                                 throw new RuntimeException("Unknown error when saving file", e);
786                         }
787                         
788                 } catch (InterruptedException e) {
789                         throw new RuntimeException("EDT was interrupted", e);
790                 }
791             
792             return saved;
793         }
794         
795         
796         private boolean closeAction() {
797                 if (!document.isSaved()) {
798                         ComponentConfigDialog.hideDialog();
799                         int result = JOptionPane.showConfirmDialog(this, 
800                                         "Design '"+rocket.getName()+"' has not been saved.  " +
801                                                         "Do you want to save it?", 
802                                         "Design not saved", JOptionPane.YES_NO_CANCEL_OPTION, 
803                                         JOptionPane.QUESTION_MESSAGE);
804                         if (result == JOptionPane.YES_OPTION) {
805                                 // Save
806                                 if (!saveAction())
807                                         return false;  // If save was interrupted
808                         } else if (result == JOptionPane.NO_OPTION) {
809                                 // Don't save: No-op
810                         } else {
811                                 // Cancel or close
812                                 return false;
813                         }
814                 }
815                 
816                 // Rocket has been saved or discarded
817                 this.dispose();
818
819                 // TODO: LOW: Close only dialogs that have this frame as their parent
820                 ComponentConfigDialog.hideDialog();
821                 ComponentAnalysisDialog.hideDialog();
822                 
823                 frames.remove(this);
824                 if (frames.isEmpty())
825                         System.exit(0);
826                 return true;
827         }
828         
829         
830         /**
831          * Closes this frame if it is replaceable.
832          */
833         public void closeIfReplaceable() {
834                 if (this.replaceable && document.isSaved()) {
835                         closeAction();
836                 }
837         }
838         
839         /**
840          * Open a new design window with a basic rocket+stage.
841          */
842         public static void newAction() {
843                 Rocket rocket = new Rocket();
844                 Stage stage = new Stage();
845                 stage.setName("Sustainer");
846                 rocket.addChild(stage);
847                 OpenRocketDocument doc = new OpenRocketDocument(rocket);
848                 doc.setSaved(true);
849                 
850                 BasicFrame frame = new BasicFrame(doc);
851                 frame.replaceable = true;
852                 frame.setVisible(true);
853                 ComponentConfigDialog.showDialog(frame, doc, rocket);
854         }
855         
856         /**
857          * Quit the application.  Confirms saving unsaved designs.  The action of File->Quit.
858          */
859         public static void quitAction() {
860                 for (int i=frames.size()-1; i>=0; i--) {
861                         if (!frames.get(i).closeAction()) {
862                                 // Close canceled
863                                 return;
864                         }
865                 }
866                 // Should not be reached, but just in case
867                 System.exit(0);
868         }
869         
870         
871         /**
872          * Set the title of the frame, taking into account the name of the rocket, file it 
873          * has been saved to (if any) and saved status.
874          */
875         private void setTitle() {
876                 File file = document.getFile();
877                 boolean saved = document.isSaved();
878                 String title;
879                 
880                 title = rocket.getName();
881                 if (file!=null) {
882                         title = title + " ("+file.getName()+")";
883                 }
884                 if (!saved)
885                         title = "*" + title;
886                 
887                 setTitle(title);
888         }
889         
890         
891         
892         /**
893          * Find a currently open BasicFrame containing the specified rocket.  This method
894          * can be used to map a Rocket to a BasicFrame from GUI methods.
895          * 
896          * @param rocket the Rocket.
897          * @return               the corresponding BasicFrame, or <code>null</code> if none found.
898          */
899         public static BasicFrame findFrame(Rocket rocket) {
900                 for (BasicFrame f: frames) {
901                         if (f.rocket == rocket)
902                                 return f;
903                 }
904                 return null;
905         }
906         
907         /**
908          * Find a currently open document by the rocket object.  This method can be used
909          * to map a Rocket to OpenRocketDocument from GUI methods.
910          * 
911          * @param rocket the Rocket.
912          * @return               the corresponding OpenRocketDocument, or <code>null</code> if not found.
913          */
914         public static OpenRocketDocument findDocument(Rocket rocket) {
915                 for (BasicFrame f: frames) {
916                         if (f.rocket == rocket)
917                                 return f.document;
918                 }
919                 return null;
920         }
921         
922         
923         public static void main(final String[] args) {
924                 
925                 // Run the actual startup method in the EDT since it can use dialogs etc. 
926                 try {
927                         SwingUtilities.invokeAndWait(new Runnable() {
928                                 @Override
929                                 public void run() {
930                                         runMain(args);
931                                 }
932                         });
933                 } catch (InterruptedException e) {
934                         e.printStackTrace();
935                 } catch (InvocationTargetException e) {
936                         e.printStackTrace();
937                 }
938                 
939         }
940         
941         
942         private static void runMain(String[] args) {
943                 
944                 /*
945                  * Set the look-and-feel.  On Linux, Motif/Metal is sometimes incorrectly used 
946                  * which is butt-ugly, so if the system l&f is Motif/Metal, we search for a few
947                  * other alternatives.
948                  */
949                 try {
950                         // Set system L&F
951                         UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
952                         
953                         // Check whether we have an ugly L&F
954                         LookAndFeel laf = UIManager.getLookAndFeel();
955                         if (laf == null ||
956                                         laf.getName().matches(".*[mM][oO][tT][iI][fF].*") ||
957                                         laf.getName().matches(".*[mM][eE][tT][aA][lL].*")) {
958                                 
959                                 // Search for better LAF
960                                 UIManager.LookAndFeelInfo[] info = UIManager.getInstalledLookAndFeels();
961                                 String lafNames[] = {
962                                                 ".*[gG][tT][kK].*",
963                                                 ".*[wW][iI][nN].*",
964                                                 ".*[mM][aA][cC].*",
965                                                 ".*[aA][qQ][uU][aA].*",
966                                                 ".*[nN][iI][mM][bB].*"
967                                 };
968                                 
969                                 lf: for (String lafName: lafNames) {
970                                         for (UIManager.LookAndFeelInfo l: info) {
971                                                 if (l.getName().matches(lafName)) {
972                                                         UIManager.setLookAndFeel(l.getClassName());
973                                                         break lf;
974                                                 }
975                                         }                                       
976                                 }
977                         }
978                 } catch (Exception e) {
979                         System.err.println("Error setting LAF: " + e);
980                 }
981
982                 // Set tooltip delay time.  Tooltips are used in MotorChooserDialog extensively.
983                 ToolTipManager.sharedInstance().setDismissDelay(30000);
984                 
985                 
986                 // Load defaults
987                 Prefs.loadDefaultUnits();
988
989                 
990                 // Check command-line for files
991                 boolean opened = false;
992                 for (String file: args) {
993                         if (open(new File(file), null)) {
994                                 opened = true;
995                         }
996                 }
997                 
998                 if (!opened) {
999                         newAction();
1000                 }
1001         }
1002
1003 }