/* * MacOSXConfiguraton.java 6 sept. 2006 * * Sweet Home 3D, Copyright (c) 2006 Emmanuel PUYBARET / eTeks <info@eteks.com> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package com.eteks.sweethome3d; import java.awt.Color; import java.awt.Component; import java.awt.Container; import java.awt.EventQueue; import java.awt.Frame; import java.awt.Image; import java.awt.Window; import java.awt.event.ActionEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.util.List; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicBoolean; import javax.imageio.ImageIO; import javax.media.j3d.Canvas3D; import javax.swing.AbstractAction; import javax.swing.JCheckBoxMenuItem; import javax.swing.JFrame; import javax.swing.JMenu; import javax.swing.JMenuBar; import javax.swing.JMenuItem; import javax.swing.UIManager; import javax.swing.event.MenuEvent; import javax.swing.event.MenuListener; import com.apple.eawt.Application; import com.apple.eawt.ApplicationAdapter; import com.apple.eawt.ApplicationEvent; import com.eteks.sweethome3d.model.CollectionEvent; import com.eteks.sweethome3d.model.CollectionListener; import com.eteks.sweethome3d.model.Home; import com.eteks.sweethome3d.model.UserPreferences; import com.eteks.sweethome3d.swing.HomePane; import com.eteks.sweethome3d.swing.ResourceAction; import com.eteks.sweethome3d.tools.OperatingSystem; import com.eteks.sweethome3d.viewcontroller.HomeController; import com.sun.j3d.exp.swing.JCanvas3D; /** * Configuration class that accesses to Mac OS X specifics. * Do not invoke methods of this class without checking first if * <code>os.name</code> System property is <code>Mac OS X</code>. * This class requires some classes of <code>com.apple.eawt</code> package * to compile. * @author Emmanuel Puybaret */ class MacOSXConfiguration { /** * Binds <code>homeApplication</code> to Mac OS X application menu. */ public static void bindToApplicationMenu(final SweetHome3D homeApplication) { final Application macosxApplication = Application.getApplication(); // Create a default controller for an empty home and disable unrelated actions final HomeController defaultController = homeApplication.createHomeFrameController(homeApplication.createHome()).getHomeController(); final HomePane defaultHomeView = (HomePane)defaultController.getView(); for (HomePane.ActionType action : HomePane.ActionType.values()) { switch (action) { case ABOUT : case NEW_HOME : case OPEN : case DELETE_RECENT_HOMES : case HELP : break; default : defaultHomeView.setEnabled(action, false); } } final JMenuBar defaultMenuBar = defaultHomeView.getJMenuBar(); JFrame frame = null; try { if (OperatingSystem.isJavaVersionGreaterOrEqual("1.7")) { // Application#setDefaultMenuBar does nothing under Java 7 frame = createDummyFrameWithDefaultMenuBar(homeApplication, defaultHomeView, defaultMenuBar); } else if (UIManager.getLookAndFeel().getClass().getName().equals(UIManager.getSystemLookAndFeelClassName())) { macosxApplication.setDefaultMenuBar(defaultMenuBar); addWindowMenu(null, defaultMenuBar, homeApplication, true); } } catch (NoSuchMethodError ex) { // Create default frame if setDefaultMenuBar isn't available frame = createDummyFrameWithDefaultMenuBar(homeApplication, defaultHomeView, defaultMenuBar); } final JFrame defaultFrame = frame; // Add a listener to Mac OS X application that will call // controller methods of the active frame macosxApplication.addApplicationListener(new ApplicationAdapter() { @Override public void handleQuit(ApplicationEvent ev) { handleAction(new Runnable() { public void run() { defaultController.exit(); } }); if (homeApplication.getHomes().isEmpty()) { System.exit(0); } } @Override public void handleAbout(ApplicationEvent ev) { handleAction(new Runnable() { public void run() { defaultController.about(); } }); ev.setHandled(true); } @Override public void handlePreferences(ApplicationEvent ev) { handleAction(new Runnable() { public void run() { defaultController.editPreferences(); } }); } private void handleAction(Runnable runnable) { Frame activeFrame = null; for (Frame frame : Frame.getFrames()) { if (frame != defaultFrame && frame.isActive()) { activeFrame = frame; break; } } if (defaultFrame != null) { // Move default frame to center to display dialogs at center defaultFrame.setLocationRelativeTo(null); defaultFrame.toFront(); defaultFrame.setAlwaysOnTop(true); // Disable About and Preferences menu items macosxApplication.setEnabledAboutMenu(false); macosxApplication.setEnabledPreferencesMenu(false); } runnable.run(); // Activate previous frame again if (activeFrame != null) { activeFrame.toFront(); } if (defaultFrame != null) { defaultFrame.setAlwaysOnTop(false); // Move default frame out of user view defaultFrame.toBack(); defaultFrame.setLocation(-10, 0); // Enable About and Preferences menu items again macosxApplication.setEnabledAboutMenu(true); macosxApplication.setEnabledPreferencesMenu(true); } } @Override public void handleOpenFile(ApplicationEvent ev) { // handleOpenFile is called when user opens a document // associated with a Java Web Start application // Just call main with -open file arguments as JNLP specifies homeApplication.start(new String [] {"-open", ev.getFilename()}); } @Override public void handleReOpenApplication(ApplicationEvent ev) { // handleReOpenApplication is called when user launches // the application when it's already open homeApplication.start(new String [0]); } }); macosxApplication.setEnabledAboutMenu(true); macosxApplication.setEnabledPreferencesMenu(true); homeApplication.addHomesListener(new CollectionListener<Home>() { public void collectionChanged(CollectionEvent<Home> ev) { if (ev.getType() == CollectionEvent.Type.ADD) { final JFrame homeFrame = homeApplication.getHomeFrame(ev.getItem()); if (!Boolean.getBoolean("com.eteks.sweethome3d.no3D")) { // To avoid a possible freeze of the program when the user requests a window enlargement // while the frame canvas 3D is instantiated, forbid window to be resized homeFrame.setResizable(false); Executors.newSingleThreadExecutor().submit(new Runnable() { public void run() { try { final AtomicBoolean canvas3D = new AtomicBoolean(); do { Thread.sleep(50); EventQueue.invokeAndWait(new Runnable() { public void run() { canvas3D.set(homeFrame.isShowing() && isParentOfCanvas3D(homeFrame, Canvas3D.class, JCanvas3D.class)); } }); } while (!canvas3D.get()); } catch (InterruptedException ex) { } catch (InvocationTargetException ex) { ex.printStackTrace(); } finally { EventQueue.invokeLater(new Runnable() { public void run() { homeFrame.setResizable(true); } }); } } private boolean isParentOfCanvas3D(Container parent, Class<?> ... canvas3DClasses) { // Search 3D canvas among children and child windows in case the 3D view was detached for (int i = 0; i < parent.getComponentCount(); i++) { Component child = parent.getComponent(i); for (Class<?> canvas3DClass : canvas3DClasses) { if (canvas3DClass.isInstance(child) || child instanceof Container && isParentOfCanvas3D((Container)child, canvas3DClasses)) { return true; } } } if (parent instanceof Window) { for (Window window : ((Window)parent).getOwnedWindows()) { if (isParentOfCanvas3D(window, canvas3DClasses)) { return true; } } } return false; } }); } // Add Mac OS X Window menu on new homes MacOSXConfiguration.addWindowMenu( homeFrame, homeFrame.getJMenuBar(), homeApplication, false); if (OperatingSystem.isJavaVersionGreaterOrEqual("1.7")) { // Help system to understand it should display the main menu of one of the remaining windows when a window is closed homeFrame.addWindowListener(new WindowAdapter() { @Override public void windowClosed(WindowEvent ev) { List<Home> homes = homeApplication.getHomes(); defaultFrame.setVisible(false); defaultFrame.setVisible(true); if (homes.size() > 0) { homeApplication.getHomeFrame(homes.get(0)).toFront(); defaultFrame.setVisible(false); } } }); } } }; }); // Set application icon if program wasn't launch from bundle if (!Boolean.getBoolean("sweethome3d.bundle")) { try { String iconPath = homeApplication.getUserPreferences().getLocalizedString(HomePane.class, "about.icon"); Image icon = ImageIO.read(HomePane.class.getResource(iconPath)); macosxApplication.setDockIconImage(icon); } catch (NoSuchMethodError ex) { // Ignore icon change if setDockIconImage isn't available } catch (IOException ex) { } } } /** * Returns a dummy frame used to display the default menu bar. */ private static JFrame createDummyFrameWithDefaultMenuBar(final SweetHome3D homeApplication, final HomePane defaultHomeView, final JMenuBar defaultMenuBar) { final JFrame frame = new JFrame(); EventQueue.invokeLater(new Runnable() { public void run() { // Create a default undecorated frame out of sight // and attach the application menu bar of empty view to it frame.setLocation(-10, 0); frame.setUndecorated(true); frame.setBackground(new Color(0, 0, 0, 0)); frame.setVisible(true); frame.setJMenuBar(defaultMenuBar); frame.setContentPane(defaultHomeView); addWindowMenu(frame, defaultMenuBar, homeApplication, true); } }); return frame; } /** * Adds Mac OS X standard Window menu to frame. */ private static void addWindowMenu(final JFrame frame, final JMenuBar menuBar, final SweetHome3D homeApplication, boolean defaultFrame) { UserPreferences preferences = homeApplication.getUserPreferences(); final JMenu windowMenu = new JMenu( new ResourceAction(preferences, MacOSXConfiguration.class, "WINDOW_MENU", true)); // Add Window menu before Help menu menuBar.add(windowMenu, menuBar.getComponentCount() - 1); windowMenu.add(new JMenuItem( new ResourceAction(preferences, MacOSXConfiguration.class, "MINIMIZE", !defaultFrame) { @Override public void actionPerformed(ActionEvent ev) { frame.setState(JFrame.ICONIFIED); } })); windowMenu.add(new JMenuItem( new ResourceAction(preferences, MacOSXConfiguration.class, "ZOOM", !defaultFrame) { @Override public void actionPerformed(ActionEvent ev) { if ((frame.getExtendedState() & JFrame.MAXIMIZED_BOTH) != 0) { frame.setExtendedState(frame.getExtendedState() & ~JFrame.MAXIMIZED_BOTH); } else { frame.setExtendedState(frame.getExtendedState() | JFrame.MAXIMIZED_BOTH); } } })); windowMenu.addSeparator(); windowMenu.add(new JMenuItem( new ResourceAction(preferences, MacOSXConfiguration.class, "BRING_ALL_TO_FRONT", !defaultFrame) { @Override public void actionPerformed(ActionEvent ev) { // Avoid blinking while bringing other windows to front frame.setAlwaysOnTop(true); for (Home home : homeApplication.getHomes()) { JFrame applicationFrame = homeApplication.getHomeFrame(home); if (applicationFrame != frame && applicationFrame.getState() != JFrame.ICONIFIED) { applicationFrame.setFocusableWindowState(false); applicationFrame.toFront(); applicationFrame.setFocusableWindowState(true); } } frame.setAlwaysOnTop(false); } })); windowMenu.addMenuListener(new MenuListener() { public void menuSelected(MenuEvent ev) { boolean firstMenuItem = true; // Fill menu dynamically with a menu item for the frame of each application home for (Home home : homeApplication.getHomes()) { final JFrame applicationFrame = homeApplication.getHomeFrame(home); JCheckBoxMenuItem windowMenuItem = new JCheckBoxMenuItem( new AbstractAction(applicationFrame.getTitle()) { public void actionPerformed(ActionEvent ev) { applicationFrame.toFront(); } }); if (frame == applicationFrame) { windowMenuItem.setSelected(true); } if (firstMenuItem) { windowMenu.addSeparator(); firstMenuItem = false; } windowMenu.add(windowMenuItem); } } public void menuDeselected(MenuEvent ev) { // Remove dynamically filled part of menu for (int i = windowMenu.getMenuComponentCount() - 1; i >= 4; i--) { windowMenu.remove(i); } } public void menuCanceled(MenuEvent ev) { menuDeselected(ev); } }); } }