/* * Copyright 2010-2015 Institut Pasteur. * * This file is part of Icy. * * Icy 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 3 of the License, or * (at your option) any later version. * * Icy 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 Icy. If not, see <http://www.gnu.org/licenses/>. */ package icy.gui.main; import icy.action.FileActions; import icy.action.GeneralActions; import icy.action.SequenceOperationActions; import icy.file.FileUtil; import icy.file.Loader; import icy.gui.component.ExternalizablePanel; import icy.gui.component.ExternalizablePanel.StateListener; import icy.gui.frame.IcyExternalFrame; import icy.gui.inspector.ChatPanel; import icy.gui.inspector.InspectorPanel; import icy.gui.menu.ApplicationMenu; import icy.gui.menu.MainRibbon; import icy.gui.menu.search.SearchBar; import icy.gui.util.ComponentUtil; import icy.gui.util.WindowPositionSaver; import icy.gui.viewer.Viewer; import icy.imagej.ImageJWrapper; import icy.main.Icy; import icy.math.HungarianAlgorithm; import icy.preferences.GeneralPreferences; import icy.resource.ResourceUtil; import icy.resource.icon.IcyApplicationIcon; import icy.system.FileDrop; import icy.system.FileDrop.FileDropExtListener; import icy.system.FileDrop.FileDropListener; import icy.system.SystemUtil; import icy.system.thread.ThreadUtil; import icy.type.collection.CollectionUtil; import icy.util.StringUtil; import ij.IJ; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Dimension; import java.awt.GraphicsConfiguration; import java.awt.GraphicsDevice; import java.awt.HeadlessException; import java.awt.Insets; import java.awt.Rectangle; import java.awt.Toolkit; import java.awt.dnd.DropTargetDropEvent; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.geom.Point2D; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.File; import java.util.ArrayList; import java.util.List; import javax.swing.ActionMap; import javax.swing.BorderFactory; import javax.swing.InputMap; import javax.swing.JComponent; import javax.swing.JInternalFrame; import javax.swing.JPanel; import javax.swing.JSplitPane; import org.pushingpixels.flamingo.api.ribbon.JRibbon; import org.pushingpixels.flamingo.api.ribbon.JRibbonFrame; /** * @author fab & Stephane */ public class MainFrame extends JRibbonFrame { private static Rectangle getDefaultBounds() { Rectangle r = SystemUtil.getMaximumWindowBounds(); r.width -= 100; r.height -= 100; r.x += 50; r.y += 50; return r; } /** * Returns the list of internal viewers. * * @param bounds * If not null only viewers visible in the specified bounds are returned. * @param wantNotVisible * Also return not visible viewers * @param wantIconized * Also return iconized viewers */ public static Viewer[] getExternalViewers(Rectangle bounds, boolean wantNotVisible, boolean wantIconized) { final List<Viewer> result = new ArrayList<Viewer>(); for (Viewer viewer : Icy.getMainInterface().getViewers()) { if (viewer.isExternalized()) { final IcyExternalFrame externalFrame = viewer.getIcyExternalFrame(); if ((wantNotVisible || externalFrame.isVisible()) && (wantIconized || !ComponentUtil.isMinimized(externalFrame)) && ((bounds == null) || bounds.contains(ComponentUtil.getCenter(externalFrame)))) result.add(viewer); } } return result.toArray(new Viewer[result.size()]); } /** * Returns the list of internal viewers. * * @param wantNotVisible * Also return not visible viewers * @param wantIconized * Also return iconized viewers */ public static Viewer[] getExternalViewers(boolean wantNotVisible, boolean wantIconized) { return getExternalViewers(null, wantNotVisible, wantIconized); } /** * */ private static final long serialVersionUID = 1113003570969611614L; public static final String TITLE = "Icy"; public static final String PROPERTY_DETACHEDMODE = "detachedMode"; public static final int TILE_HORIZONTAL = 0; public static final int TILE_VERTICAL = 1; public static final int TILE_GRID = 2; public static final String ID_PREVIOUS_STATE = "previousState"; final MainRibbon mainRibbon; JSplitPane mainPane; private final JPanel centerPanel; private final IcyDesktopPane desktopPane; InspectorPanel inspector; boolean detachedMode; int lastInspectorWidth; boolean inspectorWidthSet; // state save for detached mode private int previousHeight; private boolean previousMaximized; private boolean previousInspectorInternalized; // we need to keep reference on it as the object only use weak reference final WindowPositionSaver positionSaver; /** * @throws HeadlessException */ public MainFrame() throws HeadlessException { super(TITLE); // RibbonFrame force these properties to false // but this might add problems with mac OSX // JPopupMenu.setDefaultLightWeightPopupEnabled(true); // ToolTipManager.sharedInstance().setLightWeightPopupEnabled(true); // FIXME : remove this when Ribbon with have fixed KeyTipLayer component getRootPane().getLayeredPane().getComponent(0).setVisible(false); // SubstanceRibbonFrameTitlePane titlePane = (SubstanceRibbonFrameTitlePane) // LookAndFeelUtil.getTitlePane(this); // JCheckBox comp = new JCheckBox("test") // comp.setP // titlePane.add(); // // "substancelaf.internal.titlePane.extraComponentKind" // titlePane.m final Rectangle defaultBounds = getDefaultBounds(); positionSaver = new WindowPositionSaver(this, "frame/main", defaultBounds.getLocation(), defaultBounds.getSize()); previousInspectorInternalized = positionSaver.getPreferences().getBoolean(ID_PREVIOUS_STATE, true); // set "always on top" state setAlwaysOnTop(GeneralPreferences.getAlwaysOnTop()); // default close operation setDefaultCloseOperation(DO_NOTHING_ON_CLOSE); // build ribbon mainRibbon = new MainRibbon(getRibbon()); // set application icons setIconImages(ResourceUtil.getIcyIconImages()); setApplicationIcon(new IcyApplicationIcon()); // set minimized state getRibbon().setMinimized(GeneralPreferences.getRibbonMinimized()); // main center pane (contains desktop pane) centerPanel = new JPanel(); centerPanel.setLayout(new BorderLayout()); // desktop pane desktopPane = new IcyDesktopPane(); desktopPane.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { if (e.getClickCount() == 2) { final Insets insets = mainPane.getInsets(); final int lastLoc = mainPane.getLastDividerLocation(); final int currentLoc = mainPane.getDividerLocation(); final int maxLoc = mainPane.getWidth() - (mainPane.getDividerSize() + insets.left); // just hide / unhide inspector if (currentLoc != maxLoc) mainPane.setDividerLocation(maxLoc); else mainPane.setDividerLocation(lastLoc); // if (isInpectorInternalized()) // externalizeInspector(); // else // internalizeInspector(); } } }); // set the desktop pane in center pane centerPanel.add(desktopPane, BorderLayout.CENTER); // action on file drop final FileDropListener desktopFileDropListener = new FileDropListener() { @Override public void filesDropped(File[] files) { Loader.load(CollectionUtil.asList(FileUtil.toPaths(files)), false, true, true); } }; final FileDropExtListener bandFileDropListener = new FileDropExtListener() { @Override public void filesDropped(DropTargetDropEvent evt, File[] files) { if (getRibbon().getSelectedTask() == mainRibbon.getImageJTask()) { final ImageJWrapper imageJ = mainRibbon.getImageJTask().getImageJ(); final JPanel imageJPanel = imageJ.getSwingPanel(); // drop point was inside ImageJ ? if (imageJPanel.contains(ComponentUtil.convertPoint(getRibbon(), evt.getLocation(), imageJPanel))) { if (files.length > 0) { final String file = files[0].getAbsolutePath(); ThreadUtil.bgRun(new Runnable() { @Override public void run() { IJ.open(file); } }); } return; } } // classic file loading Loader.load(CollectionUtil.asList(FileUtil.toPaths(files)), false, true, true); } }; // handle file drop in desktop pane and in ribbon pane new FileDrop(desktopPane, BorderFactory.createLineBorder(Color.blue.brighter(), 2), false, desktopFileDropListener); new FileDrop(getRibbon(), BorderFactory.createLineBorder(Color.blue.brighter(), 1), false, bandFileDropListener); // listen ribbon minimization event getRibbon().addPropertyChangeListener(JRibbon.PROPERTY_MINIMIZED, new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent evt) { final boolean value = ((Boolean) evt.getNewValue()).booleanValue(); // pack the frame in detached mode if (detachedMode) pack(); // save state in preferene GeneralPreferences.setRibbonMinimized(value); } }); } /** * Process init.<br> * Inspector is an ExternalizablePanel and requires MainFrame to be created. */ public void init() { // inspector inspector = new InspectorPanel(); inspectorWidthSet = false; addComponentListener(new ComponentAdapter() { @Override public void componentResized(ComponentEvent e) { // only need to do it at first display if (!inspectorWidthSet) { // main frame resized --> adjust divider location so inspector keep its size. // we need to use this method as getWidth() do not return immediate correct // value on OSX when initial state is maximized. if (inspector.isInternalized()) mainPane.setDividerLocation(getWidth() - lastInspectorWidth); inspectorWidthSet = true; } if (detachedMode) { // fix height final int prefH = getPreferredSize().height; if (getHeight() > prefH) setSize(getWidth(), prefH); } } }); // main pane mainPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, centerPanel, null); mainPane.setContinuousLayout(true); mainPane.setOneTouchExpandable(true); // get saved inspector width lastInspectorWidth = inspector.getPreferredSize().width; // add the divider and border size if inspector was visible if (lastInspectorWidth > 16) lastInspectorWidth += 6 + 8; // just force size for collapsed (divider + minimum border) else lastInspectorWidth = 6 + 4; if (inspector.isInternalized()) { mainPane.setRightComponent(inspector); mainPane.setDividerSize(6); } else { mainPane.setDividerSize(0); inspector.setParent(mainPane); } mainPane.setResizeWeight(1); inspector.addStateListener(new StateListener() { @Override public void stateChanged(ExternalizablePanel source, boolean externalized) { if (externalized) mainPane.setDividerSize(0); else { mainPane.setDividerSize(6); // restore previous location mainPane.setDividerLocation(getWidth() - lastInspectorWidth); } } }); previousHeight = getHeight(); previousMaximized = ComponentUtil.isMaximized(this); detachedMode = GeneralPreferences.getMultiWindowMode(); // detached mode if (detachedMode) { // resize window to ribbon dimension if (previousMaximized) ComponentUtil.setMaximized(this, false); setSize(getWidth(), getMinimumSize().height); } else add(mainPane, BorderLayout.CENTER); validate(); // initialize now some stuff that need main frame to be initialized mainRibbon.init(); setVisible(true); // can be done after setVisible buildActionMap(); } void buildActionMap() { // global input map buildActionMap(mainPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW), mainPane.getActionMap()); } private void buildActionMap(InputMap imap, ActionMap amap) { imap.put(GeneralActions.searchAction.getKeyStroke(), GeneralActions.searchAction.getName()); imap.put(FileActions.openSequenceAction.getKeyStroke(), FileActions.openSequenceAction.getName()); imap.put(FileActions.saveAsSequenceAction.getKeyStroke(), FileActions.saveAsSequenceAction.getName()); imap.put(GeneralActions.onlineHelpAction.getKeyStroke(), GeneralActions.onlineHelpAction.getName()); imap.put(SequenceOperationActions.undoAction.getKeyStroke(), SequenceOperationActions.undoAction.getName()); imap.put(SequenceOperationActions.redoAction.getKeyStroke(), SequenceOperationActions.redoAction.getName()); amap.put(GeneralActions.searchAction.getName(), GeneralActions.searchAction); amap.put(FileActions.openSequenceAction.getName(), FileActions.openSequenceAction); amap.put(FileActions.saveAsSequenceAction.getName(), FileActions.saveAsSequenceAction); amap.put(GeneralActions.onlineHelpAction.getName(), GeneralActions.onlineHelpAction); amap.put(SequenceOperationActions.undoAction.getName(), SequenceOperationActions.undoAction); amap.put(SequenceOperationActions.redoAction.getName(), SequenceOperationActions.redoAction); } public ApplicationMenu getApplicationMenu() { return (ApplicationMenu) getRibbon().getApplicationMenu(); } /** * Returns the center pane, this pane contains the desktop pane.<br> * Feel free to add temporary top/left/right or bottom pane to it. */ public JPanel getCenterPanel() { return centerPanel; } /** * Returns the {@link SearchBar} component. */ public SearchBar getSearchBar() { if (mainRibbon != null) return mainRibbon.getSearchBar(); return null; } /** * Returns the desktopPane which contains InternalFrame. */ public IcyDesktopPane getDesktopPane() { return desktopPane; } /** * Return all internal frames */ public ArrayList<JInternalFrame> getInternalFrames() { if (desktopPane != null) return CollectionUtil.asArrayList(desktopPane.getAllFrames()); return new ArrayList<JInternalFrame>(); } /** * @return the inspector */ public InspectorPanel getInspector() { return inspector; } /** * @return the mainRibbon */ public MainRibbon getMainRibbon() { return mainRibbon; } /** * @return the chat component */ public ChatPanel getChat() { return inspector.getChatPanel(); } /** * Return true if the main frame is in "detached" mode */ public boolean isDetachedMode() { return detachedMode; } /** * Return content pane dimension (available area in main frame).<br> * If the main frame is in "detached" mode this actually return the system desktop dimension. */ public Dimension getDesktopSize() { if (detachedMode) return SystemUtil.getMaximumWindowBounds().getSize(); return desktopPane.getSize(); } /** * Return content pane width */ public int getDesktopWidth() { return getDesktopSize().width; } /** * Return content pane height */ public int getDesktopHeight() { return getDesktopSize().height; } public int getPreviousHeight() { return previousHeight; } public boolean getPreviousMaximized() { return previousMaximized; } /** * Returns true if the inspector is internalized in main container.<br> * Always returns false in detached mode. */ public boolean isInpectorInternalized() { return inspector.isInternalized(); } /** * Internalize the inspector in main container.<br> * The method fails and returns false in detached mode. */ public boolean internalizeInspector() { if (inspector.isExternalized() && inspector.isInternalizationAutorized()) { inspector.internalize(); return true; } return false; } /** * Externalize the inspector in main container.<br> * Returns false if the methods failed. */ public boolean externalizeInspector() { if (inspector.isInternalized() && inspector.isExternalizationAutorized()) { // save diviser location lastInspectorWidth = getWidth() - mainPane.getDividerLocation(); inspector.externalize(); return true; } return false; } /** * Organize all frames in cascade */ public void organizeCascade() { // all screen devices final GraphicsDevice screenDevices[] = SystemUtil.getLocalGraphicsEnvironment().getScreenDevices(); // screen devices to process final ArrayList<GraphicsDevice> devices = new ArrayList<GraphicsDevice>(); // detached mode ? if (isDetachedMode()) { // process all available screen for cascade organization for (GraphicsDevice dev : screenDevices) if (dev.getType() == GraphicsDevice.TYPE_RASTER_SCREEN) devices.add(dev); } else { // process desktop pane cascade organization desktopPane.organizeCascade(); // we process screen where the mainFrame is not visible for (GraphicsDevice dev : screenDevices) if (dev.getType() == GraphicsDevice.TYPE_RASTER_SCREEN) if (!dev.getDefaultConfiguration().getBounds().contains(getLocation())) devices.add(dev); } // organize frames on different screen for (GraphicsDevice dev : devices) organizeCascade(dev); } /** * Organize frames in cascade on the specified graphics device. */ protected void organizeCascade(GraphicsDevice graphicsDevice) { final GraphicsConfiguration graphicsConfiguration = graphicsDevice.getDefaultConfiguration(); final Rectangle bounds = graphicsConfiguration.getBounds(); final Insets inset = getToolkit().getScreenInsets(graphicsConfiguration); // adjust bounds of current screen bounds.x += inset.left; bounds.y += inset.top; bounds.width -= inset.left + inset.right; bounds.height -= inset.top + inset.bottom; // prepare viewers to process final Viewer[] viewers = getExternalViewers(bounds, false, false); // this screen contains the main frame ? if (bounds.contains(getLocation())) { // move main frame at top setLocation(bounds.x, bounds.y); final int mainFrameW = getWidth(); final int mainFrameH = getHeight(); // adjust available bounds of current screen if (mainFrameW > mainFrameH) { bounds.y += mainFrameH; bounds.height -= mainFrameH; } else { bounds.x += mainFrameW; bounds.width -= mainFrameW; } } // available space final int w = bounds.width; final int h = bounds.height; final int xMax = bounds.x + w; final int yMax = bounds.y + h; final int fw = (int) (w * 0.6f); final int fh = (int) (h * 0.6f); int x = bounds.x + 32; int y = bounds.y + 32; for (Viewer v : viewers) { final IcyExternalFrame externalFrame = v.getIcyExternalFrame(); if (externalFrame.isMaximized()) externalFrame.setMaximized(false); externalFrame.setBounds(x, y, fw, fh); externalFrame.toFront(); x += 30; y += 20; if ((x + fw) > xMax) x = bounds.x + 32; if ((y + fh) > yMax) y = bounds.y + 32; } } /** * Organize all frames in tile.<br> * * @param type * tile type.<br> * TILE_HORIZONTAL, TILE_VERTICAL or TILE_GRID. */ public void organizeTile(int type) { // all screen devices final GraphicsDevice screenDevices[] = SystemUtil.getLocalGraphicsEnvironment().getScreenDevices(); // screen devices to process final ArrayList<GraphicsDevice> devices = new ArrayList<GraphicsDevice>(); // detached mode ? if (isDetachedMode()) { // process all available screen for cascade organization for (GraphicsDevice dev : screenDevices) if (dev.getType() == GraphicsDevice.TYPE_RASTER_SCREEN) devices.add(dev); } else { // process desktop pane tile organization desktopPane.organizeTile(type); // we process screen where the mainFrame is not visible for (GraphicsDevice dev : screenDevices) if (dev.getType() == GraphicsDevice.TYPE_RASTER_SCREEN) if (!dev.getDefaultConfiguration().getBounds().contains(getLocation())) devices.add(dev); } // organize frames on different screen for (GraphicsDevice dev : devices) organizeTile(dev, type); } /** * Organize frames in tile on the specified graphics device. */ protected void organizeTile(GraphicsDevice graphicsDevice, int type) { final GraphicsConfiguration graphicsConfiguration = graphicsDevice.getDefaultConfiguration(); final Rectangle bounds = graphicsConfiguration.getBounds(); final Insets inset = Toolkit.getDefaultToolkit().getScreenInsets(graphicsConfiguration); // adjust bounds of current screen bounds.x += inset.left; bounds.y += inset.top; bounds.width -= inset.left + inset.right; bounds.height -= inset.top + inset.bottom; // prepare viewers to process final Viewer[] viewers = getExternalViewers(bounds, false, false); // this screen contains the main frame ? if (bounds.contains(getLocation())) { // move main frame at top setLocation(bounds.x, bounds.y); final int mainFrameW = getWidth(); final int mainFrameH = getHeight(); // adjust available bounds of current screen if (mainFrameW > mainFrameH) { bounds.y += mainFrameH; bounds.height -= mainFrameH; } else { bounds.x += mainFrameW; bounds.width -= mainFrameW; } } final int numFrames = viewers.length; // nothing to do if (numFrames == 0) return; // available space final int w = bounds.width; final int h = bounds.height; final int x = bounds.x; final int y = bounds.y; int numCol; int numLine; switch (type) { case MainFrame.TILE_HORIZONTAL: numCol = 1; numLine = numFrames; break; case MainFrame.TILE_VERTICAL: numCol = numFrames; numLine = 1; break; default: numCol = (int) Math.sqrt(numFrames); if (numFrames != (numCol * numCol)) numCol++; numLine = numFrames / numCol; if (numFrames > (numCol * numLine)) numLine++; break; } final double[][] framesDistances = new double[numCol * numLine][numFrames]; final int dx = w / numCol; final int dy = h / numLine; int k = 0; for (int i = 0; i < numLine; i++) { for (int j = 0; j < numCol; j++, k++) { final double[] distances = framesDistances[k]; final double fx = x + (j * dx) + (dx / 2d); final double fy = y + (i * dy) + (dy / 2d); for (int f = 0; f < numFrames; f++) { final Point2D.Double center = ComponentUtil.getCenter(viewers[f].getExternalFrame()); distances[f] = Point2D.distanceSq(center.x, center.y, fx, fy); } } } final int[] framePos = new HungarianAlgorithm(framesDistances).resolve(); k = 0; for (int i = 0; i < numLine; i++) { for (int j = 0; j < numCol; j++, k++) { final int f = framePos[k]; if (f < numFrames) { final IcyExternalFrame externalFrame = viewers[f].getIcyExternalFrame(); if (externalFrame.isMaximized()) externalFrame.setMaximized(false); externalFrame.setBounds(x + (j * dx), y + (i * dy), dx, dy); externalFrame.toFront(); } } } } /** * Set detached window mode. */ public void setDetachedMode(boolean value) { if (detachedMode != value) { // detached mode if (value) { // save inspector state previousInspectorInternalized = inspector.isInternalized(); // save it in preferences... positionSaver.getPreferences().putBoolean(ID_PREVIOUS_STATE, previousInspectorInternalized); // externalize inspector externalizeInspector(); // no more internalization possible inspector.setInternalizationAutorized(false); // save the current height & state previousHeight = getHeight(); previousMaximized = ComponentUtil.isMaximized(this); // hide main pane and remove maximized state remove(mainPane); ComponentUtil.setMaximized(this, false); // and pack the frame pack(); } // single window mode else { // show main pane & resize window back to original dimension add(mainPane, BorderLayout.CENTER); setSize(getWidth(), previousHeight); if (previousMaximized) ComponentUtil.setMaximized(this, true); // recompute layout validate(); // internalization possible inspector.setInternalizationAutorized(true); // restore inspector internalization if (previousInspectorInternalized) internalizeInspector(); } detachedMode = value; // notify mode change firePropertyChange(PROPERTY_DETACHEDMODE, !value, value); } } /** * Refresh connected username informations */ public void refreshUserInfos() { final String login = GeneralPreferences.getUserLogin(); final String userName = GeneralPreferences.getUserName(); if (!StringUtil.isEmpty(userName)) setTitle(TITLE + " - " + userName); else if (!StringUtil.isEmpty(login)) setTitle(TITLE + " - " + login); else setTitle(TITLE); } @Override public void reshape(int x, int y, int width, int height) { final Rectangle r = new Rectangle(x, y, width, height); final boolean detached; // test detached mode by using mainPane parent as resize is called inside setDetachedMode(..) and // detachedMode variable is not yet updated if (mainPane == null) detached = detachedMode; else detached = mainPane.getParent() == null; if (detached) { // fix height final int prefH = getPreferredSize().height; if (r.height > prefH) r.height = prefH; } ComponentUtil.fixPosition(this, r); super.reshape(r.x, r.y, r.width, r.height); } // @Override // public synchronized void setMaximizedBounds(Rectangle bounds) // { // Rectangle bnds = SystemUtil.getScreenBounds(ComponentUtil.getScreen(this), true); // // if (bnds.isEmpty()) // bnds = bounds; // // at least use the location from original bounds // else if (bounds != null) // bnds.setLocation(bounds.getLocation()); // else bnds.setLocation(0, 0); // // super.setMaximizedBounds(bnds); // } }