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.scalefigure.RocketPanel;
63 import net.sf.openrocket.rocketcomponent.ComponentChangeEvent;
64 import net.sf.openrocket.rocketcomponent.ComponentChangeListener;
65 import net.sf.openrocket.rocketcomponent.Rocket;
66 import net.sf.openrocket.rocketcomponent.RocketComponent;
67 import net.sf.openrocket.rocketcomponent.Stage;
68 import net.sf.openrocket.util.Icons;
69 import net.sf.openrocket.util.Prefs;
71 public class BasicFrame extends JFrame {
72 private static final long serialVersionUID = 1L;
75 * The RocketLoader instance used for loading all rocket designs.
77 private static final RocketLoader ROCKET_LOADER = new GeneralRocketLoader();
81 * File filter for filtering only rocket designs.
83 private static final FileFilter ROCKET_DESIGN_FILTER = new FileFilter() {
85 public String getDescription() {
86 return "OpenRocket designs (*.ork)";
89 public boolean accept(File f) {
90 String name = f.getName().toLowerCase();
91 return name.endsWith(".ork") || name.endsWith(".ork.gz");
98 * List of currently open frames. When the list goes empty
99 * it is time to exit the application.
101 private static final ArrayList<BasicFrame> frames = new ArrayList<BasicFrame>();
108 * Whether "New" and "Open" should replace this frame.
109 * Should be set to false on the first rocket modification.
111 private boolean replaceable = false;
115 private final OpenRocketDocument document;
116 private final Rocket rocket;
118 private RocketPanel rocketpanel;
119 private ComponentTree tree = null;
120 private final TreeSelectionModel selectionModel;
122 /** Actions available for rocket modifications */
123 private final RocketActions actions;
128 * Sole constructor. Creates a new frame based on the supplied document
129 * and adds it to the current frames list.
131 * @param document the document to show.
133 public BasicFrame(OpenRocketDocument document) {
135 this.document = document;
136 this.rocket = document.getRocket();
137 this.rocket.getDefaultConfiguration().setAllStages();
140 // Set replaceable flag to false at first modification
141 rocket.addComponentChangeListener(new ComponentChangeListener() {
142 public void componentChanged(ComponentChangeEvent e) {
144 BasicFrame.this.rocket.removeComponentChangeListener(this);
149 // Create the selection model that will be used
150 selectionModel = new DefaultTreeSelectionModel();
151 selectionModel.setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
153 actions = new RocketActions(document, selectionModel, this);
156 // The main vertical split pane
157 JSplitPane vertical = new JSplitPane(JSplitPane.VERTICAL_SPLIT, true);
158 vertical.setResizeWeight(0.5);
162 // The top tabbed pane
163 JTabbedPane tabbed = new JTabbedPane();
164 tabbed.addTab("Rocket design", null, designTab());
165 tabbed.addTab("Flight simulations", null, simulationsTab());
167 vertical.setTopComponent(tabbed);
171 // Bottom segment, rocket figure
173 rocketpanel = new RocketPanel(document);
174 vertical.setBottomComponent(rocketpanel);
176 rocketpanel.setSelectionModel(tree.getSelectionModel());
182 rocket.addComponentChangeListener(new ComponentChangeListener() {
183 public void componentChanged(ComponentChangeEvent e) {
191 Dimension size = Prefs.getWindowSize(this.getClass());
193 size = Toolkit.getDefaultToolkit().getScreenSize();
194 size.width = size.width*9/10;
195 size.height = size.height*9/10;
198 this.addComponentListener(new ComponentAdapter() {
200 public void componentResized(ComponentEvent e) {
201 Prefs.setWindowSize(BasicFrame.this.getClass(), BasicFrame.this.getSize());
204 this.setLocationByPlatform(true);
207 vertical.setDividerLocation(0.4);
208 setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
209 addWindowListener(new WindowAdapter() {
211 public void windowClosing(WindowEvent e) {
221 * Construct the "Rocket design" tab. This contains a horizontal split pane
222 * with the left component the design tree and the right component buttons
223 * for adding components.
225 private JComponent designTab() {
226 JSplitPane horizontal = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT,true);
227 horizontal.setResizeWeight(0.5);
230 // Upper-left segment, component tree
232 JPanel panel = new JPanel(new MigLayout("fill, flowy","","[grow]"));
234 tree = new ComponentTree(rocket);
235 tree.setSelectionModel(selectionModel);
237 // Remove JTree key events that interfere with menu accelerators
238 InputMap im = SwingUtilities.getUIInputMap(tree, JComponent.WHEN_FOCUSED);
239 im.put(KeyStroke.getKeyStroke(KeyEvent.VK_X, ActionEvent.CTRL_MASK), null);
240 im.put(KeyStroke.getKeyStroke(KeyEvent.VK_C, ActionEvent.CTRL_MASK), null);
241 im.put(KeyStroke.getKeyStroke(KeyEvent.VK_V, ActionEvent.CTRL_MASK), null);
242 im.put(KeyStroke.getKeyStroke(KeyEvent.VK_A, ActionEvent.CTRL_MASK), null);
243 im.put(KeyStroke.getKeyStroke(KeyEvent.VK_S, ActionEvent.CTRL_MASK), null);
244 im.put(KeyStroke.getKeyStroke(KeyEvent.VK_O, ActionEvent.CTRL_MASK), null);
245 im.put(KeyStroke.getKeyStroke(KeyEvent.VK_N, ActionEvent.CTRL_MASK), null);
249 // Double-click opens config dialog
250 MouseListener ml = new MouseAdapter() {
252 public void mousePressed(MouseEvent e) {
253 int selRow = tree.getRowForLocation(e.getX(), e.getY());
254 TreePath selPath = tree.getPathForLocation(e.getX(), e.getY());
256 if(e.getClickCount() == 2) {
258 RocketComponent c = (RocketComponent)selPath.getLastPathComponent();
259 ComponentConfigDialog.showDialog(BasicFrame.this,
260 BasicFrame.this.document, c);
265 tree.addMouseListener(ml);
267 // Update dialog when selection is changed
268 selectionModel.addTreeSelectionListener(new TreeSelectionListener() {
269 public void valueChanged(TreeSelectionEvent e) {
270 // Scroll tree to the selected item
271 TreePath path = selectionModel.getSelectionPath();
274 tree.scrollPathToVisible(path);
276 if (!ComponentConfigDialog.isDialogVisible())
278 RocketComponent c = (RocketComponent)path.getLastPathComponent();
279 ComponentConfigDialog.showDialog(BasicFrame.this,
280 BasicFrame.this.document, c);
284 // Place tree inside scroll pane
285 JScrollPane scroll = new JScrollPane(tree);
286 panel.add(scroll,"spany, grow, wrap");
290 JButton button = new JButton(actions.getMoveUpAction());
291 panel.add(button,"sizegroup buttons, aligny 65%");
293 button = new JButton(actions.getMoveDownAction());
294 panel.add(button,"sizegroup buttons, aligny 0%");
296 button = new JButton(actions.getEditAction());
297 panel.add(button, "sizegroup buttons");
299 button = new JButton(actions.getNewStageAction());
300 panel.add(button,"sizegroup buttons");
302 button = new JButton(actions.getDeleteAction());
303 button.setIcon(null);
304 button.setMnemonic(0);
305 panel.add(button,"sizegroup buttons");
307 horizontal.setLeftComponent(panel);
310 // Upper-right segment, component addition buttons
312 panel = new JPanel(new MigLayout("fill, insets 0","[0::]"));
314 scroll = new JScrollPane(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED,
315 ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
316 scroll.setViewportView(new ComponentAddButtons(document, selectionModel,
317 scroll.getViewport()));
318 scroll.setBorder(null);
319 scroll.setViewportBorder(null);
321 TitledBorder border = new TitledBorder("Add new component");
322 border.setTitleFont(border.getTitleFont().deriveFont(Font.BOLD));
323 scroll.setBorder(border);
325 panel.add(scroll,"grow");
327 horizontal.setRightComponent(panel);
334 * Construct the "Flight simulations" tab.
337 private JComponent simulationsTab() {
338 return new SimulationPanel(document);
344 * Creates the menu for the window.
346 private void createMenu() {
347 JMenuBar menubar = new JMenuBar();
352 menu = new JMenu("File");
353 menu.setMnemonic(KeyEvent.VK_F);
354 menu.getAccessibleContext().setAccessibleDescription("File-handling related tasks");
357 item = new JMenuItem("New",KeyEvent.VK_N);
358 item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_N, ActionEvent.CTRL_MASK));
359 item.setMnemonic(KeyEvent.VK_N);
360 item.getAccessibleContext().setAccessibleDescription("Create a new rocket design");
361 item.setIcon(Icons.FILE_NEW);
362 item.addActionListener(new ActionListener() {
363 public void actionPerformed(ActionEvent e) {
371 item = new JMenuItem("Open...",KeyEvent.VK_O);
372 item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, ActionEvent.CTRL_MASK));
373 item.getAccessibleContext().setAccessibleDescription("Open a rocket design");
374 item.setIcon(Icons.FILE_OPEN);
375 item.addActionListener(new ActionListener() {
376 public void actionPerformed(ActionEvent e) {
384 item = new JMenuItem("Save",KeyEvent.VK_S);
385 item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, ActionEvent.CTRL_MASK));
386 item.getAccessibleContext().setAccessibleDescription("Save the current rocket design");
387 item.setIcon(Icons.FILE_SAVE);
388 item.addActionListener(new ActionListener() {
389 public void actionPerformed(ActionEvent e) {
395 item = new JMenuItem("Save as...",KeyEvent.VK_A);
396 item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S,
397 ActionEvent.CTRL_MASK | ActionEvent.SHIFT_MASK));
398 item.getAccessibleContext().setAccessibleDescription("Save the current rocket design "+
400 item.setIcon(Icons.FILE_SAVE_AS);
401 item.addActionListener(new ActionListener() {
402 public void actionPerformed(ActionEvent e) {
408 // menu.addSeparator();
409 menu.add(new JSeparator());
411 item = new JMenuItem("Close",KeyEvent.VK_C);
412 item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_W, ActionEvent.CTRL_MASK));
413 item.getAccessibleContext().setAccessibleDescription("Close the current rocket design");
414 item.setIcon(Icons.FILE_CLOSE);
415 item.addActionListener(new ActionListener() {
416 public void actionPerformed(ActionEvent e) {
424 item = new JMenuItem("Quit",KeyEvent.VK_Q);
425 item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Q, ActionEvent.CTRL_MASK));
426 item.getAccessibleContext().setAccessibleDescription("Quit the program");
427 item.setIcon(Icons.FILE_QUIT);
428 item.addActionListener(new ActionListener() {
429 public void actionPerformed(ActionEvent e) {
438 menu = new JMenu("Edit");
439 menu.setMnemonic(KeyEvent.VK_E);
440 menu.getAccessibleContext().setAccessibleDescription("Rocket editing");
444 Action action = document.getUndoAction();
445 item = new JMenuItem(action);
446 item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Z, ActionEvent.CTRL_MASK));
447 item.setMnemonic(KeyEvent.VK_U);
448 item.getAccessibleContext().setAccessibleDescription("Undo the previous operation");
452 action = document.getRedoAction();
453 item = new JMenuItem(action);
454 item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Y, ActionEvent.CTRL_MASK));
455 item.setMnemonic(KeyEvent.VK_R);
456 item.getAccessibleContext().setAccessibleDescription("Redo the previously undone " +
463 item = new JMenuItem(actions.getCutAction());
466 item = new JMenuItem(actions.getCopyAction());
469 item = new JMenuItem(actions.getPasteAction());
472 item = new JMenuItem(actions.getDeleteAction());
477 item = new JMenuItem("Preferences");
478 item.setIcon(Icons.PREFERENCES);
479 item.getAccessibleContext().setAccessibleDescription("Setup the application "+
481 item.addActionListener(new ActionListener() {
482 public void actionPerformed(ActionEvent e) {
483 PreferencesDialog.showPreferences();
492 menu = new JMenu("Analyze");
493 menu.setMnemonic(KeyEvent.VK_A);
494 menu.getAccessibleContext().setAccessibleDescription("Analyzing the rocket");
497 item = new JMenuItem("Component analysis",KeyEvent.VK_C);
498 item.getAccessibleContext().setAccessibleDescription("Analyze the rocket components " +
500 item.addActionListener(new ActionListener() {
501 public void actionPerformed(ActionEvent e) {
502 ComponentAnalysisDialog.showDialog(rocketpanel);
511 menu = new JMenu("Help");
512 menu.setMnemonic(KeyEvent.VK_H);
513 menu.getAccessibleContext().setAccessibleDescription("Information about OpenRocket");
516 item = new JMenuItem("License",KeyEvent.VK_L);
517 item.getAccessibleContext().setAccessibleDescription("OpenRocket license information");
518 item.addActionListener(new ActionListener() {
519 public void actionPerformed(ActionEvent e) {
520 new LicenseDialog(BasicFrame.this).setVisible(true);
525 item = new JMenuItem("About",KeyEvent.VK_A);
526 item.getAccessibleContext().setAccessibleDescription("About OpenRocket");
527 item.addActionListener(new ActionListener() {
528 public void actionPerformed(ActionEvent e) {
529 new AboutDialog(BasicFrame.this).setVisible(true);
535 this.setJMenuBar(menubar);
540 // TODO: HIGH: Remember last directory on open/save
542 private void openAction() {
543 JFileChooser chooser = new JFileChooser();
544 chooser.setFileFilter(ROCKET_DESIGN_FILTER);
545 chooser.setMultiSelectionEnabled(true);
546 chooser.setCurrentDirectory(Prefs.getDefaultDirectory());
547 if (chooser.showOpenDialog(BasicFrame.this) != JFileChooser.APPROVE_OPTION)
550 Prefs.setDefaultDirectory(chooser.getCurrentDirectory());
552 File[] files = chooser.getSelectedFiles();
553 boolean opened = false;
555 for (File file: files) {
556 System.out.println("Opening file: " + file);
562 // Close this frame if replaceable and file opened successfully
563 if (replaceable && opened) {
570 * Open the specified file in a new design frame. If an error occurs, an error dialog
571 * is shown and <code>false</code> is returned.
573 * @param file the file to open.
574 * @return whether the file was successfully loaded and opened.
576 private static boolean open(File file) {
577 OpenRocketDocument doc = null;
579 doc = ROCKET_LOADER.load(file);
580 } catch (RocketLoadException e) {
581 JOptionPane.showMessageDialog(null, "Unable to open file '" + file.getName()
582 +"': " + e.getMessage(), "Error opening file", JOptionPane.ERROR_MESSAGE);
588 throw new RuntimeException("BUG: Rocket loader returned null");
592 Iterator<Warning> warns = ROCKET_LOADER.getWarnings().iterator();
593 System.out.println("Warnings:");
594 while (warns.hasNext()) {
595 System.out.println(" "+warns.next());
596 // TODO: HIGH: dialog
599 // Set document state
604 BasicFrame frame = new BasicFrame(doc);
605 frame.setVisible(true);
612 private boolean saveAction() {
613 File file = document.getFile();
615 return saveAsAction();
621 private boolean saveAsAction() {
623 while (file == null) {
624 StorageOptionChooser storageChooser =
625 new StorageOptionChooser(document.getDefaultStorageOptions());
626 JFileChooser chooser = new JFileChooser();
627 chooser.setFileFilter(ROCKET_DESIGN_FILTER);
628 chooser.setCurrentDirectory(Prefs.getDefaultDirectory());
629 chooser.setAccessory(storageChooser);
630 if (document.getFile() != null)
631 chooser.setSelectedFile(document.getFile());
633 if (chooser.showSaveDialog(BasicFrame.this) != JFileChooser.APPROVE_OPTION)
636 file = chooser.getSelectedFile();
640 Prefs.setDefaultDirectory(chooser.getCurrentDirectory());
641 storageChooser.storeOptions(document.getDefaultStorageOptions());
643 if (file.getName().indexOf('.') < 0) {
644 String name = file.getAbsolutePath();
645 name = name + ".ork";
646 file = new File(name);
650 int result = JOptionPane.showConfirmDialog(this,
651 "File '"+file.getName()+"' exists. Do you want to overwrite it?",
652 "File exists", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE);
653 if (result != JOptionPane.YES_OPTION)
662 private boolean saveAs(File file) {
663 System.out.println("Saving to file: " + file.getName());
664 boolean saved = false;
666 if (!StorageOptionChooser.verifyStorageOptions(document, this)) {
667 // User cancelled the dialog
671 RocketSaver saver = new OpenRocketSaver();
673 saver.save(file, document);
674 document.setFile(file);
675 document.setSaved(true);
677 } catch (IOException e) {
678 JOptionPane.showMessageDialog(this, new String[] {
679 "An I/O error occurred while saving:",
680 e.getMessage() }, "Saving failed", JOptionPane.ERROR_MESSAGE);
687 private boolean closeAction() {
688 if (!document.isSaved()) {
689 ComponentConfigDialog.hideDialog();
690 int result = JOptionPane.showConfirmDialog(this,
691 "Design '"+rocket.getName()+"' has not been saved. " +
692 "Do you want to save it?",
693 "Design not saved", JOptionPane.YES_NO_CANCEL_OPTION,
694 JOptionPane.QUESTION_MESSAGE);
695 if (result == JOptionPane.YES_OPTION) {
698 return false; // If save was interrupted
699 } else if (result == JOptionPane.NO_OPTION) {
707 // Rocket has been saved or discarded
710 // TODO: LOW: Close only dialogs that have this frame as their parent
711 ComponentConfigDialog.hideDialog();
712 ComponentAnalysisDialog.hideDialog();
715 if (frames.isEmpty())
721 * Open a new design window with a basic rocket+stage.
723 public static void newAction() {
724 Rocket rocket = new Rocket();
725 Stage stage = new Stage();
726 stage.setName("Sustainer");
727 rocket.addChild(stage);
728 OpenRocketDocument doc = new OpenRocketDocument(rocket);
731 BasicFrame frame = new BasicFrame(doc);
732 frame.replaceable = true;
733 frame.setVisible(true);
734 ComponentConfigDialog.showDialog(frame, doc, rocket);
738 * Quit the application. Confirms saving unsaved designs. The action of File->Quit.
740 public static void quitAction() {
741 for (int i=frames.size()-1; i>=0; i--) {
742 if (!frames.get(i).closeAction()) {
747 // Should not be reached, but just in case
753 * Set the title of the frame, taking into account the name of the rocket, file it
754 * has been saved to (if any) and saved status.
756 private void setTitle() {
757 File file = document.getFile();
758 boolean saved = document.isSaved();
761 title = rocket.getName();
763 title = title + " ("+file.getName()+")";
776 public static void main(String[] args) {
779 * Set the look-and-feel. On Linux, Motif/Metal is sometimes incorrectly used
780 * which is butt-ugly, so if the system l&f is Motif/Metal, we search for a few
781 * other alternatives.
784 UIManager.LookAndFeelInfo[] info = UIManager.getInstalledLookAndFeels();
785 // System.out.println("Available look-and-feels:");
786 // for (int i=0; i<info.length; i++) {
787 // System.out.println(" "+info[i]);
791 UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
793 // Check whether we have an ugly L&F
794 LookAndFeel laf = UIManager.getLookAndFeel();
796 laf.getName().matches(".*[mM][oO][tT][iI][fF].*") ||
797 laf.getName().matches(".*[mM][eE][tT][aA][lL].*")) {
799 // Search for better LAF
800 for (UIManager.LookAndFeelInfo l: info) {
801 if (l.getName().matches(".*[gG][tT][kK].*")) {
802 UIManager.setLookAndFeel(l.getClassName());
805 if (l.getName().contains(".*[wW][iI][nN].*")) {
806 UIManager.setLookAndFeel(l.getClassName());
809 if (l.getName().contains(".*[mM][aA][cC].*")) {
810 UIManager.setLookAndFeel(l.getClassName());
815 } catch (Exception e) {
816 System.err.println("Error setting LAF: " + e);
819 // Set tooltip delay time. Tooltips are used in MotorChooserDialog extensively.
820 ToolTipManager.sharedInstance().setDismissDelay(30000);
824 Prefs.loadDefaultUnits();
827 // Check command-line for files
828 boolean opened = false;
829 for (String file: args) {
830 if (open(new File(file))) {