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}