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 }