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