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