ShadowPopup.java
package com.jsql.view.swing.shadow;
/*
* 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
* HOLDER 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.
*/
import com.jsql.util.LogLevelUtil;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import javax.swing.*;
import javax.swing.border.Border;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.List;
/**
* 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 {
/**
* Log4j logger sent to view.
*/
private static final Logger LOGGER = LogManager.getRootLogger();
/**
* 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;
/**
* 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;
private int 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 heavyweight container of the popup contents, may be null.
*/
private Container heavyWeightContainer;
/**
* 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();
/**
* Returns a previously used {@code ShadowPopup}, or a new one
* if none of the popups have been recycled.
*/
public static Popup getInstance(Component owner, Component contents, int x, int y, Popup delegate) {
ShadowPopup result;
synchronized (ShadowPopup.class) {
if (cache == null) {
cache = new ArrayList<>(MAX_CACHE_SIZE);
}
if (!cache.isEmpty()) {
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);
}
}
}
/**
* 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 (this.contents == null) {
return;
}
JComponent parent = (JComponent) this.contents.getParent();
this.popup.hide();
if (parent != null && SHADOW_BORDER.equals(parent.getBorder())) {
parent.setBorder(this.oldBorder);
parent.setOpaque(this.oldOpaque);
this.oldBorder = null;
if (this.heavyWeightContainer != null) {
parent.putClientProperty(ShadowPopupFactory.PROP_HORIZONTAL_BACKGROUND, null);
parent.putClientProperty(ShadowPopupFactory.PROP_VERTICAL_BACKGROUND, null);
this.heavyWeightContainer = null;
}
}
this.owner = null;
this.contents = null;
this.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 (this.heavyWeightContainer != null) {
this.snapshot();
}
// Fix #95477: IllegalArgumentException on show()
try {
this.popup.show();
} catch (IllegalArgumentException e) {
LOGGER.log(LogLevelUtil.CONSOLE_JAVA, e.getMessage(), e);
}
}
/**
* 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;
// 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.
var contentsPrefSize = new Dimension();
// Fix #4172: NullPointerException on getPreferredSize()
// Implementation by javax.swing.plaf.metal.MetalToolTipUI.getPreferredSize()
try {
contentsPrefSize = contents.getPreferredSize();
} catch(NullPointerException e) {
LOGGER.log(LogLevelUtil.CONSOLE_JAVA, e, e);
}
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());
this.heavyWeightContainer = p;
break;
}
}
JComponent parent = (JComponent) contents.getParent();
this.oldOpaque = parent.isOpaque();
this.oldBorder = parent.getBorder();
parent.setOpaque(false);
parent.setBorder(SHADOW_BORDER);
// Pack it because we have changed the border.
if (this.heavyWeightContainer != null) {
this.heavyWeightContainer.setSize(this.heavyWeightContainer.getPreferredSize());
} else {
parent.setSize(parent.getPreferredSize());
}
}
/**
* 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 = this.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;
}
var robot = new Robot(); // uses the default screen device
RECT.setBounds(this.x, this.y + height - SHADOW_SIZE, width, SHADOW_SIZE);
BufferedImage hShadowBg = robot.createScreenCapture(RECT);
RECT.setBounds(this.x + width - SHADOW_SIZE, this.y, SHADOW_SIZE, height - SHADOW_SIZE);
BufferedImage vShadowBg = robot.createScreenCapture(RECT);
JComponent parent = (JComponent) this.contents.getParent();
parent.putClientProperty(ShadowPopupFactory.PROP_HORIZONTAL_BACKGROUND, hShadowBg);
parent.putClientProperty(ShadowPopupFactory.PROP_VERTICAL_BACKGROUND, vShadowBg);
Container layeredPane = this.getLayeredPane();
if (layeredPane == null) {
return; // This could happen if owner is null.
}
int layeredPaneWidth = layeredPane.getWidth();
int layeredPaneHeight = layeredPane.getHeight();
POINT.x = this.x;
POINT.y = this.y;
SwingUtilities.convertPointFromScreen(POINT, layeredPane);
this.paintHorizontalSnapshot(width, height, hShadowBg, layeredPane, layeredPaneWidth, layeredPaneHeight);
this.paintVerticalSnapshot(width, height, vShadowBg, layeredPane, layeredPaneWidth, layeredPaneHeight);
} catch (AWTException | SecurityException | IllegalArgumentException e) {
LOGGER.log(LogLevelUtil.IGNORE, e);
}
}
private void paintVerticalSnapshot(int width, int height, BufferedImage vShadowBg, Container layeredPane, int layeredPaneWidth, int layeredPaneHeight) {
// 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;
this.extracted(vShadowBg, layeredPane, layeredPaneWidth, layeredPaneHeight);
}
private void paintHorizontalSnapshot(int width, int height, BufferedImage hShadowBg, Container layeredPane, int layeredPaneWidth, int layeredPaneHeight) {
// 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;
this.extracted(hShadowBg, layeredPane, layeredPaneWidth, layeredPaneHeight);
}
private void extracted(BufferedImage shadowBg, Container layeredPane, int layeredPaneWidth, int layeredPaneHeight) {
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 = shadowBg.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);
ShadowPopup.paintAll(c, g);
c.setDoubleBuffered(doubleBuffered);
} else {
layeredPane.paintAll(g);
}
g.dispose();
}
}
private static void paintAll(JComponent c, Graphics g) {
// Fix #3127, Fix #6772, Fix #48907: Multiple Exceptions on paintAll()
try {
c.paintAll(g);
} catch (IllegalArgumentException | ArrayIndexOutOfBoundsException | NullPointerException e) {
LOGGER.log(LogLevelUtil.CONSOLE_JAVA, e.getMessage(), e);
}
}
/**
* @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 (this.owner != null) {
parent = this.owner instanceof Container
? (Container) this.owner
: this.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)) {
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;
}
}
return parent;
}
}