001/*
002 * $Id: JXCollapsiblePane.java 4186 2012-06-25 13:10:23Z kschaefe $
003 *
004 * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle,
005 * Santa Clara, California 95054, U.S.A. All rights reserved.
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 * 
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
015 * Lesser General Public License for more details.
016 * 
017 * You should have received a copy of the GNU Lesser General Public
018 * License along with this library; if not, write to the Free Software
019 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
020 */
021package org.jdesktop.swingx;
022
023import java.awt.AlphaComposite;
024import java.awt.BorderLayout;
025import java.awt.Component;
026import java.awt.ComponentOrientation;
027import java.awt.Composite;
028import java.awt.Container;
029import java.awt.Dimension;
030import java.awt.Graphics;
031import java.awt.Graphics2D;
032import java.awt.LayoutManager;
033import java.awt.Point;
034import java.awt.Rectangle;
035import java.awt.event.ActionEvent;
036import java.awt.event.ActionListener;
037import java.awt.image.BufferedImage;
038import java.beans.PropertyChangeEvent;
039import java.beans.PropertyChangeListener;
040
041import javax.swing.AbstractAction;
042import javax.swing.JComponent;
043import javax.swing.JViewport;
044import javax.swing.Scrollable;
045import javax.swing.SwingUtilities;
046import javax.swing.Timer;
047import javax.swing.border.Border;
048
049import org.jdesktop.beans.JavaBean;
050import org.jdesktop.swingx.util.GraphicsUtilities;
051
052/**
053 * <code>JXCollapsiblePane</code> provides a component which can collapse or
054 * expand its content area with animation and fade in/fade out effects.
055 * It also acts as a standard container for other Swing components.
056 * <p>
057 * The {@code JXCollapsiblePane} has a "content pane" that actually holds the 
058 * displayed contents. This means that colors, fonts, and other display 
059 * configuration items must be set on the content pane.
060 * 
061 * <pre><code>
062 * // to set the font
063 * collapsiblePane.getContentPane().setFont(font);
064 * // to set the background color
065 * collapsiblePane.getContentPane().setBackground(Color.RED);
066 * </code>
067 * </pre>
068 * 
069 * For convenience, the {@code add} and {@code remove} methods forward to the
070 * content pane.  The following code shows to ways to add a child to the
071 * content pane.
072 * 
073 * <pre><code>
074 * // to add a child
075 * collapsiblePane.getContentPane().add(component);
076 * // to add a child
077 * collapsiblePane.add(component);
078 * </code>
079 * </pre>
080 * 
081 * To set the content pane, do not use {@code add}, use {@link #setContentPane(Container)}.
082 * 
083 * <p>
084 * In this example, the <code>JXCollapsiblePane</code> is used to build
085 * a Search pane which can be shown and hidden on demand.
086 *
087 * <pre>
088 * <code>
089 * JXCollapsiblePane cp = new JXCollapsiblePane();
090 *
091 * // JXCollapsiblePane can be used like any other container
092 * cp.setLayout(new BorderLayout());
093 *
094 * // the Controls panel with a textfield to filter the tree
095 * JPanel controls = new JPanel(new FlowLayout(FlowLayout.LEFT, 4, 0));
096 * controls.add(new JLabel("Search:"));
097 * controls.add(new JTextField(10));
098 * controls.add(new JButton("Refresh"));
099 * controls.setBorder(new TitledBorder("Filters"));
100 * cp.add("Center", controls);
101 *
102 * JXFrame frame = new JXFrame();
103 * frame.setLayout(new BorderLayout());
104 *
105 * // Put the "Controls" first
106 * frame.add("North", cp);
107 *
108 * // Then the tree - we assume the Controls would somehow filter the tree
109 * JScrollPane scroll = new JScrollPane(new JTree());
110 * frame.add("Center", scroll);
111 *
112 * // Show/hide the "Controls"
113 * JButton toggle = new JButton(cp.getActionMap().get(JXCollapsiblePane.TOGGLE_ACTION));
114 * toggle.setText("Show/Hide Search Panel");
115 * frame.add("South", toggle);
116 *
117 * frame.pack();
118 * frame.setVisible(true);
119 * </code>
120 * </pre>
121 *
122 * <p>
123 * The <code>JXCollapsiblePane</code> has a default toggle action registered
124 * under the name {@link #TOGGLE_ACTION}. Bind this action to a button and
125 * pressing the button will automatically toggle the pane between expanded
126 * and collapsed states. Additionally, you can define the icons to use through
127 * the {@link #EXPAND_ICON} and {@link #COLLAPSE_ICON} properties on the action.
128 * Example
129 * <pre>
130 * <code>
131 * // get the built-in toggle action
132 * Action toggleAction = collapsible.getActionMap().
133 *   get(JXCollapsiblePane.TOGGLE_ACTION);
134 *
135 * // use the collapse/expand icons from the JTree UI
136 * toggleAction.putValue(JXCollapsiblePane.COLLAPSE_ICON,
137 *                       UIManager.getIcon("Tree.expandedIcon"));
138 * toggleAction.putValue(JXCollapsiblePane.EXPAND_ICON,
139 *                       UIManager.getIcon("Tree.collapsedIcon"));
140 * </code>
141 * </pre>
142 *
143 * <p>
144 * Note: <code>JXCollapsiblePane</code> requires its parent container to have a
145 * {@link java.awt.LayoutManager} using {@link #getPreferredSize()} when
146 * calculating its layout (example {@link org.jdesktop.swingx.VerticalLayout},
147 * {@link java.awt.BorderLayout}).
148 *
149 * @javabean.attribute
150 *          name="isContainer"
151 *          value="Boolean.TRUE"
152 *          rtexpr="true"
153 *
154 * @javabean.attribute
155 *          name="containerDelegate"
156 *          value="getContentPane"
157 *
158 * @javabean.class
159 *          name="JXCollapsiblePane"
160 *          shortDescription="A pane which hides its content with an animation."
161 *          stopClass="java.awt.Component"
162 *
163 * @author rbair (from the JDNC project)
164 * @author <a href="mailto:fred@L2FProd.com">Frederic Lavigne</a>
165 * @author Karl George Schaefer
166 */
167@JavaBean
168public class JXCollapsiblePane extends JXPanel {
169    /**
170     * The direction defines how the collapsible pane will collapse. The
171     * constant names were designed by choosing a fixed point and then
172     * determining the collapsing direction from that fixed point. This means
173     * {@code RIGHT} expands to the right and this is probably the best
174     * expansion for a component in {@link BorderLayout#EAST}.
175     */
176    public enum Direction {
177        /**
178         * Collapses left. Suitable for {@link BorderLayout#WEST}.
179         */
180        LEFT(false),
181
182        /**
183         * Collapses right. Suitable for {@link BorderLayout#EAST}.
184         */
185        RIGHT(false),
186
187        /**
188         * Collapses up. Suitable for {@link BorderLayout#NORTH}.
189         */
190        UP(true),
191
192        /**
193         * Collapses down. Suitable for {@link BorderLayout#SOUTH}.
194         */
195        DOWN(true),
196        
197        /**
198         * Collapses toward the leading edge. Suitable for {@link BorderLayout#LINE_START}.
199         */
200        LEADING(false) {
201            @Override
202            Direction getFixedDirection(ComponentOrientation co) {
203                return co.isLeftToRight() ? LEFT : RIGHT;
204            }
205        },
206        
207        /**
208         * Collapses toward the trailing edge. Suitable for {@link BorderLayout#LINE_END}.
209         */
210        TRAILING(false) {
211            @Override
212            Direction getFixedDirection(ComponentOrientation co) {
213                return co.isLeftToRight() ? RIGHT : LEFT;
214            }
215        },
216        
217        /**
218         * Collapses toward the starting edge. Suitable for {@link BorderLayout#PAGE_START}.
219         */
220        START(true) {
221            @Override
222            Direction getFixedDirection(ComponentOrientation co) {
223                return UP;
224            }
225        },
226        
227        /**
228         * Collapses toward the ending edge. Suitable for {@link BorderLayout#PAGE_END}.
229         */
230        END(true) {
231            @Override
232            Direction getFixedDirection(ComponentOrientation co) {
233                return DOWN;
234            }
235        },
236        ;
237
238        private final boolean vertical;
239
240        private Direction(boolean vertical) {
241            this.vertical = vertical;
242        }
243
244        /**
245         * Gets the orientation for this direction.
246         * 
247         * @return {@code true} if the direction is vertical, {@code false}
248         *         otherwise
249         */
250        public boolean isVertical() {
251            return vertical;
252        }
253
254        /**
255         * Gets the fixed direction equivalent to this direction for the specified orientation.
256         * 
257         * @param co
258         *            the component's orientation
259         * @return the fixed direction corresponding to the component's orietnation
260         */
261        Direction getFixedDirection(ComponentOrientation co) {
262            return this;
263        }
264    }
265
266    /**
267     * JXCollapsible has a built-in toggle action which can be bound to buttons.
268     * Accesses the action through
269     * <code>collapsiblePane.getActionMap().get(JXCollapsiblePane.TOGGLE_ACTION)</code>.
270     */
271    public final static String TOGGLE_ACTION = "toggle";
272
273    /**
274     * The icon used by the "toggle" action when the JXCollapsiblePane is
275     * expanded, i.e the icon which indicates the pane can be collapsed.
276     */
277    public final static String COLLAPSE_ICON = "collapseIcon";
278
279    /**
280     * The icon used by the "toggle" action when the JXCollapsiblePane is
281     * collapsed, i.e the icon which indicates the pane can be expanded.
282     */
283    public final static String EXPAND_ICON = "expandIcon";
284
285    /**
286     * Indicates whether the component is collapsed or expanded
287     */
288    private boolean collapsed = false;
289
290    /**
291     * Defines the orientation of the component.
292     */
293    private Direction direction = Direction.UP;
294
295    /**
296     * Timer used for doing the transparency animation (fade-in)
297     */
298    private Timer animateTimer;
299    private AnimationListener animator;
300    private int currentDimension = -1;
301    private WrapperContainer wrapper;
302    private boolean useAnimation = true;
303    private AnimationParams animationParams;
304    private boolean collapseFiringState;
305
306    /**
307     * Constructs a new JXCollapsiblePane with a {@link JXPanel} as content pane
308     * and a vertical {@link VerticalLayout} with a gap of 2 pixels as layout
309     * manager and a vertical orientation.
310     */
311    public JXCollapsiblePane() {
312        this(Direction.UP);
313    }
314    
315    /**
316     * Constructs a new JXCollapsiblePane with a {@link JXPanel} as content pane and the specified
317     * direction.
318     * 
319     * @param direction
320     *                the direction to collapse the container
321     */
322    public JXCollapsiblePane(Direction direction) {
323        super.setLayout(new BorderLayout());
324        this.direction = direction;
325        animator = new AnimationListener();
326        setAnimationParams(new AnimationParams(30, 8, 0.01f, 1.0f));
327
328        JXPanel panel = new JXPanel();
329        setContentPane(panel);
330        setDirection(direction);
331
332        // add an action to automatically toggle the state of the pane
333        getActionMap().put(TOGGLE_ACTION, new ToggleAction());
334    }
335
336    /**
337     * Toggles the JXCollapsiblePane state and updates its icon based on the
338     * JXCollapsiblePane "collapsed" status.
339     */
340    private class ToggleAction extends AbstractAction implements
341                                                      PropertyChangeListener {
342        public ToggleAction() {
343            super(TOGGLE_ACTION);
344            // the action must track the collapsed status of the pane to update its
345            // icon
346            JXCollapsiblePane.this.addPropertyChangeListener("collapsed", this);
347        }
348
349        @Override
350        public void putValue(String key, Object newValue) {
351            super.putValue(key, newValue);
352            if (EXPAND_ICON.equals(key) || COLLAPSE_ICON.equals(key)) {
353                updateIcon();
354            }
355        }
356
357        @Override
358        public void actionPerformed(ActionEvent e) {
359            setCollapsed(!isCollapsed());
360        }
361
362        @Override
363        public void propertyChange(PropertyChangeEvent evt) {
364            updateIcon();
365        }
366
367        void updateIcon() {
368            if (isCollapsed()) {
369                putValue(SMALL_ICON, getValue(EXPAND_ICON));
370            } else {
371                putValue(SMALL_ICON, getValue(COLLAPSE_ICON));
372            }
373        }
374    }
375
376    /**
377     * Sets the content pane of this JXCollapsiblePane. The {@code contentPanel}
378     * <i>should</i> implement {@code Scrollable} and return {@code true} from
379     * {@link Scrollable#getScrollableTracksViewportHeight()} and
380     * {@link Scrollable#getScrollableTracksViewportWidth()}. If the content
381     * pane fails to do so and a {@code JScrollPane} is added as a child, it is
382     * likely that the scroll pane will never correctly size. While it is not
383     * strictly necessary to implement {@code Scrollable} in this way, the
384     * default content pane does so.
385     * 
386     * @param contentPanel
387     *                the container delegate used to hold all of the contents
388     *                for this collapsible pane
389     * @throws IllegalArgumentException
390     *                 if contentPanel is null
391     */
392    public void setContentPane(Container contentPanel) {
393        if (contentPanel == null) {
394            throw new IllegalArgumentException("Content pane can't be null");
395        }
396
397        if (wrapper != null) {
398            //these next two lines are as they are because if I try to remove
399            //the "wrapper" component directly, then super.remove(comp) ends up
400            //calling remove(int), which is overridden in this class, leading to
401            //improper behavior.
402            assert super.getComponent(0) == wrapper;
403            super.remove(0);
404        }
405        wrapper = new WrapperContainer(contentPanel);
406        wrapper.collapsedState = isCollapsed();
407        wrapper.getView().setVisible(!wrapper.collapsedState);
408        super.addImpl(wrapper, BorderLayout.CENTER, -1);
409    }
410
411    /**
412     * @return the content pane
413     */
414    public Container getContentPane() {
415        if (wrapper == null) {
416            return null;
417        }
418        
419        return (Container) wrapper.getView();
420    }
421
422    /**
423     * Overridden to redirect call to the content pane.
424     */
425    @Override
426    public void setLayout(LayoutManager mgr) {
427        // wrapper can be null when setLayout is called by "super()" constructor
428        if (wrapper != null) {
429            getContentPane().setLayout(mgr);
430        }
431    }
432
433    /**
434     * Overridden to redirect call to the content pane.
435     */
436    @Override
437    protected void addImpl(Component comp, Object constraints, int index) {
438        getContentPane().add(comp, constraints, index);
439    }
440
441    /**
442     * Overridden to redirect call to the content pane
443     */
444    @Override
445    public void remove(Component comp) {
446        getContentPane().remove(comp);
447    }
448
449    /**
450     * Overridden to redirect call to the content pane.
451     */
452    @Override
453    public void remove(int index) {
454        getContentPane().remove(index);
455    }
456
457    /**
458     * Overridden to redirect call to the content pane.
459     */
460    @Override
461    public void removeAll() {
462        getContentPane().removeAll();
463    }
464
465    /**
466     * If true, enables the animation when pane is collapsed/expanded. If false,
467     * animation is turned off.
468     *
469     * <p>
470     * When animated, the <code>JXCollapsiblePane</code> will progressively
471     * reduce (when collapsing) or enlarge (when expanding) the height of its
472     * content area until it becomes 0 or until it reaches the preferred height of
473     * the components it contains. The transparency of the content area will also
474     * change during the animation.
475     *
476     * <p>
477     * If not animated, the <code>JXCollapsiblePane</code> will simply hide
478     * (collapsing) or show (expanding) its content area.
479     *
480     * @param animated
481     * @javabean.property bound="true" preferred="true"
482     */
483    public void setAnimated(boolean animated) {
484        if (animated != useAnimation) {
485            useAnimation = animated;
486            
487            if (!animated) {
488                if (animateTimer.isRunning()) {
489                        //TODO should we listen for animation state change?
490                        //yes, but we're best off creating a UI delegate for these changes
491                        SwingUtilities.invokeLater(new Runnable() {
492                                                @Override
493                                                public void run() {
494                                                        currentDimension = -1;
495                                                }
496                                        });
497                } else {
498                        currentDimension = -1;
499                }
500            }
501            firePropertyChange("animated", !useAnimation, useAnimation);
502        }
503    }
504
505    /**
506     * @return true if the pane is animated, false otherwise
507     * @see #setAnimated(boolean)
508     */
509    public boolean isAnimated() {
510        return useAnimation;
511    }
512    
513    /**
514     * {@inheritDoc}
515     */
516    @Override
517    public void setComponentOrientation(ComponentOrientation o) {
518        if (animateTimer.isRunning()) {
519            throw new IllegalStateException("cannot be change component orientation while collapsing.");
520        }
521        
522        super.setComponentOrientation(o);
523    }
524
525    /**
526     * Changes the direction of this collapsible pane. Doing so changes the
527     * layout of the underlying content pane. If the chosen direction is
528     * vertical, a vertical layout with a gap of 2 pixels is chosen. Otherwise,
529     * a horizontal layout with a gap of 2 pixels is chosen.
530     *
531     * @see #getDirection()
532     * @param direction the new {@link Direction} for this collapsible pane
533     * @throws IllegalStateException when this method is called while a
534     *                               collapsing/restore operation is running
535     * @javabean.property
536     *    bound="true"
537     *    preferred="true"
538     */
539    public void setDirection(Direction direction) {
540        if (animateTimer.isRunning()) {
541            throw new IllegalStateException("cannot be change direction while collapsing.");
542        }
543        
544        Direction oldValue = getDirection();
545        this.direction = direction;
546        
547        if (direction.isVertical()) {
548            getContentPane().setLayout(new VerticalLayout(2));
549        } else {
550            getContentPane().setLayout(new HorizontalLayout(2));
551        }
552        
553        firePropertyChange("direction", oldValue, getDirection());
554    }
555    
556    /**
557     * @return the current {@link Direction}.
558     * @see #setDirection(Direction)
559     */
560    public Direction getDirection() {
561        return direction;
562    }
563    
564    /**
565     * @return true if the pane is collapsed, false if expanded
566     */
567    public boolean isCollapsed() {
568        return collapsed;
569    }
570
571    /**
572     * Expands or collapses this <code>JXCollapsiblePane</code>.
573     *
574     * <p>
575     * If the component is collapsed and <code>val</code> is false, then this
576     * call expands the JXCollapsiblePane, such that the entire JXCollapsiblePane
577     * will be visible. If {@link #isAnimated()} returns true, the expansion will
578     * be accompanied by an animation.
579     *
580     * <p>
581     * However, if the component is expanded and <code>val</code> is true, then
582     * this call collapses the JXCollapsiblePane, such that the entire
583     * JXCollapsiblePane will be invisible. If {@link #isAnimated()} returns true,
584     * the collapse will be accompanied by an animation.
585     * 
586     * <p>
587     * As of SwingX 1.6.3, JXCollapsiblePane only fires property change events when
588     * the component's state is accurate.  This means that animated collapsible 
589     * pane's only fire events once the animation is complete.
590     *
591     * @see #isAnimated()
592     * @see #setAnimated(boolean)
593     * @javabean.property
594     *    bound="true"
595     *    preferred="true"
596     */
597    public void setCollapsed(boolean val) {
598        boolean oldValue = isCollapsed();
599        this.collapsed = val;
600        
601        if (isAnimated() && isShowing()) {
602            if (oldValue == isCollapsed()) {
603                return;
604            }
605            
606            // this ensures that if the user reverses the animation
607            // before completion that no property change is fired
608            if (!animateTimer.isRunning()) {
609                collapseFiringState = oldValue;
610            }
611            
612            if (oldValue) {
613                int dimension = direction.isVertical() ? wrapper.getHeight() : wrapper.getWidth();
614                int preferredDimension = direction.isVertical() ? getContentPane()
615                        .getPreferredSize().height : getContentPane().getPreferredSize().width;
616                int delta = Math.max(8, preferredDimension / 10);
617
618                setAnimationParams(new AnimationParams(30, delta, 0.01f, 1.0f));
619                animator.reinit(dimension, preferredDimension);
620                wrapper.getView().setVisible(true);
621            } else {
622                int dimension = direction.isVertical() ? wrapper.getHeight() : wrapper.getWidth();
623                setAnimationParams(new AnimationParams(30, Math.max(8, dimension / 10), 1.0f, 0.01f));
624                animator.reinit(dimension, 0);
625            }
626            
627            animateTimer.start();
628        } else {
629            wrapper.collapsedState = isCollapsed();
630            wrapper.getView().setVisible(!isCollapsed());
631            revalidate();
632            repaint();
633            
634            firePropertyChange("collapsed", oldValue, isCollapsed());
635        }
636    }
637
638    /**
639     * {@inheritDoc}
640     */
641    @Override
642    public Border getBorder() {
643        if (getContentPane() instanceof JComponent) {
644            return ((JComponent) getContentPane()).getBorder();
645        }
646        
647        return null;
648    }
649    
650    /**
651     * {@inheritDoc}
652     */
653    @Override
654    public void setBorder(Border border) {
655        if (getContentPane() instanceof JComponent) {
656            ((JComponent) getContentPane()).setBorder(border);
657        }
658    }
659
660    /**
661     * {@inheritDoc}
662     * <p>
663     * Internals of JXCollasiplePane are designed to be opaque because some Look and Feel
664     * implementations having painting issues otherwise. JXCollapsiblePane and its internals will
665     * respect {@code setOpaque}, calling this method will not only update the collapsible pane, but
666     * also all internals. This method does not modify the {@link #getContentPane() content pane},
667     * as it is not considered an internal.
668     */
669    @Override
670    public void setOpaque(boolean opaque) {
671        super.setOpaque(opaque);
672        
673        if (wrapper != null) {
674            wrapper.setOpaque(opaque);
675        }
676    }
677
678    /**
679     * A collapsible pane always returns its preferred size for the minimum size
680     * to ensure that the collapsing happens correctly.
681     * <p>
682     * To query the minimum size of the contents user {@code
683     * getContentPane().getMinimumSize()}.
684     * 
685     * @return the preferred size of the component
686     */
687    @Override
688    public Dimension getMinimumSize() {
689        return getPreferredSize();
690    }
691
692    /**
693     * Forwards to the content pane.
694     * 
695     * @param minimumSize
696     *            the size to set on the content pane
697     */
698    @Override
699    public void setMinimumSize(Dimension minimumSize) {
700        getContentPane().setMinimumSize(minimumSize);
701    }
702
703    /**
704     * The critical part of the animation of this <code>JXCollapsiblePane</code>
705     * relies on the calculation of its preferred size. During the animation, its
706     * preferred size (specially its height) will change, when expanding, from 0
707     * to the preferred size of the content pane, and the reverse when collapsing.
708     *
709     * @return this component preferred size
710     */
711    @Override
712    public Dimension getPreferredSize() {
713        /*
714         * The preferred size is calculated based on the current position of the
715         * component in its animation sequence. If the Component is expanded, then
716         * the preferred size will be the preferred size of the top component plus
717         * the preferred size of the embedded content container. <p>However, if the
718         * scroll up is in any state of animation, the height component of the
719         * preferred size will be the current height of the component (as contained
720         * in the currentDimension variable and when orientation is VERTICAL, otherwise
721         * the same applies to the width)
722         */
723        Dimension dim = getContentPane().getPreferredSize();
724        if (currentDimension != -1) {
725                if (direction.isVertical()) {
726                    dim.height = currentDimension;
727                } else {
728                    dim.width = currentDimension;
729                }
730        } else if(wrapper.collapsedState) {
731            if (direction.isVertical()) {
732                dim.height = 0;
733            } else {
734                dim.width = 0;
735            }
736        }
737        return dim;
738    }
739
740    @Override
741    public void setPreferredSize(Dimension preferredSize) {
742        getContentPane().setPreferredSize(preferredSize);
743    }
744
745    /**
746     * Sets the parameters controlling the animation
747     *
748     * @param params
749     * @throws IllegalArgumentException
750     *           if params is null
751     */
752    private void setAnimationParams(AnimationParams params) {
753        if (params == null) { throw new IllegalArgumentException(
754                "params can't be null"); }
755        if (animateTimer != null) {
756            animateTimer.stop();
757        }
758        animationParams = params;
759        animateTimer = new Timer(animationParams.waitTime, animator);
760        animateTimer.setInitialDelay(0);
761    }
762
763    /**
764     * Tagging interface for containers in a JXCollapsiblePane hierarchy who needs
765     * to be revalidated (invalidate/validate/repaint) when the pane is expanding
766     * or collapsing. Usually validating only the parent of the JXCollapsiblePane
767     * is enough but there might be cases where the parent's parent must be
768     * validated.
769     */
770    public static interface CollapsiblePaneContainer {
771        Container getValidatingContainer();
772    }
773
774    /**
775     * Parameters controlling the animations
776     */
777    private static class AnimationParams {
778        final int waitTime;
779        final int delta;
780        final float alphaStart;
781        final float alphaEnd;
782
783        /**
784         * @param waitTime
785         *          the amount of time in milliseconds to wait between calls to the
786         *          animation thread
787         * @param delta
788         *          the delta, in the direction as specified by the orientation,
789         *          to inc/dec the size of the scroll up by
790         * @param alphaStart
791         *          the starting alpha transparency level
792         * @param alphaEnd
793         *          the ending alpha transparency level
794         */
795        public AnimationParams(int waitTime, int delta, float alphaStart,
796                               float alphaEnd) {
797            this.waitTime = waitTime;
798            this.delta = delta;
799            this.alphaStart = alphaStart;
800            this.alphaEnd = alphaEnd;
801        }
802    }
803
804    /**
805     * This class actual provides the animation support for scrolling up/down this
806     * component. This listener is called whenever the animateTimer fires off. It
807     * fires off in response to scroll up/down requests. This listener is
808     * responsible for modifying the size of the content container and causing it
809     * to be repainted.
810     *
811     * @author Richard Bair
812     */
813    private final class AnimationListener implements ActionListener {
814        /**
815         * Mutex used to ensure that the startDimension/finalDimension are not changed
816         * during a repaint operation.
817         */
818        private final Object ANIMATION_MUTEX = "Animation Synchronization Mutex";
819        /**
820         * This is the starting dimension when animating. If > finalDimension, then the
821         * animation is going to be to scroll up the component. If it is less than
822         * finalDimension, then the animation will scroll down the component.
823         */
824        private int startDimension = 0;
825        /**
826         * This is the final dimension that the content container is going to be when
827         * scrolling is finished.
828         */
829        private int finalDimension = 0;
830        /**
831         * The current alpha setting used during "animation" (fade-in/fade-out)
832         */
833        @SuppressWarnings({"FieldCanBeLocal"})
834        private float animateAlpha = 1.0f;
835
836        @Override
837        public void actionPerformed(ActionEvent e) {
838            /*
839            * Pre-1) If startDimension == finalDimension, then we're done so stop the timer
840            * 1) Calculate whether we're contracting or expanding. 2) Calculate the
841            * delta (which is either positive or negative, depending on the results
842            * of (1)) 3) Calculate the alpha value 4) Resize the ContentContainer 5)
843            * Revalidate/Repaint the content container
844            */
845            synchronized (ANIMATION_MUTEX) {
846                if (startDimension == finalDimension) {
847                    animateTimer.stop();
848                    animateAlpha = animationParams.alphaEnd;
849                    // keep the content pane hidden when it is collapsed, other it may
850                    // still receive focus.
851                    if (finalDimension > 0) {
852                        currentDimension = -1;
853                        wrapper.collapsedState = false;
854                        validate();
855                        JXCollapsiblePane.this.firePropertyChange("collapsed", collapseFiringState, false);
856                        return;
857                    } else {
858                        wrapper.collapsedState = true;
859                        wrapper.getView().setVisible(false);
860                        JXCollapsiblePane.this.firePropertyChange("collapsed", collapseFiringState, true);
861                    }
862                }
863
864                final boolean contracting = startDimension > finalDimension;
865                final int delta = contracting?-1 * animationParams.delta
866                                  :animationParams.delta;
867                int newDimension;
868                if (direction.isVertical()) {
869                    newDimension = wrapper.getHeight() + delta;
870                } else {
871                    newDimension = wrapper.getWidth() + delta;
872                }
873                if (contracting) {
874                    if (newDimension < finalDimension) {
875                        newDimension = finalDimension;
876                    }
877                } else {
878                    if (newDimension > finalDimension) {
879                        newDimension = finalDimension;
880                    }
881                }
882                int dimension;
883                if (direction.isVertical()) {
884                    dimension = wrapper.getView().getPreferredSize().height;
885                } else {
886                    dimension = wrapper.getView().getPreferredSize().width;
887                }
888                animateAlpha = (float)newDimension / (float)dimension;
889
890                Rectangle bounds = wrapper.getBounds();
891
892                if (direction.isVertical()) {
893                    int oldHeight = bounds.height;
894                    bounds.height = newDimension;
895                    wrapper.setBounds(bounds);
896                    
897                    if (direction.getFixedDirection(getComponentOrientation()) == Direction.DOWN) {
898                        wrapper.setViewPosition(new Point(0, wrapper.getView().getPreferredSize().height - newDimension));
899                    } else {
900                        wrapper.setViewPosition(new Point(0, newDimension));
901                    }
902                    
903                    bounds = getBounds();
904                    bounds.height = (bounds.height - oldHeight) + newDimension;
905                    currentDimension = bounds.height;
906                } else {
907                    int oldWidth = bounds.width;
908                    bounds.width = newDimension;
909                    wrapper.setBounds(bounds);
910                    
911                    if (direction.getFixedDirection(getComponentOrientation()) == Direction.RIGHT) {
912                        wrapper.setViewPosition(new Point(wrapper.getView().getPreferredSize().width - newDimension, 0));
913                    } else {
914                        wrapper.setViewPosition(new Point(newDimension, 0));
915                    }
916                    
917                    bounds = getBounds();
918                    bounds.width = (bounds.width - oldWidth) + newDimension;
919                    currentDimension = bounds.width;
920                }
921
922                setBounds(bounds);
923                startDimension = newDimension;
924
925                // it happens the animateAlpha goes over the alphaStart/alphaEnd range
926                // this code ensures it stays in bounds. This behavior is seen when
927                // component such as JTextComponents are used in the container.
928                if (contracting) {
929                    // alphaStart > animateAlpha > alphaEnd
930                    if (animateAlpha < animationParams.alphaEnd) {
931                        animateAlpha = animationParams.alphaEnd;
932                    }
933                    if (animateAlpha > animationParams.alphaStart) {
934                        animateAlpha = animationParams.alphaStart;
935                    }
936                } else {
937                    // alphaStart < animateAlpha < alphaEnd
938                    if (animateAlpha > animationParams.alphaEnd) {
939                        animateAlpha = animationParams.alphaEnd;
940                    }
941                    if (animateAlpha < animationParams.alphaStart) {
942                        animateAlpha = animationParams.alphaStart;
943                    }
944                }
945                
946                wrapper.setAlpha(animateAlpha);
947                
948                validate();
949            }
950        }
951
952        void validate() {
953            Container parent = SwingUtilities.getAncestorOfClass(
954                    CollapsiblePaneContainer.class, JXCollapsiblePane.this);
955            if (parent != null) {
956                parent = ((CollapsiblePaneContainer)parent).getValidatingContainer();
957            } else {
958                parent = getParent();
959            }
960
961            if (parent != null) {
962                if (parent instanceof JComponent) {
963                    ((JComponent)parent).revalidate();
964                } else {
965                    parent.invalidate();
966                }
967                parent.doLayout();
968                parent.repaint();
969            }
970        }
971
972        /**
973         * Reinitializes the timer for scrolling up/down the component. This method
974         * is properly synchronized, so you may make this call regardless of whether
975         * the timer is currently executing or not.
976         *
977         * @param startDimension
978         * @param stopDimension
979         */
980        public void reinit(int startDimension, int stopDimension) {
981            synchronized (ANIMATION_MUTEX) {
982                this.startDimension = startDimension;
983                this.finalDimension = stopDimension;
984                animateAlpha = animationParams.alphaStart;
985                currentDimension = -1;
986            }
987        }
988    }
989
990    private final class WrapperContainer extends JViewport implements AlphaPaintable {
991        boolean collapsedState;
992        private float alpha;
993        private boolean oldOpaque;
994
995        public WrapperContainer(Container c) {
996            alpha = 1.0f;
997            collapsedState = false;
998            setView(c);
999
1000            // we must ensure the container is opaque. It is not opaque it introduces
1001            // painting glitches specially on Linux with JDK 1.5 and GTK look and feel.
1002            // GTK look and feel calls setOpaque(false)
1003            if (c instanceof JComponent && !c.isOpaque()) {
1004                ((JComponent) c).setOpaque(true);
1005            }
1006        }
1007
1008        /**
1009         * {@inheritDoc} <p>
1010         * 
1011         * Overridden to not have JViewPort behaviour (that is scroll the view)
1012         * but delegate to parent scrollRectToVisible just a JComponent does.<p>
1013         */
1014        @Override
1015        public void scrollRectToVisible(Rectangle aRect) {
1016                //avoids JViewport's implementation
1017                //by using JXCollapsiblePane's it will delegate upward
1018                //getting any core fixes, by avoiding c&p
1019                JXCollapsiblePane.this.scrollRectToVisible(aRect);
1020        }
1021
1022        @Override
1023        public float getAlpha() {
1024            return alpha;
1025        }
1026
1027        @Override
1028        public void setAlpha(float alpha) {
1029            if (alpha < 0f || alpha > 1f) {
1030                throw new IllegalArgumentException("invalid alpha value " + alpha);
1031            }
1032            
1033            float oldValue = getAlpha();
1034            this.alpha = alpha;
1035            
1036            if (getAlpha() < 1f) {
1037                if (oldValue == 1) {
1038                    //it used to be 1, but now is not. Save the oldOpaque
1039                    oldOpaque = isOpaque();
1040                    setOpaque(false);
1041                }
1042            } else {
1043                //restore the oldOpaque if it was true (since opaque is false now)
1044                if (oldOpaque) {
1045                    setOpaque(true);
1046                }
1047            }
1048            
1049            firePropertyChange("alpha", oldValue, getAlpha());
1050            repaint();
1051        }
1052
1053        @Override
1054        public boolean isInheritAlpha() {
1055            return false;
1056        }
1057
1058        @Override
1059        public void setInheritAlpha(boolean inheritAlpha) {
1060            //does nothing; always false;
1061        }
1062
1063        @Override
1064        public float getEffectiveAlpha() {
1065            return getAlpha();
1066        }
1067
1068        /**
1069         * Overridden paint method to take into account the alpha setting.
1070         * 
1071         * @param g
1072         *            the <code>Graphics</code> context in which to paint
1073         */
1074        @Override
1075        public void paint(Graphics g) {
1076            //short circuit painting if no transparency
1077            if (getAlpha() == 1f) {
1078                super.paint(g);
1079            } else {
1080                //the component is translucent, so we need to render to
1081                //an intermediate image before painting
1082                // TODO should we cache this image? repaint to same image unless size changes?
1083                BufferedImage img = GraphicsUtilities.createCompatibleTranslucentImage(getWidth(), getHeight());
1084                Graphics2D gfx = img.createGraphics();
1085                
1086                try {
1087                    super.paint(gfx);
1088                } finally {
1089                    gfx.dispose();
1090                }
1091                
1092                Graphics2D g2d = (Graphics2D) g;
1093                Composite oldComp = g2d.getComposite();
1094                
1095                try {
1096                    Composite alphaComp = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, getEffectiveAlpha());
1097                    g2d.setComposite(alphaComp);
1098                    //TODO should we cache the image?
1099                    g2d.drawImage(img, null, 0, 0);
1100                } finally {
1101                    g2d.setComposite(oldComp);
1102                }
1103            }
1104        }
1105    }
1106
1107// TEST CASE
1108//    public static void main(String[] args) {
1109//        SwingUtilities.invokeLater(new Runnable() {
1110//            public void run() {
1111//                JFrame f = new JFrame("Test Oriented Collapsible Pane");
1112//
1113//                f.add(new JLabel("Press Ctrl+F or Ctrl+G to collapse panes."),
1114//                      BorderLayout.NORTH);
1115//
1116//                JTree tree1 = new JTree();
1117//                tree1.setBorder(BorderFactory.createEtchedBorder());
1118//                f.add(tree1);
1119//
1120//                JXCollapsiblePane pane = new JXCollapsiblePane(Orientation.VERTICAL);
1121//                pane.setCollapsed(true);
1122//                JTree tree2 = new JTree();
1123//                tree2.setBorder(BorderFactory.createEtchedBorder());
1124//                pane.add(tree2);
1125//                f.add(pane, BorderLayout.SOUTH);
1126//
1127//                pane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
1128//                        KeyStroke.getKeyStroke("ctrl F"),
1129//                        JXCollapsiblePane.TOGGLE_ACTION);
1130//                    
1131//                pane = new JXCollapsiblePane(Orientation.HORIZONTAL);
1132//                JTree tree3 = new JTree();
1133//                pane.add(tree3);
1134//                tree3.setBorder(BorderFactory.createEtchedBorder());
1135//                f.add(pane, BorderLayout.WEST);
1136//
1137//                pane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
1138//                        KeyStroke.getKeyStroke("ctrl G"),
1139//                        JXCollapsiblePane.TOGGLE_ACTION);
1140//
1141//                f.setSize(640, 480);
1142//                f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
1143//                f.setVisible(true);
1144//        }
1145//        });
1146//    }
1147}