001/*
002 * Copyright 2010-2015 Institut Pasteur.
003 * 
004 * This file is part of Icy.
005 * 
006 * Icy is free software: you can redistribute it and/or modify
007 * it under the terms of the GNU General Public License as published by
008 * the Free Software Foundation, either version 3 of the License, or
009 * (at your option) any later version.
010 * 
011 * Icy is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
014 * GNU General Public License for more details.
015 * 
016 * You should have received a copy of the GNU General Public License
017 * along with Icy. If not, see <http://www.gnu.org/licenses/>.
018 */
019package icy.gui.main;
020
021import java.awt.BorderLayout;
022import java.awt.Color;
023import java.awt.Dimension;
024import java.awt.GraphicsConfiguration;
025import java.awt.GraphicsDevice;
026import java.awt.HeadlessException;
027import java.awt.Insets;
028import java.awt.Rectangle;
029import java.awt.Toolkit;
030import java.awt.dnd.DropTargetDropEvent;
031import java.awt.event.ComponentAdapter;
032import java.awt.event.ComponentEvent;
033import java.awt.event.MouseAdapter;
034import java.awt.event.MouseEvent;
035import java.awt.geom.Point2D;
036import java.beans.PropertyChangeEvent;
037import java.beans.PropertyChangeListener;
038import java.io.File;
039import java.util.ArrayList;
040import java.util.List;
041
042import javax.swing.ActionMap;
043import javax.swing.BorderFactory;
044import javax.swing.InputMap;
045import javax.swing.JComponent;
046import javax.swing.JInternalFrame;
047import javax.swing.JPanel;
048import javax.swing.JSplitPane;
049
050import org.pushingpixels.flamingo.api.ribbon.JRibbon;
051import org.pushingpixels.flamingo.api.ribbon.JRibbonFrame;
052
053import icy.action.FileActions;
054import icy.action.GeneralActions;
055import icy.action.SequenceOperationActions;
056import icy.file.FileUtil;
057import icy.file.Loader;
058import icy.gui.component.ExternalizablePanel;
059import icy.gui.component.ExternalizablePanel.StateListener;
060import icy.gui.frame.IcyExternalFrame;
061import icy.gui.inspector.ChatPanel;
062import icy.gui.inspector.InspectorPanel;
063import icy.gui.menu.ApplicationMenu;
064import icy.gui.menu.MainRibbon;
065import icy.gui.menu.search.SearchBar;
066import icy.gui.util.ComponentUtil;
067import icy.gui.util.WindowPositionSaver;
068import icy.gui.viewer.Viewer;
069import icy.image.cache.ImageCache;
070import icy.imagej.ImageJWrapper;
071import icy.main.Icy;
072import icy.math.HungarianAlgorithm;
073import icy.preferences.GeneralPreferences;
074import icy.resource.ResourceUtil;
075import icy.resource.icon.IcyApplicationIcon;
076import icy.system.FileDrop;
077import icy.system.FileDrop.FileDropExtListener;
078import icy.system.FileDrop.FileDropListener;
079import icy.system.SystemUtil;
080import icy.system.thread.ThreadUtil;
081import icy.type.collection.CollectionUtil;
082import icy.util.StringUtil;
083import ij.IJ;
084
085/**
086 * @author fab & Stephane
087 */
088public class MainFrame extends JRibbonFrame
089{
090    private static Rectangle getDefaultBounds()
091    {
092        Rectangle r = SystemUtil.getMaximumWindowBounds();
093
094        r.width -= 100;
095        r.height -= 100;
096        r.x += 50;
097        r.y += 50;
098
099        return r;
100    }
101
102    /**
103     * Returns the list of internal viewers.
104     * 
105     * @param bounds
106     *        If not null only viewers visible in the specified bounds are returned.
107     * @param wantNotVisible
108     *        Also return not visible viewers
109     * @param wantIconized
110     *        Also return iconized viewers
111     */
112    public static Viewer[] getExternalViewers(Rectangle bounds, boolean wantNotVisible, boolean wantIconized)
113    {
114        final List<Viewer> result = new ArrayList<Viewer>();
115
116        for (Viewer viewer : Icy.getMainInterface().getViewers())
117        {
118            if (viewer.isExternalized())
119            {
120                final IcyExternalFrame externalFrame = viewer.getIcyExternalFrame();
121
122                if ((wantNotVisible || externalFrame.isVisible())
123                        && (wantIconized || !ComponentUtil.isMinimized(externalFrame))
124                        && ((bounds == null) || bounds.contains(ComponentUtil.getCenter(externalFrame))))
125                    result.add(viewer);
126            }
127        }
128
129        return result.toArray(new Viewer[result.size()]);
130    }
131
132    /**
133     * Returns the list of internal viewers.
134     * 
135     * @param wantNotVisible
136     *        Also return not visible viewers
137     * @param wantIconized
138     *        Also return iconized viewers
139     */
140    public static Viewer[] getExternalViewers(boolean wantNotVisible, boolean wantIconized)
141    {
142        return getExternalViewers(null, wantNotVisible, wantIconized);
143    }
144
145    /**
146     * 
147     */
148    private static final long serialVersionUID = 1113003570969611614L;
149
150    public static final String TITLE = "Icy";
151
152    public static final String PROPERTY_DETACHEDMODE = "detachedMode";
153
154    public static final int TILE_HORIZONTAL = 0;
155    public static final int TILE_VERTICAL = 1;
156    public static final int TILE_GRID = 2;
157
158    public static final String ID_PREVIOUS_STATE = "previousState";
159
160    final MainRibbon mainRibbon;
161    JSplitPane mainPane;
162    private final JPanel centerPanel;
163    private final IcyDesktopPane desktopPane;
164    InspectorPanel inspector;
165    boolean detachedMode;
166    int lastInspectorWidth;
167    boolean inspectorWidthSet;
168
169    // state save for detached mode
170    private int previousHeight;
171    private boolean previousMaximized;
172    private boolean previousInspectorInternalized;
173
174    // we need to keep reference on it as the object only use weak reference
175    final WindowPositionSaver positionSaver;
176
177    /**
178     * @throws HeadlessException
179     */
180    public MainFrame() throws HeadlessException
181    {
182        super(TITLE);
183
184        // RibbonFrame force these properties to false
185        // but this might add problems with mac OSX
186        // JPopupMenu.setDefaultLightWeightPopupEnabled(true);
187        // ToolTipManager.sharedInstance().setLightWeightPopupEnabled(true);
188
189        // FIXME : remove this when Ribbon with have fixed KeyTipLayer component
190        getRootPane().getLayeredPane().getComponent(0).setVisible(false);
191
192        // SubstanceRibbonFrameTitlePane titlePane = (SubstanceRibbonFrameTitlePane)
193        // LookAndFeelUtil.getTitlePane(this);
194        // JCheckBox comp = new JCheckBox("test")
195        // comp.setP
196        // titlePane.add();
197        //
198        // "substancelaf.internal.titlePane.extraComponentKind"
199        // titlePane.m
200
201        final Rectangle defaultBounds = getDefaultBounds();
202
203        positionSaver = new WindowPositionSaver(this, "frame/main", defaultBounds.getLocation(),
204                defaultBounds.getSize());
205        previousInspectorInternalized = positionSaver.getPreferences().getBoolean(ID_PREVIOUS_STATE, true);
206
207        // set "always on top" state
208        setAlwaysOnTop(GeneralPreferences.getAlwaysOnTop());
209        // default close operation
210        setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
211
212        // build ribbon
213        mainRibbon = new MainRibbon(getRibbon());
214
215        // set application icons
216        setIconImages(ResourceUtil.getIcyIconImages());
217        setApplicationIcon(new IcyApplicationIcon());
218
219        // set minimized state
220        getRibbon().setMinimized(GeneralPreferences.getRibbonMinimized());
221
222        // main center pane (contains desktop pane)
223        centerPanel = new JPanel();
224        centerPanel.setLayout(new BorderLayout());
225
226        // desktop pane
227        desktopPane = new IcyDesktopPane();
228        desktopPane.addMouseListener(new MouseAdapter()
229        {
230            @Override
231            public void mouseClicked(MouseEvent e)
232            {
233                if (e.getClickCount() == 2)
234                {
235                    final Insets insets = mainPane.getInsets();
236                    final int lastLoc = mainPane.getLastDividerLocation();
237                    final int currentLoc = mainPane.getDividerLocation();
238                    final int maxLoc = mainPane.getWidth() - (mainPane.getDividerSize() + insets.left);
239
240                    // just hide / unhide inspector
241                    if (currentLoc != maxLoc)
242                        mainPane.setDividerLocation(maxLoc);
243                    else
244                        mainPane.setDividerLocation(lastLoc);
245
246                    // if (isInpectorInternalized())
247                    // externalizeInspector();
248                    // else
249                    // internalizeInspector();
250                }
251            }
252        });
253
254        // set the desktop pane in center pane
255        centerPanel.add(desktopPane, BorderLayout.CENTER);
256
257        // action on file drop
258        final FileDropListener desktopFileDropListener = new FileDropListener()
259        {
260            @Override
261            public void filesDropped(File[] files)
262            {
263                Loader.load(CollectionUtil.asList(FileUtil.toPaths(files)), false, true, true);
264            }
265        };
266        final FileDropExtListener bandFileDropListener = new FileDropExtListener()
267        {
268            @Override
269            public void filesDropped(DropTargetDropEvent evt, File[] files)
270            {
271                if (getRibbon().getSelectedTask() == mainRibbon.getImageJTask())
272                {
273                    final ImageJWrapper imageJ = mainRibbon.getImageJTask().getImageJ();
274                    final JPanel imageJPanel = imageJ.getSwingPanel();
275
276                    // drop point was inside ImageJ ?
277                    if (imageJPanel.contains(ComponentUtil.convertPoint(getRibbon(), evt.getLocation(), imageJPanel)))
278                    {
279                        if (files.length > 0)
280                        {
281                            final String file = files[0].getAbsolutePath();
282
283                            ThreadUtil.bgRun(new Runnable()
284                            {
285                                @Override
286                                public void run()
287                                {
288                                    IJ.open(file);
289                                }
290                            });
291                        }
292
293                        return;
294                    }
295                }
296
297                // classic file loading
298                Loader.load(CollectionUtil.asList(FileUtil.toPaths(files)), false, true, true);
299            }
300        };
301
302        // handle file drop in desktop pane and in ribbon pane
303        new FileDrop(desktopPane, BorderFactory.createLineBorder(Color.blue.brighter(), 2), false,
304                desktopFileDropListener);
305        new FileDrop(getRibbon(), BorderFactory.createLineBorder(Color.blue.brighter(), 1), false,
306                bandFileDropListener);
307
308        // listen ribbon minimization event
309        getRibbon().addPropertyChangeListener(JRibbon.PROPERTY_MINIMIZED, new PropertyChangeListener()
310        {
311            @Override
312            public void propertyChange(PropertyChangeEvent evt)
313            {
314                final boolean value = ((Boolean) evt.getNewValue()).booleanValue();
315
316                // pack the frame in detached mode
317                if (detachedMode)
318                    pack();
319
320                // save state in preference
321                GeneralPreferences.setRibbonMinimized(value);
322            }
323        });
324    }
325
326    /**
327     * Process init.<br>
328     * Inspector is an ExternalizablePanel and requires MainFrame to be created.
329     */
330    public void init()
331    {
332        // inspector
333        inspector = new InspectorPanel();
334        inspectorWidthSet = false;
335
336        addComponentListener(new ComponentAdapter()
337        {
338            @Override
339            public void componentResized(ComponentEvent e)
340            {
341                // only need to do it at first display
342                if (!inspectorWidthSet)
343                {
344                    // main frame resized --> adjust divider location so inspector keep its size.
345                    // we need to use this method as getWidth() do not return immediate correct
346                    // value on OSX when initial state is maximized.
347                    if (inspector.isInternalized())
348                        mainPane.setDividerLocation(getWidth() - lastInspectorWidth);
349
350                    inspectorWidthSet = true;
351                }
352
353                if (detachedMode)
354                {
355                    // fix height
356                    final int prefH = getPreferredSize().height;
357
358                    if (getHeight() > prefH)
359                        setSize(getWidth(), prefH);
360                }
361            }
362        });
363
364        // main pane
365        mainPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, centerPanel, null);
366        mainPane.setContinuousLayout(true);
367        mainPane.setOneTouchExpandable(true);
368
369        // get saved inspector width
370        lastInspectorWidth = inspector.getPreferredSize().width;
371        // add the divider and border size if inspector was visible
372        if (lastInspectorWidth > 16)
373            lastInspectorWidth += 6 + 8;
374        // just force size for collapsed (divider + minimum border)
375        else
376            lastInspectorWidth = 6 + 4;
377
378        if (inspector.isInternalized())
379        {
380            mainPane.setRightComponent(inspector);
381            mainPane.setDividerSize(6);
382        }
383        else
384        {
385            mainPane.setDividerSize(0);
386            inspector.setParent(mainPane);
387        }
388        mainPane.setResizeWeight(1);
389
390        inspector.addStateListener(new StateListener()
391        {
392            @Override
393            public void stateChanged(ExternalizablePanel source, boolean externalized)
394            {
395                if (externalized)
396                    mainPane.setDividerSize(0);
397                else
398                {
399                    mainPane.setDividerSize(6);
400                    // restore previous location
401                    mainPane.setDividerLocation(getWidth() - lastInspectorWidth);
402                }
403            }
404        });
405
406        previousHeight = getHeight();
407        previousMaximized = ComponentUtil.isMaximized(this);
408        detachedMode = GeneralPreferences.getMultiWindowMode();
409
410        // detached mode
411        if (detachedMode)
412        {
413            // resize window to ribbon dimension
414            if (previousMaximized)
415                ComponentUtil.setMaximized(this, false);
416            setSize(getWidth(), getMinimumSize().height);
417        }
418        else
419            add(mainPane, BorderLayout.CENTER);
420
421        validate();
422
423        // initialize now some stuff that need main frame to be initialized
424        mainRibbon.init();
425        // refresh title
426        refreshTitle();
427
428        setVisible(true);
429
430        // can be done after setVisible
431        buildActionMap();
432    }
433
434    void buildActionMap()
435    {
436        // global input map
437        buildActionMap(mainPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW), mainPane.getActionMap());
438    }
439
440    private void buildActionMap(InputMap imap, ActionMap amap)
441    {
442        imap.put(GeneralActions.searchAction.getKeyStroke(), GeneralActions.searchAction.getName());
443        imap.put(FileActions.openSequenceAction.getKeyStroke(), FileActions.openSequenceAction.getName());
444        imap.put(FileActions.saveAsSequenceAction.getKeyStroke(), FileActions.saveAsSequenceAction.getName());
445        imap.put(GeneralActions.onlineHelpAction.getKeyStroke(), GeneralActions.onlineHelpAction.getName());
446        imap.put(SequenceOperationActions.undoAction.getKeyStroke(), SequenceOperationActions.undoAction.getName());
447        imap.put(SequenceOperationActions.redoAction.getKeyStroke(), SequenceOperationActions.redoAction.getName());
448
449        amap.put(GeneralActions.searchAction.getName(), GeneralActions.searchAction);
450        amap.put(FileActions.openSequenceAction.getName(), FileActions.openSequenceAction);
451        amap.put(FileActions.saveAsSequenceAction.getName(), FileActions.saveAsSequenceAction);
452        amap.put(GeneralActions.onlineHelpAction.getName(), GeneralActions.onlineHelpAction);
453        amap.put(SequenceOperationActions.undoAction.getName(), SequenceOperationActions.undoAction);
454        amap.put(SequenceOperationActions.redoAction.getName(), SequenceOperationActions.redoAction);
455    }
456
457    public ApplicationMenu getApplicationMenu()
458    {
459        return (ApplicationMenu) getRibbon().getApplicationMenu();
460    }
461
462    /**
463     * Returns the center pane, this pane contains the desktop pane.<br>
464     * Feel free to add temporary top/left/right or bottom pane to it.
465     */
466    public JPanel getCenterPanel()
467    {
468        return centerPanel;
469    }
470
471    /**
472     * Returns the {@link SearchBar} component.
473     */
474    public SearchBar getSearchBar()
475    {
476        if (mainRibbon != null)
477            return mainRibbon.getSearchBar();
478
479        return null;
480    }
481
482    /**
483     * Returns the desktopPane which contains InternalFrame.
484     */
485    public IcyDesktopPane getDesktopPane()
486    {
487        return desktopPane;
488    }
489
490    /**
491     * Return all internal frames
492     */
493    public ArrayList<JInternalFrame> getInternalFrames()
494    {
495        if (desktopPane != null)
496            return CollectionUtil.asArrayList(desktopPane.getAllFrames());
497
498        return new ArrayList<JInternalFrame>();
499    }
500
501    /**
502     * @return the inspector
503     */
504    public InspectorPanel getInspector()
505    {
506        return inspector;
507    }
508
509    /**
510     * @return the mainRibbon
511     */
512    public MainRibbon getMainRibbon()
513    {
514        return mainRibbon;
515    }
516
517    /**
518     * @deprecated IRC has been removed since Icy 1.9.8.0
519     */
520    public ChatPanel getChat()
521    {
522        return inspector.getChatPanel();
523    }
524
525    /**
526     * Return true if the main frame is in "detached" mode
527     */
528    public boolean isDetachedMode()
529    {
530        return detachedMode;
531    }
532
533    /**
534     * Return content pane dimension (available area in main frame).<br>
535     * If the main frame is in "detached" mode this actually return the system desktop dimension.
536     */
537    public Dimension getDesktopSize()
538    {
539        if (detachedMode)
540            return SystemUtil.getMaximumWindowBounds().getSize();
541
542        return desktopPane.getSize();
543    }
544
545    /**
546     * Return content pane width
547     */
548    public int getDesktopWidth()
549    {
550        return getDesktopSize().width;
551    }
552
553    /**
554     * Return content pane height
555     */
556    public int getDesktopHeight()
557    {
558        return getDesktopSize().height;
559    }
560
561    public int getPreviousHeight()
562    {
563        return previousHeight;
564    }
565
566    public boolean getPreviousMaximized()
567    {
568        return previousMaximized;
569    }
570
571    /**
572     * Returns true if the inspector is internalized in main container.<br>
573     * Always returns false in detached mode.
574     */
575    public boolean isInpectorInternalized()
576    {
577        return inspector.isInternalized();
578    }
579
580    /**
581     * Internalize the inspector in main container.<br>
582     * The method fails and returns false in detached mode.
583     */
584    public boolean internalizeInspector()
585    {
586        if (inspector.isExternalized() && inspector.isInternalizationAutorized())
587        {
588            inspector.internalize();
589            return true;
590        }
591
592        return false;
593    }
594
595    /**
596     * Externalize the inspector in main container.<br>
597     * Returns false if the methods failed.
598     */
599    public boolean externalizeInspector()
600    {
601        if (inspector.isInternalized() && inspector.isExternalizationAutorized())
602        {
603            // save diviser location
604            lastInspectorWidth = getWidth() - mainPane.getDividerLocation();
605            inspector.externalize();
606            return true;
607        }
608
609        return false;
610    }
611
612    /**
613     * Organize all frames in cascade
614     */
615    public void organizeCascade()
616    {
617        // all screen devices
618        final GraphicsDevice screenDevices[] = SystemUtil.getLocalGraphicsEnvironment().getScreenDevices();
619        // screen devices to process
620        final ArrayList<GraphicsDevice> devices = new ArrayList<GraphicsDevice>();
621
622        // detached mode ?
623        if (isDetachedMode())
624        {
625            // process all available screen for cascade organization
626            for (GraphicsDevice dev : screenDevices)
627                if (dev.getType() == GraphicsDevice.TYPE_RASTER_SCREEN)
628                    devices.add(dev);
629        }
630        else
631        {
632            // process desktop pane cascade organization
633            desktopPane.organizeCascade();
634
635            // we process screen where the mainFrame is not visible
636            for (GraphicsDevice dev : screenDevices)
637                if (dev.getType() == GraphicsDevice.TYPE_RASTER_SCREEN)
638                    if (!dev.getDefaultConfiguration().getBounds().contains(getLocation()))
639                        devices.add(dev);
640        }
641
642        // organize frames on different screen
643        for (GraphicsDevice dev : devices)
644            organizeCascade(dev);
645    }
646
647    /**
648     * Organize frames in cascade on the specified graphics device.
649     */
650    protected void organizeCascade(GraphicsDevice graphicsDevice)
651    {
652        final GraphicsConfiguration graphicsConfiguration = graphicsDevice.getDefaultConfiguration();
653        final Rectangle bounds = graphicsConfiguration.getBounds();
654        final Insets inset = getToolkit().getScreenInsets(graphicsConfiguration);
655
656        // adjust bounds of current screen
657        bounds.x += inset.left;
658        bounds.y += inset.top;
659        bounds.width -= inset.left + inset.right;
660        bounds.height -= inset.top + inset.bottom;
661
662        // prepare viewers to process
663        final Viewer[] viewers = getExternalViewers(bounds, false, false);
664
665        // this screen contains the main frame ?
666        if (bounds.contains(getLocation()))
667        {
668            // move main frame at top
669            setLocation(bounds.x, bounds.y);
670
671            final int mainFrameW = getWidth();
672            final int mainFrameH = getHeight();
673
674            // adjust available bounds of current screen
675            if (mainFrameW > mainFrameH)
676            {
677                bounds.y += mainFrameH;
678                bounds.height -= mainFrameH;
679            }
680            else
681            {
682                bounds.x += mainFrameW;
683                bounds.width -= mainFrameW;
684            }
685        }
686
687        // available space
688        final int w = bounds.width;
689        final int h = bounds.height;
690
691        final int xMax = bounds.x + w;
692        final int yMax = bounds.y + h;
693
694        final int fw = (int) (w * 0.6f);
695        final int fh = (int) (h * 0.6f);
696
697        int x = bounds.x + 32;
698        int y = bounds.y + 32;
699
700        for (Viewer v : viewers)
701        {
702            final IcyExternalFrame externalFrame = v.getIcyExternalFrame();
703
704            if (externalFrame.isMaximized())
705                externalFrame.setMaximized(false);
706            externalFrame.setBounds(x, y, fw, fh);
707            externalFrame.toFront();
708
709            x += 30;
710            y += 20;
711            if ((x + fw) > xMax)
712                x = bounds.x + 32;
713            if ((y + fh) > yMax)
714                y = bounds.y + 32;
715        }
716    }
717
718    /**
719     * Organize all frames in tile.<br>
720     * 
721     * @param type
722     *        tile type.<br>
723     *        TILE_HORIZONTAL, TILE_VERTICAL or TILE_GRID.
724     */
725    public void organizeTile(int type)
726    {
727        // all screen devices
728        final GraphicsDevice screenDevices[] = SystemUtil.getLocalGraphicsEnvironment().getScreenDevices();
729        // screen devices to process
730        final ArrayList<GraphicsDevice> devices = new ArrayList<GraphicsDevice>();
731
732        // detached mode ?
733        if (isDetachedMode())
734        {
735            // process all available screen for cascade organization
736            for (GraphicsDevice dev : screenDevices)
737                if (dev.getType() == GraphicsDevice.TYPE_RASTER_SCREEN)
738                    devices.add(dev);
739        }
740        else
741        {
742            // process desktop pane tile organization
743            desktopPane.organizeTile(type);
744
745            // we process screen where the mainFrame is not visible
746            for (GraphicsDevice dev : screenDevices)
747                if (dev.getType() == GraphicsDevice.TYPE_RASTER_SCREEN)
748                    if (!dev.getDefaultConfiguration().getBounds().contains(getLocation()))
749                        devices.add(dev);
750        }
751
752        // organize frames on different screen
753        for (GraphicsDevice dev : devices)
754            organizeTile(dev, type);
755    }
756
757    /**
758     * Organize frames in tile on the specified graphics device.
759     */
760    protected void organizeTile(GraphicsDevice graphicsDevice, int type)
761    {
762        final GraphicsConfiguration graphicsConfiguration = graphicsDevice.getDefaultConfiguration();
763        final Rectangle bounds = graphicsConfiguration.getBounds();
764        final Insets inset = Toolkit.getDefaultToolkit().getScreenInsets(graphicsConfiguration);
765
766        // adjust bounds of current screen
767        bounds.x += inset.left;
768        bounds.y += inset.top;
769        bounds.width -= inset.left + inset.right;
770        bounds.height -= inset.top + inset.bottom;
771
772        // prepare viewers to process
773        final Viewer[] viewers = getExternalViewers(bounds, false, false);
774
775        // this screen contains the main frame ?
776        if (bounds.contains(getLocation()))
777        {
778            // move main frame at top
779            setLocation(bounds.x, bounds.y);
780
781            final int mainFrameW = getWidth();
782            final int mainFrameH = getHeight();
783
784            // adjust available bounds of current screen
785            if (mainFrameW > mainFrameH)
786            {
787                bounds.y += mainFrameH;
788                bounds.height -= mainFrameH;
789            }
790            else
791            {
792                bounds.x += mainFrameW;
793                bounds.width -= mainFrameW;
794            }
795        }
796
797        final int numFrames = viewers.length;
798
799        // nothing to do
800        if (numFrames == 0)
801            return;
802
803        // available space
804        final int w = bounds.width;
805        final int h = bounds.height;
806        final int x = bounds.x;
807        final int y = bounds.y;
808
809        int numCol;
810        int numLine;
811
812        switch (type)
813        {
814            case MainFrame.TILE_HORIZONTAL:
815                numCol = 1;
816                numLine = numFrames;
817                break;
818
819            case MainFrame.TILE_VERTICAL:
820                numCol = numFrames;
821                numLine = 1;
822                break;
823
824            default:
825                numCol = (int) Math.sqrt(numFrames);
826                if (numFrames != (numCol * numCol))
827                    numCol++;
828                numLine = numFrames / numCol;
829                if (numFrames > (numCol * numLine))
830                    numLine++;
831                break;
832        }
833
834        final double[][] framesDistances = new double[numCol * numLine][numFrames];
835
836        final int dx = w / numCol;
837        final int dy = h / numLine;
838        int k = 0;
839
840        for (int i = 0; i < numLine; i++)
841        {
842            for (int j = 0; j < numCol; j++, k++)
843            {
844                final double[] distances = framesDistances[k];
845                final double fx = x + (j * dx) + (dx / 2d);
846                final double fy = y + (i * dy) + (dy / 2d);
847
848                for (int f = 0; f < numFrames; f++)
849                {
850                    final Point2D.Double center = ComponentUtil.getCenter(viewers[f].getExternalFrame());
851                    distances[f] = Point2D.distanceSq(center.x, center.y, fx, fy);
852                }
853            }
854        }
855
856        final int[] framePos = new HungarianAlgorithm(framesDistances).resolve();
857
858        k = 0;
859        for (int i = 0; i < numLine; i++)
860        {
861            for (int j = 0; j < numCol; j++, k++)
862            {
863                final int f = framePos[k];
864
865                if (f < numFrames)
866                {
867                    final IcyExternalFrame externalFrame = viewers[f].getIcyExternalFrame();
868
869                    if (externalFrame.isMaximized())
870                        externalFrame.setMaximized(false);
871                    externalFrame.setBounds(x + (j * dx), y + (i * dy), dx, dy);
872                    externalFrame.toFront();
873                }
874            }
875        }
876    }
877
878    /**
879     * Set detached window mode.
880     */
881    public void setDetachedMode(boolean value)
882    {
883        if (detachedMode != value)
884        {
885            // detached mode
886            if (value)
887            {
888                // save inspector state
889                previousInspectorInternalized = inspector.isInternalized();
890                // save it in preferences...
891                positionSaver.getPreferences().putBoolean(ID_PREVIOUS_STATE, previousInspectorInternalized);
892
893                // externalize inspector
894                externalizeInspector();
895                // no more internalization possible
896                inspector.setInternalizationAutorized(false);
897
898                // save the current height & state
899                previousHeight = getHeight();
900                previousMaximized = ComponentUtil.isMaximized(this);
901
902                // hide main pane and remove maximized state
903                remove(mainPane);
904                ComponentUtil.setMaximized(this, false);
905                // and pack the frame
906                pack();
907            }
908            // single window mode
909            else
910            {
911                // show main pane & resize window back to original dimension
912                add(mainPane, BorderLayout.CENTER);
913                setSize(getWidth(), previousHeight);
914                if (previousMaximized)
915                    ComponentUtil.setMaximized(this, true);
916                // recompute layout
917                validate();
918
919                // internalization possible
920                inspector.setInternalizationAutorized(true);
921                // restore inspector internalization
922                if (previousInspectorInternalized)
923                    internalizeInspector();
924            }
925
926            detachedMode = value;
927
928            // notify mode change
929            firePropertyChange(PROPERTY_DETACHEDMODE, !value, value);
930        }
931    }
932
933    /**
934     * Refresh application title
935     */
936    public void refreshTitle()
937    {
938        final String login = GeneralPreferences.getUserLogin();
939        final String userName = GeneralPreferences.getUserName();
940        final String virtual = ImageCache.isEnabled() && GeneralPreferences.getVirtualMode() ? " (virtual mode)" : "";
941
942        if (!StringUtil.isEmpty(userName))
943            setTitle(TITLE + virtual + " - " + userName);
944        else if (!StringUtil.isEmpty(login))
945            setTitle(TITLE + virtual + " - " + login);
946        else
947            setTitle(TITLE + virtual);
948    }
949
950    @Override
951    public void reshape(int x, int y, int width, int height)
952    {
953        final Rectangle r = new Rectangle(x, y, width, height);
954        final boolean detached;
955
956        // test detached mode by using mainPane parent as resize is called inside setDetachedMode(..) and
957        // detachedMode variable is not yet updated
958        if (mainPane == null)
959            detached = detachedMode;
960        else
961            detached = mainPane.getParent() == null;
962
963        if (detached)
964        {
965            // fix height
966            final int prefH = getPreferredSize().height;
967
968            if (r.height > prefH)
969                r.height = prefH;
970        }
971
972        ComponentUtil.fixPosition(this, r);
973
974        super.reshape(r.x, r.y, r.width, r.height);
975    }
976
977    // @Override
978    // public synchronized void setMaximizedBounds(Rectangle bounds)
979    // {
980    // Rectangle bnds = SystemUtil.getScreenBounds(ComponentUtil.getScreen(this), true);
981    //
982    // if (bnds.isEmpty())
983    // bnds = bounds;
984    // // at least use the location from original bounds
985    // else if (bounds != null)
986    // bnds.setLocation(bounds.getLocation());
987    // else bnds.setLocation(0, 0);
988    //
989    // super.setMaximizedBounds(bnds);
990    // }
991}