enhanced motor selection dialog
[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.io.InputStream;
21 import java.io.UnsupportedEncodingException;
22 import java.lang.reflect.InvocationTargetException;
23 import java.net.URI;
24 import java.net.URISyntaxException;
25 import java.net.URL;
26 import java.net.URLDecoder;
27 import java.util.ArrayList;
28 import java.util.concurrent.ExecutionException;
29
30 import javax.swing.Action;
31 import javax.swing.InputMap;
32 import javax.swing.JButton;
33 import javax.swing.JComponent;
34 import javax.swing.JFileChooser;
35 import javax.swing.JFrame;
36 import javax.swing.JMenu;
37 import javax.swing.JMenuBar;
38 import javax.swing.JMenuItem;
39 import javax.swing.JOptionPane;
40 import javax.swing.JPanel;
41 import javax.swing.JScrollPane;
42 import javax.swing.JSeparator;
43 import javax.swing.JSplitPane;
44 import javax.swing.JTabbedPane;
45 import javax.swing.JTextField;
46 import javax.swing.KeyStroke;
47 import javax.swing.ListSelectionModel;
48 import javax.swing.ScrollPaneConstants;
49 import javax.swing.SwingUtilities;
50 import javax.swing.Timer;
51 import javax.swing.ToolTipManager;
52 import javax.swing.border.TitledBorder;
53 import javax.swing.event.TreeSelectionEvent;
54 import javax.swing.event.TreeSelectionListener;
55 import javax.swing.filechooser.FileFilter;
56 import javax.swing.tree.DefaultTreeSelectionModel;
57 import javax.swing.tree.TreePath;
58 import javax.swing.tree.TreeSelectionModel;
59
60 import net.miginfocom.swing.MigLayout;
61 import net.sf.openrocket.aerodynamics.WarningSet;
62 import net.sf.openrocket.communication.UpdateInfo;
63 import net.sf.openrocket.communication.UpdateInfoRetriever;
64 import net.sf.openrocket.database.Databases;
65 import net.sf.openrocket.document.OpenRocketDocument;
66 import net.sf.openrocket.file.GeneralRocketLoader;
67 import net.sf.openrocket.file.RocketLoadException;
68 import net.sf.openrocket.file.RocketLoader;
69 import net.sf.openrocket.file.RocketSaver;
70 import net.sf.openrocket.file.openrocket.OpenRocketSaver;
71 import net.sf.openrocket.gui.StorageOptionChooser;
72 import net.sf.openrocket.gui.configdialog.ComponentConfigDialog;
73 import net.sf.openrocket.gui.dialogs.AboutDialog;
74 import net.sf.openrocket.gui.dialogs.BugReportDialog;
75 import net.sf.openrocket.gui.dialogs.ComponentAnalysisDialog;
76 import net.sf.openrocket.gui.dialogs.ExampleDesignDialog;
77 import net.sf.openrocket.gui.dialogs.LicenseDialog;
78 import net.sf.openrocket.gui.dialogs.MotorDatabaseLoadingDialog;
79 import net.sf.openrocket.gui.dialogs.SwingWorkerDialog;
80 import net.sf.openrocket.gui.dialogs.UpdateInfoDialog;
81 import net.sf.openrocket.gui.dialogs.WarningDialog;
82 import net.sf.openrocket.gui.dialogs.preferences.PreferencesDialog;
83 import net.sf.openrocket.gui.scalefigure.RocketPanel;
84 import net.sf.openrocket.logging.LogHelper;
85 import net.sf.openrocket.rocketcomponent.ComponentChangeEvent;
86 import net.sf.openrocket.rocketcomponent.ComponentChangeListener;
87 import net.sf.openrocket.rocketcomponent.Rocket;
88 import net.sf.openrocket.rocketcomponent.RocketComponent;
89 import net.sf.openrocket.rocketcomponent.Stage;
90 import net.sf.openrocket.startup.Application;
91 import net.sf.openrocket.util.BugException;
92 import net.sf.openrocket.util.GUIUtil;
93 import net.sf.openrocket.util.Icons;
94 import net.sf.openrocket.util.OpenFileWorker;
95 import net.sf.openrocket.util.Prefs;
96 import net.sf.openrocket.util.Reflection;
97 import net.sf.openrocket.util.SaveFileWorker;
98 import net.sf.openrocket.util.TestRockets;
99
100 public class BasicFrame extends JFrame {
101         private static final LogHelper log = Application.getLogger();
102         
103         /**
104          * The RocketLoader instance used for loading all rocket designs.
105          */
106         private static final RocketLoader ROCKET_LOADER = new GeneralRocketLoader();
107         
108         private static final RocketSaver ROCKET_SAVER = new OpenRocketSaver();
109         
110
111         // FileFilters for different types of rocket design files
112         private static final FileFilter ALL_DESIGNS_FILTER =
113                         new SimpleFileFilter("All rocket designs (*.ork; *.rkt)",
114                                         ".ork", ".ork.gz", ".rkt", ".rkt.gz");
115         
116         private static final FileFilter OPENROCKET_DESIGN_FILTER =
117                         new SimpleFileFilter("OpenRocket designs (*.ork)", ".ork", ".ork.gz");
118         
119         private static final FileFilter ROCKSIM_DESIGN_FILTER =
120                         new SimpleFileFilter("RockSim designs (*.rkt)", ".rkt", ".rkt.gz");
121         
122
123
124
125         public static final int COMPONENT_TAB = 0;
126         public static final int SIMULATION_TAB = 1;
127         
128
129         /**
130          * List of currently open frames.  When the list goes empty
131          * it is time to exit the application.
132          */
133         private static final ArrayList<BasicFrame> frames = new ArrayList<BasicFrame>();
134         
135
136
137
138
139         /**
140          * Whether "New" and "Open" should replace this frame.
141          * Should be set to false on the first rocket modification.
142          */
143         private boolean replaceable = false;
144         
145
146
147         private final OpenRocketDocument document;
148         private final Rocket rocket;
149         
150         private JTabbedPane tabbedPane;
151         private RocketPanel rocketpanel;
152         private ComponentTree tree = null;
153         
154         private final DocumentSelectionModel selectionModel;
155         private final TreeSelectionModel componentSelectionModel;
156         private final ListSelectionModel simulationSelectionModel;
157         
158         /** Actions available for rocket modifications */
159         private final RocketActions actions;
160         
161         
162
163         /**
164          * Sole constructor.  Creates a new frame based on the supplied document
165          * and adds it to the current frames list.
166          * 
167          * @param document      the document to show.
168          */
169         public BasicFrame(OpenRocketDocument document) {
170                 log.debug("Instantiating new BasicFrame");
171                 
172                 this.document = document;
173                 this.rocket = document.getRocket();
174                 this.rocket.getDefaultConfiguration().setAllStages();
175                 
176
177                 // Set replaceable flag to false at first modification
178                 rocket.addComponentChangeListener(new ComponentChangeListener() {
179                         public void componentChanged(ComponentChangeEvent e) {
180                                 replaceable = false;
181                                 BasicFrame.this.rocket.removeComponentChangeListener(this);
182                         }
183                 });
184                 
185
186                 // Create the component tree selection model that will be used
187                 componentSelectionModel = new DefaultTreeSelectionModel();
188                 componentSelectionModel.setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
189                 
190                 // Obtain the simulation selection model that will be used
191                 SimulationPanel simulationPanel = new SimulationPanel(document);
192                 simulationSelectionModel = simulationPanel.getSimulationListSelectionModel();
193                 
194                 // Combine into a DocumentSelectionModel
195                 selectionModel = new DocumentSelectionModel(document);
196                 selectionModel.attachComponentTreeSelectionModel(componentSelectionModel);
197                 selectionModel.attachSimulationListSelectionModel(simulationSelectionModel);
198                 
199
200                 actions = new RocketActions(document, selectionModel, this);
201                 
202
203                 log.debug("Constructing the BasicFrame UI");
204                 
205                 // The main vertical split pane         
206                 JSplitPane vertical = new JSplitPane(JSplitPane.VERTICAL_SPLIT, true);
207                 vertical.setResizeWeight(0.5);
208                 this.add(vertical);
209                 
210
211                 // The top tabbed pane
212                 tabbedPane = new JTabbedPane();
213                 tabbedPane.addTab("Rocket design", null, designTab());
214                 tabbedPane.addTab("Flight simulations", null, simulationPanel);
215                 
216                 vertical.setTopComponent(tabbedPane);
217                 
218
219
220                 //  Bottom segment, rocket figure
221                 
222                 rocketpanel = new RocketPanel(document);
223                 vertical.setBottomComponent(rocketpanel);
224                 
225                 rocketpanel.setSelectionModel(tree.getSelectionModel());
226                 
227
228                 createMenu();
229                 
230
231                 rocket.addComponentChangeListener(new ComponentChangeListener() {
232                         public void componentChanged(ComponentChangeEvent e) {
233                                 setTitle();
234                         }
235                 });
236                 
237                 setTitle();
238                 this.pack();
239                 
240                 Dimension size = Prefs.getWindowSize(this.getClass());
241                 if (size == null) {
242                         size = Toolkit.getDefaultToolkit().getScreenSize();
243                         size.width = size.width * 9 / 10;
244                         size.height = size.height * 9 / 10;
245                 }
246                 this.setSize(size);
247                 this.addComponentListener(new ComponentAdapter() {
248                         @Override
249                         public void componentResized(ComponentEvent e) {
250                                 Prefs.setWindowSize(BasicFrame.this.getClass(), BasicFrame.this.getSize());
251                         }
252                 });
253                 this.setLocationByPlatform(true);
254                 
255                 GUIUtil.setWindowIcons(this);
256                 
257                 this.validate();
258                 vertical.setDividerLocation(0.4);
259                 setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
260                 addWindowListener(new WindowAdapter() {
261                         @Override
262                         public void windowClosing(WindowEvent e) {
263                                 closeAction();
264                         }
265                 });
266                 frames.add(this);
267                 
268                 log.debug("BasicFrame instantiation complete");
269         }
270         
271         
272         /**
273          * Construct the "Rocket design" tab.  This contains a horizontal split pane
274          * with the left component the design tree and the right component buttons
275          * for adding components.
276          */
277         private JComponent designTab() {
278                 JSplitPane horizontal = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, true);
279                 horizontal.setResizeWeight(0.5);
280                 
281
282                 //  Upper-left segment, component tree
283                 
284                 JPanel panel = new JPanel(new MigLayout("fill, flowy", "", "[grow]"));
285                 
286                 tree = new ComponentTree(rocket);
287                 tree.setSelectionModel(componentSelectionModel);
288                 
289                 // Remove JTree key events that interfere with menu accelerators
290                 InputMap im = SwingUtilities.getUIInputMap(tree, JComponent.WHEN_FOCUSED);
291                 im.put(KeyStroke.getKeyStroke(KeyEvent.VK_X, ActionEvent.CTRL_MASK), null);
292                 im.put(KeyStroke.getKeyStroke(KeyEvent.VK_C, ActionEvent.CTRL_MASK), null);
293                 im.put(KeyStroke.getKeyStroke(KeyEvent.VK_V, ActionEvent.CTRL_MASK), null);
294                 im.put(KeyStroke.getKeyStroke(KeyEvent.VK_A, ActionEvent.CTRL_MASK), null);
295                 im.put(KeyStroke.getKeyStroke(KeyEvent.VK_S, ActionEvent.CTRL_MASK), null);
296                 im.put(KeyStroke.getKeyStroke(KeyEvent.VK_O, ActionEvent.CTRL_MASK), null);
297                 im.put(KeyStroke.getKeyStroke(KeyEvent.VK_N, ActionEvent.CTRL_MASK), null);
298                 
299
300
301                 // Double-click opens config dialog
302                 MouseListener ml = new MouseAdapter() {
303                         @Override
304                         public void mousePressed(MouseEvent e) {
305                                 int selRow = tree.getRowForLocation(e.getX(), e.getY());
306                                 TreePath selPath = tree.getPathForLocation(e.getX(), e.getY());
307                                 if (selRow != -1) {
308                                         if ((e.getClickCount() == 2) && !ComponentConfigDialog.isDialogVisible()) {
309                                                 // Double-click
310                                                 RocketComponent c = (RocketComponent) selPath.getLastPathComponent();
311                                                 ComponentConfigDialog.showDialog(BasicFrame.this,
312                                                                 BasicFrame.this.document, c);
313                                         }
314                                 }
315                         }
316                 };
317                 tree.addMouseListener(ml);
318                 
319                 // Update dialog when selection is changed
320                 componentSelectionModel.addTreeSelectionListener(new TreeSelectionListener() {
321                         public void valueChanged(TreeSelectionEvent e) {
322                                 // Scroll tree to the selected item
323                                 TreePath path = componentSelectionModel.getSelectionPath();
324                                 if (path == null)
325                                         return;
326                                 tree.scrollPathToVisible(path);
327                                 
328                                 if (!ComponentConfigDialog.isDialogVisible())
329                                         return;
330                                 RocketComponent c = (RocketComponent) path.getLastPathComponent();
331                                 ComponentConfigDialog.showDialog(BasicFrame.this,
332                                                 BasicFrame.this.document, c);
333                         }
334                 });
335                 
336                 // Place tree inside scroll pane
337                 JScrollPane scroll = new JScrollPane(tree);
338                 panel.add(scroll, "spany, grow, wrap");
339                 
340
341                 // Buttons
342                 JButton button = new JButton(actions.getMoveUpAction());
343                 panel.add(button, "sizegroup buttons, aligny 65%");
344                 
345                 button = new JButton(actions.getMoveDownAction());
346                 panel.add(button, "sizegroup buttons, aligny 0%");
347                 
348                 button = new JButton(actions.getEditAction());
349                 panel.add(button, "sizegroup buttons");
350                 
351                 button = new JButton(actions.getNewStageAction());
352                 panel.add(button, "sizegroup buttons");
353                 
354                 button = new JButton(actions.getDeleteAction());
355                 button.setIcon(null);
356                 button.setMnemonic(0);
357                 panel.add(button, "sizegroup buttons");
358                 
359                 horizontal.setLeftComponent(panel);
360                 
361
362                 //  Upper-right segment, component addition buttons
363                 
364                 panel = new JPanel(new MigLayout("fill, insets 0", "[0::]"));
365                 
366                 scroll = new JScrollPane(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED,
367                                 ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
368                 scroll.setViewportView(new ComponentAddButtons(document, componentSelectionModel,
369                                 scroll.getViewport()));
370                 scroll.setBorder(null);
371                 scroll.setViewportBorder(null);
372                 
373                 TitledBorder border = new TitledBorder("Add new component");
374                 border.setTitleFont(border.getTitleFont().deriveFont(Font.BOLD));
375                 scroll.setBorder(border);
376                 
377                 panel.add(scroll, "grow");
378                 
379                 horizontal.setRightComponent(panel);
380                 
381                 return horizontal;
382         }
383         
384         
385
386         /**
387          * Creates the menu for the window.
388          */
389         private void createMenu() {
390                 JMenuBar menubar = new JMenuBar();
391                 JMenu menu;
392                 JMenuItem item;
393                 
394                 ////  File
395                 menu = new JMenu("File");
396                 menu.setMnemonic(KeyEvent.VK_F);
397                 menu.getAccessibleContext().setAccessibleDescription("File-handling related tasks");
398                 menubar.add(menu);
399                 
400                 item = new JMenuItem("New", KeyEvent.VK_N);
401                 item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_N, ActionEvent.CTRL_MASK));
402                 item.setMnemonic(KeyEvent.VK_N);
403                 item.getAccessibleContext().setAccessibleDescription("Create a new rocket design");
404                 item.setIcon(Icons.FILE_NEW);
405                 item.addActionListener(new ActionListener() {
406                         public void actionPerformed(ActionEvent e) {
407                                 newAction();
408                                 if (replaceable)
409                                         closeAction();
410                         }
411                 });
412                 menu.add(item);
413                 
414                 item = new JMenuItem("Open...", KeyEvent.VK_O);
415                 item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, ActionEvent.CTRL_MASK));
416                 item.getAccessibleContext().setAccessibleDescription("Open a rocket design");
417                 item.setIcon(Icons.FILE_OPEN);
418                 item.addActionListener(new ActionListener() {
419                         public void actionPerformed(ActionEvent e) {
420                                 openAction();
421                         }
422                 });
423                 menu.add(item);
424                 
425                 item = new JMenuItem("Open example...");
426                 item.getAccessibleContext().setAccessibleDescription("Open an example rocket design");
427                 item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O,
428                                 ActionEvent.CTRL_MASK | ActionEvent.SHIFT_MASK));
429                 item.setIcon(Icons.FILE_OPEN_EXAMPLE);
430                 item.addActionListener(new ActionListener() {
431                         public void actionPerformed(ActionEvent e) {
432                                 URL[] urls = ExampleDesignDialog.selectExampleDesigns(BasicFrame.this);
433                                 if (urls != null) {
434                                         for (URL u : urls) {
435                                                 open(u, BasicFrame.this);
436                                         }
437                                 }
438                         }
439                 });
440                 menu.add(item);
441                 
442                 menu.addSeparator();
443                 
444                 item = new JMenuItem("Save", KeyEvent.VK_S);
445                 item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, ActionEvent.CTRL_MASK));
446                 item.getAccessibleContext().setAccessibleDescription("Save the current rocket design");
447                 item.setIcon(Icons.FILE_SAVE);
448                 item.addActionListener(new ActionListener() {
449                         public void actionPerformed(ActionEvent e) {
450                                 saveAction();
451                         }
452                 });
453                 menu.add(item);
454                 
455                 item = new JMenuItem("Save as...", KeyEvent.VK_A);
456                 item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S,
457                                 ActionEvent.CTRL_MASK | ActionEvent.SHIFT_MASK));
458                 item.getAccessibleContext().setAccessibleDescription("Save the current rocket design " +
459                                 "to a new file");
460                 item.setIcon(Icons.FILE_SAVE_AS);
461                 item.addActionListener(new ActionListener() {
462                         public void actionPerformed(ActionEvent e) {
463                                 saveAsAction();
464                         }
465                 });
466                 menu.add(item);
467                 
468                 //              menu.addSeparator();
469                 menu.add(new JSeparator());
470                 
471                 item = new JMenuItem("Close", KeyEvent.VK_C);
472                 item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_W, ActionEvent.CTRL_MASK));
473                 item.getAccessibleContext().setAccessibleDescription("Close the current rocket design");
474                 item.setIcon(Icons.FILE_CLOSE);
475                 item.addActionListener(new ActionListener() {
476                         public void actionPerformed(ActionEvent e) {
477                                 closeAction();
478                         }
479                 });
480                 menu.add(item);
481                 
482                 menu.addSeparator();
483                 
484                 item = new JMenuItem("Quit", KeyEvent.VK_Q);
485                 item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Q, ActionEvent.CTRL_MASK));
486                 item.getAccessibleContext().setAccessibleDescription("Quit the program");
487                 item.setIcon(Icons.FILE_QUIT);
488                 item.addActionListener(new ActionListener() {
489                         public void actionPerformed(ActionEvent e) {
490                                 quitAction();
491                         }
492                 });
493                 menu.add(item);
494                 
495
496
497                 ////  Edit
498                 menu = new JMenu("Edit");
499                 menu.setMnemonic(KeyEvent.VK_E);
500                 menu.getAccessibleContext().setAccessibleDescription("Rocket editing");
501                 menubar.add(menu);
502                 
503
504                 Action action = document.getUndoAction();
505                 item = new JMenuItem(action);
506                 item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Z, ActionEvent.CTRL_MASK));
507                 item.setMnemonic(KeyEvent.VK_U);
508                 item.getAccessibleContext().setAccessibleDescription("Undo the previous operation");
509                 
510                 menu.add(item);
511                 
512                 action = document.getRedoAction();
513                 item = new JMenuItem(action);
514                 item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Y, ActionEvent.CTRL_MASK));
515                 item.setMnemonic(KeyEvent.VK_R);
516                 item.getAccessibleContext().setAccessibleDescription("Redo the previously undone " +
517                                 "operation");
518                 menu.add(item);
519                 
520                 menu.addSeparator();
521                 
522
523                 item = new JMenuItem(actions.getCutAction());
524                 menu.add(item);
525                 
526                 item = new JMenuItem(actions.getCopyAction());
527                 menu.add(item);
528                 
529                 item = new JMenuItem(actions.getPasteAction());
530                 menu.add(item);
531                 
532                 item = new JMenuItem(actions.getDeleteAction());
533                 menu.add(item);
534                 
535                 menu.addSeparator();
536                 
537                 item = new JMenuItem("Preferences");
538                 item.setIcon(Icons.PREFERENCES);
539                 item.getAccessibleContext().setAccessibleDescription("Setup the application " +
540                                 "preferences");
541                 item.addActionListener(new ActionListener() {
542                         public void actionPerformed(ActionEvent e) {
543                                 PreferencesDialog.showPreferences();
544                         }
545                 });
546                 menu.add(item);
547                 
548
549
550
551                 ////  Analyze
552                 menu = new JMenu("Analyze");
553                 menu.setMnemonic(KeyEvent.VK_A);
554                 menu.getAccessibleContext().setAccessibleDescription("Analyzing the rocket");
555                 menubar.add(menu);
556                 
557                 item = new JMenuItem("Component analysis", KeyEvent.VK_C);
558                 item.getAccessibleContext().setAccessibleDescription("Analyze the rocket components " +
559                                 "separately");
560                 item.addActionListener(new ActionListener() {
561                         public void actionPerformed(ActionEvent e) {
562                                 ComponentAnalysisDialog.showDialog(rocketpanel);
563                         }
564                 });
565                 menu.add(item);
566                 
567
568                 ////  Debug
569                 // (shown if openrocket.debug.menu is defined)
570                 if (System.getProperty("openrocket.debug.menu") != null) {
571                         menubar.add(makeDebugMenu());
572                 }
573                 
574
575
576                 ////  Help
577                 
578                 menu = new JMenu("Help");
579                 menu.setMnemonic(KeyEvent.VK_H);
580                 menu.getAccessibleContext().setAccessibleDescription("Information about OpenRocket");
581                 menubar.add(menu);
582                 
583
584
585                 item = new JMenuItem("License", KeyEvent.VK_L);
586                 item.getAccessibleContext().setAccessibleDescription("OpenRocket license information");
587                 item.addActionListener(new ActionListener() {
588                         public void actionPerformed(ActionEvent e) {
589                                 new LicenseDialog(BasicFrame.this).setVisible(true);
590                         }
591                 });
592                 menu.add(item);
593                 
594                 item = new JMenuItem("Bug report", KeyEvent.VK_B);
595                 item.getAccessibleContext().setAccessibleDescription("Information about reporting " +
596                                 "bugs in OpenRocket");
597                 item.addActionListener(new ActionListener() {
598                         public void actionPerformed(ActionEvent e) {
599                                 //                              new BugDialog(BasicFrame.this).setVisible(true);
600                                 BugReportDialog.showBugReportDialog(BasicFrame.this);
601                         }
602                 });
603                 menu.add(item);
604                 
605                 item = new JMenuItem("About", KeyEvent.VK_A);
606                 item.getAccessibleContext().setAccessibleDescription("About OpenRocket");
607                 item.addActionListener(new ActionListener() {
608                         public void actionPerformed(ActionEvent e) {
609                                 new AboutDialog(BasicFrame.this).setVisible(true);
610                         }
611                 });
612                 menu.add(item);
613                 
614
615                 this.setJMenuBar(menubar);
616         }
617         
618         
619         private JMenu makeDebugMenu() {
620                 JMenu menu;
621                 JMenuItem item;
622                 
623                 ////  Debug menu
624                 menu = new JMenu("Debug");
625                 menu.getAccessibleContext().setAccessibleDescription("OpenRocket debugging tasks");
626                 
627                 item = new JMenuItem("What is this menu?");
628                 item.addActionListener(new ActionListener() {
629                         public void actionPerformed(ActionEvent e) {
630                                 JOptionPane.showMessageDialog(BasicFrame.this,
631                                                 new Object[] {
632                                                                 "The 'Debug' menu includes actions for testing and debugging " +
633                                                                                 "OpenRocket.", " ",
634                                                                 "The menu is made visible by defining the system property " +
635                                                                                 "'openrocket.debug.menu' when starting OpenRocket.",
636                                                                 "It should not be visible by default." },
637                                                 "Debug menu", JOptionPane.INFORMATION_MESSAGE);
638                         }
639                 });
640                 menu.add(item);
641                 
642                 menu.addSeparator();
643                 
644                 item = new JMenuItem("Create test rocket");
645                 item.addActionListener(new ActionListener() {
646                         @Override
647                         public void actionPerformed(ActionEvent e) {
648                                 JTextField field = new JTextField();
649                                 int sel = JOptionPane.showOptionDialog(BasicFrame.this, new Object[] {
650                                                 "Input text key to generate random rocket:",
651                                                 field
652                                         }, "Generate random test rocket", JOptionPane.DEFAULT_OPTION,
653                                                 JOptionPane.QUESTION_MESSAGE, null, new Object[] {
654                                                                 "Random", "OK"
655                                 }, "OK");
656                                 
657                                 Rocket r;
658                                 if (sel == 0) {
659                                         r = new TestRockets(null).makeTestRocket();
660                                 } else if (sel == 1) {
661                                         r = new TestRockets(field.getText()).makeTestRocket();
662                                 } else {
663                                         return;
664                                 }
665                                 
666                                 OpenRocketDocument doc = new OpenRocketDocument(r);
667                                 doc.setSaved(true);
668                                 BasicFrame frame = new BasicFrame(doc);
669                                 frame.setVisible(true);
670                         }
671                 });
672                 menu.add(item);
673                 
674
675
676                 item = new JMenuItem("Create 'Iso-Haisu'");
677                 item.addActionListener(new ActionListener() {
678                         @Override
679                         public void actionPerformed(ActionEvent e) {
680                                 Rocket r = TestRockets.makeIsoHaisu();
681                                 OpenRocketDocument doc = new OpenRocketDocument(r);
682                                 doc.setSaved(true);
683                                 BasicFrame frame = new BasicFrame(doc);
684                                 frame.setVisible(true);
685                         }
686                 });
687                 menu.add(item);
688                 
689
690                 item = new JMenuItem("Create 'Big Blue'");
691                 item.addActionListener(new ActionListener() {
692                         @Override
693                         public void actionPerformed(ActionEvent e) {
694                                 Rocket r = TestRockets.makeBigBlue();
695                                 OpenRocketDocument doc = new OpenRocketDocument(r);
696                                 doc.setSaved(true);
697                                 BasicFrame frame = new BasicFrame(doc);
698                                 frame.setVisible(true);
699                         }
700                 });
701                 menu.add(item);
702                 
703
704
705                 menu.addSeparator();
706                 
707                 item = new JMenuItem("Exception here");
708                 item.addActionListener(new ActionListener() {
709                         public void actionPerformed(ActionEvent e) {
710                                 throw new RuntimeException("Testing exception from menu action listener");
711                         }
712                 });
713                 menu.add(item);
714                 
715                 item = new JMenuItem("Exception from EDT");
716                 item.addActionListener(new ActionListener() {
717                         public void actionPerformed(ActionEvent e) {
718                                 SwingUtilities.invokeLater(new Runnable() {
719                                         @Override
720                                         public void run() {
721                                                 throw new RuntimeException("Testing exception from " +
722                                                                 "later invoked EDT thread");
723                                         }
724                                 });
725                         }
726                 });
727                 menu.add(item);
728                 
729                 item = new JMenuItem("Exception from other thread");
730                 item.addActionListener(new ActionListener() {
731                         public void actionPerformed(ActionEvent e) {
732                                 new Thread() {
733                                         @Override
734                                         public void run() {
735                                                 throw new RuntimeException("Testing exception from " +
736                                                                 "newly created thread");
737                                         }
738                                 }.start();
739                         }
740                 });
741                 menu.add(item);
742                 
743
744
745                 return menu;
746         }
747         
748         
749
750         /**
751          * Select the tab on the main pane.
752          * 
753          * @param tab   one of {@link #COMPONENT_TAB} or {@link #SIMULATION_TAB}.
754          */
755         public void selectTab(int tab) {
756                 tabbedPane.setSelectedIndex(tab);
757         }
758         
759         
760
761         private void openAction() {
762                 JFileChooser chooser = new JFileChooser();
763                 
764                 chooser.addChoosableFileFilter(ALL_DESIGNS_FILTER);
765                 chooser.addChoosableFileFilter(OPENROCKET_DESIGN_FILTER);
766                 chooser.addChoosableFileFilter(ROCKSIM_DESIGN_FILTER);
767                 chooser.setFileFilter(ALL_DESIGNS_FILTER);
768                 
769                 chooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
770                 chooser.setMultiSelectionEnabled(true);
771                 chooser.setCurrentDirectory(Prefs.getDefaultDirectory());
772                 if (chooser.showOpenDialog(this) != JFileChooser.APPROVE_OPTION)
773                         return;
774                 
775                 Prefs.setDefaultDirectory(chooser.getCurrentDirectory());
776                 
777                 File[] files = chooser.getSelectedFiles();
778                 
779                 for (File file : files) {
780                         System.out.println("Opening file: " + file);
781                         if (open(file, this)) {
782                                 
783                                 // Close previous window if replacing
784                                 if (replaceable && document.isSaved()) {
785                                         closeAction();
786                                         replaceable = false;
787                                 }
788                         }
789                 }
790         }
791         
792         
793
794         private static boolean open(URL url, BasicFrame parent) {
795                 String filename = null;
796                 
797                 // Try using URI.getPath();
798                 try {
799                         URI uri = url.toURI();
800                         filename = uri.getPath();
801                 } catch (URISyntaxException ignore) {
802                 }
803                 
804                 // Try URL-decoding the URL
805                 if (filename == null) {
806                         try {
807                                 filename = URLDecoder.decode(url.toString(), "UTF-8");
808                         } catch (UnsupportedEncodingException ignore) {
809                         }
810                 }
811                 
812                 // Last resort
813                 if (filename == null) {
814                         filename = "";
815                 }
816                 
817                 // Remove path from filename
818                 if (filename.lastIndexOf('/') >= 0) {
819                         filename = filename.substring(filename.lastIndexOf('/') + 1);
820                 }
821                 
822                 try {
823                         InputStream is = url.openStream();
824                         if (open(is, filename, parent)) {
825                                 // Close previous window if replacing
826                                 if (parent.replaceable && parent.document.isSaved()) {
827                                         parent.closeAction();
828                                         parent.replaceable = false;
829                                 }
830                         }
831                 } catch (IOException e) {
832                         JOptionPane.showMessageDialog(parent,
833                                         "An error occurred while opening the file " + filename,
834                                         "Error loading file", JOptionPane.ERROR_MESSAGE);
835                 }
836                 
837                 return false;
838         }
839         
840         
841         /**
842          * Open the specified file from an InputStream in a new design frame.  If an error
843          * occurs, an error dialog is shown and <code>false</code> is returned.
844          * 
845          * @param stream        the stream to load from.
846          * @param filename      the file name to display in dialogs (not set to the document).
847          * @param parent        the parent component for which a progress dialog is opened.
848          * @return                      whether the file was successfully loaded and opened.
849          */
850         private static boolean open(InputStream stream, String filename, Window parent) {
851                 OpenFileWorker worker = new OpenFileWorker(stream, ROCKET_LOADER);
852                 return open(worker, filename, null, parent);
853         }
854         
855         
856         /**
857          * Open the specified file in a new design frame.  If an error occurs, an error
858          * dialog is shown and <code>false</code> is returned.
859          * 
860          * @param file          the file to open.
861          * @param parent        the parent component for which a progress dialog is opened.
862          * @return                      whether the file was successfully loaded and opened.
863          */
864         public static boolean open(File file, Window parent) {
865                 OpenFileWorker worker = new OpenFileWorker(file, ROCKET_LOADER);
866                 return open(worker, file.getName(), file, parent);
867         }
868         
869         
870         /**
871          * Open the specified file using the provided worker.
872          * 
873          * @param worker        the OpenFileWorker that loads the file.
874          * @param filename      the file name to display in dialogs.
875          * @param file          the File to set the document to (may be null).
876          * @param parent
877          * @return
878          */
879         private static boolean open(OpenFileWorker worker, String filename, File file,
880                         Window parent) {
881                 
882                 MotorDatabaseLoadingDialog.check(null);
883                 
884                 // Open the file in a Swing worker thread
885                 if (!SwingWorkerDialog.runWorker(parent, "Opening file",
886                                 "Reading " + filename + "...", worker)) {
887                         
888                         // User cancelled the operation
889                         return false;
890                 }
891                 
892
893                 // Handle the document
894                 OpenRocketDocument doc = null;
895                 try {
896                         
897                         doc = worker.get();
898                         
899                 } catch (ExecutionException e) {
900                         
901                         Throwable cause = e.getCause();
902                         
903                         if (cause instanceof FileNotFoundException) {
904                                 
905                                 JOptionPane.showMessageDialog(parent,
906                                                 "File not found: " + filename,
907                                                 "Error opening file", JOptionPane.ERROR_MESSAGE);
908                                 return false;
909                                 
910                         } else if (cause instanceof RocketLoadException) {
911                                 
912                                 JOptionPane.showMessageDialog(parent,
913                                                 "Unable to open file '" + filename + "': "
914                                                                 + cause.getMessage(),
915                                                 "Error opening file", JOptionPane.ERROR_MESSAGE);
916                                 return false;
917                                 
918                         } else {
919                                 
920                                 throw new BugException("Unknown error when opening file", e);
921                                 
922                         }
923                         
924                 } catch (InterruptedException e) {
925                         throw new BugException("EDT was interrupted", e);
926                 }
927                 
928                 if (doc == null) {
929                         throw new BugException("BUG: Document loader returned null");
930                 }
931                 
932
933                 // Show warnings
934                 WarningSet warnings = worker.getRocketLoader().getWarnings();
935                 if (!warnings.isEmpty()) {
936                         WarningDialog.showWarnings(parent,
937                                         new Object[] {
938                                                         "The following problems were encountered while opening " + filename + ".",
939                                                         "Some design features may not have been loaded correctly."
940                                         },
941                                         "Warnings while opening file", warnings);
942                 }
943                 
944
945                 // Set document state
946                 doc.setFile(file);
947                 doc.setSaved(true);
948                 
949                 // Open the frame
950                 BasicFrame frame = new BasicFrame(doc);
951                 frame.setVisible(true);
952                 
953                 return true;
954         }
955         
956         
957
958
959
960         private boolean saveAction() {
961                 File file = document.getFile();
962                 if (file == null) {
963                         return saveAsAction();
964                 }
965                 
966                 // Saving RockSim designs is not supported
967                 if (ROCKSIM_DESIGN_FILTER.accept(file)) {
968                         file = new File(file.getAbsolutePath().replaceAll(".[rR][kK][tT](.[gG][zZ])?$",
969                                         ".ork"));
970                         
971                         int option = JOptionPane.showConfirmDialog(this, new Object[] {
972                                         "Saving designs in RockSim format is not supported.",
973                                         "Save in OpenRocket format instead (" + file.getName() + ")?"
974                                 }, "Save " + file.getName(), JOptionPane.YES_NO_OPTION,
975                                         JOptionPane.QUESTION_MESSAGE, null);
976                         if (option != JOptionPane.YES_OPTION)
977                                 return false;
978                         
979                         document.setFile(file);
980                 }
981                 return saveAs(file);
982         }
983         
984         
985         private boolean saveAsAction() {
986                 File file = null;
987                 while (file == null) {
988                         // TODO: HIGH: what if *.rkt chosen?
989                         StorageOptionChooser storageChooser =
990                                         new StorageOptionChooser(document, document.getDefaultStorageOptions());
991                         JFileChooser chooser = new JFileChooser();
992                         chooser.setFileFilter(OPENROCKET_DESIGN_FILTER);
993                         chooser.setCurrentDirectory(Prefs.getDefaultDirectory());
994                         chooser.setAccessory(storageChooser);
995                         if (document.getFile() != null)
996                                 chooser.setSelectedFile(document.getFile());
997                         
998                         if (chooser.showSaveDialog(BasicFrame.this) != JFileChooser.APPROVE_OPTION)
999                                 return false;
1000                         
1001                         file = chooser.getSelectedFile();
1002                         if (file == null)
1003                                 return false;
1004                         
1005                         Prefs.setDefaultDirectory(chooser.getCurrentDirectory());
1006                         storageChooser.storeOptions(document.getDefaultStorageOptions());
1007                         
1008                         if (file.getName().indexOf('.') < 0) {
1009                                 String name = file.getAbsolutePath();
1010                                 name = name + ".ork";
1011                                 file = new File(name);
1012                         }
1013                         
1014                         if (file.exists()) {
1015                                 int result = JOptionPane.showConfirmDialog(this,
1016                                                 "File '" + file.getName() + "' exists.  Do you want to overwrite it?",
1017                                                 "File exists", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE);
1018                                 if (result != JOptionPane.YES_OPTION)
1019                                         return false;
1020                         }
1021                 }
1022                 saveAs(file);
1023                 return true;
1024         }
1025         
1026         
1027         private boolean saveAs(File file) {
1028                 System.out.println("Saving to file: " + file.getName());
1029                 boolean saved = false;
1030                 
1031                 if (!StorageOptionChooser.verifyStorageOptions(document, this)) {
1032                         // User cancelled the dialog
1033                         return false;
1034                 }
1035                 
1036
1037                 SaveFileWorker worker = new SaveFileWorker(document, file, ROCKET_SAVER);
1038                 
1039                 if (!SwingWorkerDialog.runWorker(this, "Saving file",
1040                                 "Writing " + file.getName() + "...", worker)) {
1041                         
1042                         // User cancelled the save
1043                         file.delete();
1044                         return false;
1045                 }
1046                 
1047                 try {
1048                         worker.get();
1049                         document.setFile(file);
1050                         document.setSaved(true);
1051                         saved = true;
1052                         setTitle();
1053                 } catch (ExecutionException e) {
1054                         
1055                         Throwable cause = e.getCause();
1056                         
1057                         if (cause instanceof IOException) {
1058                                 JOptionPane.showMessageDialog(this, new String[] {
1059                                                 "An I/O error occurred while saving:",
1060                                                 e.getMessage() }, "Saving failed", JOptionPane.ERROR_MESSAGE);
1061                                 return false;
1062                         } else {
1063                                 Reflection.handleWrappedException(e);
1064                         }
1065                         
1066                 } catch (InterruptedException e) {
1067                         throw new BugException("EDT was interrupted", e);
1068                 }
1069                 
1070                 return saved;
1071         }
1072         
1073         
1074         private boolean closeAction() {
1075                 if (!document.isSaved()) {
1076                         ComponentConfigDialog.hideDialog();
1077                         int result = JOptionPane.showConfirmDialog(this,
1078                                         "Design '" + rocket.getName() + "' has not been saved.  " +
1079                                                         "Do you want to save it?",
1080                                         "Design not saved", JOptionPane.YES_NO_CANCEL_OPTION,
1081                                         JOptionPane.QUESTION_MESSAGE);
1082                         if (result == JOptionPane.YES_OPTION) {
1083                                 // Save
1084                                 if (!saveAction())
1085                                         return false; // If save was interrupted
1086                         } else if (result == JOptionPane.NO_OPTION) {
1087                                 // Don't save: No-op
1088                         } else {
1089                                 // Cancel or close
1090                                 return false;
1091                         }
1092                 }
1093                 
1094                 // Rocket has been saved or discarded
1095                 this.dispose();
1096                 
1097                 // TODO: LOW: Close only dialogs that have this frame as their parent
1098                 ComponentConfigDialog.hideDialog();
1099                 ComponentAnalysisDialog.hideDialog();
1100                 
1101                 frames.remove(this);
1102                 if (frames.isEmpty())
1103                         System.exit(0);
1104                 return true;
1105         }
1106         
1107         
1108         /**
1109          * Closes this frame if it is replaceable.
1110          */
1111         public void closeIfReplaceable() {
1112                 if (this.replaceable && document.isSaved()) {
1113                         closeAction();
1114                 }
1115         }
1116         
1117         /**
1118          * Open a new design window with a basic rocket+stage.
1119          */
1120         public static void newAction() {
1121                 log.debug("New action initiated");
1122                 Rocket rocket = new Rocket();
1123                 Stage stage = new Stage();
1124                 stage.setName("Sustainer");
1125                 rocket.addChild(stage);
1126                 OpenRocketDocument doc = new OpenRocketDocument(rocket);
1127                 doc.setSaved(true);
1128                 
1129                 BasicFrame frame = new BasicFrame(doc);
1130                 frame.replaceable = true;
1131                 frame.setVisible(true);
1132                 ComponentConfigDialog.showDialog(frame, doc, rocket);
1133         }
1134         
1135         /**
1136          * Quit the application.  Confirms saving unsaved designs.  The action of File->Quit.
1137          */
1138         public static void quitAction() {
1139                 for (int i = frames.size() - 1; i >= 0; i--) {
1140                         if (!frames.get(i).closeAction()) {
1141                                 // Close canceled
1142                                 return;
1143                         }
1144                 }
1145                 // Should not be reached, but just in case
1146                 System.exit(0);
1147         }
1148         
1149         
1150         /**
1151          * Set the title of the frame, taking into account the name of the rocket, file it 
1152          * has been saved to (if any) and saved status.
1153          */
1154         private void setTitle() {
1155                 File file = document.getFile();
1156                 boolean saved = document.isSaved();
1157                 String title;
1158                 
1159                 title = rocket.getName();
1160                 if (file != null) {
1161                         title = title + " (" + file.getName() + ")";
1162                 }
1163                 if (!saved)
1164                         title = "*" + title;
1165                 
1166                 setTitle(title);
1167         }
1168         
1169         
1170
1171         /**
1172          * Find a currently open BasicFrame containing the specified rocket.  This method
1173          * can be used to map a Rocket to a BasicFrame from GUI methods.
1174          * 
1175          * @param rocket the Rocket.
1176          * @return               the corresponding BasicFrame, or <code>null</code> if none found.
1177          */
1178         public static BasicFrame findFrame(Rocket rocket) {
1179                 for (BasicFrame f : frames) {
1180                         if (f.rocket == rocket)
1181                                 return f;
1182                 }
1183                 return null;
1184         }
1185         
1186         /**
1187          * Find a currently open document by the rocket object.  This method can be used
1188          * to map a Rocket to OpenRocketDocument from GUI methods.
1189          * 
1190          * @param rocket the Rocket.
1191          * @return               the corresponding OpenRocketDocument, or <code>null</code> if not found.
1192          */
1193         public static OpenRocketDocument findDocument(Rocket rocket) {
1194                 for (BasicFrame f : frames) {
1195                         if (f.rocket == rocket)
1196                                 return f.document;
1197                 }
1198                 return null;
1199         }
1200         
1201         
1202         public static void main(final String[] args) {
1203                 
1204                 // Run the actual startup method in the EDT since it can use progress dialogs etc.
1205                 try {
1206                         SwingUtilities.invokeAndWait(new Runnable() {
1207                                 @Override
1208                                 public void run() {
1209                                         runMain(args);
1210                                 }
1211                         });
1212                 } catch (InterruptedException e) {
1213                         e.printStackTrace();
1214                 } catch (InvocationTargetException e) {
1215                         e.printStackTrace();
1216                 }
1217                 
1218         }
1219         
1220         
1221         private static void runMain(String[] args) {
1222                 
1223                 // Initialize the splash screen with version info
1224                 Splash.init();
1225                 
1226
1227                 // Start update info fetching
1228                 final UpdateInfoRetriever updateInfo;
1229                 if (Prefs.getCheckUpdates()) {
1230                         updateInfo = new UpdateInfoRetriever();
1231                         updateInfo.start();
1232                 } else {
1233                         updateInfo = null;
1234                 }
1235                 
1236
1237                 // Set the best available look-and-feel
1238                 GUIUtil.setBestLAF();
1239                 
1240                 // Set tooltip delay time.  Tooltips are used in MotorChooserDialog extensively.
1241                 ToolTipManager.sharedInstance().setDismissDelay(30000);
1242                 
1243
1244                 // Setup the uncaught exception handler
1245                 ExceptionHandler.registerExceptionHandler();
1246                 
1247
1248                 // Load defaults
1249                 Prefs.loadDefaultUnits();
1250                 
1251
1252                 // Load motors etc.
1253                 Databases.fakeMethod();
1254                 
1255                 // Starting action (load files or open new document)
1256                 if (!handleCommandLine(args)) {
1257                         newAction();
1258                 }
1259                 
1260
1261                 // Check whether update info has been fetched or whether it needs more time
1262                 checkUpdateStatus(updateInfo);
1263         }
1264         
1265         
1266         private static void checkUpdateStatus(final UpdateInfoRetriever updateInfo) {
1267                 if (updateInfo == null)
1268                         return;
1269                 
1270                 int delay = 1000;
1271                 if (!updateInfo.isRunning())
1272                         delay = 100;
1273                 
1274                 final Timer timer = new Timer(delay, null);
1275                 
1276                 ActionListener listener = new ActionListener() {
1277                         private int count = 5;
1278                         
1279                         @Override
1280                         public void actionPerformed(ActionEvent e) {
1281                                 if (!updateInfo.isRunning()) {
1282                                         timer.stop();
1283                                         
1284                                         String current = Prefs.getVersion();
1285                                         String last = Prefs.getString(Prefs.LAST_UPDATE, "");
1286                                         
1287                                         UpdateInfo info = updateInfo.getUpdateInfo();
1288                                         if (info != null && info.getLatestVersion() != null &&
1289                                                         !current.equals(info.getLatestVersion()) &&
1290                                                         !last.equals(info.getLatestVersion())) {
1291                                                 
1292                                                 UpdateInfoDialog infoDialog = new UpdateInfoDialog(info);
1293                                                 infoDialog.setVisible(true);
1294                                                 if (infoDialog.isReminderSelected()) {
1295                                                         Prefs.putString(Prefs.LAST_UPDATE, "");
1296                                                 } else {
1297                                                         Prefs.putString(Prefs.LAST_UPDATE, info.getLatestVersion());
1298                                                 }
1299                                         }
1300                                 }
1301                                 count--;
1302                                 if (count <= 0)
1303                                         timer.stop();
1304                         }
1305                 };
1306                 timer.addActionListener(listener);
1307                 timer.start();
1308         }
1309         
1310         
1311         /**
1312          * Handles arguments passed from the command line.  This may be used either
1313          * when starting the first instance of OpenRocket or later when OpenRocket is
1314          * executed again while running.
1315          * 
1316          * @param args  the command-line arguments.
1317          * @return              whether a new frame was opened or similar user desired action was
1318          *                              performed as a result.
1319          */
1320         public static boolean handleCommandLine(String[] args) {
1321                 
1322                 // Check command-line for files
1323                 boolean opened = false;
1324                 for (String file : args) {
1325                         if (open(new File(file), null)) {
1326                                 opened = true;
1327                         }
1328                 }
1329                 return opened;
1330         }
1331         
1332 }