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 }