001    /*
002     * $Id: JXCollapsiblePane.java 3195 2009-01-21 14:22:40Z 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     */
021    package org.jdesktop.swingx;
022    
023    import java.awt.BorderLayout;
024    import java.awt.Component;
025    import java.awt.Container;
026    import java.awt.Dimension;
027    import java.awt.LayoutManager;
028    import java.awt.Point;
029    import java.awt.Rectangle;
030    import java.awt.event.ActionEvent;
031    import java.awt.event.ActionListener;
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.JViewport;
039    import javax.swing.Scrollable;
040    import javax.swing.SwingUtilities;
041    import javax.swing.Timer;
042    import javax.swing.border.Border;
043    
044    /**
045     * <code>JXCollapsiblePane</code> provides a component which can collapse or
046     * expand its content area with animation and fade in/fade out effects.
047     * It also acts as a standard container for other Swing components.
048     * <p>
049     * The {@code JXCollapsiblePane} has a "content pane" that actually holds the 
050     * displayed contents. This means that colors, fonts, and other display 
051     * configuration items must be set on the content pane.
052     * 
053     * <pre><code>
054     * // to set the font
055     * collapsiblePane.getContentPane().setFont(font);
056     * // to set the background color
057     * collapsiblePane.getContentPane().setBackground(Color.RED);
058     * </code>
059     * </pre>
060     * 
061     * For convenience, the {@code add} and {@code remove} methods forward to the
062     * content pane.  The following code shows to ways to add a child to the
063     * content pane.
064     * 
065     * <pre><code>
066     * // to add a child
067     * collapsiblePane.getContentPane().add(component);
068     * // to add a child
069     * collapsiblePane.add(component);
070     * </code>
071     * </pre>
072     * 
073     * To set the content pane, do not use {@code add}, use {@link #setContentPane(Container)}.
074     * 
075     * <p>
076     * In this example, the <code>JXCollapsiblePane</code> is used to build
077     * a Search pane which can be shown and hidden on demand.
078     *
079     * <pre>
080     * <code>
081     * JXCollapsiblePane cp = new JXCollapsiblePane();
082     *
083     * // JXCollapsiblePane can be used like any other container
084     * cp.setLayout(new BorderLayout());
085     *
086     * // the Controls panel with a textfield to filter the tree
087     * JPanel controls = new JPanel(new FlowLayout(FlowLayout.LEFT, 4, 0));
088     * controls.add(new JLabel("Search:"));
089     * controls.add(new JTextField(10));
090     * controls.add(new JButton("Refresh"));
091     * controls.setBorder(new TitledBorder("Filters"));
092     * cp.add("Center", controls);
093     *
094     * JXFrame frame = new JXFrame();
095     * frame.setLayout(new BorderLayout());
096     *
097     * // Put the "Controls" first
098     * frame.add("North", cp);
099     *
100     * // Then the tree - we assume the Controls would somehow filter the tree
101     * JScrollPane scroll = new JScrollPane(new JTree());
102     * frame.add("Center", scroll);
103     *
104     * // Show/hide the "Controls"
105     * JButton toggle = new JButton(cp.getActionMap().get(JXCollapsiblePane.TOGGLE_ACTION));
106     * toggle.setText("Show/Hide Search Panel");
107     * frame.add("South", toggle);
108     *
109     * frame.pack();
110     * frame.setVisible(true);
111     * </code>
112     * </pre>
113     *
114     * <p>
115     * The <code>JXCollapsiblePane</code> has a default toggle action registered
116     * under the name {@link #TOGGLE_ACTION}. Bind this action to a button and
117     * pressing the button will automatically toggle the pane between expanded
118     * and collapsed states. Additionally, you can define the icons to use through
119     * the {@link #EXPAND_ICON} and {@link #COLLAPSE_ICON} properties on the action.
120     * Example
121     * <pre>
122     * <code>
123     * // get the built-in toggle action
124     * Action toggleAction = collapsible.getActionMap().
125     *   get(JXCollapsiblePane.TOGGLE_ACTION);
126     *
127     * // use the collapse/expand icons from the JTree UI
128     * toggleAction.putValue(JXCollapsiblePane.COLLAPSE_ICON,
129     *                       UIManager.getIcon("Tree.expandedIcon"));
130     * toggleAction.putValue(JXCollapsiblePane.EXPAND_ICON,
131     *                       UIManager.getIcon("Tree.collapsedIcon"));
132     * </code>
133     * </pre>
134     *
135     * <p>
136     * Note: <code>JXCollapsiblePane</code> requires its parent container to have a
137     * {@link java.awt.LayoutManager} using {@link #getPreferredSize()} when
138     * calculating its layout (example {@link org.jdesktop.swingx.VerticalLayout},
139     * {@link java.awt.BorderLayout}).
140     *
141     * @javabean.attribute
142     *          name="isContainer"
143     *          value="Boolean.TRUE"
144     *          rtexpr="true"
145     *
146     * @javabean.attribute
147     *          name="containerDelegate"
148     *          value="getContentPane"
149     *
150     * @javabean.class
151     *          name="JXCollapsiblePane"
152     *          shortDescription="A pane which hides its content with an animation."
153     *          stopClass="java.awt.Component"
154     *
155     * @author rbair (from the JDNC project)
156     * @author <a href="mailto:fred@L2FProd.com">Frederic Lavigne</a>
157     * @author Karl George Schaefer
158     */
159    public class JXCollapsiblePane extends JXPanel {
160        /**
161         * The direction defines how the collapsible pane will collapse. The
162         * constant names were designed by choosing a fixed point and then
163         * determining the collapsing direction from that fixed point. This means
164         * {@code RIGHT} expands to the right and this is probably the best
165         * expansion for a component in {@link BorderLayout#EAST}.
166         */
167        public enum Direction {
168            /**
169             * Collapses left. Suitable for {@link BorderLayout#WEST}.
170             */
171            LEFT(false),
172    
173            /**
174             * Collapses right. Suitable for {@link BorderLayout#EAST}.
175             */
176            RIGHT(false),
177    
178            /**
179             * Collapses up. Suitable for {@link BorderLayout#NORTH}.
180             */
181            UP(true),
182    
183            /**
184             * Collapses down. Suitable for {@link BorderLayout#SOUTH}.
185             */
186            DOWN(true);
187    
188            private final boolean vertical;
189    
190            private Direction(boolean vertical) {
191                this.vertical = vertical;
192            }
193    
194            /**
195             * Gets the orientation for this direction.
196             * 
197             * @return {@code true} if the direction is vertical, {@code false}
198             *         otherwise
199             */
200            public boolean isVertical() {
201                return vertical;
202            }
203        }
204        
205        /**
206         * Used when generating PropertyChangeEvents for the "animationState"
207         * property. The PropertyChangeEvent will takes the following different
208         * values for {@link PropertyChangeEvent#getNewValue()}:
209         * <ul>
210         * <li><code>reinit</code> every time the animation starts
211         * <li><code>expanded</code> when the animation ends and the pane is
212         * expanded
213         * <li><code>collapsed</code> when the animation ends and the pane is
214         * collapsed
215         * </ul>
216         */
217        public final static String ANIMATION_STATE_KEY = "animationState";
218    
219        /**
220         * JXCollapsible has a built-in toggle action which can be bound to buttons.
221         * Accesses the action through
222         * <code>collapsiblePane.getActionMap().get(JXCollapsiblePane.TOGGLE_ACTION)</code>.
223         */
224        public final static String TOGGLE_ACTION = "toggle";
225    
226        /**
227         * The icon used by the "toggle" action when the JXCollapsiblePane is
228         * expanded, i.e the icon which indicates the pane can be collapsed.
229         */
230        public final static String COLLAPSE_ICON = "collapseIcon";
231    
232        /**
233         * The icon used by the "toggle" action when the JXCollapsiblePane is
234         * collapsed, i.e the icon which indicates the pane can be expanded.
235         */
236        public final static String EXPAND_ICON = "expandIcon";
237    
238        /**
239         * Indicates whether the component is collapsed or expanded
240         */
241        private boolean collapsed = false;
242    
243        /**
244         * Defines the orientation of the component.
245         */
246        private Direction direction = Direction.UP;
247    
248        /**
249         * Timer used for doing the transparency animation (fade-in)
250         */
251        private Timer animateTimer;
252        private AnimationListener animator;
253        private int currentDimension = -1;
254        private WrapperContainer wrapper;
255        private boolean useAnimation = true;
256        private AnimationParams animationParams;
257    
258        /**
259         * Constructs a new JXCollapsiblePane with a {@link JXPanel} as content pane
260         * and a vertical {@link VerticalLayout} with a gap of 2 pixels as layout
261         * manager and a vertical orientation.
262         */
263        public JXCollapsiblePane() {
264            this(Direction.UP, new BorderLayout(0, 0));
265        }
266        
267        /**
268         * Constructs a new JXCollapsiblePane with a {@link JXPanel} as content pane and the specified
269         * direction.
270         * 
271         * @param direction
272         *                the direction to collapse the container
273         */
274        public JXCollapsiblePane(Direction direction) {
275            this(direction, new BorderLayout(0, 0));
276        }
277    
278        /**
279         * Constructs a new JXCollapsiblePane with a {@link JPanel} as content pane
280         * and the given LayoutManager and a vertical orientation
281         */
282        public JXCollapsiblePane(LayoutManager layout) {
283            this(Direction.UP, layout);
284        }
285        
286        /**
287         * Constructs a new JXCollapsiblePane with a {@link JPanel} as content pane
288         * and the given LayoutManager and orientation. A vertical orientation
289         * enables a vertical {@link VerticalLayout} with a gap of 2 pixels as
290         * layout manager. A horizontal orientation enables a horizontal
291         * {@link HorizontalLayout} with a gap of 2 pixels as layout manager
292         * 
293         * @param direction
294         *            the direction this pane collapses
295         * @param layout
296         *            of this collapsible pane
297         */
298        public JXCollapsiblePane(Direction direction, LayoutManager layout) {
299            super.setLayout(layout);
300    
301            this.direction = direction;
302            animator = new AnimationListener();
303            setAnimationParams(new AnimationParams(30, 8, 0.01f, 1.0f));
304    
305            JXPanel panel = new JXPanel();
306            setContentPane(panel);
307            setDirection(direction);
308    
309            // add an action to automatically toggle the state of the pane
310            getActionMap().put(TOGGLE_ACTION, new ToggleAction());
311        }
312    
313        /**
314         * Toggles the JXCollapsiblePane state and updates its icon based on the
315         * JXCollapsiblePane "collapsed" status.
316         */
317        private class ToggleAction extends AbstractAction implements
318                                                          PropertyChangeListener {
319            public ToggleAction() {
320                super(TOGGLE_ACTION);
321                // the action must track the collapsed status of the pane to update its
322                // icon
323                JXCollapsiblePane.this.addPropertyChangeListener("collapsed", this);
324            }
325    
326            @Override
327            public void putValue(String key, Object newValue) {
328                super.putValue(key, newValue);
329                if (EXPAND_ICON.equals(key) || COLLAPSE_ICON.equals(key)) {
330                    updateIcon();
331                }
332            }
333    
334            public void actionPerformed(ActionEvent e) {
335                setCollapsed(!isCollapsed());
336            }
337    
338            public void propertyChange(PropertyChangeEvent evt) {
339                updateIcon();
340            }
341    
342            void updateIcon() {
343                if (isCollapsed()) {
344                    putValue(SMALL_ICON, getValue(EXPAND_ICON));
345                } else {
346                    putValue(SMALL_ICON, getValue(COLLAPSE_ICON));
347                }
348            }
349        }
350    
351        /**
352         * Sets the content pane of this JXCollapsiblePane. The {@code contentPanel}
353         * <i>should</i> implement {@code Scrollable} and return {@code true} from
354         * {@link Scrollable#getScrollableTracksViewportHeight()} and
355         * {@link Scrollable#getScrollableTracksViewportWidth()}. If the content
356         * pane fails to do so and a {@code JScrollPane} is added as a child, it is
357         * likely that the scroll pane will never correctly size. While it is not
358         * strictly necessary to implement {@code Scrollable} in this way, the
359         * default content pane does so.
360         * 
361         * @param contentPanel
362         *                the container delegate used to hold all of the contents
363         *                for this collapsible pane
364         * @throws IllegalArgumentException
365         *                 if contentPanel is null
366         */
367        public void setContentPane(Container contentPanel) {
368            if (contentPanel == null) {
369                throw new IllegalArgumentException("Content pane can't be null");
370            }
371    
372            if (wrapper != null) {
373                //these next two lines are as they are because if I try to remove
374                //the "wrapper" component directly, then super.remove(comp) ends up
375                //calling remove(int), which is overridden in this class, leading to
376                //improper behavior.
377                assert super.getComponent(0) == wrapper;
378                super.remove(0);
379            }
380            wrapper = new WrapperContainer(contentPanel);
381            wrapper.collapsedState = isCollapsed();
382            super.addImpl(wrapper, BorderLayout.CENTER, -1);
383        }
384    
385        /**
386         * @return the content pane
387         */
388        public Container getContentPane() {
389            if (wrapper == null) {
390                return null;
391            }
392            
393            return (Container) wrapper.getView();
394        }
395    
396        /**
397         * Overriden to redirect call to the content pane.
398         */
399        @Override
400        public void setLayout(LayoutManager mgr) {
401            // wrapper can be null when setLayout is called by "super()" constructor
402            if (wrapper != null) {
403                getContentPane().setLayout(mgr);
404            }
405        }
406    
407        /**
408         * Overriden to redirect call to the content pane.
409         */
410        @Override
411        protected void addImpl(Component comp, Object constraints, int index) {
412            getContentPane().add(comp, constraints, index);
413        }
414    
415        /**
416         * Overriden to redirect call to the content pane
417         */
418        @Override
419        public void remove(Component comp) {
420            getContentPane().remove(comp);
421        }
422    
423        /**
424         * Overriden to redirect call to the content pane.
425         */
426        @Override
427        public void remove(int index) {
428            getContentPane().remove(index);
429        }
430    
431        /**
432         * Overriden to redirect call to the content pane.
433         */
434        @Override
435        public void removeAll() {
436            getContentPane().removeAll();
437        }
438    
439        /**
440         * If true, enables the animation when pane is collapsed/expanded. If false,
441         * animation is turned off.
442         *
443         * <p>
444         * When animated, the <code>JXCollapsiblePane</code> will progressively
445         * reduce (when collapsing) or enlarge (when expanding) the height of its
446         * content area until it becomes 0 or until it reaches the preferred height of
447         * the components it contains. The transparency of the content area will also
448         * change during the animation.
449         *
450         * <p>
451         * If not animated, the <code>JXCollapsiblePane</code> will simply hide
452         * (collapsing) or show (expanding) its content area.
453         *
454         * @param animated
455         * @javabean.property bound="true" preferred="true"
456         */
457        public void setAnimated(boolean animated) {
458            if (animated != useAnimation) {
459                useAnimation = animated;
460                firePropertyChange("animated", !useAnimation, useAnimation);
461            }
462        }
463    
464        /**
465         * @return true if the pane is animated, false otherwise
466         * @see #setAnimated(boolean)
467         */
468        public boolean isAnimated() {
469            return useAnimation;
470        }
471        
472        /**
473         * Changes the direction of this collapsible pane. Doing so changes the
474         * layout of the underlying content pane. If the chosen direction is
475         * vertical, a vertical layout with a gap of 2 pixels is chosen. Otherwise,
476         * a horizontal layout with a gap of 2 pixels is chosen.
477         *
478         * @see #getDirection()
479         * @param direction the new {@link Direction} for this collapsible pane
480         * @throws IllegalStateException when this method is called while a
481         *                               collapsing/restore operation is running
482         * @javabean.property
483         *    bound="true"
484         *    preferred="true"
485         */
486        public void setDirection(Direction direction) {
487            if (animateTimer.isRunning()) {
488                throw new IllegalStateException("cannot be change direction while collapsing.");
489            }
490            
491            Direction oldValue = getDirection();
492            this.direction = direction;
493            
494            if (direction.isVertical()) {
495                getContentPane().setLayout(new VerticalLayout(2));
496            } else {
497                getContentPane().setLayout(new HorizontalLayout(2));
498            }
499            
500            firePropertyChange("direction", oldValue, getDirection());
501        }
502        
503        /**
504         * @return the current {@link Direction}.
505         * @see #setDirection(Direction)
506         */
507        public Direction getDirection() {
508            return direction;
509        }
510        
511        /**
512         * @return true if the pane is collapsed, false if expanded
513         */
514        public boolean isCollapsed() {
515            return collapsed;
516        }
517    
518        /**
519         * Expands or collapses this <code>JXCollapsiblePane</code>.
520         *
521         * <p>
522         * If the component is collapsed and <code>val</code> is false, then this
523         * call expands the JXCollapsiblePane, such that the entire JXCollapsiblePane
524         * will be visible. If {@link #isAnimated()} returns true, the expansion will
525         * be accompanied by an animation.
526         *
527         * <p>
528         * However, if the component is expanded and <code>val</code> is true, then
529         * this call collapses the JXCollapsiblePane, such that the entire
530         * JXCollapsiblePane will be invisible. If {@link #isAnimated()} returns true,
531         * the collapse will be accompanied by an animation.
532         *
533         * @see #isAnimated()
534         * @see #setAnimated(boolean)
535         * @javabean.property
536         *    bound="true"
537         *    preferred="true"
538         */
539        public void setCollapsed(boolean val) {
540            if (collapsed != val) {
541                collapsed = val;
542                if (isAnimated()) {
543                    if (collapsed) {
544                        int dimension = direction.isVertical() ?
545                                        wrapper.getHeight() : wrapper.getWidth();
546                        setAnimationParams(new AnimationParams(30,
547                                                               Math.max(8, dimension / 10), 1.0f, 0.01f));
548                        animator.reinit(dimension, 0);
549                        animateTimer.start();
550                    } else {
551                        int dimension = direction.isVertical() ?
552                                        wrapper.getHeight() : wrapper.getWidth();
553                        int preferredDimension = direction.isVertical() ?
554                                                 getContentPane().getPreferredSize().height :
555                                                 getContentPane().getPreferredSize().width;
556                        int delta = Math.max(8, preferredDimension / 10);
557    
558                        setAnimationParams(new AnimationParams(30, delta, 0.01f, 1.0f));
559                        animator.reinit(dimension, preferredDimension);
560                        animateTimer.start();
561                    }
562                } else {
563                    wrapper.collapsedState = collapsed;
564                    revalidate();
565                }
566                repaint();
567                firePropertyChange("collapsed", !collapsed, collapsed);
568            }
569        }
570    
571        /**
572         * {@inheritDoc}
573         */
574        public Border getBorder() {
575            if (getContentPane() instanceof JComponent) {
576                return ((JComponent) getContentPane()).getBorder();
577            }
578            
579            return null;
580        }
581        
582        /**
583         * {@inheritDoc}
584         */
585        public void setBorder(Border border) {
586            if (getContentPane() instanceof JComponent) {
587                ((JComponent) getContentPane()).setBorder(border);
588            }
589        }
590    
591        /**
592         * A collapsible pane always returns its preferred size for the minimum size
593         * to ensure that the collapsing happens correctly.
594         * <p>
595         * To query the minimum size of the contents user {@code
596         * getContentPane().getMinimumSize()}.
597         * 
598         * @return the preferred size of the component
599         */
600        @Override
601        public Dimension getMinimumSize() {
602            return getPreferredSize();
603        }
604    
605        /**
606         * Forwards to the content pane.
607         * 
608         * @param minimumSize
609         *            the size to set on the content pane
610         */
611        public void setMinimumSize(Dimension minimumSize) {
612            getContentPane().setMinimumSize(minimumSize);
613        }
614    
615        /**
616         * The critical part of the animation of this <code>JXCollapsiblePane</code>
617         * relies on the calculation of its preferred size. During the animation, its
618         * preferred size (specially its height) will change, when expanding, from 0
619         * to the preferred size of the content pane, and the reverse when collapsing.
620         *
621         * @return this component preferred size
622         */
623        @Override
624        public Dimension getPreferredSize() {
625            /*
626             * The preferred size is calculated based on the current position of the
627             * component in its animation sequence. If the Component is expanded, then
628             * the preferred size will be the preferred size of the top component plus
629             * the preferred size of the embedded content container. <p>However, if the
630             * scroll up is in any state of animation, the height component of the
631             * preferred size will be the current height of the component (as contained
632             * in the currentDimension variable and when orientation is VERTICAL, otherwise
633             * the same applies to the width)
634             */
635            Dimension dim = getContentPane().getPreferredSize();
636            if (currentDimension != -1) {
637                    if (direction.isVertical()) {
638                        dim.height = currentDimension;
639                    } else {
640                        dim.width = currentDimension;
641                    }
642            } else if(wrapper.collapsedState) {
643                if (direction.isVertical()) {
644                    dim.height = 0;
645                } else {
646                    dim.width = 0;
647                }
648            }
649            return dim;
650        }
651    
652        @Override
653        public void setPreferredSize(Dimension preferredSize) {
654            getContentPane().setPreferredSize(preferredSize);
655        }
656    
657        /**
658         * Sets the parameters controlling the animation
659         *
660         * @param params
661         * @throws IllegalArgumentException
662         *           if params is null
663         */
664        private void setAnimationParams(AnimationParams params) {
665            if (params == null) { throw new IllegalArgumentException(
666                    "params can't be null"); }
667            if (animateTimer != null) {
668                animateTimer.stop();
669            }
670            animationParams = params;
671            animateTimer = new Timer(animationParams.waitTime, animator);
672            animateTimer.setInitialDelay(0);
673        }
674    
675        /**
676         * Tagging interface for containers in a JXCollapsiblePane hierarchy who needs
677         * to be revalidated (invalidate/validate/repaint) when the pane is expanding
678         * or collapsing. Usually validating only the parent of the JXCollapsiblePane
679         * is enough but there might be cases where the parent parent must be
680         * validated.
681         */
682        public static interface CollapsiblePaneContainer {
683            Container getValidatingContainer();
684        }
685    
686        /**
687         * Parameters controlling the animations
688         */
689        private static class AnimationParams {
690            final int waitTime;
691            final int delta;
692            final float alphaStart;
693            final float alphaEnd;
694    
695            /**
696             * @param waitTime
697             *          the amount of time in milliseconds to wait between calls to the
698             *          animation thread
699             * @param delta
700             *          the delta, in the direction as specified by the orientation,
701             *          to inc/dec the size of the scroll up by
702             * @param alphaStart
703             *          the starting alpha transparency level
704             * @param alphaEnd
705             *          the ending alpha transparency level
706             */
707            public AnimationParams(int waitTime, int delta, float alphaStart,
708                                   float alphaEnd) {
709                this.waitTime = waitTime;
710                this.delta = delta;
711                this.alphaStart = alphaStart;
712                this.alphaEnd = alphaEnd;
713            }
714        }
715    
716        /**
717         * This class actual provides the animation support for scrolling up/down this
718         * component. This listener is called whenever the animateTimer fires off. It
719         * fires off in response to scroll up/down requests. This listener is
720         * responsible for modifying the size of the content container and causing it
721         * to be repainted.
722         *
723         * @author Richard Bair
724         */
725        private final class AnimationListener implements ActionListener {
726            /**
727             * Mutex used to ensure that the startDimension/finalDimension are not changed
728             * during a repaint operation.
729             */
730            private final Object ANIMATION_MUTEX = "Animation Synchronization Mutex";
731            /**
732             * This is the starting dimension when animating. If > finalDimension, then the
733             * animation is going to be to scroll up the component. If it is less than
734             * finalDimension, then the animation will scroll down the component.
735             */
736            private int startDimension = 0;
737            /**
738             * This is the final dimension that the content container is going to be when
739             * scrolling is finished.
740             */
741            private int finalDimension = 0;
742            /**
743             * The current alpha setting used during "animation" (fade-in/fade-out)
744             */
745            @SuppressWarnings({"FieldCanBeLocal"})
746            private float animateAlpha = 1.0f;
747    
748            public void actionPerformed(ActionEvent e) {
749                /*
750                * Pre-1) If startDimension == finalDimension, then we're done so stop the timer
751                * 1) Calculate whether we're contracting or expanding. 2) Calculate the
752                * delta (which is either positive or negative, depending on the results
753                * of (1)) 3) Calculate the alpha value 4) Resize the ContentContainer 5)
754                * Revalidate/Repaint the content container
755                */
756                synchronized (ANIMATION_MUTEX) {
757                    if (startDimension == finalDimension) {
758                        animateTimer.stop();
759                        animateAlpha = animationParams.alphaEnd;
760                        // keep the content pane hidden when it is collapsed, other it may
761                        // still receive focus.
762                        if (finalDimension > 0) {
763                            currentDimension = -1;
764                            wrapper.collapsedState = false;
765                            validate();
766                            JXCollapsiblePane.this.firePropertyChange(ANIMATION_STATE_KEY, null,
767                                                                      "expanded");
768                            return;
769                        } else {
770                            wrapper.collapsedState = true;
771                            JXCollapsiblePane.this.firePropertyChange(ANIMATION_STATE_KEY, null,
772                                                                      "collapsed");
773                        }
774                    }
775    
776                    final boolean contracting = startDimension > finalDimension;
777                    final int delta = contracting?-1 * animationParams.delta
778                                      :animationParams.delta;
779                    int newDimension;
780                    if (direction.isVertical()) {
781                        newDimension = wrapper.getHeight() + delta;
782                    } else {
783                        newDimension = wrapper.getWidth() + delta;
784                    }
785                    if (contracting) {
786                        if (newDimension < finalDimension) {
787                            newDimension = finalDimension;
788                        }
789                    } else {
790                        if (newDimension > finalDimension) {
791                            newDimension = finalDimension;
792                        }
793                    }
794                    int dimension;
795                    if (direction.isVertical()) {
796                        dimension = wrapper.getView().getPreferredSize().height;
797                    } else {
798                        dimension = wrapper.getView().getPreferredSize().width;
799                    }
800                    animateAlpha = (float)newDimension / (float)dimension;
801    
802                    Rectangle bounds = wrapper.getBounds();
803    
804                    if (direction.isVertical()) {
805                        int oldHeight = bounds.height;
806                        bounds.height = newDimension;
807                        wrapper.setBounds(bounds);
808                        
809                        if (direction == Direction.DOWN) {
810                            wrapper.setViewPosition(new Point(0, wrapper.getView().getPreferredSize().height - newDimension));
811                        } else {
812                            wrapper.setViewPosition(new Point(0, newDimension));
813                        }
814                        
815                        bounds = getBounds();
816                        bounds.height = (bounds.height - oldHeight) + newDimension;
817                        currentDimension = bounds.height;
818                    } else {
819                        int oldWidth = bounds.width;
820                        bounds.width = newDimension;
821                        wrapper.setBounds(bounds);
822                        
823                        if (direction == Direction.RIGHT) {
824                            wrapper.setViewPosition(new Point(wrapper.getView().getPreferredSize().width - newDimension, 0));
825                        } else {
826                            wrapper.setViewPosition(new Point(newDimension, 0));
827                        }
828                        
829                        bounds = getBounds();
830                        bounds.width = (bounds.width - oldWidth) + newDimension;
831                        currentDimension = bounds.width;
832                    }
833    
834                    setBounds(bounds);
835                    startDimension = newDimension;
836    
837                    // it happens the animateAlpha goes over the alphaStart/alphaEnd range
838                    // this code ensures it stays in bounds. This behavior is seen when
839                    // component such as JTextComponents are used in the container.
840                    if (contracting) {
841                        // alphaStart > animateAlpha > alphaEnd
842                        if (animateAlpha < animationParams.alphaEnd) {
843                            animateAlpha = animationParams.alphaEnd;
844                        }
845                        if (animateAlpha > animationParams.alphaStart) {
846                            animateAlpha = animationParams.alphaStart;
847                        }
848                    } else {
849                        // alphaStart < animateAlpha < alphaEnd
850                        if (animateAlpha > animationParams.alphaEnd) {
851                            animateAlpha = animationParams.alphaEnd;
852                        }
853                        if (animateAlpha < animationParams.alphaStart) {
854                            animateAlpha = animationParams.alphaStart;
855                        }
856                    }
857                    wrapper.alpha = animateAlpha;
858    
859                    validate();
860                }
861            }
862    
863            void validate() {
864                Container parent = SwingUtilities.getAncestorOfClass(
865                        CollapsiblePaneContainer.class, JXCollapsiblePane.this);
866                if (parent != null) {
867                    parent = ((CollapsiblePaneContainer)parent).getValidatingContainer();
868                } else {
869                    parent = getParent();
870                }
871    
872                if (parent != null) {
873                    if (parent instanceof JComponent) {
874                        ((JComponent)parent).revalidate();
875                    } else {
876                        parent.invalidate();
877                    }
878                    parent.doLayout();
879                    parent.repaint();
880                }
881            }
882    
883            /**
884             * Reinitializes the timer for scrolling up/down the component. This method
885             * is properly synchronized, so you may make this call regardless of whether
886             * the timer is currently executing or not.
887             *
888             * @param startDimension
889             * @param stopDimension
890             */
891            public void reinit(int startDimension, int stopDimension) {
892                synchronized (ANIMATION_MUTEX) {
893                    JXCollapsiblePane.this.firePropertyChange(ANIMATION_STATE_KEY, null,
894                                                              "reinit");
895                    this.startDimension = startDimension;
896                    this.finalDimension = stopDimension;
897                    animateAlpha = animationParams.alphaStart;
898                    currentDimension = -1;
899                }
900            }
901        }
902    
903        private final class WrapperContainer extends JViewport {
904            float alpha;
905            boolean collapsedState;
906    
907            public WrapperContainer(Container c) {
908                alpha = 1.0f;
909                collapsedState = false;
910                setView(c);
911    
912                // we must ensure the container is opaque. It is not opaque it introduces
913                // painting glitches specially on Linux with JDK 1.5 and GTK look and feel.
914                // GTK look and feel calls setOpaque(false)
915                if (c instanceof JComponent && !c.isOpaque()) {
916                    ((JComponent) c).setOpaque(true);
917                }
918            }
919        }
920    
921    // TEST CASE
922    //    public static void main(String[] args) {
923    //        SwingUtilities.invokeLater(new Runnable() {
924    //            public void run() {
925    //                JFrame f = new JFrame("Test Oriented Collapsible Pane");
926    //
927    //                f.add(new JLabel("Press Ctrl+F or Ctrl+G to collapse panes."),
928    //                      BorderLayout.NORTH);
929    //
930    //                JTree tree1 = new JTree();
931    //                tree1.setBorder(BorderFactory.createEtchedBorder());
932    //                f.add(tree1);
933    //
934    //                JXCollapsiblePane pane = new JXCollapsiblePane(Orientation.VERTICAL);
935    //                pane.setCollapsed(true);
936    //                JTree tree2 = new JTree();
937    //                tree2.setBorder(BorderFactory.createEtchedBorder());
938    //                pane.add(tree2);
939    //                f.add(pane, BorderLayout.SOUTH);
940    //
941    //                pane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
942    //                        KeyStroke.getKeyStroke("ctrl F"),
943    //                        JXCollapsiblePane.TOGGLE_ACTION);
944    //                    
945    //                pane = new JXCollapsiblePane(Orientation.HORIZONTAL);
946    //                JTree tree3 = new JTree();
947    //                pane.add(tree3);
948    //                tree3.setBorder(BorderFactory.createEtchedBorder());
949    //                f.add(pane, BorderLayout.WEST);
950    //
951    //                pane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
952    //                        KeyStroke.getKeyStroke("ctrl G"),
953    //                        JXCollapsiblePane.TOGGLE_ACTION);
954    //
955    //                f.setSize(640, 480);
956    //                f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
957    //                f.setVisible(true);
958    //        }
959    //        });
960    //    }
961    }