001: /**
002: * L2FProd.com Common Components 7.3 License.
003: *
004: * Copyright 2005-2007 L2FProd.com
005: *
006: * Licensed under the Apache License, Version 2.0 (the "License");
007: * you may not use this file except in compliance with the License.
008: * You may obtain a copy of the License at
009: *
010: * http://www.apache.org/licenses/LICENSE-2.0
011: *
012: * Unless required by applicable law or agreed to in writing, software
013: * distributed under the License is distributed on an "AS IS" BASIS,
014: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015: * See the License for the specific language governing permissions and
016: * limitations under the License.
017: */package com.l2fprod.common.swing;
018:
019: import java.awt.AlphaComposite;
020: import java.awt.BorderLayout;
021: import java.awt.Component;
022: import java.awt.Composite;
023: import java.awt.Container;
024: import java.awt.Dimension;
025: import java.awt.Graphics;
026: import java.awt.Graphics2D;
027: import java.awt.LayoutManager;
028: import java.awt.Rectangle;
029: import java.awt.event.ActionEvent;
030: import java.awt.event.ActionListener;
031: import java.awt.image.BufferedImage;
032: import java.beans.PropertyChangeEvent;
033: import java.beans.PropertyChangeListener;
034:
035: import javax.swing.AbstractAction;
036: import javax.swing.JComponent;
037: import javax.swing.JPanel;
038: import javax.swing.SwingUtilities;
039: import javax.swing.Timer;
040:
041: /**
042: * <code>JCollapsiblePane</code> provides a component which can collapse or
043: * expand its content area with animation and fade in/fade out effects.
044: * It also acts as a standard container for other Swing components.
045: *
046: * <p>
047: * In this example, the <code>JCollapsiblePane</code> is used to build
048: * a Search pane which can be shown and hidden on demand.
049: *
050: * <pre>
051: * <code>
052: * JCollapsiblePane cp = new JCollapsiblePane();
053: *
054: * // JCollapsiblePane can be used like any other container
055: * cp.setLayout(new BorderLayout());
056: *
057: * // the Controls panel with a textfield to filter the tree
058: * JPanel controls = new JPanel(new FlowLayout(FlowLayout.LEFT, 4, 0));
059: * controls.add(new JLabel("Search:"));
060: * controls.add(new JTextField(10));
061: * controls.add(new JButton("Refresh"));
062: * controls.setBorder(new TitledBorder("Filters"));
063: * cp.add("Center", controls);
064: *
065: * JFrame frame = new JFrame();
066: * frame.setLayout(new BorderLayout());
067: *
068: * // Put the "Controls" first
069: * frame.add("North", cp);
070: *
071: * // Then the tree - we assume the Controls would somehow filter the tree
072: * JScrollPane scroll = new JScrollPane(new JTree());
073: * frame.add("Center", scroll);
074: *
075: * // Show/hide the "Controls"
076: * JButton toggle = new JButton(cp.getActionMap().get(JCollapsiblePane.TOGGLE_ACTION));
077: * toggle.setText("Show/Hide Search Panel");
078: * frame.add("South", toggle);
079: *
080: * frame.pack();
081: * frame.setVisible(true);
082: * </code>
083: * </pre>
084: *
085: * <p>
086: * Note: <code>JCollapsiblePane</code> requires its parent container to have a
087: * {@link java.awt.LayoutManager} using {@link #getPreferredSize()} when
088: * calculating its layout (example {@link com.l2fprod.common.swing.PercentLayout},
089: * {@link java.awt.BorderLayout}).
090: *
091: * @javabean.attribute
092: * name="isContainer"
093: * value="Boolean.TRUE"
094: * rtexpr="true"
095: *
096: * @javabean.attribute
097: * name="containerDelegate"
098: * value="getContentPane"
099: *
100: * @javabean.class
101: * name="JCollapsiblePane"
102: * shortDescription="A pane which hides its content with an animation."
103: * stopClass="java.awt.Component"
104: *
105: * @author rbair (from the JDNC project)
106: * @author <a href="mailto:fred@L2FProd.com">Frederic Lavigne</a>
107: */
108: public class JCollapsiblePane extends JPanel {
109:
110: /**
111: * Used when generating PropertyChangeEvents for the "animationState" property
112: */
113: public final static String ANIMATION_STATE_KEY = "animationState";
114:
115: /**
116: * JCollapsible has a built-in toggle action which can be bound to buttons.
117: * Accesses the action through
118: * <code>collapsiblePane.getActionMap().get(JCollapsiblePane.TOGGLE_ACTION)</code>.
119: */
120: public final static String TOGGLE_ACTION = "toggle";
121:
122: /**
123: * The icon used by the "toggle" action when the JCollapsiblePane is
124: * expanded, i.e the icon which indicates the pane can be collapsed.
125: */
126: public final static String COLLAPSE_ICON = "collapseIcon";
127:
128: /**
129: * The icon used by the "toggle" action when the JCollapsiblePane is
130: * collapsed, i.e the icon which indicates the pane can be expanded.
131: */
132: public final static String EXPAND_ICON = "expandIcon";
133:
134: /**
135: * Indicates whether the component is collapsed or expanded
136: */
137: private boolean collapsed = false;
138:
139: /**
140: * Timer used for doing the transparency animation (fade-in)
141: */
142: private Timer animateTimer;
143: private AnimationListener animator;
144: private int currentHeight = -1;
145: private WrapperContainer wrapper;
146: private boolean useAnimation = true;
147: private AnimationParams animationParams;
148:
149: /**
150: * Constructs a new JCollapsiblePane with a {@link JPanel} as content pane and
151: * a vertical {@link PercentLayout} with a gap of 2 pixels as layout manager.
152: */
153: public JCollapsiblePane() {
154: super .setLayout(new BorderLayout(0, 0));
155:
156: JPanel panel = new JPanel();
157: panel.setLayout(new PercentLayout(PercentLayout.VERTICAL, 2));
158: setContentPane(panel);
159:
160: animator = new AnimationListener();
161: setAnimationParams(new AnimationParams(30, 8, 0.01f, 1.0f));
162:
163: // add an action to automatically toggle the state of the pane
164: getActionMap().put(TOGGLE_ACTION, new ToggleAction());
165: }
166:
167: /**
168: * Toggles the JCollapsiblePane state and updates its icon based on the
169: * JCollapsiblePane "collapsed" status.
170: */
171: private class ToggleAction extends AbstractAction implements
172: PropertyChangeListener {
173: public ToggleAction() {
174: super (TOGGLE_ACTION);
175: updateIcon();
176: // the action must track the collapsed status of the pane to update its
177: // icon
178: JCollapsiblePane.this .addPropertyChangeListener(
179: "collapsed", this );
180: }
181:
182: public void putValue(String key, Object newValue) {
183: super .putValue(key, newValue);
184: if (EXPAND_ICON.equals(key) || COLLAPSE_ICON.equals(key)) {
185: updateIcon();
186: }
187: }
188:
189: public void actionPerformed(ActionEvent e) {
190: setCollapsed(!isCollapsed());
191: }
192:
193: public void propertyChange(PropertyChangeEvent evt) {
194: updateIcon();
195: }
196:
197: void updateIcon() {
198: if (isCollapsed()) {
199: putValue(SMALL_ICON, getValue(EXPAND_ICON));
200: } else {
201: putValue(SMALL_ICON, getValue(COLLAPSE_ICON));
202: }
203: }
204: }
205:
206: /**
207: * Sets the content pane of this JCollapsiblePane. Components must be added
208: * to this content pane, not to the JCollapsiblePane.
209: *
210: * @param contentPanel
211: * @throws IllegalArgumentException
212: * if contentPanel is null
213: */
214: public void setContentPane(Container contentPanel) {
215: if (contentPanel == null) {
216: throw new IllegalArgumentException(
217: "Content pane can't be null");
218: }
219:
220: if (wrapper != null) {
221: super .remove(wrapper);
222: }
223: wrapper = new WrapperContainer(contentPanel);
224: super .addImpl(wrapper, BorderLayout.CENTER, -1);
225: }
226:
227: /**
228: * @return the content pane
229: */
230: public Container getContentPane() {
231: return wrapper.c;
232: }
233:
234: /**
235: * Overriden to redirect call to the content pane.
236: */
237: public void setLayout(LayoutManager mgr) {
238: // wrapper can be null when setLayout is called by "super()" constructor
239: if (wrapper != null) {
240: getContentPane().setLayout(mgr);
241: }
242: }
243:
244: /**
245: * Overriden to redirect call to the content pane.
246: */
247: protected void addImpl(Component comp, Object constraints, int index) {
248: getContentPane().add(comp, constraints, index);
249: }
250:
251: /**
252: * Overriden to redirect call to the content pane
253: */
254: public void remove(Component comp) {
255: getContentPane().remove(comp);
256: }
257:
258: /**
259: * Overriden to redirect call to the content pane.
260: */
261: public void remove(int index) {
262: getContentPane().remove(index);
263: }
264:
265: /**
266: * Overriden to redirect call to the content pane.
267: */
268: public void removeAll() {
269: getContentPane().removeAll();
270: }
271:
272: /**
273: * If true, enables the animation when pane is collapsed/expanded. If false,
274: * animation is turned off.
275: *
276: * <p>
277: * When animated, the <code>JCollapsiblePane</code> will progressively
278: * reduce (when collapsing) or enlarge (when expanding) the height of its
279: * content area until it becomes 0 or until it reaches the preferred height of
280: * the components it contains. The transparency of the content area will also
281: * change during the animation.
282: *
283: * <p>
284: * If not animated, the <code>JCollapsiblePane</code> will simply hide
285: * (collapsing) or show (expanding) its content area.
286: *
287: * @param animated
288: * @javabean.property bound="true" preferred="true"
289: */
290: public void setAnimated(boolean animated) {
291: if (animated != useAnimation) {
292: useAnimation = animated;
293: firePropertyChange("animated", !useAnimation, useAnimation);
294: }
295: }
296:
297: /**
298: * @return true if the pane is animated, false otherwise
299: * @see #setAnimated(boolean)
300: */
301: public boolean isAnimated() {
302: return useAnimation;
303: }
304:
305: /**
306: * @return true if the pane is collapsed, false if expanded
307: */
308: public boolean isCollapsed() {
309: return collapsed;
310: }
311:
312: /**
313: * Expands or collapses this <code>JCollapsiblePane</code>.
314: *
315: * <p>
316: * If the component is collapsed and <code>val</code> is false, then this
317: * call expands the JCollapsiblePane, such that the entire JCollapsiblePane
318: * will be visible. If {@link #isAnimated()} returns true, the expansion will
319: * be accompanied by an animation.
320: *
321: * <p>
322: * However, if the component is expanded and <code>val</code> is true, then
323: * this call collapses the JCollapsiblePane, such that the entire
324: * JCollapsiblePane will be invisible. If {@link #isAnimated()} returns true,
325: * the collapse will be accompanied by an animation.
326: *
327: * @see #isAnimated()
328: * @see #setAnimated(boolean)
329: * @javabean.property
330: * bound="true"
331: * preferred="true"
332: */
333: public void setCollapsed(boolean val) {
334: if (collapsed != val) {
335: collapsed = val;
336: if (isAnimated()) {
337: if (collapsed) {
338: setAnimationParams(new AnimationParams(30, Math
339: .max(8, wrapper.getHeight() / 10), 1.0f,
340: 0.01f));
341: animator.reinit(wrapper.getHeight(), 0);
342: animateTimer.start();
343: } else {
344: setAnimationParams(new AnimationParams(30,
345: Math.max(8, getContentPane()
346: .getPreferredSize().height / 10),
347: 0.01f, 1.0f));
348: animator.reinit(wrapper.getHeight(),
349: getContentPane().getPreferredSize().height);
350: animateTimer.start();
351: }
352: } else {
353: wrapper.c.setVisible(!collapsed);
354: invalidate();
355: doLayout();
356: }
357: repaint();
358: firePropertyChange("collapsed", !collapsed, collapsed);
359: }
360: }
361:
362: public Dimension getMinimumSize() {
363: return getPreferredSize();
364: }
365:
366: /**
367: * The critical part of the animation of this <code>JCollapsiblePane</code>
368: * relies on the calculation of its preferred size. During the animation, its
369: * preferred size (specially its height) will change, when expanding, from 0
370: * to the preferred size of the content pane, and the reverse when collapsing.
371: *
372: * @return this component preferred size
373: */
374: public Dimension getPreferredSize() {
375: /*
376: * The preferred size is calculated based on the current position of the
377: * component in its animation sequence. If the Component is expanded, then
378: * the preferred size will be the preferred size of the top component plus
379: * the preferred size of the embedded content container. <p>However, if the
380: * scroll up is in any state of animation, the height component of the
381: * preferred size will be the current height of the component (as contained
382: * in the currentHeight variable)
383: */
384: Dimension dim;
385: if (!isAnimated()) {
386: if (getContentPane().isVisible()) {
387: dim = getContentPane().getPreferredSize();
388: } else {
389: dim = super .getPreferredSize();
390: }
391: } else {
392: dim = new Dimension(getContentPane().getPreferredSize());
393: if (!getContentPane().isVisible() && currentHeight != -1) {
394: dim.height = currentHeight;
395: }
396: }
397: return dim;
398: }
399:
400: /**
401: * Sets the parameters controlling the animation
402: *
403: * @param params
404: * @throws IllegalArgumentException
405: * if params is null
406: */
407: private void setAnimationParams(AnimationParams params) {
408: if (params == null) {
409: throw new IllegalArgumentException("params can't be null");
410: }
411: if (animateTimer != null) {
412: animateTimer.stop();
413: }
414: animationParams = params;
415: animateTimer = new Timer(animationParams.waitTime, animator);
416: animateTimer.setInitialDelay(0);
417: }
418:
419: /**
420: * Tagging interface for containers in a JCollapsiblePane hierarchy who needs
421: * to be revalidated (invalidate/validate/repaint) when the pane is expanding
422: * or collapsing. Usually validating only the parent of the JCollapsiblePane
423: * is enough but there might be cases where the parent parent must be
424: * validated.
425: */
426: public static interface JCollapsiblePaneContainer {
427: Container getValidatingContainer();
428: }
429:
430: /**
431: * Parameters controlling the animations
432: */
433: private static class AnimationParams {
434: final int waitTime;
435: final int deltaY;
436: final float alphaStart;
437: final float alphaEnd;
438:
439: /**
440: * @param waitTime
441: * the amount of time in milliseconds to wait between calls to the
442: * animation thread
443: * @param deltaY
444: * the delta in the Y direction to inc/dec the size of the scroll
445: * up by
446: * @param alphaStart
447: * the starting alpha transparency level
448: * @param alphaEnd
449: * the ending alpha transparency level
450: */
451: public AnimationParams(int waitTime, int deltaY,
452: float alphaStart, float alphaEnd) {
453: this .waitTime = waitTime;
454: this .deltaY = deltaY;
455: this .alphaStart = alphaStart;
456: this .alphaEnd = alphaEnd;
457: }
458: }
459:
460: /**
461: * This class actual provides the animation support for scrolling up/down this
462: * component. This listener is called whenever the animateTimer fires off. It
463: * fires off in response to scroll up/down requests. This listener is
464: * responsible for modifying the size of the content container and causing it
465: * to be repainted.
466: *
467: * @author Richard Bair
468: */
469: private final class AnimationListener implements ActionListener {
470: /**
471: * Mutex used to ensure that the startHeight/finalHeight are not changed
472: * during a repaint operation.
473: */
474: private final Object ANIMATION_MUTEX = "Animation Synchronization Mutex";
475: /**
476: * This is the starting height when animating. If > finalHeight, then the
477: * animation is going to be to scroll up the component. If it is < then
478: * finalHeight, then the animation will scroll down the component.
479: */
480: private int startHeight = 0;
481: /**
482: * This is the final height that the content container is going to be when
483: * scrolling is finished.
484: */
485: private int finalHeight = 0;
486: /**
487: * The current alpha setting used during "animation" (fade-in/fade-out)
488: */
489: private float animateAlpha = 1.0f;
490:
491: public void actionPerformed(ActionEvent e) {
492: /*
493: * Pre-1) If startHeight == finalHeight, then we're done so stop the timer
494: * 1) Calculate whether we're contracting or expanding. 2) Calculate the
495: * delta (which is either positive or negative, depending on the results
496: * of (1)) 3) Calculate the alpha value 4) Resize the ContentContainer 5)
497: * Revalidate/Repaint the content container
498: */
499: synchronized (ANIMATION_MUTEX) {
500: if (startHeight == finalHeight) {
501: animateTimer.stop();
502: animateAlpha = animationParams.alphaEnd;
503: // keep the content pane hidden when it is collapsed, other it may
504: // still receive focus.
505: if (finalHeight > 0) {
506: wrapper.showContent();
507: validate();
508: JCollapsiblePane.this .firePropertyChange(
509: ANIMATION_STATE_KEY, null, "expanded");
510: return;
511: }
512: }
513:
514: final boolean contracting = startHeight > finalHeight;
515: final int delta_y = contracting ? -1
516: * animationParams.deltaY
517: : animationParams.deltaY;
518: int newHeight = wrapper.getHeight() + delta_y;
519: if (contracting) {
520: if (newHeight < finalHeight) {
521: newHeight = finalHeight;
522: }
523: } else {
524: if (newHeight > finalHeight) {
525: newHeight = finalHeight;
526: }
527: }
528: animateAlpha = (float) newHeight
529: / (float) wrapper.c.getPreferredSize().height;
530:
531: Rectangle bounds = wrapper.getBounds();
532: int oldHeight = bounds.height;
533: bounds.height = newHeight;
534: wrapper.setBounds(bounds);
535: bounds = getBounds();
536: bounds.height = (bounds.height - oldHeight) + newHeight;
537: currentHeight = bounds.height;
538: setBounds(bounds);
539: startHeight = newHeight;
540:
541: // it happens the animateAlpha goes over the alphaStart/alphaEnd range
542: // this code ensures it stays in bounds. This behavior is seen when
543: // component such as JTextComponents are used in the container.
544: if (contracting) {
545: // alphaStart > animateAlpha > alphaEnd
546: if (animateAlpha < animationParams.alphaEnd) {
547: animateAlpha = animationParams.alphaEnd;
548: }
549: if (animateAlpha > animationParams.alphaStart) {
550: animateAlpha = animationParams.alphaStart;
551: }
552: } else {
553: // alphaStart < animateAlpha < alphaEnd
554: if (animateAlpha > animationParams.alphaEnd) {
555: animateAlpha = animationParams.alphaEnd;
556: }
557: if (animateAlpha < animationParams.alphaStart) {
558: animateAlpha = animationParams.alphaStart;
559: }
560: }
561: wrapper.alpha = animateAlpha;
562:
563: validate();
564: }
565: }
566:
567: void validate() {
568: Container parent = SwingUtilities.getAncestorOfClass(
569: JCollapsiblePaneContainer.class,
570: JCollapsiblePane.this );
571: if (parent != null) {
572: parent = ((JCollapsiblePaneContainer) parent)
573: .getValidatingContainer();
574: } else {
575: parent = getParent();
576: }
577:
578: if (parent != null) {
579: if (parent instanceof JComponent) {
580: ((JComponent) parent).revalidate();
581: } else {
582: parent.invalidate();
583: }
584: parent.doLayout();
585: parent.repaint();
586: }
587: }
588:
589: /**
590: * Reinitializes the timer for scrolling up/down the component. This method
591: * is properly synchronized, so you may make this call regardless of whether
592: * the timer is currently executing or not.
593: *
594: * @param startHeight
595: * @param stopHeight
596: */
597: public void reinit(int startHeight, int stopHeight) {
598: synchronized (ANIMATION_MUTEX) {
599: JCollapsiblePane.this .firePropertyChange(
600: ANIMATION_STATE_KEY, null, "reinit");
601: this .startHeight = startHeight;
602: this .finalHeight = stopHeight;
603: animateAlpha = animationParams.alphaStart;
604: currentHeight = -1;
605: wrapper.showImage();
606: }
607: }
608: }
609:
610: private final class WrapperContainer extends JPanel {
611: private BufferedImage img;
612: private Container c;
613: float alpha = 1.0f;
614:
615: public WrapperContainer(Container c) {
616: super (new BorderLayout());
617: this .c = c;
618: add(c, BorderLayout.CENTER);
619:
620: // we must ensure the container is opaque. It is not opaque it introduces
621: // painting glitches specially on Linux with JDK 1.5 and GTK look and feel.
622: // GTK look and feel calls setOpaque(false)
623: if (c instanceof JComponent && !((JComponent) c).isOpaque()) {
624: ((JComponent) c).setOpaque(true);
625: }
626: }
627:
628: public void showImage() {
629: // render c into the img
630: makeImage();
631: c.setVisible(false);
632: }
633:
634: public void showContent() {
635: currentHeight = -1;
636: c.setVisible(true);
637: }
638:
639: void makeImage() {
640: // if we have no image or if the image has changed
641: if (getGraphicsConfiguration() != null && getWidth() > 0) {
642: Dimension dim = c.getPreferredSize();
643: // width and height must be > 0 to be able to create an image
644: if (dim.height > 0) {
645: img = getGraphicsConfiguration()
646: .createCompatibleImage(getWidth(),
647: dim.height);
648: c.setSize(getWidth(), dim.height);
649: c.paint(img.getGraphics());
650: } else {
651: img = null;
652: }
653: }
654: }
655:
656: public void paintComponent(Graphics g) {
657: if (!useAnimation || c.isVisible()) {
658: super .paintComponent(g);
659: } else {
660: // within netbeans, it happens we arrive here and the image has not been
661: // created yet. We ensure it is.
662: if (img == null) {
663: makeImage();
664: }
665: // and we paint it only if it has been created and only if we have a
666: // valid graphics
667: if (g != null && img != null) {
668: // draw the image with y being height - imageHeight
669: g.drawImage(img, 0, getHeight() - img.getHeight(),
670: null);
671: }
672: }
673: }
674:
675: public void paint(Graphics g) {
676: Graphics2D g2d = (Graphics2D) g;
677: Composite oldComp = g2d.getComposite();
678: Composite alphaComp = AlphaComposite.getInstance(
679: AlphaComposite.SRC_OVER, alpha);
680: g2d.setComposite(alphaComp);
681: super.paint(g2d);
682: g2d.setComposite(oldComp);
683: }
684:
685: }
686: }
|