/* * Copyright (c) 2005-2016 Substance Kirill Grouchnikov. All Rights Reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * o Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * o Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * o Neither the name of Substance Kirill Grouchnikov nor the names of * its contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package org.pushingpixels.substance.internal.ui; import java.awt.Color; import java.awt.Component; import java.awt.Container; import java.awt.Cursor; import java.awt.Dialog; import java.awt.Dimension; import java.awt.Frame; import java.awt.Graphics; import java.awt.GraphicsConfiguration; import java.awt.GraphicsDevice; import java.awt.GraphicsEnvironment; import java.awt.HeadlessException; import java.awt.Insets; import java.awt.LayoutManager; import java.awt.LayoutManager2; import java.awt.MouseInfo; import java.awt.Point; import java.awt.Rectangle; import java.awt.Toolkit; import java.awt.Window; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.awt.event.ComponentListener; import java.awt.event.HierarchyEvent; import java.awt.event.HierarchyListener; import java.awt.event.InputEvent; import java.awt.event.MouseEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.awt.event.WindowListener; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.security.AccessController; import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; import java.util.Set; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JFrame; import javax.swing.JInternalFrame; import javax.swing.JLayeredPane; import javax.swing.JRootPane; import javax.swing.LookAndFeel; import javax.swing.SwingUtilities; import javax.swing.event.MouseInputAdapter; import javax.swing.event.MouseInputListener; import javax.swing.plaf.ColorUIResource; import javax.swing.plaf.ComponentUI; import javax.swing.plaf.UIResource; import javax.swing.plaf.basic.BasicRootPaneUI; import org.pushingpixels.lafwidget.LafWidget; import org.pushingpixels.lafwidget.LafWidgetRepository; import org.pushingpixels.substance.api.SubstanceLookAndFeel; import org.pushingpixels.substance.api.SubstanceSkin; import org.pushingpixels.substance.internal.animation.RootPaneDefaultButtonTracker; import org.pushingpixels.substance.internal.painter.BackgroundPaintingUtils; import org.pushingpixels.substance.internal.utils.MemoryAnalyzer; import org.pushingpixels.substance.internal.utils.SubstanceColorUtilities; import org.pushingpixels.substance.internal.utils.SubstanceCoreUtilities; import org.pushingpixels.substance.internal.utils.SubstanceTitlePane; /** * UI for root panes in <b>Substance </b> look and feel. * * @author Kirill Grouchnikov * @author Larry Salibra (fix for defect 198) */ public class SubstanceRootPaneUI extends BasicRootPaneUI { private enum CursorState { EXITED, ENTERED, NIL } /** * The amount of space (in pixels) that the cursor is changed on. */ private static final int CORNER_DRAG_WIDTH = 16; /** * Region from edges that dragging is active from. */ private static final int BORDER_DRAG_THICKNESS = 5; /** * Window the <code>JRootPane</code> is in. */ private Window window; /** * <code>JComponent</code> providing window decorations. This will be null * if not providing window decorations. */ private JComponent titlePane; /** * <code>MouseInputListener</code> that is added to the parent * <code>Window</code> the <code>JRootPane</code> is contained in. */ private MouseInputListener substanceMouseInputListener; /** * Mouse listener on the title pane (dragging). */ private MouseInputListener substanceTitleMouseInputListener; /** * The <code>LayoutManager</code> that is set on the <code>JRootPane</code>. */ private LayoutManager layoutManager; /** * <code>LayoutManager</code> of the <code>JRootPane</code> before we * replaced it. */ private LayoutManager savedOldLayout; /** * <code>JRootPane</code> providing the look and feel for. */ protected JRootPane root; /** * Window listener that stops all Substance thread when the last frame is * disposed. */ protected WindowListener substanceWindowListener; /** * The current window. */ protected Window substanceCurrentWindow; /** * Hierarchy listener to keep track of the associated top-level window. */ protected HierarchyListener substanceHierarchyListener; /** * Component listener to keep track of the primary graphics configuration * (for recomputing the maximized bounds) - fix for defect 213. */ protected ComponentListener substanceWindowComponentListener; /** * The graphics configuration that contains the top-left corner of the * window (fix for defect 213). */ protected GraphicsConfiguration currentRootPaneGC; protected PropertyChangeListener substancePropertyChangeListener; /** * <code>Cursor</code> used to track the cursor set by the user. This is * initially <code>Cursor.DEFAULT_CURSOR</code>. */ private Cursor lastCursor = Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR); /** * Optimization to speed up the * {@link SubstanceCoreUtilities#getSkin(Component)} implementation. */ private static int rootPanesWithCustomSkin = 0; private Set<LafWidget> lafWidgets; /** * Creates a UI for a <code>JRootPane</code>. * * @param comp * the JRootPane the RootPaneUI will be created for * @return the RootPaneUI implementation for the passed in JRootPane */ public static ComponentUI createUI(JComponent comp) { SubstanceCoreUtilities.testComponentCreationThreadingViolation(comp); return new SubstanceRootPaneUI(); } /** * Invokes supers implementation of <code>installUI</code> to install the * necessary state onto the passed in <code>JRootPane</code> to render the * metal look and feel implementation of <code>RootPaneUI</code>. If the * <code>windowDecorationStyle</code> property of the <code>JRootPane</code> * is other than <code>JRootPane.NONE</code>, this will add a custom * <code>Component</code> to render the widgets to <code>JRootPane</code>, * as well as installing a custom <code>Border</code> and * <code>LayoutManager</code> on the <code>JRootPane</code>. * * @param c * the JRootPane to install state onto */ @Override public void installUI(JComponent c) { this.lafWidgets = LafWidgetRepository.getRepository().getMatchingWidgets(c); super.installUI(c); this.root = (JRootPane) c; int style = this.root.getWindowDecorationStyle(); if (style != JRootPane.NONE) { this.installClientDecorations(this.root); } if (SubstanceCoreUtilities.isRootPaneModified(this.root)) { propagateModificationState(); } if (this.root.getClientProperty(SubstanceLookAndFeel.SKIN_PROPERTY) instanceof SubstanceSkin) { rootPanesWithCustomSkin++; } for (LafWidget lafWidget : this.lafWidgets) { lafWidget.installUI(); } } /** * Invokes super implementation to uninstall any of its state. This will * also reset the <code>LayoutManager</code> of the <code>JRootPane</code>. * If a <code>Component</code> has been added to the <code>JRootPane</code> * to render the window decoration style, this method will remove it. * Similarly, this will revert the Border and LayoutManager of the * <code>JRootPane</code> to what it was before <code>installUI</code> was * invoked. * * @param c * the JRootPane to uninstall state from */ @Override public void uninstallUI(JComponent c) { super.uninstallUI(c); this.uninstallClientDecorations(this.root); this.layoutManager = null; this.substanceMouseInputListener = null; if (this.root.getClientProperty(SubstanceLookAndFeel.SKIN_PROPERTY) instanceof SubstanceSkin) { rootPanesWithCustomSkin--; } this.root = null; for (LafWidget lafWidget : this.lafWidgets) { lafWidget.uninstallUI(); } } /** * Installs the appropriate <code>Border</code> onto the * <code>JRootPane</code>. * * @param root * Root pane. */ public void installBorder(JRootPane root) { int style = root.getWindowDecorationStyle(); if (style == JRootPane.NONE) { LookAndFeel.uninstallBorder(root); } else { LookAndFeel.installBorder(root, "RootPane.border"); } } /** * Removes any border that may have been installed. * * @param root * Root pane. */ private void uninstallBorder(JRootPane root) { LookAndFeel.uninstallBorder(root); } @Override protected void installDefaults(JRootPane c) { super.installDefaults(c); // support for per-window skins Color backgr = c.getBackground(); if ((backgr == null) || (backgr instanceof UIResource)) { Color backgroundFillColor = SubstanceColorUtilities .getBackgroundFillColor(c); // fix for issue 244 - set the root pane BG color if (backgroundFillColor != null) { c.setBackground(new ColorUIResource(backgroundFillColor)); } } for (LafWidget lafWidget : this.lafWidgets) { lafWidget.installDefaults(); } } @Override protected void uninstallDefaults(JRootPane root) { for (LafWidget lafWidget : this.lafWidgets) { lafWidget.uninstallDefaults(); } super.uninstallDefaults(root); } @Override public void update(Graphics g, JComponent c) { if (!SubstanceLookAndFeel.isCurrentLookAndFeel()) return; // fix for issue 244 - paint the entire root pane so that it // picks the correct watermark if (SubstanceCoreUtilities.isOpaque(c)) { BackgroundPaintingUtils.update(g, c, false); } super.paint(g, c); } /** * Installs the necessary Listeners on the parent <code>Window</code>, if * there is one. * <p> * This takes the parent so that cleanup can be done from * <code>removeNotify</code>, at which point the parent hasn't been reset * yet. * * * @param root * Root pane. * @param parent * The parent of the JRootPane */ private void installWindowListeners(JRootPane root, Component parent) { if (parent instanceof Window) { this.window = (Window) parent; } else { this.window = SwingUtilities.getWindowAncestor(parent); } if (this.window != null) { if (this.substanceMouseInputListener == null) { this.substanceMouseInputListener = this .createWindowMouseInputListener(root); } this.window.addMouseListener(this.substanceMouseInputListener); this.window .addMouseMotionListener(this.substanceMouseInputListener); if (this.titlePane != null) { if (this.substanceTitleMouseInputListener == null) { this.substanceTitleMouseInputListener = new TitleMouseInputHandler(); } this.titlePane .addMouseMotionListener(this.substanceTitleMouseInputListener); this.titlePane .addMouseListener(this.substanceTitleMouseInputListener); } this.setMaximized(); } } /** * Uninstalls the necessary Listeners on the <code>Window</code> the * Listeners were last installed on. * * @param root * Root pane. */ private void uninstallWindowListeners(JRootPane root) { if (this.window != null) { this.window.removeMouseListener(this.substanceMouseInputListener); this.window .removeMouseMotionListener(this.substanceMouseInputListener); } if (this.titlePane != null) { this.titlePane .removeMouseListener(this.substanceTitleMouseInputListener); this.titlePane .removeMouseMotionListener(this.substanceTitleMouseInputListener); } } /** * Installs the appropriate LayoutManager on the <code>JRootPane</code> to * render the window decorations. * * @param root * Root pane. */ private void installLayout(JRootPane root) { if (this.layoutManager == null) { this.layoutManager = this.createLayoutManager(); } this.savedOldLayout = root.getLayout(); root.setLayout(this.layoutManager); } @Override protected void installListeners(final JRootPane root) { super.installListeners(root); this.substanceHierarchyListener = (HierarchyEvent e) -> { Component parent = root.getParent(); if (parent == null) { // fix for defect 271 - check for null parent // as early as possible return; } if (MemoryAnalyzer.isRunning()) { MemoryAnalyzer.enqueueUsage("Root pane @" + root.hashCode() + "\n" + SubstanceCoreUtilities.getHierarchy(parent)); } if (parent.getClass().getName().startsWith( "org.jdesktop.jdic.tray") || (parent.getClass().getName().compareTo( "javax.swing.Popup$HeavyWeightWindow") == 0)) { // Workaround for bug 240 - using JDIC system tray // menu results in an HierarchyEvent being fired right // after a MouseEvent. Somehow, the // EventQueue.getCurrentEvent() returns the HierarchyEvent // even when the MouseEvent is being processed, resulting // in zeroed modifiers set on the ActionEvent passed // to the action listeners on that menu item. SwingUtilities.invokeLater(() -> { root.removeHierarchyListener(substanceHierarchyListener); substanceHierarchyListener = null; }); } Window currWindow = null; if (parent instanceof Window) { currWindow = (Window) parent; } else { currWindow = SwingUtilities.getWindowAncestor(parent); } if (substanceWindowListener != null) { substanceCurrentWindow .removeWindowListener(substanceWindowListener); substanceWindowListener = null; } if (substanceWindowComponentListener != null) { substanceCurrentWindow .removeComponentListener(substanceWindowComponentListener); substanceWindowComponentListener = null; } if (currWindow != null) { // fix for bug 116 - stopping threads when all frames // are not displayable substanceWindowListener = new WindowAdapter() { @Override public void windowClosed(WindowEvent e) { SubstanceCoreUtilities .testWindowCloseThreadingViolation(e .getWindow()); SwingUtilities.invokeLater(new Runnable() { public void run() { Frame[] frames = Frame.getFrames(); for (Frame frame : frames) { if (frame.isDisplayable()) return; } SubstanceCoreUtilities.stopThreads(); } }); } }; if (!(parent instanceof JInternalFrame)) { currWindow.addWindowListener(substanceWindowListener); } // fix for defect 213 - maximizing frame under multiple // screens shouldn't always use insets of the primary // screen. substanceWindowComponentListener = new ComponentAdapter() { @Override public void componentMoved(ComponentEvent e) { this.processNewPosition(); } @Override public void componentResized(ComponentEvent e) { this.processNewPosition(); } protected void processNewPosition() { SwingUtilities.invokeLater(() -> { if (window == null) return; if (!window.isShowing() || !window.isDisplayable()) { currentRootPaneGC = null; return; } GraphicsEnvironment ge = GraphicsEnvironment .getLocalGraphicsEnvironment(); GraphicsDevice[] gds = ge .getScreenDevices(); if (gds.length == 1) return; Point midLoc = new Point(window .getLocationOnScreen().x + window.getWidth() / 2, window .getLocationOnScreen().y + window.getHeight() / 2); for (GraphicsDevice gd : gds) { GraphicsConfiguration gc = gd .getDefaultConfiguration(); Rectangle bounds = gc.getBounds(); if (bounds.contains(midLoc)) { if (gc != currentRootPaneGC) { currentRootPaneGC = gc; setMaximized(); } break; } } }); } }; // fix for defect 225 - install the listener only on // JFrames. if (parent instanceof JFrame) { currWindow.addComponentListener(substanceWindowComponentListener); } SubstanceRootPaneUI.this.window = currWindow; } substanceCurrentWindow = currWindow; }; root.addHierarchyListener(this.substanceHierarchyListener); JButton defaultButton = root.getDefaultButton(); if (defaultButton != null) { RootPaneDefaultButtonTracker.update(defaultButton); } this.substancePropertyChangeListener = (PropertyChangeEvent evt) -> { if ("defaultButton".equals(evt.getPropertyName())) { JButton prev = (JButton) evt.getOldValue(); JButton next = (JButton) evt.getNewValue(); RootPaneDefaultButtonTracker.update(prev); RootPaneDefaultButtonTracker.update(next); } if (SubstanceLookAndFeel.WINDOW_MODIFIED.equals(evt .getPropertyName())) { propagateModificationState(); } }; root.addPropertyChangeListener(this.substancePropertyChangeListener); for (LafWidget lafWidget : this.lafWidgets) { lafWidget.installListeners(); } } /* * (non-Javadoc) * * @see * javax.swing.plaf.basic.BasicRootPaneUI#uninstallListeners(javax.swing * .JRootPane) */ @Override protected void uninstallListeners(JRootPane root) { // fix for bug 116 - stopping threads when all frames are // not displayable if (this.window != null) { this.window.removeWindowListener(this.substanceWindowListener); this.substanceWindowListener = null; this.window .removeComponentListener(this.substanceWindowComponentListener); this.substanceWindowComponentListener = null; } root.removeHierarchyListener(this.substanceHierarchyListener); this.substanceHierarchyListener = null; root.removePropertyChangeListener(this.substancePropertyChangeListener); this.substancePropertyChangeListener = null; for (LafWidget lafWidget : this.lafWidgets) { lafWidget.uninstallListeners(); } super.uninstallListeners(root); } @Override protected void installComponents(JRootPane root) { super.installComponents(root); for (LafWidget lafWidget : this.lafWidgets) { lafWidget.installComponents(); } } @Override protected void uninstallComponents(JRootPane root) { for (LafWidget lafWidget : this.lafWidgets) { lafWidget.uninstallComponents(); } super.uninstallComponents(root); } /** * Uninstalls the previously installed <code>LayoutManager</code>. * * @param root * Root pane. */ private void uninstallLayout(JRootPane root) { if (this.savedOldLayout != null) { root.setLayout(this.savedOldLayout); this.savedOldLayout = null; } } /** * Installs the necessary state onto the JRootPane to render client * decorations. This is ONLY invoked if the <code>JRootPane</code> has a * decoration style other than <code>JRootPane.NONE</code>. * * @param root * Root pane. */ private void installClientDecorations(JRootPane root) { this.installBorder(root); JComponent titlePane = this.createTitlePane(root); this.setTitlePane(root, titlePane); this.installWindowListeners(root, root.getParent()); this.installLayout(root); if (this.window != null) { root.revalidate(); root.repaint(); } } /** * Uninstalls any state that <code>installClientDecorations</code> has * installed. * <p> * NOTE: This may be called if you haven't installed client decorations yet * (ie before <code>installClientDecorations</code> has been invoked). * * @param root * Root pane. */ private void uninstallClientDecorations(JRootPane root) { this.uninstallBorder(root); this.uninstallWindowListeners(root); this.setTitlePane(root, null); this.uninstallLayout(root); // We have to revalidate/repaint root if the style is JRootPane.NONE // only. When we needs to call revalidate/repaint with other styles // the installClientDecorations is always called after this method // imediatly and it will cause the revalidate/repaint at the proper // time. int style = root.getWindowDecorationStyle(); if (style == JRootPane.NONE) { root.repaint(); root.revalidate(); } // Reset the cursor, as we may have changed it to a resize cursor if (this.window != null) { this.window.setCursor(Cursor .getPredefinedCursor(Cursor.DEFAULT_CURSOR)); } this.window = null; } /** * Returns the <code>JComponent</code> to render the window decoration * style. * * @param root * Root pane. * @return The title pane component. */ protected JComponent createTitlePane(JRootPane root) { return new SubstanceTitlePane(root, this); } /** * Returns a <code>MouseListener</code> that will be added to the * <code>Window</code> containing the <code>JRootPane</code>. * * @param root * Root pane. * @return Window mouse listener. */ private MouseInputListener createWindowMouseInputListener(JRootPane root) { return new MouseInputHandler(); } /** * Returns a <code>LayoutManager</code> that will be set on the * <code>JRootPane</code>. * * @return Layout manager. */ protected LayoutManager createLayoutManager() { return new SubstanceRootLayout(); } /** * Sets the window title pane -- the JComponent used to provide a plaf a way * to override the native operating system's window title pane with one * whose look and feel are controlled by the plaf. The plaf creates and sets * this value; the default is null, implying a native operating system * window title pane. * * @param root * Root pane * @param titlePane * The <code>JComponent</code> to use for the window title pane. */ private void setTitlePane(JRootPane root, JComponent titlePane) { JLayeredPane layeredPane = root.getLayeredPane(); JComponent oldTitlePane = this.getTitlePane(); if (oldTitlePane != null) { // fix for defect 109 - memory leak on skin change if (oldTitlePane instanceof SubstanceTitlePane) ((SubstanceTitlePane) oldTitlePane).uninstall(); // oldTitlePane.setVisible(false); layeredPane.remove(oldTitlePane); } if (titlePane != null) { layeredPane.add(titlePane, JLayeredPane.FRAME_CONTENT_LAYER); titlePane.setVisible(true); } this.titlePane = titlePane; } /** * Sets maximized bounds according to the display screen insets. */ public void setMaximized() { Component tla = this.root.getTopLevelAncestor(); // fix for defect 213 - maximizing frame under multiple // screens shouldn't always use insets of the primary // screen. GraphicsConfiguration gc = (currentRootPaneGC != null) ? currentRootPaneGC : tla.getGraphicsConfiguration(); Rectangle screenBounds = gc.getBounds(); screenBounds.x = 0; screenBounds.y = 0; Insets screenInsets = Toolkit.getDefaultToolkit().getScreenInsets(gc); Rectangle maxBounds = new Rectangle( (screenBounds.x + screenInsets.left), (screenBounds.y + screenInsets.top), screenBounds.width - ((screenInsets.left + screenInsets.right)), screenBounds.height - ((screenInsets.top + screenInsets.bottom))); if (tla instanceof JFrame) ((JFrame) tla).setMaximizedBounds(maxBounds); if (MemoryAnalyzer.isRunning()) { MemoryAnalyzer.enqueueUsage("Frame set to bounds " + maxBounds); } } /** * Returns the <code>JComponent</code> rendering the title pane. If this * returns null, it implies there is no need to render window decorations. * This method is <b>for internal use only</b>. * * @see #setTitlePane * @return Title pane. */ public JComponent getTitlePane() { return this.titlePane; } /** * Returns the <code>JRootPane</code> we're providing the look and feel for. * * @return The associated root pane. */ protected JRootPane getRootPane() { return this.root; } /* * (non-Javadoc) * * @seejavax.swing.plaf.basic.BasicRootPaneUI#propertyChange(java.beans. * PropertyChangeEvent) */ @Override public void propertyChange(PropertyChangeEvent e) { super.propertyChange(e); String propertyName = e.getPropertyName(); if (propertyName == null) { return; } if (propertyName.equals("windowDecorationStyle")) { JRootPane root = (JRootPane) e.getSource(); int style = root.getWindowDecorationStyle(); this.uninstallClientDecorations(root); if (style != JRootPane.NONE) { this.installClientDecorations(root); } } if (propertyName.equals("ancestor")) { this.uninstallWindowListeners(this.root); if (((JRootPane) e.getSource()).getWindowDecorationStyle() != JRootPane.NONE) { this.installWindowListeners(this.root, this.root.getParent()); } } if (propertyName.equals("background")) { SubstanceLookAndFeel.getTitlePaneComponent(window).setBackground( (Color) e.getNewValue()); } if (propertyName.equals(SubstanceLookAndFeel.SKIN_PROPERTY)) { SubstanceSkin oldValue = (SubstanceSkin) e.getOldValue(); SubstanceSkin newValue = (SubstanceSkin) e.getNewValue(); if ((oldValue == null) && (newValue != null)) { rootPanesWithCustomSkin++; } if ((oldValue != null) && (newValue == null)) { rootPanesWithCustomSkin--; } } return; } /** * A custom layout manager that is responsible for the layout of * layeredPane, glassPane, menuBar and titlePane, if one has been installed. */ protected class SubstanceRootLayout implements LayoutManager2 { /** * Returns the amount of space the layout would like to have. * * * aram the Container for which this layout manager is being used * * @return a Dimension object containing the layout's preferred size */ public Dimension preferredLayoutSize(Container parent) { Dimension cpd, mbd, tpd; int cpWidth = 0; int cpHeight = 0; int mbWidth = 0; int mbHeight = 0; int tpWidth = 0; int tpHeight = 0; Insets i = parent.getInsets(); JRootPane root = (JRootPane) parent; if (root.getContentPane() != null) { cpd = root.getContentPane().getPreferredSize(); } else { cpd = root.getSize(); } if (cpd != null) { cpWidth = cpd.width; cpHeight = cpd.height; } if (root.getJMenuBar() != null) { mbd = root.getJMenuBar().getPreferredSize(); if (mbd != null) { mbWidth = mbd.width; mbHeight = mbd.height; } } if ((root.getWindowDecorationStyle() != JRootPane.NONE) && (root.getUI() instanceof SubstanceRootPaneUI)) { JComponent titlePane = ((SubstanceRootPaneUI) root.getUI()) .getTitlePane(); if (titlePane != null) { tpd = titlePane.getPreferredSize(); if (tpd != null) { tpWidth = tpd.width; tpHeight = tpd.height; } } } return new Dimension(Math.max(Math.max(cpWidth, mbWidth), tpWidth) + i.left + i.right, cpHeight + mbHeight + tpHeight + i.top + i.bottom); } /** * Returns the minimum amount of space the layout needs. * * * aram the Container for which this layout manager is being used * * @return a Dimension object containing the layout's minimum size */ public Dimension minimumLayoutSize(Container parent) { Dimension cpd, mbd, tpd; int cpWidth = 0; int cpHeight = 0; int mbWidth = 0; int mbHeight = 0; int tpWidth = 0; int tpHeight = 0; Insets i = parent.getInsets(); JRootPane root = (JRootPane) parent; if (root.getContentPane() != null) { cpd = root.getContentPane().getMinimumSize(); } else { cpd = root.getSize(); } if (cpd != null) { cpWidth = cpd.width; cpHeight = cpd.height; } if (root.getJMenuBar() != null) { mbd = root.getJMenuBar().getMinimumSize(); if (mbd != null) { mbWidth = mbd.width; mbHeight = mbd.height; } } if ((root.getWindowDecorationStyle() != JRootPane.NONE) && (root.getUI() instanceof SubstanceRootPaneUI)) { JComponent titlePane = ((SubstanceRootPaneUI) root.getUI()) .getTitlePane(); if (titlePane != null) { tpd = titlePane.getMinimumSize(); if (tpd != null) { tpWidth = tpd.width; tpHeight = tpd.height; } } } return new Dimension(Math.max(Math.max(cpWidth, mbWidth), tpWidth) + i.left + i.right, cpHeight + mbHeight + tpHeight + i.top + i.bottom); } /** * Returns the maximum amount of space the layout can use. * * * aram the Container for which this layout manager is being used * * @return a Dimension object containing the layout's maximum size */ public Dimension maximumLayoutSize(Container target) { Dimension cpd, mbd, tpd; int cpWidth = Integer.MAX_VALUE; int cpHeight = Integer.MAX_VALUE; int mbWidth = Integer.MAX_VALUE; int mbHeight = Integer.MAX_VALUE; int tpWidth = Integer.MAX_VALUE; int tpHeight = Integer.MAX_VALUE; Insets i = target.getInsets(); JRootPane root = (JRootPane) target; if (root.getContentPane() != null) { cpd = root.getContentPane().getMaximumSize(); if (cpd != null) { cpWidth = cpd.width; cpHeight = cpd.height; } } if (root.getJMenuBar() != null) { mbd = root.getJMenuBar().getMaximumSize(); if (mbd != null) { mbWidth = mbd.width; mbHeight = mbd.height; } } if ((root.getWindowDecorationStyle() != JRootPane.NONE) && (root.getUI() instanceof SubstanceRootPaneUI)) { JComponent titlePane = ((SubstanceRootPaneUI) root.getUI()) .getTitlePane(); if (titlePane != null) { tpd = titlePane.getMaximumSize(); if (tpd != null) { tpWidth = tpd.width; tpHeight = tpd.height; } } } int maxHeight = Math.max(Math.max(cpHeight, mbHeight), tpHeight); // Only overflows if 3 real non-MAX_VALUE heights, sum to > // MAX_VALUE // Only will happen if sums to more than 2 billion units. Not // likely. if (maxHeight != Integer.MAX_VALUE) { maxHeight = cpHeight + mbHeight + tpHeight + i.top + i.bottom; } int maxWidth = Math.max(Math.max(cpWidth, mbWidth), tpWidth); // Similar overflow comment as above if (maxWidth != Integer.MAX_VALUE) { maxWidth += i.left + i.right; } return new Dimension(maxWidth, maxHeight); } /** * Instructs the layout manager to perform the layout for the specified * container. * * * aram the Container for which this layout manager is being used */ public void layoutContainer(Container parent) { JRootPane root = (JRootPane) parent; Rectangle b = root.getBounds(); Insets i = root.getInsets(); int nextY = 0; int w = b.width - i.right - i.left; int h = b.height - i.top - i.bottom; if (root.getLayeredPane() != null) { root.getLayeredPane().setBounds(i.left, i.top, w, h); } if (root.getGlassPane() != null) { root.getGlassPane().setBounds(i.left, i.top, w, h); } // Note: This is laying out the children in the layeredPane, // technically, these are not our children. if ((root.getWindowDecorationStyle() != JRootPane.NONE) && (root.getUI() instanceof SubstanceRootPaneUI)) { JComponent titlePane = ((SubstanceRootPaneUI) root.getUI()) .getTitlePane(); if (titlePane != null) { Dimension tpd = titlePane.getPreferredSize(); if (tpd != null) { int tpHeight = tpd.height; titlePane.setBounds(0, 0, w, tpHeight); nextY += tpHeight; } } } if (root.getJMenuBar() != null) { Dimension mbd = root.getJMenuBar().getPreferredSize(); root.getJMenuBar().setBounds(0, nextY, w, mbd.height); nextY += mbd.height; } if (root.getContentPane() != null) { // Dimension cpd = root.getContentPane().getPreferredSize(); root.getContentPane().setBounds(0, nextY, w, h < nextY ? 0 : h - nextY); } } public void addLayoutComponent(String name, Component comp) { } public void removeLayoutComponent(Component comp) { } public void addLayoutComponent(Component comp, Object constraints) { } public float getLayoutAlignmentX(Container target) { return 0.0f; } public float getLayoutAlignmentY(Container target) { return 0.0f; } public void invalidateLayout(Container target) { } } /** * Maps from positions to cursor type. Refer to calculateCorner and * calculatePosition for details of this. */ private static final int[] cursorMapping = new int[] { Cursor.NW_RESIZE_CURSOR, Cursor.NW_RESIZE_CURSOR, Cursor.N_RESIZE_CURSOR, Cursor.NE_RESIZE_CURSOR, Cursor.NE_RESIZE_CURSOR, Cursor.NW_RESIZE_CURSOR, 0, 0, 0, Cursor.NE_RESIZE_CURSOR, Cursor.W_RESIZE_CURSOR, 0, 0, 0, Cursor.E_RESIZE_CURSOR, Cursor.SW_RESIZE_CURSOR, 0, 0, 0, Cursor.SE_RESIZE_CURSOR, Cursor.SW_RESIZE_CURSOR, Cursor.SW_RESIZE_CURSOR, Cursor.S_RESIZE_CURSOR, Cursor.SE_RESIZE_CURSOR, Cursor.SE_RESIZE_CURSOR }; /** * MouseInputHandler is responsible for handling resize/moving of the * Window. It sets the cursor directly on the Window when then mouse moves * over a hot spot. */ private class MouseInputHandler implements MouseInputListener { /** * Set to true if the drag operation is moving the window. */ private boolean isMovingWindow; private boolean isMousePressed; /** * Used to determine the corner the resize is occuring from. */ private int dragCursor; /** * X location the mouse went down on for a drag operation. */ private int dragOffsetX; /** * Y location the mouse went down on for a drag operation. */ private int dragOffsetY; /** * Width of the window when the drag started. */ private int dragWidth; /** * Height of the window when the drag started. */ private int dragHeight; /** * PrivilegedExceptionAction needed by mouseDragged method to obtain new * location of window on screen during the drag. */ private final PrivilegedExceptionAction getLocationAction = new PrivilegedExceptionAction() { public Object run() throws HeadlessException { return MouseInfo.getPointerInfo().getLocation(); } }; public void mousePressed(MouseEvent ev) { JRootPane rootPane = SubstanceRootPaneUI.this.getRootPane(); this.isMousePressed = true; if (rootPane.getWindowDecorationStyle() == JRootPane.NONE) { return; } Point dragWindowOffset = ev.getPoint(); Window w = (Window) ev.getSource(); if (w != null) { w.toFront(); } Point convertedDragWindowOffset = SwingUtilities.convertPoint(w, dragWindowOffset, SubstanceRootPaneUI.this.getTitlePane()); Frame f = null; Dialog d = null; if (w instanceof Frame) { f = (Frame) w; } else if (w instanceof Dialog) { d = (Dialog) w; } int frameState = (f != null) ? f.getExtendedState() : 0; if ((SubstanceRootPaneUI.this.getTitlePane() != null) && SubstanceRootPaneUI.this.getTitlePane().contains( convertedDragWindowOffset)) { if ((((f != null) && ((frameState & Frame.MAXIMIZED_BOTH) == 0)) || (d != null)) && (dragWindowOffset.y >= SubstanceRootPaneUI.BORDER_DRAG_THICKNESS) && (dragWindowOffset.x >= SubstanceRootPaneUI.BORDER_DRAG_THICKNESS) && (dragWindowOffset.x < w.getWidth() - SubstanceRootPaneUI.BORDER_DRAG_THICKNESS)) { this.isMovingWindow = true; this.dragOffsetX = dragWindowOffset.x; this.dragOffsetY = dragWindowOffset.y; } } else if (((f != null) && f.isResizable() && ((frameState & Frame.MAXIMIZED_BOTH) == 0)) || ((d != null) && d.isResizable())) { this.dragOffsetX = dragWindowOffset.x; this.dragOffsetY = dragWindowOffset.y; this.dragWidth = w.getWidth(); this.dragHeight = w.getHeight(); this.dragCursor = this.getCursor(this.calculateCorner(w, dragWindowOffset.x, dragWindowOffset.y)); } } public void mouseReleased(MouseEvent ev) { if ((this.dragCursor != 0) && (SubstanceRootPaneUI.this.window != null) && !SubstanceRootPaneUI.this.window.isValid()) { // Some Window systems validate as you resize, others won't, // thus the check for validity before repainting. SubstanceRootPaneUI.this.window.validate(); SubstanceRootPaneUI.this.getRootPane().repaint(); } this.isMousePressed = false; this.isMovingWindow = false; this.dragCursor = 0; } public void mouseMoved(MouseEvent ev) { JRootPane root = SubstanceRootPaneUI.this.getRootPane(); if (root.getWindowDecorationStyle() == JRootPane.NONE) { return; } Window w = (Window) ev.getSource(); Frame f = null; Dialog d = null; if (w instanceof Frame) { f = (Frame) w; } else if (w instanceof Dialog) { d = (Dialog) w; } // Update the cursor int cursor = this.getCursor(this.calculateCorner(w, ev.getX(), ev .getY())); boolean isFrameResizable = (f != null) && (f.isResizable() && (f.getExtendedState() & Frame.MAXIMIZED_BOTH) == 0); boolean isDialogResizable = (d != null) && d.isResizable(); if ((cursor != 0) && (isFrameResizable || isDialogResizable)) { w.setCursor(Cursor.getPredefinedCursor(cursor)); } else { w.setCursor(SubstanceRootPaneUI.this.lastCursor); SubstanceRootPaneUI.this.lastCursor = null; } } /** * Adjusts the bounds. * * @param bounds * Original bounds. * @param min * Minimum dimension. * @param deltaX * Delta X. * @param deltaY * Delta Y. * @param deltaWidth * Delta width. * @param deltaHeight * Delta height. */ private void adjust(Rectangle bounds, Dimension min, int deltaX, int deltaY, int deltaWidth, int deltaHeight) { bounds.x += deltaX; bounds.y += deltaY; bounds.width += deltaWidth; bounds.height += deltaHeight; if (min != null) { if (bounds.width < min.width) { int correction = min.width - bounds.width; if (deltaX != 0) { bounds.x -= correction; } bounds.width = min.width; } if (bounds.height < min.height) { int correction = min.height - bounds.height; if (deltaY != 0) { bounds.y -= correction; } bounds.height = min.height; } } } @SuppressWarnings("unchecked") public void mouseDragged(MouseEvent ev) { Window w = (Window) ev.getSource(); Point pt = ev.getPoint(); if (this.isMovingWindow) { Point windowPt; try { windowPt = (Point) AccessController .doPrivileged(this.getLocationAction); windowPt.x = windowPt.x - this.dragOffsetX; windowPt.y = windowPt.y - this.dragOffsetY; w.setLocation(windowPt); } catch (PrivilegedActionException e) { } } else if (this.dragCursor != 0) { Rectangle r = w.getBounds(); Rectangle startBounds = new Rectangle(r); Dimension min = w.getMinimumSize(); switch (this.dragCursor) { case Cursor.E_RESIZE_CURSOR: this.adjust(r, min, 0, 0, pt.x + (this.dragWidth - this.dragOffsetX) - r.width, 0); break; case Cursor.S_RESIZE_CURSOR: this.adjust(r, min, 0, 0, 0, pt.y + (this.dragHeight - this.dragOffsetY) - r.height); break; case Cursor.N_RESIZE_CURSOR: this.adjust(r, min, 0, pt.y - this.dragOffsetY, 0, -(pt.y - this.dragOffsetY)); break; case Cursor.W_RESIZE_CURSOR: this.adjust(r, min, pt.x - this.dragOffsetX, 0, -(pt.x - this.dragOffsetX), 0); break; case Cursor.NE_RESIZE_CURSOR: this.adjust(r, min, 0, pt.y - this.dragOffsetY, pt.x + (this.dragWidth - this.dragOffsetX) - r.width, -(pt.y - this.dragOffsetY)); break; case Cursor.SE_RESIZE_CURSOR: this.adjust(r, min, 0, 0, pt.x + (this.dragWidth - this.dragOffsetX) - r.width, pt.y + (this.dragHeight - this.dragOffsetY) - r.height); break; case Cursor.NW_RESIZE_CURSOR: this.adjust(r, min, pt.x - this.dragOffsetX, pt.y - this.dragOffsetY, -(pt.x - this.dragOffsetX), -(pt.y - this.dragOffsetY)); break; case Cursor.SW_RESIZE_CURSOR: this.adjust(r, min, pt.x - this.dragOffsetX, 0, -(pt.x - this.dragOffsetX), pt.y + (this.dragHeight - this.dragOffsetY) - r.height); break; default: break; } if (!r.equals(startBounds)) { w.setBounds(r); // Defer repaint/validate on mouseReleased unless dynamic // layout is active. if (Toolkit.getDefaultToolkit().isDynamicLayoutActive()) { w.validate(); SubstanceRootPaneUI.this.getRootPane().repaint(); } } } } private CursorState cursorState = CursorState.NIL; public void mouseEntered(MouseEvent ev) { if (isMousePressed) { return; } Window w = (Window) ev.getSource(); if ((SubstanceRootPaneUI.this.lastCursor == null) && (cursorState != CursorState.ENTERED)) { // fix for defect 107 SubstanceRootPaneUI.this.lastCursor = w.getCursor(); } cursorState = CursorState.ENTERED; this.mouseMoved(ev); } public void mouseExited(MouseEvent ev) { if (isMousePressed) { return; } Window w = (Window) ev.getSource(); w.setCursor(SubstanceRootPaneUI.this.lastCursor); SubstanceRootPaneUI.this.lastCursor = null; cursorState = CursorState.EXITED; } public void mouseClicked(MouseEvent ev) { Window w = (Window) ev.getSource(); Frame f = null; if (w instanceof Frame) { f = (Frame) w; } else { return; } JComponent windowTitlePane = SubstanceRootPaneUI.this .getTitlePane(); // fix for issue 444 - ignore double clicks when the title pane // is not showing (for example under JRootPane.NONE decoration // style). if (windowTitlePane == null) return; Point convertedPoint = SwingUtilities.convertPoint(w, ev.getPoint(), windowTitlePane); int state = f.getExtendedState(); if ((windowTitlePane != null) && windowTitlePane.contains(convertedPoint)) { if (((ev.getClickCount() % 2) == 0) && ((ev.getModifiers() & InputEvent.BUTTON1_MASK) != 0)) { if (f.isResizable()) { if ((state & Frame.MAXIMIZED_BOTH) != 0) { setMaximized(); f.setExtendedState(state & ~Frame.MAXIMIZED_BOTH); } else { setMaximized(); f.setExtendedState(state | Frame.MAXIMIZED_BOTH); } return; } } } } /** * Returns the corner that contains the point <code>x</code>, * <code>y</code>, or -1 if the position doesn't match a corner. * * @param w * Window. * @param x * X coordinate. * @param y * Y coordinate. * @return Corner that contains the specified point. */ private int calculateCorner(Window w, int x, int y) { Insets insets = w.getInsets(); int xPosition = this.calculatePosition(x - insets.left, w.getWidth() - insets.left - insets.right); int yPosition = this.calculatePosition(y - insets.top, w.getHeight() - insets.top - insets.bottom); if ((xPosition == -1) || (yPosition == -1)) { return -1; } return yPosition * 5 + xPosition; } /** * Returns the Cursor to render for the specified corner. This returns 0 * if the corner doesn't map to a valid Cursor * * @param corner * Corner * @return Cursor to render for the specified corner. */ private int getCursor(int corner) { if (corner == -1) { return 0; } return SubstanceRootPaneUI.cursorMapping[corner]; } /** * Returns an integer indicating the position of <code>spot</code> in * <code>width</code>. The return value will be: 0 if < * BORDER_DRAG_THICKNESS 1 if < CORNER_DRAG_WIDTH 2 if >= * CORNER_DRAG_WIDTH && < width - BORDER_DRAG_THICKNESS 3 if >= width - * CORNER_DRAG_WIDTH 4 if >= width - BORDER_DRAG_THICKNESS 5 otherwise * * @param spot * Spot. * @param width * Width. * @return The position of spot in width. */ private int calculatePosition(int spot, int width) { if (spot < SubstanceRootPaneUI.BORDER_DRAG_THICKNESS) { return 0; } if (spot < SubstanceRootPaneUI.CORNER_DRAG_WIDTH) { return 1; } if (spot >= (width - SubstanceRootPaneUI.BORDER_DRAG_THICKNESS)) { return 4; } if (spot >= (width - SubstanceRootPaneUI.CORNER_DRAG_WIDTH)) { return 3; } return 2; } } /** * Mouse handler on the title pane. * * @author Kirill Grouchnikov */ private class TitleMouseInputHandler extends MouseInputAdapter { /** * Pointer location when the mouse was pressed for a drag relative to * the upper-lefthand corner of the window. */ private Point dragOffset = new Point(0, 0); @Override public void mousePressed(MouseEvent ev) { JRootPane rootPane = SubstanceRootPaneUI.this.getRootPane(); if (rootPane.getWindowDecorationStyle() == JRootPane.NONE) { return; } Point dragWindowOffset = ev.getPoint(); Component source = (Component) ev.getSource(); Point convertedDragWindowOffset = SwingUtilities.convertPoint( source, dragWindowOffset, getTitlePane()); dragWindowOffset = SwingUtilities.convertPoint(source, dragWindowOffset, SubstanceRootPaneUI.this.window); if (getTitlePane() != null && getTitlePane().contains(convertedDragWindowOffset)) { if (SubstanceRootPaneUI.this.window != null) { SubstanceRootPaneUI.this.window.toFront(); dragOffset = dragWindowOffset; } } } @Override public void mouseDragged(MouseEvent ev) { Component source = (Component) ev.getSource(); // Point pt = SwingUtilities.convertPoint(source, ev.getPoint(), // SubstanceRootPaneUI.this.window); // fix for issue 198 Point eventLocationOnScreen = ev.getLocationOnScreen(); if (eventLocationOnScreen == null) { eventLocationOnScreen = new Point(ev.getX() + source.getLocationOnScreen().x, ev.getY() + source.getLocationOnScreen().y); } // Fix for issue 192 - disable dragging maximized frame. if (SubstanceRootPaneUI.this.window instanceof Frame) { Frame f = (Frame) SubstanceRootPaneUI.this.window; int frameState = (f != null) ? f.getExtendedState() : 0; if ((f != null) && ((frameState & Frame.MAXIMIZED_BOTH) == 0)) { SubstanceRootPaneUI.this.window.setLocation( eventLocationOnScreen.x - dragOffset.x, eventLocationOnScreen.y - dragOffset.y); } } else { // fix for issue 193 - allow dragging decorated dialogs. SubstanceRootPaneUI.this.window.setLocation( eventLocationOnScreen.x - dragOffset.x, eventLocationOnScreen.y - dragOffset.y); } } @Override public void mouseClicked(MouseEvent ev) { Frame f = null; if (SubstanceRootPaneUI.this.window instanceof Frame) { f = (Frame) SubstanceRootPaneUI.this.window; } else { return; } Point convertedPoint = SwingUtilities.convertPoint( SubstanceRootPaneUI.this.window, ev.getPoint(), SubstanceRootPaneUI.this.getTitlePane()); int state = f.getExtendedState(); if ((SubstanceRootPaneUI.this.getTitlePane() != null) && SubstanceRootPaneUI.this.getTitlePane().contains( convertedPoint)) { if (((ev.getClickCount() % 2) == 0) && ((ev.getModifiers() & InputEvent.BUTTON1_MASK) != 0)) { if (f.isResizable()) { if ((state & Frame.MAXIMIZED_BOTH) != 0) { setMaximized(); f.setExtendedState(state & ~Frame.MAXIMIZED_BOTH); } else { setMaximized(); f.setExtendedState(state | Frame.MAXIMIZED_BOTH); } return; } } } } } private void propagateModificationState() { JComponent titlePane = getTitlePane(); if (titlePane instanceof SubstanceTitlePane) { ((SubstanceTitlePane) titlePane) .getCloseButton() .putClientProperty( SubstanceLookAndFeel.WINDOW_MODIFIED, root .getClientProperty(SubstanceLookAndFeel.WINDOW_MODIFIED)); return; } JInternalFrame jif = (JInternalFrame) SwingUtilities .getAncestorOfClass(JInternalFrame.class, this.root); if (jif != null) { SubstanceInternalFrameUI internalFrameUI = (SubstanceInternalFrameUI) jif .getUI(); internalFrameUI.setWindowModified(Boolean.TRUE.equals(root .getClientProperty(SubstanceLookAndFeel.WINDOW_MODIFIED))); } } public static boolean hasCustomSkinOnAtLeastOneRootPane() { return (rootPanesWithCustomSkin > 0); } }