1 package net.sf.openrocket.gui.main;
3 import java.awt.Dimension;
5 import java.awt.Toolkit;
6 import java.awt.event.ActionEvent;
7 import java.awt.event.ActionListener;
8 import java.awt.event.ComponentAdapter;
9 import java.awt.event.ComponentEvent;
10 import java.awt.event.KeyEvent;
11 import java.awt.event.MouseAdapter;
12 import java.awt.event.MouseEvent;
13 import java.awt.event.MouseListener;
14 import java.awt.event.WindowAdapter;
15 import java.awt.event.WindowEvent;
17 import java.io.IOException;
18 import java.util.ArrayList;
19 import java.util.Iterator;
21 import javax.swing.Action;
22 import javax.swing.InputMap;
23 import javax.swing.JButton;
24 import javax.swing.JComponent;
25 import javax.swing.JFileChooser;
26 import javax.swing.JFrame;
27 import javax.swing.JMenu;
28 import javax.swing.JMenuBar;
29 import javax.swing.JMenuItem;
30 import javax.swing.JOptionPane;
31 import javax.swing.JPanel;
32 import javax.swing.JScrollPane;
33 import javax.swing.JSeparator;
34 import javax.swing.JSplitPane;
35 import javax.swing.JTabbedPane;
36 import javax.swing.KeyStroke;
37 import javax.swing.LookAndFeel;
38 import javax.swing.ScrollPaneConstants;
39 import javax.swing.SwingUtilities;
40 import javax.swing.ToolTipManager;
41 import javax.swing.UIManager;
42 import javax.swing.border.TitledBorder;
43 import javax.swing.event.TreeSelectionEvent;
44 import javax.swing.event.TreeSelectionListener;
45 import javax.swing.filechooser.FileFilter;
46 import javax.swing.tree.DefaultTreeSelectionModel;
47 import javax.swing.tree.TreePath;
48 import javax.swing.tree.TreeSelectionModel;
50 import net.miginfocom.swing.MigLayout;
51 import net.sf.openrocket.aerodynamics.Warning;
52 import net.sf.openrocket.document.OpenRocketDocument;
53 import net.sf.openrocket.file.GeneralRocketLoader;
54 import net.sf.openrocket.file.OpenRocketSaver;
55 import net.sf.openrocket.file.RocketLoadException;
56 import net.sf.openrocket.file.RocketLoader;
57 import net.sf.openrocket.file.RocketSaver;
58 import net.sf.openrocket.gui.ComponentAnalysisDialog;
59 import net.sf.openrocket.gui.PreferencesDialog;
60 import net.sf.openrocket.gui.StorageOptionChooser;
61 import net.sf.openrocket.gui.configdialog.ComponentConfigDialog;
62 import net.sf.openrocket.gui.dialogs.BugDialog;
63 import net.sf.openrocket.gui.scalefigure.RocketPanel;
64 import net.sf.openrocket.rocketcomponent.ComponentChangeEvent;
65 import net.sf.openrocket.rocketcomponent.ComponentChangeListener;
66 import net.sf.openrocket.rocketcomponent.Rocket;
67 import net.sf.openrocket.rocketcomponent.RocketComponent;
68 import net.sf.openrocket.rocketcomponent.Stage;
69 import net.sf.openrocket.util.Icons;
70 import net.sf.openrocket.util.Prefs;
72 public class BasicFrame extends JFrame {
73 private static final long serialVersionUID = 1L;
76 * The RocketLoader instance used for loading all rocket designs.
78 private static final RocketLoader ROCKET_LOADER = new GeneralRocketLoader();
82 * File filter for filtering only rocket designs.
84 private static final FileFilter ROCKET_DESIGN_FILTER = new FileFilter() {
86 public String getDescription() {
87 return "OpenRocket designs (*.ork)";
90 public boolean accept(File f) {
93 String name = f.getName().toLowerCase();
94 return name.endsWith(".ork") || name.endsWith(".ork.gz");
101 * List of currently open frames. When the list goes empty
102 * it is time to exit the application.
104 private static final ArrayList<BasicFrame> frames = new ArrayList<BasicFrame>();
111 * Whether "New" and "Open" should replace this frame.
112 * Should be set to false on the first rocket modification.
114 private boolean replaceable = false;
118 private final OpenRocketDocument document;
119 private final Rocket rocket;
121 private RocketPanel rocketpanel;
122 private ComponentTree tree = null;
123 private final TreeSelectionModel componentSelectionModel;
124 // private final ListSelectionModel simulationSelectionModel; ...
126 /** Actions available for rocket modifications */
127 private final RocketActions actions;
132 * Sole constructor. Creates a new frame based on the supplied document
133 * and adds it to the current frames list.
135 * @param document the document to show.
137 public BasicFrame(OpenRocketDocument document) {
139 this.document = document;
140 this.rocket = document.getRocket();
141 this.rocket.getDefaultConfiguration().setAllStages();
144 // Set replaceable flag to false at first modification
145 rocket.addComponentChangeListener(new ComponentChangeListener() {
146 public void componentChanged(ComponentChangeEvent e) {
148 BasicFrame.this.rocket.removeComponentChangeListener(this);
153 // Create the selection model that will be used
154 componentSelectionModel = new DefaultTreeSelectionModel();
155 componentSelectionModel.setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
157 actions = new RocketActions(document, componentSelectionModel, this);
160 // The main vertical split pane
161 JSplitPane vertical = new JSplitPane(JSplitPane.VERTICAL_SPLIT, true);
162 vertical.setResizeWeight(0.5);
166 // The top tabbed pane
167 JTabbedPane tabbed = new JTabbedPane();
168 tabbed.addTab("Rocket design", null, designTab());
169 tabbed.addTab("Flight simulations", null, simulationsTab());
171 vertical.setTopComponent(tabbed);
175 // Bottom segment, rocket figure
177 rocketpanel = new RocketPanel(document);
178 vertical.setBottomComponent(rocketpanel);
180 rocketpanel.setSelectionModel(tree.getSelectionModel());
186 rocket.addComponentChangeListener(new ComponentChangeListener() {
187 public void componentChanged(ComponentChangeEvent e) {
195 Dimension size = Prefs.getWindowSize(this.getClass());
197 size = Toolkit.getDefaultToolkit().getScreenSize();
198 size.width = size.width*9/10;
199 size.height = size.height*9/10;
202 this.addComponentListener(new ComponentAdapter() {
204 public void componentResized(ComponentEvent e) {
205 Prefs.setWindowSize(BasicFrame.this.getClass(), BasicFrame.this.getSize());
208 this.setLocationByPlatform(true);
211 vertical.setDividerLocation(0.4);
212 setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
213 addWindowListener(new WindowAdapter() {
215 public void windowClosing(WindowEvent e) {
225 * Construct the "Rocket design" tab. This contains a horizontal split pane
226 * with the left component the design tree and the right component buttons
227 * for adding components.
229 private JComponent designTab() {
230 JSplitPane horizontal = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT,true);
231 horizontal.setResizeWeight(0.5);
234 // Upper-left segment, component tree
236 JPanel panel = new JPanel(new MigLayout("fill, flowy","","[grow]"));
238 tree = new ComponentTree(rocket);
239 tree.setSelectionModel(componentSelectionModel);
241 // Remove JTree key events that interfere with menu accelerators
242 InputMap im = SwingUtilities.getUIInputMap(tree, JComponent.WHEN_FOCUSED);
243 im.put(KeyStroke.getKeyStroke(KeyEvent.VK_X, ActionEvent.CTRL_MASK), null);
244 im.put(KeyStroke.getKeyStroke(KeyEvent.VK_C, ActionEvent.CTRL_MASK), null);
245 im.put(KeyStroke.getKeyStroke(KeyEvent.VK_V, ActionEvent.CTRL_MASK), null);
246 im.put(KeyStroke.getKeyStroke(KeyEvent.VK_A, ActionEvent.CTRL_MASK), null);
247 im.put(KeyStroke.getKeyStroke(KeyEvent.VK_S, ActionEvent.CTRL_MASK), null);
248 im.put(KeyStroke.getKeyStroke(KeyEvent.VK_O, ActionEvent.CTRL_MASK), null);
249 im.put(KeyStroke.getKeyStroke(KeyEvent.VK_N, ActionEvent.CTRL_MASK), null);
253 // Double-click opens config dialog
254 MouseListener ml = new MouseAdapter() {
256 public void mousePressed(MouseEvent e) {
257 int selRow = tree.getRowForLocation(e.getX(), e.getY());
258 TreePath selPath = tree.getPathForLocation(e.getX(), e.getY());
260 if(e.getClickCount() == 2) {
262 RocketComponent c = (RocketComponent)selPath.getLastPathComponent();
263 ComponentConfigDialog.showDialog(BasicFrame.this,
264 BasicFrame.this.document, c);
269 tree.addMouseListener(ml);
271 // Update dialog when selection is changed
272 componentSelectionModel.addTreeSelectionListener(new TreeSelectionListener() {
273 public void valueChanged(TreeSelectionEvent e) {
274 // Scroll tree to the selected item
275 TreePath path = componentSelectionModel.getSelectionPath();
278 tree.scrollPathToVisible(path);
280 if (!ComponentConfigDialog.isDialogVisible())
282 RocketComponent c = (RocketComponent)path.getLastPathComponent();
283 ComponentConfigDialog.showDialog(BasicFrame.this,
284 BasicFrame.this.document, c);
288 // Place tree inside scroll pane
289 JScrollPane scroll = new JScrollPane(tree);
290 panel.add(scroll,"spany, grow, wrap");
294 JButton button = new JButton(actions.getMoveUpAction());
295 panel.add(button,"sizegroup buttons, aligny 65%");
297 button = new JButton(actions.getMoveDownAction());
298 panel.add(button,"sizegroup buttons, aligny 0%");
300 button = new JButton(actions.getEditAction());
301 panel.add(button, "sizegroup buttons");
303 button = new JButton(actions.getNewStageAction());
304 panel.add(button,"sizegroup buttons");
306 button = new JButton(actions.getDeleteAction());
307 button.setIcon(null);
308 button.setMnemonic(0);
309 panel.add(button,"sizegroup buttons");
311 horizontal.setLeftComponent(panel);
314 // Upper-right segment, component addition buttons
316 panel = new JPanel(new MigLayout("fill, insets 0","[0::]"));
318 scroll = new JScrollPane(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED,
319 ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
320 scroll.setViewportView(new ComponentAddButtons(document, componentSelectionModel,
321 scroll.getViewport()));
322 scroll.setBorder(null);
323 scroll.setViewportBorder(null);
325 TitledBorder border = new TitledBorder("Add new component");
326 border.setTitleFont(border.getTitleFont().deriveFont(Font.BOLD));
327 scroll.setBorder(border);
329 panel.add(scroll,"grow");
331 horizontal.setRightComponent(panel);
338 * Construct the "Flight simulations" tab.
341 private JComponent simulationsTab() {
342 return new SimulationPanel(document);
348 * Creates the menu for the window.
350 private void createMenu() {
351 JMenuBar menubar = new JMenuBar();
356 menu = new JMenu("File");
357 menu.setMnemonic(KeyEvent.VK_F);
358 menu.getAccessibleContext().setAccessibleDescription("File-handling related tasks");
361 item = new JMenuItem("New",KeyEvent.VK_N);
362 item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_N, ActionEvent.CTRL_MASK));
363 item.setMnemonic(KeyEvent.VK_N);
364 item.getAccessibleContext().setAccessibleDescription("Create a new rocket design");
365 item.setIcon(Icons.FILE_NEW);
366 item.addActionListener(new ActionListener() {
367 public void actionPerformed(ActionEvent e) {
375 item = new JMenuItem("Open...",KeyEvent.VK_O);
376 item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, ActionEvent.CTRL_MASK));
377 item.getAccessibleContext().setAccessibleDescription("Open a rocket design");
378 item.setIcon(Icons.FILE_OPEN);
379 item.addActionListener(new ActionListener() {
380 public void actionPerformed(ActionEvent e) {
388 item = new JMenuItem("Save",KeyEvent.VK_S);
389 item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, ActionEvent.CTRL_MASK));
390 item.getAccessibleContext().setAccessibleDescription("Save the current rocket design");
391 item.setIcon(Icons.FILE_SAVE);
392 item.addActionListener(new ActionListener() {
393 public void actionPerformed(ActionEvent e) {
399 item = new JMenuItem("Save as...",KeyEvent.VK_A);
400 item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S,
401 ActionEvent.CTRL_MASK | ActionEvent.SHIFT_MASK));
402 item.getAccessibleContext().setAccessibleDescription("Save the current rocket design "+
404 item.setIcon(Icons.FILE_SAVE_AS);
405 item.addActionListener(new ActionListener() {
406 public void actionPerformed(ActionEvent e) {
412 // menu.addSeparator();
413 menu.add(new JSeparator());
415 item = new JMenuItem("Close",KeyEvent.VK_C);
416 item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_W, ActionEvent.CTRL_MASK));
417 item.getAccessibleContext().setAccessibleDescription("Close the current rocket design");
418 item.setIcon(Icons.FILE_CLOSE);
419 item.addActionListener(new ActionListener() {
420 public void actionPerformed(ActionEvent e) {
428 item = new JMenuItem("Quit",KeyEvent.VK_Q);
429 item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Q, ActionEvent.CTRL_MASK));
430 item.getAccessibleContext().setAccessibleDescription("Quit the program");
431 item.setIcon(Icons.FILE_QUIT);
432 item.addActionListener(new ActionListener() {
433 public void actionPerformed(ActionEvent e) {
442 menu = new JMenu("Edit");
443 menu.setMnemonic(KeyEvent.VK_E);
444 menu.getAccessibleContext().setAccessibleDescription("Rocket editing");
448 Action action = document.getUndoAction();
449 item = new JMenuItem(action);
450 item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Z, ActionEvent.CTRL_MASK));
451 item.setMnemonic(KeyEvent.VK_U);
452 item.getAccessibleContext().setAccessibleDescription("Undo the previous operation");
456 action = document.getRedoAction();
457 item = new JMenuItem(action);
458 item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Y, ActionEvent.CTRL_MASK));
459 item.setMnemonic(KeyEvent.VK_R);
460 item.getAccessibleContext().setAccessibleDescription("Redo the previously undone " +
467 item = new JMenuItem(actions.getCutAction());
470 item = new JMenuItem(actions.getCopyAction());
473 item = new JMenuItem(actions.getPasteAction());
476 item = new JMenuItem(actions.getDeleteAction());
481 item = new JMenuItem("Preferences");
482 item.setIcon(Icons.PREFERENCES);
483 item.getAccessibleContext().setAccessibleDescription("Setup the application "+
485 item.addActionListener(new ActionListener() {
486 public void actionPerformed(ActionEvent e) {
487 PreferencesDialog.showPreferences();
496 menu = new JMenu("Analyze");
497 menu.setMnemonic(KeyEvent.VK_A);
498 menu.getAccessibleContext().setAccessibleDescription("Analyzing the rocket");
501 item = new JMenuItem("Component analysis",KeyEvent.VK_C);
502 item.getAccessibleContext().setAccessibleDescription("Analyze the rocket components " +
504 item.addActionListener(new ActionListener() {
505 public void actionPerformed(ActionEvent e) {
506 ComponentAnalysisDialog.showDialog(rocketpanel);
515 menu = new JMenu("Help");
516 menu.setMnemonic(KeyEvent.VK_H);
517 menu.getAccessibleContext().setAccessibleDescription("Information about OpenRocket");
522 item = new JMenuItem("License",KeyEvent.VK_L);
523 item.getAccessibleContext().setAccessibleDescription("OpenRocket license information");
524 item.addActionListener(new ActionListener() {
525 public void actionPerformed(ActionEvent e) {
526 new LicenseDialog(BasicFrame.this).setVisible(true);
531 item = new JMenuItem("Bug report",KeyEvent.VK_B);
532 item.getAccessibleContext().setAccessibleDescription("Information about reporting " +
533 "bugs in OpenRocket");
534 item.addActionListener(new ActionListener() {
535 public void actionPerformed(ActionEvent e) {
536 new BugDialog(BasicFrame.this).setVisible(true);
541 item = new JMenuItem("About",KeyEvent.VK_A);
542 item.getAccessibleContext().setAccessibleDescription("About OpenRocket");
543 item.addActionListener(new ActionListener() {
544 public void actionPerformed(ActionEvent e) {
545 new AboutDialog(BasicFrame.this).setVisible(true);
551 this.setJMenuBar(menubar);
557 private void openAction() {
558 JFileChooser chooser = new JFileChooser();
559 chooser.setFileFilter(ROCKET_DESIGN_FILTER);
560 chooser.setMultiSelectionEnabled(true);
561 chooser.setCurrentDirectory(Prefs.getDefaultDirectory());
562 if (chooser.showOpenDialog(this) != JFileChooser.APPROVE_OPTION)
565 Prefs.setDefaultDirectory(chooser.getCurrentDirectory());
567 File[] files = chooser.getSelectedFiles();
568 boolean opened = false;
570 for (File file: files) {
571 System.out.println("Opening file: " + file);
577 // Close this frame if replaceable and file opened successfully
578 if (replaceable && opened) {
585 * Open the specified file in a new design frame. If an error occurs, an error dialog
586 * is shown and <code>false</code> is returned.
588 * @param file the file to open.
589 * @return whether the file was successfully loaded and opened.
591 private static boolean open(File file) {
592 OpenRocketDocument doc = null;
594 doc = ROCKET_LOADER.load(file);
595 } catch (RocketLoadException e) {
596 JOptionPane.showMessageDialog(null, "Unable to open file '" + file.getName()
597 +"': " + e.getMessage(), "Error opening file", JOptionPane.ERROR_MESSAGE);
603 throw new RuntimeException("BUG: Rocket loader returned null");
607 Iterator<Warning> warns = ROCKET_LOADER.getWarnings().iterator();
608 System.out.println("Warnings:");
609 while (warns.hasNext()) {
610 System.out.println(" "+warns.next());
611 // TODO: HIGH: dialog
614 // Set document state
619 BasicFrame frame = new BasicFrame(doc);
620 frame.setVisible(true);
627 private boolean saveAction() {
628 File file = document.getFile();
630 return saveAsAction();
636 private boolean saveAsAction() {
638 while (file == null) {
639 StorageOptionChooser storageChooser =
640 new StorageOptionChooser(document.getDefaultStorageOptions());
641 JFileChooser chooser = new JFileChooser();
642 chooser.setFileFilter(ROCKET_DESIGN_FILTER);
643 chooser.setCurrentDirectory(Prefs.getDefaultDirectory());
644 chooser.setAccessory(storageChooser);
645 if (document.getFile() != null)
646 chooser.setSelectedFile(document.getFile());
648 if (chooser.showSaveDialog(BasicFrame.this) != JFileChooser.APPROVE_OPTION)
651 file = chooser.getSelectedFile();
655 Prefs.setDefaultDirectory(chooser.getCurrentDirectory());
656 storageChooser.storeOptions(document.getDefaultStorageOptions());
658 if (file.getName().indexOf('.') < 0) {
659 String name = file.getAbsolutePath();
660 name = name + ".ork";
661 file = new File(name);
665 int result = JOptionPane.showConfirmDialog(this,
666 "File '"+file.getName()+"' exists. Do you want to overwrite it?",
667 "File exists", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE);
668 if (result != JOptionPane.YES_OPTION)
677 private boolean saveAs(File file) {
678 System.out.println("Saving to file: " + file.getName());
679 boolean saved = false;
681 if (!StorageOptionChooser.verifyStorageOptions(document, this)) {
682 // User cancelled the dialog
686 RocketSaver saver = new OpenRocketSaver();
688 saver.save(file, document);
689 document.setFile(file);
690 document.setSaved(true);
692 } catch (IOException e) {
693 JOptionPane.showMessageDialog(this, new String[] {
694 "An I/O error occurred while saving:",
695 e.getMessage() }, "Saving failed", JOptionPane.ERROR_MESSAGE);
702 private boolean closeAction() {
703 if (!document.isSaved()) {
704 ComponentConfigDialog.hideDialog();
705 int result = JOptionPane.showConfirmDialog(this,
706 "Design '"+rocket.getName()+"' has not been saved. " +
707 "Do you want to save it?",
708 "Design not saved", JOptionPane.YES_NO_CANCEL_OPTION,
709 JOptionPane.QUESTION_MESSAGE);
710 if (result == JOptionPane.YES_OPTION) {
713 return false; // If save was interrupted
714 } else if (result == JOptionPane.NO_OPTION) {
722 // Rocket has been saved or discarded
725 // TODO: LOW: Close only dialogs that have this frame as their parent
726 ComponentConfigDialog.hideDialog();
727 ComponentAnalysisDialog.hideDialog();
730 if (frames.isEmpty())
736 * Open a new design window with a basic rocket+stage.
738 public static void newAction() {
739 Rocket rocket = new Rocket();
740 Stage stage = new Stage();
741 stage.setName("Sustainer");
742 rocket.addChild(stage);
743 OpenRocketDocument doc = new OpenRocketDocument(rocket);
746 BasicFrame frame = new BasicFrame(doc);
747 frame.replaceable = true;
748 frame.setVisible(true);
749 ComponentConfigDialog.showDialog(frame, doc, rocket);
753 * Quit the application. Confirms saving unsaved designs. The action of File->Quit.
755 public static void quitAction() {
756 for (int i=frames.size()-1; i>=0; i--) {
757 if (!frames.get(i).closeAction()) {
762 // Should not be reached, but just in case
768 * Set the title of the frame, taking into account the name of the rocket, file it
769 * has been saved to (if any) and saved status.
771 private void setTitle() {
772 File file = document.getFile();
773 boolean saved = document.isSaved();
776 title = rocket.getName();
778 title = title + " ("+file.getName()+")";
791 public static void main(String[] args) {
794 * Set the look-and-feel. On Linux, Motif/Metal is sometimes incorrectly used
795 * which is butt-ugly, so if the system l&f is Motif/Metal, we search for a few
796 * other alternatives.
799 UIManager.LookAndFeelInfo[] info = UIManager.getInstalledLookAndFeels();
800 // System.out.println("Available look-and-feels:");
801 // for (int i=0; i<info.length; i++) {
802 // System.out.println(" "+info[i]);
806 UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
808 // Check whether we have an ugly L&F
809 LookAndFeel laf = UIManager.getLookAndFeel();
811 laf.getName().matches(".*[mM][oO][tT][iI][fF].*") ||
812 laf.getName().matches(".*[mM][eE][tT][aA][lL].*")) {
814 // Search for better LAF
815 for (UIManager.LookAndFeelInfo l: info) {
816 if (l.getName().matches(".*[gG][tT][kK].*")) {
817 UIManager.setLookAndFeel(l.getClassName());
820 if (l.getName().contains(".*[wW][iI][nN].*")) {
821 UIManager.setLookAndFeel(l.getClassName());
824 if (l.getName().contains(".*[mM][aA][cC].*")) {
825 UIManager.setLookAndFeel(l.getClassName());
830 } catch (Exception e) {
831 System.err.println("Error setting LAF: " + e);
834 // Set tooltip delay time. Tooltips are used in MotorChooserDialog extensively.
835 ToolTipManager.sharedInstance().setDismissDelay(30000);
839 Prefs.loadDefaultUnits();
842 // Check command-line for files
843 boolean opened = false;
844 for (String file: args) {
845 if (open(new File(file))) {