001: /*
002: * Copyright (c) 2007 JGoodies Karsten Lentzsch. All Rights Reserved.
003: *
004: * Redistribution and use in source and binary forms, with or without
005: * modification, are permitted provided that the following conditions are met:
006: *
007: * o Redistributions of source code must retain the above copyright notice,
008: * this list of conditions and the following disclaimer.
009: *
010: * o Redistributions in binary form must reproduce the above copyright notice,
011: * this list of conditions and the following disclaimer in the documentation
012: * and/or other materials provided with the distribution.
013: *
014: * o Neither the name of JGoodies Karsten Lentzsch nor the names of
015: * its contributors may be used to endorse or promote products derived
016: * from this software without specific prior written permission.
017: *
018: * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
019: * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
020: * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
021: * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
022: * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
023: * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
024: * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
025: * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
026: * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
027: * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
028: * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
029: */
030:
031: package com.jgoodies.looks.common;
032:
033: import java.awt.*;
034: import java.awt.image.BufferedImage;
035: import java.util.ArrayList;
036: import java.util.List;
037:
038: import javax.swing.*;
039: import javax.swing.border.Border;
040:
041: /**
042: * Does all the magic for getting popups with drop shadows.
043: * It adds the drop shadow border to the Popup,
044: * in <code>#show</code> it snapshots the screen background as needed,
045: * and in <code>#hide</code> it cleans up all changes made before.
046: *
047: * @author Andrej Golovnin
048: * @version $Revision: 1.6 $
049: *
050: * @see com.jgoodies.looks.common.ShadowPopupBorder
051: * @see com.jgoodies.looks.common.ShadowPopupFactory
052: */
053: public final class ShadowPopup extends Popup {
054:
055: /**
056: * Max number of items to store in the cache.
057: */
058: private static final int MAX_CACHE_SIZE = 5;
059:
060: /**
061: * The cache to use for ShadowPopups.
062: */
063: private static List cache;
064:
065: /**
066: * The singleton instance used to draw all borders.
067: */
068: private static final Border SHADOW_BORDER = ShadowPopupBorder
069: .getInstance();
070:
071: /**
072: * The size of the drop shadow.
073: */
074: private static final int SHADOW_SIZE = 5;
075:
076: /**
077: * Indicates whether we can make snapshots from screen or not.
078: */
079: private static boolean canSnapshot = true;
080:
081: /**
082: * The component mouse coordinates are relative to, may be null.
083: */
084: private Component owner;
085:
086: /**
087: * The contents of the popup.
088: */
089: private Component contents;
090:
091: /**
092: * The desired x and y location of the popup.
093: */
094: private int x, y;
095:
096: /**
097: * The real popup. The #show() and #hide() methods will delegate
098: * all calls to these popup.
099: */
100: private Popup popup;
101:
102: /**
103: * The border of the contents' parent replaced by SHADOW_BORDER.
104: */
105: private Border oldBorder;
106:
107: /**
108: * The old value of the opaque property of the contents' parent.
109: */
110: private boolean oldOpaque;
111:
112: /**
113: * The heavy weight container of the popup contents, may be null.
114: */
115: private Container heavyWeightContainer;
116:
117: /**
118: * Returns a previously used <code>ShadowPopup</code>, or a new one
119: * if none of the popups have been recycled.
120: */
121: static Popup getInstance(Component owner, Component contents,
122: int x, int y, Popup delegate) {
123: ShadowPopup result;
124: synchronized (ShadowPopup.class) {
125: if (cache == null) {
126: cache = new ArrayList(MAX_CACHE_SIZE);
127: }
128: if (cache.size() > 0) {
129: result = (ShadowPopup) cache.remove(0);
130: } else {
131: result = new ShadowPopup();
132: }
133: }
134: result.reset(owner, contents, x, y, delegate);
135: return result;
136: }
137:
138: /**
139: * Recycles the ShadowPopup.
140: */
141: private static void recycle(ShadowPopup popup) {
142: synchronized (ShadowPopup.class) {
143: if (cache.size() < MAX_CACHE_SIZE) {
144: cache.add(popup);
145: }
146: }
147: }
148:
149: public static boolean canSnapshot() {
150: return canSnapshot;
151: }
152:
153: /**
154: * Hides and disposes of the <code>Popup</code>. Once a <code>Popup</code>
155: * has been disposed you should no longer invoke methods on it. A
156: * <code>dispose</code>d <code>Popup</code> may be reclaimed and later used
157: * based on the <code>PopupFactory</code>. As such, if you invoke methods
158: * on a <code>disposed</code> <code>Popup</code>, indeterminate
159: * behavior will result.<p>
160: *
161: * In addition to the superclass behavior, we reset the stored
162: * horizontal and vertical drop shadows - if any.
163: */
164: public void hide() {
165: if (contents == null)
166: return;
167:
168: JComponent parent = (JComponent) contents.getParent();
169: popup.hide();
170: if ((parent != null) && parent.getBorder() == SHADOW_BORDER) {
171: parent.setBorder(oldBorder);
172: parent.setOpaque(oldOpaque);
173: oldBorder = null;
174: if (heavyWeightContainer != null) {
175: parent.putClientProperty(
176: ShadowPopupFactory.PROP_HORIZONTAL_BACKGROUND,
177: null);
178: parent.putClientProperty(
179: ShadowPopupFactory.PROP_VERTICAL_BACKGROUND,
180: null);
181: heavyWeightContainer = null;
182: }
183: }
184: owner = null;
185: contents = null;
186: popup = null;
187: recycle(this );
188: }
189:
190: /**
191: * Makes the <code>Popup</code> visible. If the popup has a
192: * heavy-weight container, we try to snapshot the background.
193: * If the <code>Popup</code> is currently visible, it remains visible.
194: */
195: public void show() {
196: if (heavyWeightContainer != null) {
197: snapshot();
198: }
199: popup.show();
200: }
201:
202: /**
203: * Reinitializes this ShadowPopup using the given parameters.
204: *
205: * @param owner component mouse coordinates are relative to, may be null
206: * @param contents the contents of the popup
207: * @param x the desired x location of the popup
208: * @param y the desired y location of the popup
209: * @param popup the popup to wrap
210: */
211: private void reset(Component owner, Component contents, int x,
212: int y, Popup popup) {
213: this .owner = owner;
214: this .contents = contents;
215: this .popup = popup;
216: this .x = x;
217: this .y = y;
218: if (owner instanceof JComboBox) {
219: return;
220: }
221: // Do not install the shadow border when the contents
222: // has a preferred size less than or equal to 0.
223: // We can't use the size, because it is(0, 0) for new popups.
224: Dimension contentsPrefSize = contents.getPreferredSize();
225: if ((contentsPrefSize.width <= 0)
226: || (contentsPrefSize.height <= 0)) {
227: return;
228: }
229: for (Container p = contents.getParent(); p != null; p = p
230: .getParent()) {
231: if ((p instanceof JWindow) || (p instanceof Panel)) {
232: // Workaround for the gray rect problem.
233: p.setBackground(contents.getBackground());
234: heavyWeightContainer = p;
235: break;
236: }
237: }
238: JComponent parent = (JComponent) contents.getParent();
239: oldOpaque = parent.isOpaque();
240: oldBorder = parent.getBorder();
241: parent.setOpaque(false);
242: parent.setBorder(SHADOW_BORDER);
243: // Pack it because we have changed the border.
244: if (heavyWeightContainer != null) {
245: heavyWeightContainer.setSize(heavyWeightContainer
246: .getPreferredSize());
247: } else {
248: parent.setSize(parent.getPreferredSize());
249: }
250: }
251:
252: /**
253: * The 'scratch pad' objects used to calculate dirty regions of
254: * the screen snapshots.
255: *
256: * @see #snapshot()
257: */
258: private static final Point POINT = new Point();
259: private static final Rectangle RECT = new Rectangle();
260:
261: /**
262: * Snapshots the background. The snapshots are stored as client
263: * properties of the contents' parent. The next time the border is drawn,
264: * this background will be used.<p>
265: *
266: * Uses a robot on the default screen device to capture the screen
267: * region under the drop shadow. Does <em>not</em> use the window's
268: * device, because that may be an outdated device (due to popup reuse)
269: * and the robot's origin seems to be adjusted with the default screen
270: * device.
271: *
272: * @see #show()
273: * @see com.jgoodies.looks.common.ShadowPopupBorder
274: * @see Robot#createScreenCapture(Rectangle)
275: */
276: private void snapshot() {
277: try {
278: Dimension size = heavyWeightContainer.getPreferredSize();
279: int width = size.width;
280: int height = size.height;
281:
282: // Avoid unnecessary and illegal screen captures
283: // for degenerated popups.
284: if ((width <= 0) || (height <= SHADOW_SIZE)) {
285: return;
286: }
287:
288: Robot robot = new Robot(); // uses the default screen device
289:
290: RECT.setBounds(x, y + height - SHADOW_SIZE, width,
291: SHADOW_SIZE);
292: BufferedImage hShadowBg = robot.createScreenCapture(RECT);
293:
294: RECT.setBounds(x + width - SHADOW_SIZE, y, SHADOW_SIZE,
295: height - SHADOW_SIZE);
296: BufferedImage vShadowBg = robot.createScreenCapture(RECT);
297:
298: JComponent parent = (JComponent) contents.getParent();
299: parent.putClientProperty(
300: ShadowPopupFactory.PROP_HORIZONTAL_BACKGROUND,
301: hShadowBg);
302: parent.putClientProperty(
303: ShadowPopupFactory.PROP_VERTICAL_BACKGROUND,
304: vShadowBg);
305:
306: Container layeredPane = getLayeredPane();
307: if (layeredPane == null) {
308: // This could happen if owner is null.
309: return;
310: }
311:
312: int layeredPaneWidth = layeredPane.getWidth();
313: int layeredPaneHeight = layeredPane.getHeight();
314:
315: POINT.x = x;
316: POINT.y = y;
317: SwingUtilities.convertPointFromScreen(POINT, layeredPane);
318:
319: // If needed paint dirty region of the horizontal snapshot.
320: RECT.x = POINT.x;
321: RECT.y = POINT.y + height - SHADOW_SIZE;
322: RECT.width = width;
323: RECT.height = SHADOW_SIZE;
324:
325: if ((RECT.x + RECT.width) > layeredPaneWidth) {
326: RECT.width = layeredPaneWidth - RECT.x;
327: }
328: if ((RECT.y + RECT.height) > layeredPaneHeight) {
329: RECT.height = layeredPaneHeight - RECT.y;
330: }
331: if (!RECT.isEmpty()) {
332: Graphics g = hShadowBg.createGraphics();
333: g.translate(-RECT.x, -RECT.y);
334: g.setClip(RECT);
335: if (layeredPane instanceof JComponent) {
336: JComponent c = (JComponent) layeredPane;
337: boolean doubleBuffered = c.isDoubleBuffered();
338: c.setDoubleBuffered(false);
339: c.paintAll(g);
340: c.setDoubleBuffered(doubleBuffered);
341: } else {
342: layeredPane.paintAll(g);
343: }
344: g.dispose();
345: }
346:
347: // If needed paint dirty region of the vertical snapshot.
348: RECT.x = POINT.x + width - SHADOW_SIZE;
349: RECT.y = POINT.y;
350: RECT.width = SHADOW_SIZE;
351: RECT.height = height - SHADOW_SIZE;
352:
353: if ((RECT.x + RECT.width) > layeredPaneWidth) {
354: RECT.width = layeredPaneWidth - RECT.x;
355: }
356: if ((RECT.y + RECT.height) > layeredPaneHeight) {
357: RECT.height = layeredPaneHeight - RECT.y;
358: }
359: if (!RECT.isEmpty()) {
360: Graphics g = vShadowBg.createGraphics();
361: g.translate(-RECT.x, -RECT.y);
362: g.setClip(RECT);
363: if (layeredPane instanceof JComponent) {
364: JComponent c = (JComponent) layeredPane;
365: boolean doubleBuffered = c.isDoubleBuffered();
366: c.setDoubleBuffered(false);
367: c.paintAll(g);
368: c.setDoubleBuffered(doubleBuffered);
369: } else {
370: layeredPane.paintAll(g);
371: }
372: g.dispose();
373: }
374: } catch (AWTException e) {
375: canSnapshot = false;
376: } catch (SecurityException e) {
377: canSnapshot = false;
378: }
379: }
380:
381: /**
382: * @return the top level layered pane which contains the owner.
383: */
384: private Container getLayeredPane() {
385: // The code below is copied from PopupFactory#LightWeightPopup#show()
386: Container parent = null;
387: if (owner != null) {
388: parent = owner instanceof Container ? (Container) owner
389: : owner.getParent();
390: }
391: // Try to find a JLayeredPane and Window to add
392: for (Container p = parent; p != null; p = p.getParent()) {
393: if (p instanceof JRootPane) {
394: if (p.getParent() instanceof JInternalFrame) {
395: continue;
396: }
397: parent = ((JRootPane) p).getLayeredPane();
398: // Continue, so that if there is a higher JRootPane, we'll
399: // pick it up.
400: } else if (p instanceof Window) {
401: if (parent == null) {
402: parent = p;
403: }
404: break;
405: } else if (p instanceof JApplet) {
406: // Painting code stops at Applets, we don't want
407: // to add to a Component above an Applet otherwise
408: // you'll never see it painted.
409: break;
410: }
411: }
412: return parent;
413: }
414:
415: }
|