/* * Copyright (c) 2007-2013 JGoodies Software GmbH. 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 JGoodies Software GmbH 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.contrib.jgoodies.looks.common; import java.awt.AWTException; import java.awt.Component; import java.awt.Container; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Panel; import java.awt.Point; import java.awt.Rectangle; import java.awt.Robot; import java.awt.Window; import java.awt.image.BufferedImage; import java.util.ArrayList; import java.util.List; import javax.swing.JApplet; import javax.swing.JComboBox; import javax.swing.JComponent; import javax.swing.JInternalFrame; import javax.swing.JRootPane; import javax.swing.JWindow; import javax.swing.Popup; import javax.swing.SwingUtilities; import javax.swing.border.Border; /** * Does all the magic for getting popups with drop shadows. * It adds the drop shadow border to the Popup, * in {@code #show} it snapshots the screen background as needed, * and in {@code #hide} it cleans up all changes made before. * * @author Karsten Lentzsch * @version $Revision: 1.12 $ * * @see com.jgoodies.looks.common.ShadowPopupBorder * @see com.jgoodies.looks.common.ShadowPopupFactory */ public final class ShadowPopup extends Popup { /** * Max number of items to store in the cache. */ private static final int MAX_CACHE_SIZE = 5; /** * The cache to use for ShadowPopups. */ private static List<ShadowPopup> cache; /** * The singleton instance used to draw all borders. */ private static final Border SHADOW_BORDER = ShadowPopupBorder.getInstance(); /** * The size of the drop shadow. */ private static final int SHADOW_SIZE = 5; /** * Indicates whether we can make snapshots from screen or not. */ private static boolean canSnapshot = true; /** * The component mouse coordinates are relative to, may be null. */ private Component owner; /** * The contents of the popup. */ private Component contents; /** * The desired x and y location of the popup. */ private int x, y; /** * The real popup. The #show() and #hide() methods will delegate * all calls to these popup. */ private Popup popup; /** * The border of the contents' parent replaced by SHADOW_BORDER. */ private Border oldBorder; /** * The old value of the opaque property of the contents' parent. */ private boolean oldOpaque; /** * The heavy weight container of the popup contents, may be null. */ private Container heavyWeightContainer; /** * Returns a previously used {@code ShadowPopup}, or a new one * if none of the popups have been recycled. */ static Popup getInstance(Component owner, Component contents, int x, int y, Popup delegate) { ShadowPopup result; synchronized (ShadowPopup.class) { if (cache == null) { cache = new ArrayList<ShadowPopup>(MAX_CACHE_SIZE); } if (cache.size() > 0) { result = cache.remove(0); } else { result = new ShadowPopup(); } } result.reset(owner, contents, x, y, delegate); return result; } /** * Recycles the ShadowPopup. */ private static void recycle(ShadowPopup popup) { synchronized (ShadowPopup.class) { if (cache.size() < MAX_CACHE_SIZE) { cache.add(popup); } } } public static boolean canSnapshot() { return canSnapshot; } /** * Hides and disposes of the {@code Popup}. Once a {@code Popup} * has been disposed you should no longer invoke methods on it. A * {@code dispose}d {@code Popup} may be reclaimed and later used * based on the {@code PopupFactory}. As such, if you invoke methods * on a {@code disposed} {@code Popup}, indeterminate * behavior will result.<p> * * In addition to the superclass behavior, we reset the stored * horizontal and vertical drop shadows - if any. */ @Override public void hide() { if (contents == null) { return; } JComponent parent = (JComponent) contents.getParent(); popup.hide(); if ((parent != null) && parent.getBorder() == SHADOW_BORDER) { parent.setBorder(oldBorder); parent.setOpaque(oldOpaque); oldBorder = null; if (heavyWeightContainer != null) { parent.putClientProperty(ShadowPopupFactory.PROP_HORIZONTAL_BACKGROUND, null); parent.putClientProperty(ShadowPopupFactory.PROP_VERTICAL_BACKGROUND, null); heavyWeightContainer = null; } } owner = null; contents = null; popup = null; recycle(this); } /** * Makes the {@code Popup} visible. If the popup has a * heavy-weight container, we try to snapshot the background. * If the {@code Popup} is currently visible, it remains visible. */ @Override public void show() { if (heavyWeightContainer != null) { snapshot(); } popup.show(); } /** * Reinitializes this ShadowPopup using the given parameters. * * @param owner component mouse coordinates are relative to, may be null * @param contents the contents of the popup * @param x the desired x location of the popup * @param y the desired y location of the popup * @param popup the popup to wrap */ private void reset(Component owner, Component contents, int x, int y, Popup popup) { this.owner = owner; this.contents = contents; this.popup = popup; this.x = x; this.y = y; if (owner instanceof JComboBox) { return; } // Do not install the shadow border when the contents // has a preferred size less than or equal to 0. // We can't use the size, because it is(0, 0) for new popups. Dimension contentsPrefSize = contents.getPreferredSize(); if ((contentsPrefSize.width <= 0) || (contentsPrefSize.height <= 0)) { return; } for (Container p = contents.getParent(); p != null; p = p.getParent()) { if ((p instanceof JWindow) || (p instanceof Panel)) { // Workaround for the gray rect problem. p.setBackground(contents.getBackground()); heavyWeightContainer = p; break; } } JComponent parent = (JComponent) contents.getParent(); oldOpaque = parent.isOpaque(); oldBorder = parent.getBorder(); parent.setOpaque(false); parent.setBorder(SHADOW_BORDER); // Pack it because we have changed the border. if (heavyWeightContainer != null) { heavyWeightContainer.setSize( heavyWeightContainer.getPreferredSize()); } else { parent.setSize(parent.getPreferredSize()); } } /** * The 'scratch pad' objects used to calculate dirty regions of * the screen snapshots. * * @see #snapshot() */ private static final Point POINT = new Point(); private static final Rectangle RECT = new Rectangle(); /** * Snapshots the background. The snapshots are stored as client * properties of the contents' parent. The next time the border is drawn, * this background will be used.<p> * * Uses a robot on the default screen device to capture the screen * region under the drop shadow. Does <em>not</em> use the window's * device, because that may be an outdated device (due to popup reuse) * and the robot's origin seems to be adjusted with the default screen * device. * * @see #show() * @see com.jgoodies.looks.common.ShadowPopupBorder * @see Robot#createScreenCapture(Rectangle) */ private void snapshot() { try { Dimension size = heavyWeightContainer.getPreferredSize(); int width = size.width; int height = size.height; // Avoid unnecessary and illegal screen captures // for degenerated popups. if ((width <= 0) || (height <= SHADOW_SIZE)) { return; } Robot robot = new Robot(); // uses the default screen device RECT.setBounds(x, y + height - SHADOW_SIZE, width, SHADOW_SIZE); BufferedImage hShadowBg = robot.createScreenCapture(RECT); RECT.setBounds(x + width - SHADOW_SIZE, y, SHADOW_SIZE, height - SHADOW_SIZE); BufferedImage vShadowBg = robot.createScreenCapture(RECT); JComponent parent = (JComponent) contents.getParent(); parent.putClientProperty(ShadowPopupFactory.PROP_HORIZONTAL_BACKGROUND, hShadowBg); parent.putClientProperty(ShadowPopupFactory.PROP_VERTICAL_BACKGROUND, vShadowBg); Container layeredPane = getLayeredPane(); if (layeredPane == null) { // This could happen if owner is null. return; } int layeredPaneWidth = layeredPane.getWidth(); int layeredPaneHeight = layeredPane.getHeight(); POINT.x = x; POINT.y = y; SwingUtilities.convertPointFromScreen(POINT, layeredPane); // If needed paint dirty region of the horizontal snapshot. RECT.x = POINT.x; RECT.y = POINT.y + height - SHADOW_SIZE; RECT.width = width; RECT.height = SHADOW_SIZE; if ((RECT.x + RECT.width) > layeredPaneWidth) { RECT.width = layeredPaneWidth - RECT.x; } if ((RECT.y + RECT.height) > layeredPaneHeight) { RECT.height = layeredPaneHeight - RECT.y; } if (!RECT.isEmpty()) { Graphics g = hShadowBg.createGraphics(); g.translate(-RECT.x, -RECT.y); g.setClip(RECT); if (layeredPane instanceof JComponent) { JComponent c = (JComponent) layeredPane; boolean doubleBuffered = c.isDoubleBuffered(); c.setDoubleBuffered(false); c.paintAll(g); c.setDoubleBuffered(doubleBuffered); } else { layeredPane.paintAll(g); } g.dispose(); } // If needed paint dirty region of the vertical snapshot. RECT.x = POINT.x + width - SHADOW_SIZE; RECT.y = POINT.y; RECT.width = SHADOW_SIZE; RECT.height = height - SHADOW_SIZE; if ((RECT.x + RECT.width) > layeredPaneWidth) { RECT.width = layeredPaneWidth - RECT.x; } if ((RECT.y + RECT.height) > layeredPaneHeight) { RECT.height = layeredPaneHeight - RECT.y; } if (!RECT.isEmpty()) { Graphics g = vShadowBg.createGraphics(); g.translate(-RECT.x, -RECT.y); g.setClip(RECT); if (layeredPane instanceof JComponent) { JComponent c = (JComponent) layeredPane; boolean doubleBuffered = c.isDoubleBuffered(); c.setDoubleBuffered(false); c.paintAll(g); c.setDoubleBuffered(doubleBuffered); } else { layeredPane.paintAll(g); } g.dispose(); } } catch (AWTException e) { canSnapshot = false; } catch (SecurityException e) { canSnapshot = false; } } /** * @return the top level layered pane which contains the owner. */ private Container getLayeredPane() { // The code below is copied from PopupFactory#LightWeightPopup#show() Container parent = null; if (owner != null) { parent = owner instanceof Container ? (Container) owner : owner.getParent(); } // Try to find a JLayeredPane and Window to add for (Container p = parent; p != null; p = p.getParent()) { if (p instanceof JRootPane) { if (p.getParent() instanceof JInternalFrame) { continue; } parent = ((JRootPane) p).getLayeredPane(); // Continue, so that if there is a higher JRootPane, we'll // pick it up. } else if (p instanceof Window) { if (parent == null) { parent = p; } break; } else if (p instanceof JApplet) { // Painting code stops at Applets, we don't want // to add to a Component above an Applet otherwise // you'll never see it painted. break; } } return parent; } }