diff --git a/swing/src/net/sf/openrocket/gui/main/BasicFrame.java b/swing/src/net/sf/openrocket/gui/main/BasicFrame.java index d6a84bab3..54ea0c45d 100644 --- a/swing/src/net/sf/openrocket/gui/main/BasicFrame.java +++ b/swing/src/net/sf/openrocket/gui/main/BasicFrame.java @@ -34,6 +34,7 @@ import javax.swing.tree.TreeSelectionModel; import net.miginfocom.swing.MigLayout; import net.sf.openrocket.aerodynamics.WarningSet; import net.sf.openrocket.appearance.DecalImage; +import net.sf.openrocket.arch.SystemInfo; import net.sf.openrocket.document.OpenRocketDocument; import net.sf.openrocket.document.OpenRocketDocumentFactory; import net.sf.openrocket.document.StorageOptions; @@ -60,6 +61,7 @@ import net.sf.openrocket.gui.help.tours.GuidedTourSelectionDialog; import net.sf.openrocket.gui.main.componenttree.ComponentTree; import net.sf.openrocket.gui.main.flightconfigpanel.FlightConfigurationPanel; import net.sf.openrocket.gui.scalefigure.RocketPanel; +import net.sf.openrocket.gui.util.DummyFrameMenuOSX; import net.sf.openrocket.gui.util.FileHelper; import net.sf.openrocket.gui.util.GUIUtil; import net.sf.openrocket.gui.util.Icons; @@ -99,9 +101,9 @@ public class BasicFrame extends JFrame { private static final Translator trans = Application.getTranslator(); private static final Preferences prefs = Application.getPreferences(); - private static final int SHORTCUT_KEY = Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx(); + public static final int SHORTCUT_KEY = Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx(); - private static final int SHIFT_SHORTCUT_KEY = Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx() | + public static final int SHIFT_SHORTCUT_KEY = Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx() | SHIFT_DOWN_MASK; public static final int COMPONENT_TAB = 0; @@ -1191,6 +1193,10 @@ public class BasicFrame extends JFrame { private void openAction() { + openAction(this); + } + + public static void openAction(Window parent) { JFileChooser chooser = new JFileChooser(); chooser.addChoosableFileFilter(FileHelper.ALL_DESIGNS_FILTER); @@ -1200,7 +1206,7 @@ public class BasicFrame extends JFrame { chooser.setFileSelectionMode(JFileChooser.FILES_ONLY); chooser.setMultiSelectionEnabled(true); chooser.setCurrentDirectory(((SwingPreferences) Application.getPreferences()).getDefaultDirectory()); - int option = chooser.showOpenDialog(this); + int option = chooser.showOpenDialog(parent); if (option != JFileChooser.APPROVE_OPTION) { log.info(Markers.USER_MARKER, "Decided not to open files, option=" + option); return; @@ -1213,7 +1219,7 @@ public class BasicFrame extends JFrame { for (File file : files) { log.info("Opening file: " + file); - if (open(file, this)) { + if (open(file, parent)) { MRUDesignFile opts = MRUDesignFile.getInstance(); opts.addFile(file.getAbsolutePath()); } @@ -1691,8 +1697,13 @@ public class BasicFrame extends JFrame { frames.remove(this); if (frames.isEmpty()) { - log.info("Last frame closed, exiting"); - System.exit(0); + // Don't quit the application on macOS, but keep the application open + if (SystemInfo.getPlatform() == SystemInfo.Platform.MAC_OS) { + DummyFrameMenuOSX.createDummyDialog(); + } else { + log.info("Last frame closed, exiting"); + System.exit(0); + } } return true; } @@ -1707,6 +1718,31 @@ public class BasicFrame extends JFrame { new PrintDialog(this, document, rotation).setVisible(true); } + /** + * Opens a new design file or the last design file, if set in the preferences. + * Can be used for reopening the application or opening it the first time. + */ + public static void reopen() { + if (!Application.getPreferences().isAutoOpenLastDesignOnStartupEnabled()) { + BasicFrame.newAction(); + } else { + String lastFile = MRUDesignFile.getInstance().getLastEditedDesignFile(); + if (lastFile != null) { + log.info("Opening last design file: " + lastFile); + if (!BasicFrame.open(new File(lastFile), null)) { + MRUDesignFile.getInstance().removeFile(lastFile); + BasicFrame.newAction(); + } + else { + MRUDesignFile.getInstance().addFile(lastFile); + } + } + else { + BasicFrame.newAction(); + } + } + } + /** * Open a new design window with a basic rocket+stage. @@ -1779,6 +1815,14 @@ public class BasicFrame extends JFrame { return null; } + /** + * Checks whether all the BasicFrames are closed. + * @return true if all the BasicFrames are closed, false if not + */ + public static boolean isFramesEmpty() { + return frames.isEmpty(); + } + /** * Find a currently open document by the rocket object. This method can be used * to map a Rocket to OpenRocketDocument from GUI methods. diff --git a/swing/src/net/sf/openrocket/gui/util/DummyFrameMenuOSX.java b/swing/src/net/sf/openrocket/gui/util/DummyFrameMenuOSX.java new file mode 100644 index 000000000..1d0bf444e --- /dev/null +++ b/swing/src/net/sf/openrocket/gui/util/DummyFrameMenuOSX.java @@ -0,0 +1,185 @@ +package net.sf.openrocket.gui.util; + +import net.sf.openrocket.gui.dialogs.AboutDialog; +import net.sf.openrocket.gui.dialogs.LicenseDialog; +import net.sf.openrocket.gui.help.tours.GuidedTourSelectionDialog; +import net.sf.openrocket.gui.main.BasicFrame; +import net.sf.openrocket.gui.main.ExampleDesignFileAction; +import net.sf.openrocket.gui.main.MRUDesignFileAction; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.logging.Markers; +import net.sf.openrocket.startup.Application; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.swing.JFrame; +import javax.swing.JMenu; +import javax.swing.JMenuBar; +import javax.swing.JMenuItem; +import javax.swing.KeyStroke; +import java.awt.Color; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.KeyEvent; + +/** + * This dummy frame will be generated when all design windows are close on macOS. + * It serves to maintain and customize the application menu bar when all the BasicFrame windows are closed. + * + * @author Sibo Van Gool + */ +public class DummyFrameMenuOSX extends JFrame { + private static final Translator trans = Application.getTranslator(); + private static final Logger log = LoggerFactory.getLogger(BasicFrame.class); + + private static DummyFrameMenuOSX dialog; + + private DummyFrameMenuOSX() { + createMenu(); + setUndecorated(true); + setBackground(new Color(0, 0, 0, 0)); + setVisible(true); // this is needed to show the menu bar + } + + public static DummyFrameMenuOSX createDummyDialog() { + dialog = new DummyFrameMenuOSX(); + return dialog; + } + + public static DummyFrameMenuOSX getDummyDialog() { + return dialog; + } + + public static void removeDummyDialog() { + if (dialog != null) { + dialog.dispose(); + dialog = null; + } + } + + private void createMenu() { + JMenuBar menubar = new JMenuBar(); + JMenu menu; + JMenuItem item; + + //// File + menu = new JMenu(trans.get("main.menu.file")); + menu.setMnemonic(KeyEvent.VK_F); + //// File-handling related tasks + menu.getAccessibleContext().setAccessibleDescription(trans.get("main.menu.file.desc")); + menubar.add(menu); + + //// New + item = new JMenuItem(trans.get("main.menu.file.new"), KeyEvent.VK_N); + item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_N, BasicFrame.SHORTCUT_KEY)); + item.setMnemonic(KeyEvent.VK_N); + //// Create a new rocket design + item.getAccessibleContext().setAccessibleDescription(trans.get("main.menu.file.new.desc")); + item.setIcon(Icons.FILE_NEW); + item.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + DummyFrameMenuOSX.removeDummyDialog(); + log.info(Markers.USER_MARKER, "New... selected"); + BasicFrame.newAction(); + } + }); + menu.add(item); + + //// Open... + item = new JMenuItem(trans.get("main.menu.file.open"), KeyEvent.VK_O); + item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, BasicFrame.SHORTCUT_KEY)); + //// Open a rocket design + item.getAccessibleContext().setAccessibleDescription(trans.get("main.menu.file.open.desc")); + item.setIcon(Icons.FILE_OPEN); + item.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + DummyFrameMenuOSX.removeDummyDialog(); + log.info(Markers.USER_MARKER, "Open... selected"); + BasicFrame.openAction(DummyFrameMenuOSX.this); + } + }); + menu.add(item); + + //// Open Recent... + item = new MRUDesignFileAction(trans.get("main.menu.file.openRecent"), this); + item.getAccessibleContext().setAccessibleDescription(trans.get("main.menu.file.openRecent.desc")); + item.setIcon(Icons.FILE_OPEN); + menu.add(item); + + //// Open example... + item = new ExampleDesignFileAction(trans.get("main.menu.file.openExample"), null); + item.getAccessibleContext().setAccessibleDescription(trans.get("main.menu.file.openExample.desc")); + item.setIcon(Icons.FILE_OPEN_EXAMPLE); + menu.add(item); + + menu.addSeparator(); + + //// Quit + item = new JMenuItem(trans.get("main.menu.file.quit"), KeyEvent.VK_Q); + item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Q, BasicFrame.SHORTCUT_KEY)); + //// Quit the program + item.getAccessibleContext().setAccessibleDescription(trans.get("main.menu.file.quit.desc")); + item.setIcon(Icons.FILE_QUIT); + item.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + log.info(Markers.USER_MARKER, "Quit selected"); + BasicFrame.quitAction(); + } + }); + menu.add(item); + + + //// Help + menu = new JMenu(trans.get("main.menu.help")); + menu.setMnemonic(KeyEvent.VK_H); + menu.getAccessibleContext().setAccessibleDescription(trans.get("main.menu.help.desc")); + menubar.add(menu); + + //// Guided tours + item = new JMenuItem(trans.get("main.menu.help.tours"), KeyEvent.VK_L); + item.setIcon(Icons.HELP_TOURS); + item.getAccessibleContext().setAccessibleDescription(trans.get("main.menu.help.tours.desc")); + item.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + log.info(Markers.USER_MARKER, "Guided tours selected"); + GuidedTourSelectionDialog.showDialog(DummyFrameMenuOSX.this); + } + }); + menu.add(item); + + menu.addSeparator(); + + //// License + item = new JMenuItem(trans.get("main.menu.help.license"), KeyEvent.VK_L); + item.setIcon(Icons.HELP_LICENSE); + item.getAccessibleContext().setAccessibleDescription(trans.get("main.menu.help.license.desc")); + item.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + log.info(Markers.USER_MARKER, "License selected"); + new LicenseDialog(DummyFrameMenuOSX.this).setVisible(true); + } + }); + menu.add(item); + + //// About + item = new JMenuItem(trans.get("main.menu.help.about"), KeyEvent.VK_A); + item.setIcon(Icons.HELP_ABOUT); + item.getAccessibleContext().setAccessibleDescription(trans.get("main.menu.help.about.desc")); + item.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + log.info(Markers.USER_MARKER, "About selected"); + new AboutDialog(DummyFrameMenuOSX.this).setVisible(true); + } + }); + menu.add(item); + + + this.setJMenuBar(menubar); + } +} diff --git a/swing/src/net/sf/openrocket/startup/OSXSetup.java b/swing/src/net/sf/openrocket/startup/OSXSetup.java index fdf82f5a2..e16ac9716 100644 --- a/swing/src/net/sf/openrocket/startup/OSXSetup.java +++ b/swing/src/net/sf/openrocket/startup/OSXSetup.java @@ -5,7 +5,9 @@ import java.awt.desktop.AboutHandler; import java.awt.desktop.OpenFilesHandler; import java.awt.desktop.PreferencesHandler; import java.awt.desktop.QuitHandler; +import java.awt.desktop.AppReopenedListener; +import net.sf.openrocket.gui.util.DummyFrameMenuOSX; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -50,6 +52,13 @@ final class OSXSetup { r.cancelQuit(); }; + private static final AppReopenedListener APP_REOPENED_HANDLER = (e) -> { + if (BasicFrame.isFramesEmpty()) { + log.info("App re-opened"); + BasicFrame.reopen(); + } + }; + /** * The handler for the About item in the OSX app menu */ @@ -92,6 +101,7 @@ final class OSXSetup { osxDesktop.setAboutHandler(ABOUT_HANDLER); osxDesktop.setPreferencesHandler(PREFERENCES_HANDLER); osxDesktop.setQuitHandler(QUIT_HANDLER); + osxDesktop.addAppEventListener(APP_REOPENED_HANDLER); // Set the dock icon to the largest icon final Image dockIcon = Toolkit.getDefaultToolkit().getImage( diff --git a/swing/src/net/sf/openrocket/startup/SwingStartup.java b/swing/src/net/sf/openrocket/startup/SwingStartup.java index 167a1dfb9..5d6ff3386 100644 --- a/swing/src/net/sf/openrocket/startup/SwingStartup.java +++ b/swing/src/net/sf/openrocket/startup/SwingStartup.java @@ -216,28 +216,14 @@ public class SwingStartup { Databases.fakeMethod(); // Set up the OSX file open handler here so that it can handle files that are opened when OR is not yet running. - OSXSetup.setupOSXOpenFileHandler(); + if (SystemInfo.getPlatform() == Platform.MAC_OS) { + OSXSetup.setupOSXOpenFileHandler(); + } // Starting action (load files or open new document) log.info("Opening main application window"); if (!handleCommandLine(args)) { - if (!Application.getPreferences().isAutoOpenLastDesignOnStartupEnabled()) { - BasicFrame.newAction(); - } else { - String lastFile = MRUDesignFile.getInstance().getLastEditedDesignFile(); - if (lastFile != null) { - if (!BasicFrame.open(new File(lastFile), null)) { - MRUDesignFile.getInstance().removeFile(lastFile); - BasicFrame.newAction(); - } - else { - MRUDesignFile.getInstance().addFile(lastFile); - } - } - else { - BasicFrame.newAction(); - } - } + BasicFrame.reopen(); } // Check whether update info has been fetched or whether it needs more time