package net.bull.javamelody.swing.util; import java.awt.AWTException; import java.awt.Component; import java.awt.Container; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Image; import java.awt.Insets; 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.ImageIcon; 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.LookAndFeel; import javax.swing.Popup; import javax.swing.PopupFactory; import javax.swing.SwingUtilities; import javax.swing.border.AbstractBorder; import javax.swing.border.Border; /** * The JGoodies Looks implementation of <code>PopupFactory</code>. * Adds a drop shadow border to all popups except ComboBox popups. * * @author JGoodies Karsten Lentzsch * @author Andrej Golovnin * @author Karsten Lentzsch * @version 1.2 * * @see java.awt.AWTPermission * @see java.awt.Robot * @see javax.swing.Popup * @see LookAndFeel#initialize * @see LookAndFeel#uninitialize */ public final class ShadowPopupFactory extends PopupFactory { /** * In the case of heavy weight popups, snapshots of the screen background * will be stored as client properties of the popup contents' parent. * These snapshots will be used by the popup border to simulate the drop * shadow effect. The two following constants define the names of * these client properties. */ static final String PROP_HORIZONTAL_BACKGROUND = "hShadowBg"; static final String PROP_VERTICAL_BACKGROUND = "vShadowBg"; /** * The 'scratch pad' objects used to calculate dirty regions of * the screen snapshots. */ static final Point POINT = new Point(); static final Rectangle RECT = new Rectangle(); /** * The PopupFactory used before this PopupFactory has been installed * in <code>#install</code>. Used to restored the original state * in <code>#uninstall</code>. */ private final PopupFactory storedFactory; // Instance Creation ****************************************************** private ShadowPopupFactory(PopupFactory storedFactory) { super(); this.storedFactory = storedFactory; } // API ******************************************************************** /** * Installs the ShadowPopupFactory as the shared popup factory * on non-Mac platforms. Also stores the previously set factory, * so that it can be restored in <code>#uninstall</code>.<p> * * In some Mac Java environments the popup factory throws * a NullPointerException when we call <code>#getPopup</code>.<p> * * The Mac case shows that we may have problems replacing * non PopupFactory instances. Therefore we should consider * replacing only instances of PopupFactory. * * @see #uninstall() */ public static void install() { final String os = System.getProperty("os.name"); final boolean macintosh = os != null && os.indexOf("Mac") != -1; if (macintosh) { return; } final PopupFactory factory = PopupFactory.getSharedInstance(); if (factory instanceof ShadowPopupFactory) { return; } PopupFactory.setSharedInstance(new ShadowPopupFactory(factory)); } /** * Uninstalls the ShadowPopupFactory and restores the original * popup factory as the new shared popup factory. * * @see #install() */ public static void uninstall() { final PopupFactory factory = PopupFactory.getSharedInstance(); if (!(factory instanceof ShadowPopupFactory)) { return; } final PopupFactory stored = ((ShadowPopupFactory) factory).storedFactory; PopupFactory.setSharedInstance(stored); } /** * Creates a <code>Popup</code> for the Component <code>owner</code> * containing the Component <code>contents</code>. In addition to * the superclass behavior, we try to return a Popup that has a drop shadow, * if popup drop shadows are active - as returned by * <code>Options#isPopupDropShadowActive</code>.<p> * * <code>owner</code> is used to determine which <code>Window</code> the new * <code>Popup</code> will parent the <code>Component</code> the * <code>Popup</code> creates to. A null <code>owner</code> implies there * is no valid parent. <code>x</code> and * <code>y</code> specify the preferred initial location to place * the <code>Popup</code> at. Based on screen size, or other paramaters, * the <code>Popup</code> may not display at <code>x</code> and * <code>y</code>.<p> * * We invoke the super <code>#getPopup</code>, not the one in the * stored factory, because the popup type is set in this instance, * not in the stored one. * * @param owner Component mouse coordinates are relative to, may be null * @param contents Contents of the Popup * @param x Initial x screen coordinate * @param y Initial y screen coordinate * @return Popup containing Contents */ @Override public Popup getPopup(Component owner, Component contents, int x, int y) { final Popup popup = super.getPopup(owner, contents, x, y); return ShadowPopup.getInstance(owner, contents, x, y, popup); } /** * Does all the magic for getting popups with drop shadows. * It adds the drop shadow border to the Popup, * in <code>#show</code> it snapshots the screen background as needed, * and in <code>#hide</code> it cleans up all changes made before. * * @author Andrej Golovnin * @version 1.4 */ public static 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; /** * 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</code>, 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<>(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</code>. Once a <code>Popup</code> * has been disposed you should no longer invoke methods on it. A * <code>dispose</code>d <code>Popup</code> may be reclaimed and later used * based on the <code>PopupFactory</code>. As such, if you invoke methods * on a <code>disposed</code> <code>Popup</code>, 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; } final 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</code> visible. If the popup has a * heavy-weight container, we try to snapshot the background. * If the <code>Popup</code> is currently visible, it remains visible. */ @Override public void show() { if (heavyWeightContainer != null) { snapshot(); } popup.show(); } /** * Reinitializes this ShadowPopup using the given parameters. * * @param newOwner component mouse coordinates are relative to, may be null * @param newContents the contents of the popup * @param newX the desired x location of the popup * @param newY the desired y location of the popup * @param newPopup the popup to wrap */ private void reset(Component newOwner, Component newContents, int newX, int newY, Popup newPopup) { this.owner = newOwner; this.contents = newContents; this.popup = newPopup; this.x = newX; this.y = newY; if (newOwner 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. final Dimension contentsPrefSize = newContents.getPreferredSize(); if (contentsPrefSize.width <= 0 || contentsPrefSize.height <= 0) { return; } for (Container p = newContents.getParent(); p != null; p = p.getParent()) { if (p instanceof JWindow || p instanceof Panel) { // Workaround for the gray rect problem. p.setBackground(newContents.getBackground()); heavyWeightContainer = p; break; } } final JComponent parent = (JComponent) newContents.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()); } } /** * 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. * * @return boolean * @see #show() */ private boolean snapshot() { try { final Dimension size = heavyWeightContainer.getPreferredSize(); final int width = size.width; final int height = size.height; // Avoid unnecessary and illegal screen captures // for degenerated popups. if (width <= 0 || height <= SHADOW_SIZE) { return false; } final Robot robot = new Robot(); // uses the default screen device RECT.setBounds(x, y + height - SHADOW_SIZE, width, SHADOW_SIZE); final BufferedImage hShadowBg = robot.createScreenCapture(RECT); RECT.setBounds(x + width - SHADOW_SIZE, y, SHADOW_SIZE, height - SHADOW_SIZE); final BufferedImage vShadowBg = robot.createScreenCapture(RECT); final JComponent parent = (JComponent) contents.getParent(); parent.putClientProperty(ShadowPopupFactory.PROP_HORIZONTAL_BACKGROUND, hShadowBg); parent.putClientProperty(ShadowPopupFactory.PROP_VERTICAL_BACKGROUND, vShadowBg); final Container layeredPane = getLayeredPane(); if (layeredPane == null) { // This could happen if owner is null. return false; } 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; paintShadow(hShadowBg, layeredPane); // 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; paintShadow(vShadowBg, layeredPane); } catch (final AWTException e) { return true; } catch (final SecurityException e) { return true; } return false; } /** * If needed paint dirty region of the snapshot * * @param shadowBg * BufferedImage * @param layeredPane * Container */ private void paintShadow(final BufferedImage shadowBg, final Container layeredPane) { final int layeredPaneWidth = layeredPane.getWidth(); final int layeredPaneHeight = layeredPane.getHeight(); 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()) { final Graphics g = shadowBg.createGraphics(); g.translate(-RECT.x, -RECT.y); g.setClip(RECT); if (layeredPane instanceof JComponent) { final JComponent c = (JComponent) layeredPane; final boolean doubleBuffered = c.isDoubleBuffered(); c.setDoubleBuffered(false); c.paintAll(g); c.setDoubleBuffered(doubleBuffered); } else { layeredPane.paintAll(g); } g.dispose(); } } /** * @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; } } /** * A border with a drop shadow intended to be used as the outer border * of popups. Can paint the screen background if used with heavy-weight * popup windows. * * @author Stefan Matthias Aust * @author Karsten Lentzsch * @author Andrej Golovnin * @version 1.2 */ public static class ShadowPopupBorder extends AbstractBorder { /** * */ private static final long serialVersionUID = -8512231832213353638L; /** * The drop shadow needs 5 pixels at the bottom and the right hand side. */ private static final int SHADOW_SIZE = 5; /** * The singleton instance used to draw all borders. */ private static final ShadowPopupBorder INSTANCE = new ShadowPopupBorder(); /** * The drop shadow is created from a PNG image with 8 bit alpha channel. */ @SuppressWarnings("all") private static final Image SHADOW = new ImageIcon( ShadowPopupBorder.class.getResource("/icons/shadow.png")).getImage(); // Instance Creation ***************************************************** /** * Returns the singleton instance used to draw all borders. * @return ShadowPopupBorder */ public static ShadowPopupBorder getInstance() { return INSTANCE; } /** * Paints the border for the specified component with the specified position and size. * @param c the component for which this border is being painted * @param g the paint graphics * @param x the x position of the painted border * @param y the y position of the painted border * @param width the width of the painted border * @param height the height of the painted border */ @Override public void paintBorder(Component c, Graphics g, int x, int y, int width, int height) { // fake drop shadow effect in case of heavy weight popups final JComponent popup = (JComponent) c; final Image hShadowBg = (Image) popup .getClientProperty(ShadowPopupFactory.PROP_HORIZONTAL_BACKGROUND); if (hShadowBg != null) { g.drawImage(hShadowBg, x, y + height - 5, c); } final Image vShadowBg = (Image) popup .getClientProperty(ShadowPopupFactory.PROP_VERTICAL_BACKGROUND); if (vShadowBg != null) { g.drawImage(vShadowBg, x + width - 5, y, c); } // draw drop shadow g.drawImage(SHADOW, x + 5, y + height - 5, x + 10, y + height, 0, 6, 5, 11, null, c); g.drawImage(SHADOW, x + 10, y + height - 5, x + width - 5, y + height, 5, 6, 6, 11, null, c); g.drawImage(SHADOW, x + width - 5, y + 5, x + width, y + 10, 6, 0, 11, 5, null, c); g.drawImage(SHADOW, x + width - 5, y + 10, x + width, y + height - 5, 6, 5, 11, 6, null, c); g.drawImage(SHADOW, x + width - 5, y + height - 5, x + width, y + height, 6, 6, 11, 11, null, c); } /** * Returns the insets of the border. * @param c nothing * @return Insets */ @Override public Insets getBorderInsets(Component c) { return new Insets(0, 0, SHADOW_SIZE, SHADOW_SIZE); } /** * Reinitializes the insets parameter with this Border's current Insets. * @param c the component for which this border insets value applies * @param insets the object to be reinitialized * @return the <code>insets</code> object */ @Override public Insets getBorderInsets(Component c, Insets insets) { insets.left = 0; insets.top = 0; insets.right = SHADOW_SIZE; insets.bottom = SHADOW_SIZE; return insets; } } }