1 /* 2 * @(#)Quaqua14RootPaneUI.java 2.0.2 2009-03-16 3 * 4 * Copyright (c) 2005-2009 Werner Randelshofer 5 * Staldenmattweg 2, Immensee, CH-6405, Switzerland. 6 * All rights reserved. 7 * 8 * The copyright of this software is owned by Werner Randelshofer. 9 * You may not use, copy or modify this software, except in 10 * accordance with the license agreement you entered into with 11 * Werner Randelshofer. For details see accompanying license terms. 12 */ 13 package ch.randelshofer.quaqua; 14 15 import ch.randelshofer.quaqua.color.PaintableColor; 16 import java.awt.*; 17 import java.awt.event.*; 18 import java.awt.peer.*; 19 import java.beans.*; 20 import java.lang.reflect.*; 21 import java.security.*; 22 import java.util.ConcurrentModificationException; 23 import java.util.Iterator; 24 import java.util.LinkedList; 25 import java.util.WeakHashMap; 26 import javax.swing.*; 27 import javax.swing.event.*; 28 import javax.swing.plaf.*; 29 import javax.swing.plaf.basic.*; 30 31 /** 32 * Quaqua14RootPaneUI. 33 * 34 * @author Werner Randelshofer 35 * @version 2.0.9 2009-03-16 Handle ConcurrentModificationException in 36 * allRootPanes WeakHashMap. 37 * <br>2.0.1 2008-07-07 Don't process mouse dragged events when 38 * the root pane is not showing on screen. 39 * <br>2.0 2008-05-10 Added support for client property "Window.documentModified". 40 * <br>1.1.4 2008-03-30 Fixed memory leak in allRootPanes has map, by putting 41 * a null value into the weak hash map, instead of the RootPane object. 42 * <br>1.1.3 2007-09-29 Fixed NPE in Window snapping behavior code. 43 * <br>1.1.2 2007-08-02 Only snap to the edges of a window, if the 44 * is not far away. Don't snap, if the user 45 * holds down the alt key. 46 * <br>1.1 2007-07-26 Look and Feel decorated windows snap to other 47 * windows. Windows on secondary screens couldn't always be dragged back 48 * to primary screen. 49 * <br>1.0.6 2007-07-05 Resize icon wasn't painted for dialog windows. 50 * <br>1.0.5 2007-04-29 Repaint the root paint 100 milliseconds after it 51 * has been resized. This is a workaround for the repaint happening twice after 52 * a window has been resized. 53 * <br>1.0.4 2005-08-03 Removed error output on System.err, when no 54 * native support for windows modified property is available. 55 * <br>1.0.3 2005-06-29 Fixed NPE in method propertyChanged. Method propertyChange must call super in 56 * order to make default button work. 57 * <br>1.0.2 2005-06-19 Ancestor window was not properly determined 58 * when running under Java 1.5. 59 * <br>1.0.1 2005-04-07 Fixed NPE in method ancestorRemoved. 60 * <br>1.0 06 February 2005 Created. 61 */ 62 public class Quaqua14RootPaneUI extends BasicRootPaneUI { 63 64 /** 65 * Keys to lookup borders in defaults table. 66 */ 67 private static final String[] borderKeys = new String[]{ 68 null, "RootPane.frameBorder", "RootPane.plainDialogBorder", 69 "RootPane.informationDialogBorder", 70 "RootPane.errorDialogBorder", "RootPane.colorChooserDialogBorder", 71 "RootPane.fileChooserDialogBorder", "RootPane.questionDialogBorder", 72 "RootPane.warningDialogBorder" 73 }; 74 /** 75 * Height and width of resize handle on the lower right corner of the window. 76 * FIXME - This value depends on font size. 77 */ 78 private static final int BORDER_DRAG_THICKNESS = 15; 79 /** 80 * Window the <code>JRootPane</code> is in. 81 */ 82 private Window window; 83 /** 84 * <code>JComponent</code> providing window decorations. This will be 85 * null if not providing window decorations. 86 */ 87 private JComponent titlePane; 88 /** 89 * <code>MouseInputListener</code> that is added to the parent 90 * <code>Window</code> the <code>JRootPane</code> is contained in. 91 */ 92 private MouseInputListener mouseInputListener; 93 /** 94 * The <code>LayoutManager</code> that is set on the 95 * <code>JRootPane</code>. 96 */ 97 private LayoutManager layoutManager; 98 /** 99 * <code>LayoutManager</code> of the <code>JRootPane</code> before we 100 * replaced it. 101 */ 102 private LayoutManager savedOldLayout; 103 private AncestorListener ancestorListener; 104 private ComponentListener componentListener; 105 /** 106 * This variable is set to false, if we fail to invoke the 107 * setWindowModifiedMethod. 108 */ 109 private static boolean isWindowModifiedSupported = true; 110 /** 111 * This method is used to access the non-API peer methods of Apple's 112 * Window peers. The method is different for the different MRJ versions. 113 */ 114 private static Method setWindowModifiedMethod = null; 115 /** 116 * <code>JRootPane</code> providing the look and feel for. 117 */ 118 private JRootPane root; 119 /** 120 * Since method Window.getWindows() is only available since Java 1.6, 121 * we indirectly keep track of all windows by ourselves by storing 122 * all JRootPanes in this weak hash map. 123 * We add a JRootPane to this map, upon installUI, and remove a JRootPane 124 * from this upoin deinstallUI. 125 */ 126 private static WeakHashMap allRootPanes = new WeakHashMap(); 127 /** 128 * <code>Cursor</code> used to track the cursor set by the user. 129 * This is initially <code>Cursor.DEFAULT_CURSOR</code>. 130 */ 131 private Cursor lastCursor = 132 Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR); 133 createUI(JComponent c)134 public static ComponentUI createUI(JComponent c) { 135 return new Quaqua14RootPaneUI(); 136 } 137 138 /** Creates a new instance. */ Quaqua14RootPaneUI()139 public Quaqua14RootPaneUI() { 140 } 141 142 /** 143 * Invokes supers implementation of <code>installUI</code> to install 144 * the necessary state onto the passed in <code>JRootPane</code> 145 * to render the metal look and feel implementation of 146 * <code>RootPaneUI</code>. If 147 * the <code>windowDecorationStyle</code> property of the 148 * <code>JRootPane</code> is other than <code>JRootPane.NONE</code>, 149 * this will add a custom <code>Component</code> to render the widgets to 150 * <code>JRootPane</code>, as well as installing a custom 151 * <code>Border</code> and <code>LayoutManager</code> on the 152 * <code>JRootPane</code>. 153 * 154 * @param c the JRootPane to install state onto 155 */ installUI(JComponent c)156 public void installUI(JComponent c) { 157 super.installUI(c); 158 root = (JRootPane) c; 159 c.putClientProperty( 160 "apple.awt.draggableWindowBackground", 161 UIManager.get("RootPane.draggableWindowBackground")); 162 c.putClientProperty( 163 "apple.awt.windowShadow", 164 UIManager.get("RootPane.windowShadow")); 165 Window window = SwingUtilities.getWindowAncestor(c); 166 167 int style = root.getWindowDecorationStyle(); 168 if (style != JRootPane.NONE) { 169 installClientDecorations(root); 170 } 171 allRootPanes.put(c, null); 172 } 173 paint(Graphics g, JComponent c)174 public void paint(Graphics g, JComponent c) { 175 int style = getRootPane().getWindowDecorationStyle(); 176 if (style != JRootPane.NONE) { 177 boolean needsResizeIcon = false; 178 if (window instanceof Frame) { 179 Frame frame = (Frame) window; 180 needsResizeIcon = frame.isResizable(); 181 } else if (window instanceof Dialog) { 182 Dialog dialog = (Dialog) window; 183 needsResizeIcon = dialog.isResizable(); 184 } 185 186 if (needsResizeIcon) { 187 Icon resizeIcon = UIManager.getIcon("InternalFrame.resizeIcon"); 188 int w = c.getWidth(); 189 int h = c.getHeight(); 190 Insets insets = c.getInsets(); 191 resizeIcon.paintIcon(c, g, 192 w - resizeIcon.getIconWidth() - insets.right, 193 h - resizeIcon.getIconHeight() - insets.bottom); 194 } 195 } 196 } 197 198 /** 199 * Invokes supers implementation to uninstall any of its state. This will 200 * also reset the <code>LayoutManager</code> of the <code>JRootPane</code>. 201 * If a <code>Component</code> has been added to the <code>JRootPane</code> 202 * to render the window decoration style, this method will remove it. 203 * Similarly, this will revert the Border and LayoutManager of the 204 * <code>JRootPane</code> to what it was before <code>installUI</code> 205 * was invoked. 206 * 207 * @param c the JRootPane to uninstall state from 208 */ uninstallUI(JComponent c)209 public void uninstallUI(JComponent c) { 210 super.uninstallUI(c); 211 uninstallClientDecorations(root); 212 213 layoutManager = null; 214 mouseInputListener = null; 215 root = null; 216 allRootPanes.remove(c); 217 } 218 installDefaults(JRootPane c)219 protected void installDefaults(JRootPane c) { 220 super.installDefaults(c); 221 LookAndFeel.installColorsAndFont(c, 222 "RootPane.background", 223 "RootPane.foreground", 224 "RootPane.font"); 225 LookAndFeel.installBorder(c, "RootPane.border"); 226 // Root Pane must always be opaque 227 //LookAndFeel.installProperty(c, "opaque", UIManager.get("RootPane.opaque")); 228 229 // By default, we should delay window ordering, but 230 // it does not seem to work as expected. It appears that we need to 231 // at more code. 232 // c.putClientProperty("apple.awt.delayWindowOrdering", Boolean.TRUE); 233 234 235 c.setOpaque(true); 236 } 237 update(Graphics gr, final JComponent c)238 public void update(Graphics gr, final JComponent c) { 239 if (c.isOpaque()) { 240 Graphics2D g = (Graphics2D) gr; 241 g.setPaint(PaintableColor.getPaint(c.getBackground(), c)); 242 g.fillRect(0, 0, c.getWidth(), c.getHeight()); 243 } 244 /* 245 root.putClientProperty( 246 "apple.awt.windowShadow.revalidateNow", Boolean.TRUE 247 ); 248 */ 249 paint(gr, c); 250 } 251 installListeners(JRootPane root)252 protected void installListeners(JRootPane root) { 253 super.installListeners(root); 254 255 ancestorListener = createAncestorListener(); 256 if (ancestorListener != null) { 257 root.addAncestorListener(ancestorListener); 258 } 259 componentListener = createComponentListener(); 260 if (componentListener != null) { 261 root.addComponentListener(componentListener); 262 } 263 } 264 uninstallListeners(JRootPane root)265 protected void uninstallListeners(JRootPane root) { 266 super.uninstallListeners(root); 267 268 if (ancestorListener != null) { 269 root.removeAncestorListener(ancestorListener); 270 } 271 if (componentListener != null) { 272 root.removeComponentListener(componentListener); 273 } 274 } 275 createComponentListener()276 protected ComponentListener createComponentListener() { 277 return new ComponentAdapter() { 278 279 public void componentResized(final ComponentEvent e) { 280 Timer t = new Timer(200, new ActionListener() { 281 282 public void actionPerformed(ActionEvent evt) { 283 e.getComponent().repaint(); 284 } 285 }); 286 t.setRepeats(false); 287 t.start(); 288 } 289 }; 290 } 291 292 protected AncestorListener createAncestorListener() { 293 return new RootPaneAncestorListener(); 294 } 295 296 /** 297 * Returns the <code>JComponent</code> to render the window decoration 298 * style. 299 */ 300 private JComponent createTitlePane(JRootPane root) { 301 return new Quaqua14TitlePane(root, this); 302 } 303 304 /** 305 * Returns a <code>MouseListener</code> that will be added to the 306 * <code>Window</code> containing the <code>JRootPane</code>. 307 */ 308 private MouseInputListener createWindowMouseInputListener(JRootPane root) { 309 return new MouseInputHandler(); 310 } 311 312 /** 313 * Returns a <code>LayoutManager</code> that will be set on the 314 * <code>JRootPane</code>. 315 */ 316 private LayoutManager createLayoutManager() { 317 return new QuaquaRootLayout(); 318 } 319 320 private static void updateWindowModified(JRootPane rootpane) { 321 if (isWindowModifiedSupported) { 322 Container parent = rootpane.getParent(); 323 if (parent != null && (parent instanceof Window)) { 324 ComponentPeer peer = parent.getPeer(); 325 if (peer != null) { 326 if (setWindowModifiedMethod == null) { 327 try { 328 setWindowModifiedMethod = peer.getClass().getMethod("setDocumentEdited", new Class[]{Boolean.TYPE}); 329 } catch (NoSuchMethodException ex1) { 330 try { 331 setWindowModifiedMethod = peer.getClass().getMethod("setModified", new Class[]{Boolean.TYPE}); 332 } catch (NoSuchMethodException ex2) { 333 isWindowModifiedSupported = false; 334 //ex2.printStackTrace(); 335 } 336 } catch (AccessControlException ex1) { 337 isWindowModifiedSupported = false; 338 //System.err.println("Sorry. Quaqua14RootPaneUI can not access the native window modified API"); 339 } 340 } 341 if (setWindowModifiedMethod != null) { 342 try { 343 Object value = rootpane.getClientProperty("Window.documentModified"); 344 if (value == null) { 345 value = rootpane.getClientProperty("windowModified"); 346 } 347 if (value == null) { 348 value = Boolean.FALSE; 349 } 350 setWindowModifiedMethod.invoke(peer, new Object[]{value}); 351 } catch (IllegalAccessException ex) { 352 isWindowModifiedSupported = false; 353 //ex.printStackTrace(); 354 } catch (InvocationTargetException ex) { 355 isWindowModifiedSupported = false; 356 //ex.printStackTrace(); 357 } 358 } 359 } 360 } 361 } 362 } 363 364 /** 365 * Installs the appropriate <code>Border</code> onto the 366 * <code>JRootPane</code>. 367 */ 368 void installBorder(JRootPane root) { 369 int style = root.getWindowDecorationStyle(); 370 371 if (style == JRootPane.NONE) { 372 LookAndFeel.uninstallBorder(root); 373 } else { 374 LookAndFeel.installBorder(root, borderKeys[style]); 375 } 376 } 377 378 /** 379 * Removes any border that may have been installed. 380 */ 381 private void uninstallBorder(JRootPane root) { 382 LookAndFeel.uninstallBorder(root); 383 } 384 385 /** 386 * Installs the necessary state onto the JRootPane to render client 387 * decorations. This is ONLY invoked if the <code>JRootPane</code> 388 * has a decoration style other than <code>JRootPane.NONE</code>. 389 */ 390 private void installClientDecorations(JRootPane root) { 391 installBorder(root); 392 393 /* 394 window = SwingUtilities.getWindowAncestor(root); 395 if (window != null) { 396 window.setBackground(new Color(0, true)); 397 }*/ 398 399 root.putClientProperty( 400 "apple.awt.draggableWindowBackground", Boolean.FALSE); 401 root.putClientProperty( 402 "apple.awt.windowShadow", Boolean.TRUE); 403 404 JComponent titlePane = createTitlePane(root); 405 406 setTitlePane(root, titlePane); 407 installWindowListeners(root, root.getParent()); 408 installLayout(root); 409 if (window != null) { 410 root.revalidate(); 411 root.repaint(); 412 } 413 414 root.putClientProperty( 415 "apple.awt.windowShadow.revalidateNow", new Object()); 416 } 417 418 /** 419 * Installs the necessary Listeners on the parent <code>Window</code>, 420 * if there is one. 421 * <p> 422 * This takes the parent so that cleanup can be done from 423 * <code>removeNotify</code>, at which point the parent hasn't been 424 * reset yet. 425 * 426 * @param parent The parent of the JRootPane 427 */ 428 private void installWindowListeners(JRootPane root, Component parent) { 429 if (parent instanceof Window) { 430 window = (Window) parent; 431 } else { 432 window = SwingUtilities.getWindowAncestor(parent); 433 } 434 if (window != null) { 435 if (mouseInputListener == null) { 436 mouseInputListener = createWindowMouseInputListener(root); 437 } 438 window.addMouseListener(mouseInputListener); 439 window.addMouseMotionListener(mouseInputListener); 440 } 441 } 442 443 /** 444 * Uninstalls the necessary Listeners on the <code>Window</code> the 445 * Listeners were last installed on. 446 */ 447 private void uninstallWindowListeners(JRootPane root) { 448 if (window != null) { 449 window.removeMouseListener(mouseInputListener); 450 window.removeMouseMotionListener(mouseInputListener); 451 } 452 } 453 454 /** 455 * Installs the appropriate LayoutManager on the <code>JRootPane</code> 456 * to render the window decorations. 457 */ 458 private void installLayout(JRootPane root) { 459 if (layoutManager == null) { 460 layoutManager = createLayoutManager(); 461 } 462 savedOldLayout = root.getLayout(); 463 root.setLayout(layoutManager); 464 } 465 466 /** 467 * Uninstalls the previously installed <code>LayoutManager</code>. 468 */ 469 private void uninstallLayout(JRootPane root) { 470 if (savedOldLayout != null) { 471 root.setLayout(savedOldLayout); 472 savedOldLayout = null; 473 } 474 } 475 476 private boolean isVertical(JRootPane root) { 477 return root.getClientProperty("Quaqua.RootPane.isVertical") == Boolean.TRUE; 478 } 479 480 /** 481 * Sets the window title pane -- the JComponent used to provide a plaf a 482 * way to override the native operating system's window title pane with 483 * one whose look and feel are controlled by the plaf. The plaf creates 484 * and sets this value; the default is null, implying a native operating 485 * system window title pane. 486 * 487 * @param content the <code>JComponent</code> to use for the window title pane. 488 */ 489 private void setTitlePane(JRootPane root, JComponent titlePane) { 490 JLayeredPane layeredPane = root.getLayeredPane(); 491 JComponent oldTitlePane = getTitlePane(); 492 493 if (oldTitlePane != null) { 494 oldTitlePane.setVisible(false); 495 layeredPane.remove(oldTitlePane); 496 } 497 if (titlePane != null) { 498 layeredPane.add(titlePane, JLayeredPane.FRAME_CONTENT_LAYER); 499 titlePane.setVisible(true); 500 } 501 this.titlePane = titlePane; 502 } 503 504 /** 505 * Returns the <code>JComponent</code> rendering the title pane. If this 506 * returns null, it implies there is no need to render window decorations. 507 * 508 * @return the current window title pane, or null 509 * @see #setTitlePane 510 */ 511 private JComponent getTitlePane() { 512 return titlePane; 513 } 514 515 /** 516 * Returns the <code>JRootPane</code> we're providing the look and 517 * feel for. 518 */ 519 private JRootPane getRootPane() { 520 return root; 521 } 522 523 /** 524 * Uninstalls any state that <code>installClientDecorations</code> has 525 * installed. 526 * <p> 527 * NOTE: This may be called if you haven't installed client decorations 528 * yet (ie before <code>installClientDecorations</code> has been invoked). 529 */ 530 private void uninstallClientDecorations(JRootPane root) { 531 uninstallBorder(root); 532 uninstallWindowListeners(root); 533 setTitlePane(root, null); 534 uninstallLayout(root); 535 // We have to revalidate/repaint root if the style is JRootPane.NONE 536 // only. When we needs to call revalidate/repaint with other styles 537 // the installClientDecorations is always called after this method 538 // imediatly and it will cause the revalidate/repaint at the proper 539 // time. 540 int style = root.getWindowDecorationStyle(); 541 if (style == JRootPane.NONE) { 542 root.repaint(); 543 root.revalidate(); 544 } 545 // Reset the cursor, as we may have changed it to a resize cursor 546 if (window != null) { 547 window.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); 548 } 549 window = null; 550 } 551 552 /** 553 * Invoked when a property changes on the root pane. If the event 554 * indicates the <code>defaultButton</code> has changed, this will 555 * reinstall the keyboard actions. 556 */ 557 public void propertyChange(PropertyChangeEvent e) { 558 super.propertyChange(e); 559 String name = e.getPropertyName(); 560 561 JRootPane rootpane = (JRootPane) e.getSource(); 562 if (name.equals("Window.windowModified") || name.equals("windowModified")) { 563 updateWindowModified(rootpane); 564 } else if (name.equals("windowDecorationStyle")) { 565 int style = root.getWindowDecorationStyle(); 566 567 // This is potentially more than needs to be done, 568 // but it rarely happens and makes the install/uninstall process 569 // simpler. MetalTitlePane also assumes it will be recreated if 570 // the decoration style changes. 571 uninstallClientDecorations(root); 572 if (style != JRootPane.NONE) { 573 installClientDecorations(root); 574 } 575 } else if (name.equals("JComponent.sizeVariant")) { 576 QuaquaUtilities.applySizeVariant(rootpane); 577 } 578 } 579 580 private static class RootPaneAncestorListener implements AncestorListener, WindowListener { 581 582 public void ancestorAdded(AncestorEvent evt) { 583 Container ancestor = evt.getAncestor(); 584 Window window = (ancestor instanceof Window) 585 ? (Window) ancestor 586 : SwingUtilities.getWindowAncestor(ancestor); 587 if (window != null) { 588 window.addWindowListener(this); 589 updateWindowModified((JRootPane) evt.getSource()); 590 } 591 } 592 593 public void ancestorMoved(AncestorEvent evt) { 594 } 595 596 public void ancestorRemoved(AncestorEvent evt) { 597 Container ancestorParent = evt.getAncestorParent(); 598 if (ancestorParent != null) { 599 Window window = (ancestorParent instanceof Window) 600 ? (Window) ancestorParent 601 : SwingUtilities.getWindowAncestor(ancestorParent); 602 //Window window = SwingUtilities.getWindowAncestor(ancestorParent); 603 if (window != null) { 604 window.removeWindowListener(this); 605 } 606 } 607 } 608 609 public void windowActivated(WindowEvent e) { 610 updateComponentTreeUIActivation(e.getComponent(), Boolean.TRUE); 611 } 612 613 public void windowClosed(WindowEvent e) { 614 } 615 616 public void windowClosing(WindowEvent e) { 617 } 618 619 public void windowDeactivated(WindowEvent e) { 620 updateComponentTreeUIActivation(e.getComponent(), Boolean.FALSE); 621 } 622 623 public void windowDeiconified(WindowEvent e) { 624 } 625 626 public void windowIconified(WindowEvent e) { 627 } 628 629 public void windowOpened(WindowEvent e) { 630 } 631 632 private static void updateComponentTreeUIActivation(Component c, Boolean isActive) { 633 if (c instanceof JComponent) { 634 ((JComponent) c).putClientProperty("Frame.active", isActive); 635 } 636 Component[] children = null; 637 if (c instanceof JMenu) { 638 children = ((JMenu) c).getMenuComponents(); 639 } else if (c instanceof Container) { 640 children = ((Container) c).getComponents(); 641 } 642 if (children != null) { 643 for (int i = 0; i < children.length; i++) { 644 updateComponentTreeUIActivation(children[i], isActive); 645 } 646 } 647 } 648 } 649 650 /** 651 * A custom layout manager that is responsible for the layout of 652 * layeredPane, glassPane, menuBar and titlePane, if one has been 653 * installed. 654 */ 655 // NOTE: Ideally this would extends JRootPane.RootLayout, but that 656 // would force this to be non-static. 657 private static class QuaquaRootLayout implements LayoutManager2 { 658 659 private boolean isVertical(Container parent) { 660 if (parent instanceof JComponent) { 661 return ((JComponent) parent).getClientProperty("Quaqua.RootPane.isVertical") == Boolean.TRUE; 662 } 663 return false; 664 } 665 666 /** 667 * Returns the amount of space the layout would like to have. 668 * 669 * @param the Container for which this layout manager is being used 670 * @return a Dimension object containing the layout's preferred size 671 */ 672 public Dimension preferredLayoutSize(Container parent) { 673 boolean isVertical = isVertical(parent); 674 675 Dimension cpd, mbd, tpd; 676 int cpWidth = 0; 677 int cpHeight = 0; 678 int mbWidth = 0; 679 int mbHeight = 0; 680 int tpWidth = 0; 681 int tpHeight = 0; 682 Insets i = parent.getInsets(); 683 JRootPane root = (JRootPane) parent; 684 685 if (root.getContentPane() != null) { 686 cpd = root.getContentPane().getPreferredSize(); 687 } else { 688 cpd = root.getSize(); 689 } 690 if (cpd != null) { 691 cpWidth = cpd.width; 692 cpHeight = cpd.height; 693 } 694 695 if (root.getJMenuBar() != null) { 696 mbd = root.getJMenuBar().getPreferredSize(); 697 if (mbd != null) { 698 mbWidth = mbd.width; 699 mbHeight = mbd.height; 700 } 701 } 702 703 if (root.getWindowDecorationStyle() != JRootPane.NONE && 704 (root.getUI() instanceof Quaqua14RootPaneUI)) { 705 JComponent titlePane = ((Quaqua14RootPaneUI) root.getUI()).getTitlePane(); 706 if (titlePane != null) { 707 tpd = titlePane.getPreferredSize(); 708 if (tpd != null) { 709 tpWidth = tpd.width; 710 tpHeight = tpd.height; 711 } 712 } 713 } 714 715 if (isVertical) { 716 return new Dimension( 717 Math.max(cpWidth, mbWidth) + tpWidth + i.left + i.right, 718 Math.max(cpHeight + mbHeight, tpHeight) + i.top + i.bottom); 719 } else { 720 return new Dimension(Math.max(Math.max(cpWidth, mbWidth), tpWidth) + i.left + i.right, 721 cpHeight + mbHeight + tpWidth + i.top + i.bottom); 722 } 723 } 724 725 /** 726 * Returns the minimum amount of space the layout needs. 727 * 728 * @param the Container for which this layout manager is being used 729 * @return a Dimension object containing the layout's minimum size 730 */ 731 public Dimension minimumLayoutSize(Container parent) { 732 boolean isVertical = isVertical(parent); 733 734 Dimension cpd, mbd, tpd; 735 int cpWidth = 0; 736 int cpHeight = 0; 737 int mbWidth = 0; 738 int mbHeight = 0; 739 int tpWidth = 0; 740 int tpHeight = 0; 741 Insets i = parent.getInsets(); 742 JRootPane root = (JRootPane) parent; 743 744 if (root.getContentPane() != null) { 745 cpd = root.getContentPane().getMinimumSize(); 746 } else { 747 cpd = root.getSize(); 748 } 749 if (cpd != null) { 750 cpWidth = cpd.width; 751 cpHeight = cpd.height; 752 } 753 754 if (root.getJMenuBar() != null) { 755 mbd = root.getJMenuBar().getMinimumSize(); 756 if (mbd != null) { 757 mbWidth = mbd.width; 758 mbHeight = mbd.height; 759 } 760 } 761 if (root.getWindowDecorationStyle() != JRootPane.NONE && 762 (root.getUI() instanceof Quaqua14RootPaneUI)) { 763 JComponent titlePane = ((Quaqua14RootPaneUI) root.getUI()).getTitlePane(); 764 if (titlePane != null) { 765 tpd = titlePane.getMinimumSize(); 766 if (tpd != null) { 767 tpWidth = tpd.width; 768 tpHeight = tpd.height; 769 } 770 } 771 } 772 773 if (isVertical) { 774 return new Dimension( 775 Math.max(cpWidth, mbWidth) + tpWidth + i.left + i.right, 776 Math.max(cpHeight + mbHeight, tpHeight) + i.top + i.bottom); 777 } else { 778 return new Dimension(Math.max(Math.max(cpWidth, mbWidth), tpWidth) + i.left + i.right, 779 cpHeight + mbHeight + tpWidth + i.top + i.bottom); 780 } 781 } 782 783 /** 784 * Returns the maximum amount of space the layout can use. 785 * 786 * @param the Container for which this layout manager is being used 787 * @return a Dimension object containing the layout's maximum size 788 */ 789 public Dimension maximumLayoutSize(Container target) { 790 boolean isVertical = isVertical(target); 791 792 Dimension cpd, mbd, tpd; 793 int cpWidth = Integer.MAX_VALUE; 794 int cpHeight = Integer.MAX_VALUE; 795 int mbWidth = Integer.MAX_VALUE; 796 int mbHeight = Integer.MAX_VALUE; 797 int tpWidth = Integer.MAX_VALUE; 798 int tpHeight = Integer.MAX_VALUE; 799 Insets i = target.getInsets(); 800 JRootPane root = (JRootPane) target; 801 802 if (root.getContentPane() != null) { 803 cpd = root.getContentPane().getMaximumSize(); 804 if (cpd != null) { 805 cpWidth = cpd.width; 806 cpHeight = cpd.height; 807 } 808 } 809 810 if (root.getJMenuBar() != null) { 811 mbd = root.getJMenuBar().getMaximumSize(); 812 if (mbd != null) { 813 mbWidth = mbd.width; 814 mbHeight = mbd.height; 815 } 816 } 817 818 if (root.getWindowDecorationStyle() != JRootPane.NONE && 819 (root.getUI() instanceof Quaqua14RootPaneUI)) { 820 JComponent titlePane = ((Quaqua14RootPaneUI) root.getUI()).getTitlePane(); 821 if (titlePane != null) { 822 tpd = titlePane.getMaximumSize(); 823 if (tpd != null) { 824 tpWidth = tpd.width; 825 tpHeight = tpd.height; 826 } 827 } 828 } 829 830 int maxHeight = Math.max(Math.max(cpHeight, mbHeight), tpHeight); 831 832 // Only overflows if 3 real non-MAX_VALUE heights, sum to > MAX_VALUE 833 // Only will happen if sums to more than 2 billion units. Not likely. 834 if (maxHeight != Integer.MAX_VALUE) { 835 maxHeight = cpHeight + mbHeight + tpHeight + i.top + i.bottom; 836 } 837 838 int maxWidth = Math.max(Math.max(cpWidth, mbWidth), tpWidth); 839 // Similar overflow comment as above 840 if (maxWidth != Integer.MAX_VALUE) { 841 maxWidth += i.left + i.right; 842 } 843 844 return new Dimension(maxWidth, maxHeight); 845 } 846 847 /** 848 * Instructs the layout manager to perform the layout for the specified 849 * container. 850 * 851 * @param the Container for which this layout manager is being used 852 */ 853 public void layoutContainer(Container parent) { 854 boolean isVertical = isVertical(parent); 855 856 JRootPane root = (JRootPane) parent; 857 Rectangle b = root.getBounds(); 858 Insets i = root.getInsets(); 859 int nextY = 0; 860 int nextX = 0; 861 int w = b.width - i.right - i.left; 862 int h = b.height - i.top - i.bottom; 863 864 if (root.getLayeredPane() != null) { 865 root.getLayeredPane().setBounds(i.left, i.top, w, h); 866 } 867 if (root.getGlassPane() != null) { 868 root.getGlassPane().setBounds(i.left, i.top, w, h); 869 } 870 // Note: This is laying out the children in the layeredPane, 871 // technically, these are not our children. 872 if (root.getWindowDecorationStyle() != JRootPane.NONE && 873 (root.getUI() instanceof Quaqua14RootPaneUI)) { 874 JComponent titlePane = ((Quaqua14RootPaneUI) root.getUI()).getTitlePane(); 875 if (titlePane != null) { 876 Dimension tpd = titlePane.getPreferredSize(); 877 if (tpd != null) { 878 if (isVertical) { 879 int tpWidth = tpd.width; 880 titlePane.setBounds(0, 0, tpWidth, h); 881 nextX += tpWidth; 882 } else { 883 int tpHeight = tpd.height; 884 titlePane.setBounds(0, 0, w, tpHeight); 885 nextY += tpHeight; 886 } 887 } 888 } 889 } 890 if (root.getJMenuBar() != null) { 891 Dimension mbd = root.getJMenuBar().getPreferredSize(); 892 root.getJMenuBar().setBounds(nextX, nextY, w - nextX, mbd.height); 893 nextY += mbd.height; 894 } 895 if (root.getContentPane() != null) { 896 Dimension cpd = root.getContentPane().getPreferredSize(); 897 root.getContentPane().setBounds(nextX, nextY, w - nextX, 898 h < nextY ? 0 : h - nextY); 899 } 900 } 901 902 public void addLayoutComponent(String name, Component comp) { 903 } 904 905 public void removeLayoutComponent(Component comp) { 906 } 907 908 public void addLayoutComponent(Component comp, Object constraints) { 909 } 910 911 public float getLayoutAlignmentX(Container target) { 912 return 0.0f; 913 } 914 915 public float getLayoutAlignmentY(Container target) { 916 return 0.0f; 917 } 918 919 public void invalidateLayout(Container target) { 920 } 921 } 922 923 /** 924 * MouseInputHandler is responsible for handling resize/moving of 925 * the Window. It sets the cursor directly on the Window when then 926 * mouse moves over a hot spot. 927 */ 928 private class MouseInputHandler implements MouseInputListener { 929 930 /** 931 * Set to true if the drag operation is moving the window. 932 */ 933 private boolean isMovingWindow; 934 /** 935 * Used to determine the corner the resize is occuring from. 936 */ 937 private int dragCursor; 938 /** 939 * X location the mouse went down on for a drag operation. 940 */ 941 private int dragOffsetX; 942 /** 943 * Y location the mouse went down on for a drag operation. 944 */ 945 private int dragOffsetY; 946 /** 947 * Width of the window when the drag started. 948 */ 949 private int dragWidth; 950 /** 951 * Height of the window when the drag started. 952 */ 953 private int dragHeight; 954 /** 955 * We cache the screen bounds here, so that we don't have to retrieve 956 * them for each mouseDragged event. We clear the cache on mouseReleased. 957 */ 958 private Rectangle cachedScreenBounds; 959 960 public void mousePressed(MouseEvent ev) { 961 JRootPane rootPane = getRootPane(); 962 963 if (rootPane.getWindowDecorationStyle() == JRootPane.NONE) { 964 return; 965 } 966 Point dragWindowOffset = ev.getPoint(); 967 Window w = (Window) ev.getSource(); 968 if (w != null) { 969 w.toFront(); 970 } 971 Point convertedDragWindowOffset = SwingUtilities.convertPoint( 972 w, dragWindowOffset, getTitlePane()); 973 974 Frame f = null; 975 Dialog d = null; 976 977 if (w instanceof Frame) { 978 f = (Frame) w; 979 } else if (w instanceof Dialog) { 980 d = (Dialog) w; 981 } 982 983 int frameState = (f != null) ? f.getExtendedState() : 0; 984 985 if (getTitlePane() != null && 986 getTitlePane().contains(convertedDragWindowOffset)) { 987 if (f != null && ((frameState & Frame.MAXIMIZED_BOTH) == 0) || (d != null)) { 988 isMovingWindow = true; 989 dragOffsetX = dragWindowOffset.x; 990 dragOffsetY = dragWindowOffset.y; 991 } 992 } else if (f != null && f.isResizable() && ((frameState & Frame.MAXIMIZED_BOTH) == 0) || (d != null && d.isResizable())) { 993 dragOffsetX = dragWindowOffset.x; 994 dragOffsetY = dragWindowOffset.y; 995 dragWidth = w.getWidth(); 996 dragHeight = w.getHeight(); 997 dragCursor = 998 (dragWindowOffset.x >= dragWidth - BORDER_DRAG_THICKNESS && 999 dragWindowOffset.y >= dragHeight - BORDER_DRAG_THICKNESS) ? Cursor.SE_RESIZE_CURSOR : 0; 1000 } 1001 } 1002 1003 public void mouseReleased(MouseEvent ev) { 1004 if (dragCursor != 0 && window != null && !window.isValid()) { 1005 // Some Window systems validate as you resize, others won't, 1006 // thus the check for validity before repainting. 1007 window.validate(); 1008 getRootPane().repaint(); 1009 } 1010 isMovingWindow = false; 1011 dragCursor = 0; 1012 cachedScreenBounds = null; 1013 } 1014 1015 public void mouseMoved(MouseEvent ev) { 1016 } 1017 1018 private void adjust(Rectangle bounds, Dimension min, int deltaX, 1019 int deltaY, int deltaWidth, int deltaHeight) { 1020 bounds.x += deltaX; 1021 bounds.y += deltaY; 1022 bounds.width += deltaWidth; 1023 bounds.height += deltaHeight; 1024 if (min != null) { 1025 if (bounds.width < min.width) { 1026 int correction = min.width - bounds.width; 1027 if (deltaX != 0) { 1028 bounds.x -= correction; 1029 } 1030 bounds.width = min.width; 1031 } 1032 if (bounds.height < min.height) { 1033 int correction = min.height - bounds.height; 1034 if (deltaY != 0) { 1035 bounds.y -= correction; 1036 } 1037 bounds.height = min.height; 1038 } 1039 } 1040 } 1041 1042 public void mouseDragged(MouseEvent ev) { 1043 Window w = (Window) ev.getSource(); 1044 Point pt = ev.getPoint(); 1045 1046 if (isMovingWindow) { 1047 // Sometimes we get mouse dragged events even when we are not 1048 // showing on screen (?) 1049 if (w.isShowing()) { 1050 Point windowPt = w.getLocationOnScreen(); 1051 1052 windowPt.x += pt.x - dragOffsetX; 1053 windowPt.y += pt.y - dragOffsetY; 1054 1055 boolean isOnDefaultScreen = 1056 w.getGraphicsConfiguration().getDevice() == 1057 GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice(); 1058 1059 // If an edge of the window is within the snap distance of the 1060 // edge of another window, then align it to it. 1061 // ---------------------------------------------------------- 1062 int snap = UIManager.getInt("RootPane.windowSnapDistance"); 1063 if (snap > 0 && (ev.getModifiersEx() & InputEvent.ALT_DOWN_MASK) == 0) { 1064 // Collects all bounds to which we want to snap to 1065 LinkedList snapBounds; 1066 // Collect window bounds 1067 do { 1068 snapBounds = new LinkedList(); 1069 try { 1070 for (Iterator i = allRootPanes.keySet().iterator(); i.hasNext();) { 1071 JRootPane otherRootPane = (JRootPane) i.next(); 1072 Window other = SwingUtilities.getWindowAncestor(otherRootPane); 1073 if (other != null && other.isShowing() && other != w) { 1074 snapBounds.add(other.getBounds()); 1075 } 1076 } 1077 } catch (ConcurrentModificationException e) { 1078 // allRootPanes is a WeakHashMap, thus iterating over 1079 // it may fail sometimes because the garbage collector 1080 // removes items in a worker thread. 1081 snapBounds = null; 1082 } 1083 } while (snapBounds == null); 1084 1085 // Collect screen bounds 1086 snapBounds.add(w.getGraphicsConfiguration().getBounds()); 1087 if (isOnDefaultScreen) { 1088 Rectangle r = w.getGraphicsConfiguration().getBounds(); 1089 Insets insets = w.getToolkit().getScreenInsets(w.getGraphicsConfiguration()); 1090 r.x += insets.left; 1091 r.y += insets.top; 1092 r.width -= insets.left + insets.right; 1093 r.height -= insets.top + insets.bottom; 1094 snapBounds.add(r); 1095 } 1096 1097 Dimension windowDim = w.getSize(); 1098 Rectangle windowRect = new Rectangle(windowPt.x, windowPt.y, windowDim.width, windowDim.height); 1099 Rectangle snapper = new Rectangle(); 1100 for (Iterator i = snapBounds.iterator(); i.hasNext();) { 1101 Rectangle r = (Rectangle) i.next(); 1102 1103 snapper.setBounds(r); 1104 snapper.grow(snap, snap); 1105 if (snapper.intersects(windowRect)) { 1106 1107 if (windowPt.x > r.x - snap && 1108 windowPt.x < r.x + snap) { 1109 // align my left edge to frame left edge 1110 windowPt.x = r.x; 1111 } else if (windowPt.x > r.x + r.width - snap && 1112 windowPt.x < r.x + r.width + snap) { 1113 // align my left edge to frame right edge 1114 windowPt.x = r.x + r.width; 1115 } else if (windowPt.x + windowDim.width > r.x - snap && 1116 windowPt.x + windowDim.width < r.x + snap) { 1117 // align my right edge to frame left edge 1118 windowPt.x = r.x - windowDim.width; 1119 } else if (windowPt.x + windowDim.width > r.x + r.width - snap && 1120 windowPt.x + windowDim.width < r.x + r.width + snap) { 1121 // align my right edge to frame right edge 1122 windowPt.x = r.x + r.width - windowDim.width; 1123 } 1124 if (windowPt.y > r.y - snap && windowPt.y < r.y + snap) { 1125 // align my top edge to frame top edge 1126 windowPt.y = r.y; 1127 } else if (windowPt.y > r.y + r.height - snap && 1128 windowPt.y < r.y + r.height + snap) { 1129 // align my top edge to frame bottom edge 1130 windowPt.y = r.y + r.height; 1131 } else if (windowPt.y + windowDim.height > r.y - snap && 1132 windowPt.y + windowDim.height < r.y + snap) { 1133 // align my bottom edge to frame top edge 1134 windowPt.y = r.y - windowDim.height; 1135 } else if (windowPt.y + windowDim.height > r.y + r.height - snap && 1136 windowPt.y + windowDim.height < r.y + r.height + snap) { 1137 // align my bottom edge to frame bottom edge 1138 windowPt.y = r.y + r.height - windowDim.height; 1139 } 1140 } 1141 } 1142 } 1143 // Constrain windowPt in order to ensure that a portion of the 1144 // title pane is always visible on screen 1145 // ---------------------------------------------------------- 1146 // Get usable screen bounds 1147 if (isOnDefaultScreen) { 1148 if (cachedScreenBounds == null) { 1149 cachedScreenBounds = w.getGraphicsConfiguration().getBounds(); 1150 Insets screenInsets = w.getToolkit().getScreenInsets(w.getGraphicsConfiguration()); 1151 cachedScreenBounds.x += screenInsets.left; 1152 cachedScreenBounds.y += screenInsets.top; 1153 cachedScreenBounds.width -= screenInsets.left + screenInsets.right; 1154 cachedScreenBounds.height -= screenInsets.top + screenInsets.bottom; 1155 } 1156 Rectangle titlePaneBounds = getTitlePane().getBounds(); 1157 Dimension windowSize = window.getSize(); 1158 1159 if (isVertical(getRootPane())) { 1160 // For vertical title bar, title pane must be fully visible 1161 // on x-axis, and at least 20 pixel on y-axis. 1162 windowPt.x = Math.max(cachedScreenBounds.x + titlePaneBounds.x, windowPt.x); 1163 windowPt.x = Math.min(cachedScreenBounds.x + cachedScreenBounds.width - 1164 titlePaneBounds.x - titlePaneBounds.width, windowPt.x); 1165 1166 windowPt.y = Math.max(cachedScreenBounds.y - windowSize.height + 20, windowPt.y); 1167 windowPt.y = Math.min(cachedScreenBounds.y + cachedScreenBounds.height - 20, windowPt.y); 1168 1169 } else { 1170 // For horizontal title bar, title pane must be fully visible 1171 // on y-axis, and at least 20 pixel on x-axis. 1172 windowPt.y = Math.max(cachedScreenBounds.y + titlePaneBounds.y, windowPt.y); 1173 windowPt.y = Math.min(cachedScreenBounds.y + cachedScreenBounds.height - 1174 titlePaneBounds.y - titlePaneBounds.height, windowPt.y); 1175 1176 windowPt.x = Math.max(cachedScreenBounds.x - windowSize.width + 20, windowPt.x); 1177 windowPt.x = Math.min(cachedScreenBounds.x + cachedScreenBounds.width - 20, windowPt.x); 1178 } 1179 } 1180 w.setLocation(windowPt); 1181 } 1182 } else if (dragCursor != 0) { 1183 Rectangle r = w.getBounds(); 1184 Rectangle startBounds = new Rectangle(r); 1185 Dimension min = w.getMinimumSize(); 1186 1187 switch (dragCursor) { 1188 case Cursor.E_RESIZE_CURSOR: 1189 adjust(r, min, 0, 0, pt.x + (dragWidth - dragOffsetX) - 1190 r.width, 0); 1191 break; 1192 case Cursor.S_RESIZE_CURSOR: 1193 adjust(r, min, 0, 0, 0, pt.y + (dragHeight - dragOffsetY) - 1194 r.height); 1195 break; 1196 case Cursor.N_RESIZE_CURSOR: 1197 adjust(r, min, 0, pt.y - dragOffsetY, 0, 1198 -(pt.y - dragOffsetY)); 1199 break; 1200 case Cursor.W_RESIZE_CURSOR: 1201 adjust(r, min, pt.x - dragOffsetX, 0, 1202 -(pt.x - dragOffsetX), 0); 1203 break; 1204 case Cursor.NE_RESIZE_CURSOR: 1205 adjust(r, min, 0, pt.y - dragOffsetY, 1206 pt.x + (dragWidth - dragOffsetX) - r.width, 1207 -(pt.y - dragOffsetY)); 1208 break; 1209 case Cursor.SE_RESIZE_CURSOR: 1210 adjust(r, min, 0, 0, 1211 pt.x + (dragWidth - dragOffsetX) - r.width, 1212 pt.y + (dragHeight - dragOffsetY) - 1213 r.height); 1214 break; 1215 case Cursor.NW_RESIZE_CURSOR: 1216 adjust(r, min, pt.x - dragOffsetX, 1217 pt.y - dragOffsetY, 1218 -(pt.x - dragOffsetX), 1219 -(pt.y - dragOffsetY)); 1220 break; 1221 case Cursor.SW_RESIZE_CURSOR: 1222 adjust(r, min, pt.x - dragOffsetX, 0, 1223 -(pt.x - dragOffsetX), 1224 pt.y + (dragHeight - dragOffsetY) - r.height); 1225 break; 1226 default: 1227 break; 1228 } 1229 if (!r.equals(startBounds)) { 1230 w.setBounds(r); 1231 // Defer repaint/validate on mouseReleased unless dynamic 1232 // layout is active. 1233 if (Toolkit.getDefaultToolkit().isDynamicLayoutActive()) { 1234 w.validate(); 1235 getRootPane().repaint(); 1236 } 1237 } 1238 } 1239 } 1240 1241 public void mouseEntered(MouseEvent ev) { 1242 Window w = (Window) ev.getSource(); 1243 lastCursor = w.getCursor(); 1244 mouseMoved(ev); 1245 } 1246 1247 public void mouseExited(MouseEvent ev) { 1248 Window w = (Window) ev.getSource(); 1249 w.setCursor(lastCursor); 1250 } 1251 1252 public void mouseClicked(MouseEvent ev) { 1253 Window w = (Window) ev.getSource(); 1254 Frame f = null; 1255 1256 if (w instanceof Frame) { 1257 f = (Frame) w; 1258 } else { 1259 return; 1260 } 1261 1262 Point convertedPoint = SwingUtilities.convertPoint( 1263 w, ev.getPoint(), getTitlePane()); 1264 1265 int state = f.getExtendedState(); 1266 if (getTitlePane() != null && 1267 getTitlePane().contains(convertedPoint)) { 1268 if ((ev.getClickCount() % 2) == 0 && 1269 ((ev.getModifiers() & InputEvent.BUTTON1_MASK) != 0)) { 1270 if (f.isResizable()) { 1271 if ((state & Frame.MAXIMIZED_BOTH) != 0) { 1272 f.setExtendedState(state & ~Frame.MAXIMIZED_BOTH); 1273 } else { 1274 f.setExtendedState(state | Frame.MAXIMIZED_BOTH); 1275 } 1276 return; 1277 } 1278 } 1279 } 1280 } 1281 } 1282 } 1283