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