org.jdesktop.swingx.JXCollapsiblePane Maven / Gradle / Ivy
/*
* $Id: JXCollapsiblePane.java 4265 2012-11-28 18:15:57Z kschaefe $
*
* Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle,
* Santa Clara, California 95054, U.S.A. All rights reserved.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
package org.jdesktop.swingx;
import java.awt.AlphaComposite;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.ComponentOrientation;
import java.awt.Composite;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.LayoutManager;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import javax.swing.AbstractAction;
import javax.swing.JComponent;
import javax.swing.JViewport;
import javax.swing.Scrollable;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import javax.swing.border.Border;
import org.jdesktop.beans.JavaBean;
import org.jdesktop.swingx.util.GraphicsUtilities;
/**
* JXCollapsiblePane
provides a component which can collapse or
* expand its content area with animation and fade in/fade out effects.
* It also acts as a standard container for other Swing components.
*
* The {@code JXCollapsiblePane} has a "content pane" that actually holds the
* displayed contents. This means that colors, fonts, and other display
* configuration items must be set on the content pane.
*
*
* // to set the font
* collapsiblePane.getContentPane().setFont(font);
* // to set the background color
* collapsiblePane.getContentPane().setBackground(Color.RED);
*
*
*
* For convenience, the {@code add} and {@code remove} methods forward to the
* content pane. The following code shows to ways to add a child to the
* content pane.
*
*
* // to add a child
* collapsiblePane.getContentPane().add(component);
* // to add a child
* collapsiblePane.add(component);
*
*
*
* To set the content pane, do not use {@code add}, use {@link #setContentPane(Container)}.
*
*
* In this example, the JXCollapsiblePane
is used to build
* a Search pane which can be shown and hidden on demand.
*
*
*
* JXCollapsiblePane cp = new JXCollapsiblePane();
*
* // JXCollapsiblePane can be used like any other container
* cp.setLayout(new BorderLayout());
*
* // the Controls panel with a textfield to filter the tree
* JPanel controls = new JPanel(new FlowLayout(FlowLayout.LEFT, 4, 0));
* controls.add(new JLabel("Search:"));
* controls.add(new JTextField(10));
* controls.add(new JButton("Refresh"));
* controls.setBorder(new TitledBorder("Filters"));
* cp.add("Center", controls);
*
* JXFrame frame = new JXFrame();
* frame.setLayout(new BorderLayout());
*
* // Put the "Controls" first
* frame.add("North", cp);
*
* // Then the tree - we assume the Controls would somehow filter the tree
* JScrollPane scroll = new JScrollPane(new JTree());
* frame.add("Center", scroll);
*
* // Show/hide the "Controls"
* JButton toggle = new JButton(cp.getActionMap().get(JXCollapsiblePane.TOGGLE_ACTION));
* toggle.setText("Show/Hide Search Panel");
* frame.add("South", toggle);
*
* frame.pack();
* frame.setVisible(true);
*
*
*
*
* The JXCollapsiblePane
has a default toggle action registered
* under the name {@link #TOGGLE_ACTION}. Bind this action to a button and
* pressing the button will automatically toggle the pane between expanded
* and collapsed states. Additionally, you can define the icons to use through
* the {@link #EXPAND_ICON} and {@link #COLLAPSE_ICON} properties on the action.
* Example
*
*
* // get the built-in toggle action
* Action toggleAction = collapsible.getActionMap().
* get(JXCollapsiblePane.TOGGLE_ACTION);
*
* // use the collapse/expand icons from the JTree UI
* toggleAction.putValue(JXCollapsiblePane.COLLAPSE_ICON,
* UIManager.getIcon("Tree.expandedIcon"));
* toggleAction.putValue(JXCollapsiblePane.EXPAND_ICON,
* UIManager.getIcon("Tree.collapsedIcon"));
*
*
*
*
* Note: JXCollapsiblePane
requires its parent container to have a
* {@link java.awt.LayoutManager} using {@link #getPreferredSize()} when
* calculating its layout (example {@link org.jdesktop.swingx.VerticalLayout},
* {@link java.awt.BorderLayout}).
*
* @javabean.attribute
* name="isContainer"
* value="Boolean.TRUE"
* rtexpr="true"
*
* @javabean.attribute
* name="containerDelegate"
* value="getContentPane"
*
* @javabean.class
* name="JXCollapsiblePane"
* shortDescription="A pane which hides its content with an animation."
* stopClass="java.awt.Component"
*
* @author rbair (from the JDNC project)
* @author Frederic Lavigne
* @author Karl George Schaefer
*/
@JavaBean
public class JXCollapsiblePane extends JXPanel {
/**
* The direction defines how the collapsible pane will collapse. The
* constant names were designed by choosing a fixed point and then
* determining the collapsing direction from that fixed point. This means
* {@code RIGHT} expands to the right and this is probably the best
* expansion for a component in {@link BorderLayout#EAST}.
*/
public enum Direction {
/**
* Collapses left. Suitable for {@link BorderLayout#WEST}.
*/
LEFT(false),
/**
* Collapses right. Suitable for {@link BorderLayout#EAST}.
*/
RIGHT(false),
/**
* Collapses up. Suitable for {@link BorderLayout#NORTH}.
*/
UP(true),
/**
* Collapses down. Suitable for {@link BorderLayout#SOUTH}.
*/
DOWN(true),
/**
* Collapses toward the leading edge. Suitable for {@link BorderLayout#LINE_START}.
*/
LEADING(false) {
@Override
Direction getFixedDirection(ComponentOrientation co) {
return co.isLeftToRight() ? LEFT : RIGHT;
}
},
/**
* Collapses toward the trailing edge. Suitable for {@link BorderLayout#LINE_END}.
*/
TRAILING(false) {
@Override
Direction getFixedDirection(ComponentOrientation co) {
return co.isLeftToRight() ? RIGHT : LEFT;
}
},
/**
* Collapses toward the starting edge. Suitable for {@link BorderLayout#PAGE_START}.
*/
START(true) {
@Override
Direction getFixedDirection(ComponentOrientation co) {
return UP;
}
},
/**
* Collapses toward the ending edge. Suitable for {@link BorderLayout#PAGE_END}.
*/
END(true) {
@Override
Direction getFixedDirection(ComponentOrientation co) {
return DOWN;
}
},
;
private final boolean vertical;
private Direction(boolean vertical) {
this.vertical = vertical;
}
/**
* Gets the orientation for this direction.
*
* @return {@code true} if the direction is vertical, {@code false}
* otherwise
*/
public boolean isVertical() {
return vertical;
}
/**
* Gets the fixed direction equivalent to this direction for the specified orientation.
*
* @param co
* the component's orientation
* @return the fixed direction corresponding to the component's orietnation
*/
Direction getFixedDirection(ComponentOrientation co) {
return this;
}
}
/**
* Toggles the JXCollapsiblePane state and updates its icon based on the
* JXCollapsiblePane "collapsed" status.
*/
private class ToggleAction extends AbstractAction implements
PropertyChangeListener {
public ToggleAction() {
super(TOGGLE_ACTION);
// the action must track the collapsed status of the pane to update its icon
JXCollapsiblePane.this.addPropertyChangeListener("collapsed", this);
}
@Override
public void putValue(String key, Object newValue) {
super.putValue(key, newValue);
if (EXPAND_ICON.equals(key) || COLLAPSE_ICON.equals(key)) {
updateIcon();
}
}
@Override
public void actionPerformed(ActionEvent e) {
setCollapsed(!isCollapsed());
}
@Override
public void propertyChange(PropertyChangeEvent evt) {
updateIcon();
}
void updateIcon() {
if (isCollapsed()) {
putValue(SMALL_ICON, getValue(EXPAND_ICON));
} else {
putValue(SMALL_ICON, getValue(COLLAPSE_ICON));
}
}
}
/**
* JXCollapsible has a built-in toggle action which can be bound to buttons.
* Accesses the action through
* collapsiblePane.getActionMap().get(JXCollapsiblePane.TOGGLE_ACTION)
.
*/
public final static String TOGGLE_ACTION = "toggle";
/**
* The icon used by the "toggle" action when the JXCollapsiblePane is
* expanded, i.e the icon which indicates the pane can be collapsed.
*/
public final static String COLLAPSE_ICON = "collapseIcon";
/**
* The icon used by the "toggle" action when the JXCollapsiblePane is
* collapsed, i.e the icon which indicates the pane can be expanded.
*/
public final static String EXPAND_ICON = "expandIcon";
/**
* Indicates whether the component is collapsed or expanded
*/
private boolean collapsed = false;
/**
* Defines the orientation of the component.
*/
private Direction direction = Direction.UP;
/**
* Timer used for doing the transparency animation (fade-in)
*/
private Timer animateTimer;
private AnimationListener animator;
private int currentDimension = -1;
private WrapperContainer wrapper;
private boolean useAnimation = true;
private AnimationParams animationParams;
private boolean collapseFiringState;
/**
* Constructs a new JXCollapsiblePane with a {@link JXPanel} as content pane
* and a vertical {@link VerticalLayout} with a gap of 2 pixels as layout
* manager and a vertical orientation.
*/
public JXCollapsiblePane() {
this(Direction.UP);
}
/**
* Constructs a new JXCollapsiblePane with a {@link JXPanel} as content pane and the specified
* direction.
*
* @param direction
* the direction to collapse the container
*/
public JXCollapsiblePane(Direction direction) {
super.setLayout(new BorderLayout());
this.direction = direction;
animator = new AnimationListener();
setAnimationParams(new AnimationParams(30, 8, 0.01f, 1.0f));
setContentPane(createContentPane());
setDirection(direction);
// add an action to automatically toggle the state of the pane
getActionMap().put(TOGGLE_ACTION, new ToggleAction());
}
/**
* Creates the content pane used by this collapsible pane.
*
* @return the content pane
*/
protected Container createContentPane() {
return new JXPanel();
}
/**
* @return the content pane
*/
public Container getContentPane() {
if (wrapper == null) {
return null;
}
return (Container) wrapper.getView();
}
/**
* Sets the content pane of this JXCollapsiblePane. The {@code contentPanel}
* should implement {@code Scrollable} and return {@code true} from
* {@link Scrollable#getScrollableTracksViewportHeight()} and
* {@link Scrollable#getScrollableTracksViewportWidth()}. If the content
* pane fails to do so and a {@code JScrollPane} is added as a child, it is
* likely that the scroll pane will never correctly size. While it is not
* strictly necessary to implement {@code Scrollable} in this way, the
* default content pane does so.
*
* @param contentPanel
* the container delegate used to hold all of the contents
* for this collapsible pane
* @throws IllegalArgumentException
* if contentPanel is null
*/
public void setContentPane(Container contentPanel) {
if (contentPanel == null) {
throw new IllegalArgumentException("Content pane can't be null");
}
if (wrapper != null) {
//these next two lines are as they are because if I try to remove
//the "wrapper" component directly, then super.remove(comp) ends up
//calling remove(int), which is overridden in this class, leading to
//improper behavior.
assert super.getComponent(0) == wrapper;
super.remove(0);
}
wrapper = new WrapperContainer(contentPanel);
wrapper.collapsedState = isCollapsed();
wrapper.getView().setVisible(!wrapper.collapsedState);
super.addImpl(wrapper, BorderLayout.CENTER, -1);
}
/**
* Overridden to redirect call to the content pane.
*/
@Override
public void setLayout(LayoutManager mgr) {
// wrapper can be null when setLayout is called by "super()" constructor
if (wrapper != null) {
getContentPane().setLayout(mgr);
}
}
/**
* Overridden to redirect call to the content pane.
*/
@Override
protected void addImpl(Component comp, Object constraints, int index) {
getContentPane().add(comp, constraints, index);
}
/**
* Overridden to redirect call to the content pane
*/
@Override
public void remove(Component comp) {
getContentPane().remove(comp);
}
/**
* Overridden to redirect call to the content pane.
*/
@Override
public void remove(int index) {
getContentPane().remove(index);
}
/**
* Overridden to redirect call to the content pane.
*/
@Override
public void removeAll() {
getContentPane().removeAll();
}
/**
* If true, enables the animation when pane is collapsed/expanded. If false,
* animation is turned off.
*
*
* When animated, the JXCollapsiblePane
will progressively
* reduce (when collapsing) or enlarge (when expanding) the height of its
* content area until it becomes 0 or until it reaches the preferred height of
* the components it contains. The transparency of the content area will also
* change during the animation.
*
*
* If not animated, the JXCollapsiblePane
will simply hide
* (collapsing) or show (expanding) its content area.
*
* @param animated
* @javabean.property bound="true" preferred="true"
*/
public void setAnimated(boolean animated) {
if (animated != useAnimation) {
useAnimation = animated;
if (!animated) {
if (animateTimer.isRunning()) {
//TODO should we listen for animation state change?
//yes, but we're best off creating a UI delegate for these changes
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
currentDimension = -1;
}
});
} else {
currentDimension = -1;
}
}
firePropertyChange("animated", !useAnimation, useAnimation);
}
}
/**
* @return true if the pane is animated, false otherwise
* @see #setAnimated(boolean)
*/
public boolean isAnimated() {
return useAnimation;
}
/**
* {@inheritDoc}
*/
@Override
public void setComponentOrientation(ComponentOrientation o) {
if (animateTimer.isRunning()) {
throw new IllegalStateException("cannot be change component orientation while collapsing.");
}
super.setComponentOrientation(o);
}
/**
* Changes the direction of this collapsible pane. Doing so changes the
* layout of the underlying content pane. If the chosen direction is
* vertical, a vertical layout with a gap of 2 pixels is chosen. Otherwise,
* a horizontal layout with a gap of 2 pixels is chosen.
*
* @see #getDirection()
* @param direction the new {@link Direction} for this collapsible pane
* @throws IllegalStateException when this method is called while a
* collapsing/restore operation is running
* @javabean.property
* bound="true"
* preferred="true"
*/
public void setDirection(Direction direction) {
if (animateTimer.isRunning()) {
throw new IllegalStateException("cannot be change direction while collapsing.");
}
Direction oldValue = getDirection();
this.direction = direction;
if (direction.isVertical()) {
getContentPane().setLayout(new VerticalLayout(2));
} else {
getContentPane().setLayout(new HorizontalLayout(2));
}
firePropertyChange("direction", oldValue, getDirection());
}
/**
* @return the current {@link Direction}.
* @see #setDirection(Direction)
*/
public Direction getDirection() {
return direction;
}
/**
* @return true if the pane is collapsed, false if expanded
*/
public boolean isCollapsed() {
return collapsed;
}
/**
* Expands or collapses this JXCollapsiblePane
.
*
*
* If the component is collapsed and val
is false, then this
* call expands the JXCollapsiblePane, such that the entire JXCollapsiblePane
* will be visible. If {@link #isAnimated()} returns true, the expansion will
* be accompanied by an animation.
*
*
* However, if the component is expanded and val
is true, then
* this call collapses the JXCollapsiblePane, such that the entire
* JXCollapsiblePane will be invisible. If {@link #isAnimated()} returns true,
* the collapse will be accompanied by an animation.
*
*
* As of SwingX 1.6.3, JXCollapsiblePane only fires property change events when
* the component's state is accurate. This means that animated collapsible
* pane's only fire events once the animation is complete.
*
* @see #isAnimated()
* @see #setAnimated(boolean)
* @javabean.property
* bound="true"
* preferred="true"
*/
public void setCollapsed(boolean val) {
boolean oldValue = isCollapsed();
this.collapsed = val;
if (isAnimated() && isShowing()) {
if (oldValue == isCollapsed()) {
return;
}
// this ensures that if the user reverses the animation
// before completion that no property change is fired
if (!animateTimer.isRunning()) {
collapseFiringState = oldValue;
}
if (oldValue) {
int dimension = direction.isVertical() ? wrapper.getHeight() : wrapper.getWidth();
int preferredDimension = direction.isVertical() ? getContentPane()
.getPreferredSize().height : getContentPane().getPreferredSize().width;
int delta = Math.max(8, preferredDimension / 10);
setAnimationParams(new AnimationParams(30, delta, 0.01f, 1.0f));
animator.reinit(dimension, preferredDimension);
wrapper.getView().setVisible(true);
} else {
int dimension = direction.isVertical() ? wrapper.getHeight() : wrapper.getWidth();
setAnimationParams(new AnimationParams(30, Math.max(8, dimension / 10), 1.0f, 0.01f));
animator.reinit(dimension, 0);
}
animateTimer.start();
} else {
wrapper.collapsedState = isCollapsed();
wrapper.getView().setVisible(!isCollapsed());
revalidate();
repaint();
firePropertyChange("collapsed", oldValue, isCollapsed());
}
}
/**
* {@inheritDoc}
*/
@Override
public Border getBorder() {
if (getContentPane() instanceof JComponent) {
return ((JComponent) getContentPane()).getBorder();
}
return null;
}
/**
* {@inheritDoc}
*/
@Override
public void setBorder(Border border) {
if (getContentPane() instanceof JComponent) {
((JComponent) getContentPane()).setBorder(border);
}
}
/**
* {@inheritDoc}
*
* Internals of JXCollasiplePane are designed to be opaque because some Look and Feel
* implementations having painting issues otherwise. JXCollapsiblePane and its internals will
* respect {@code setOpaque}, calling this method will not only update the collapsible pane, but
* also all internals. This method does not modify the {@link #getContentPane() content pane},
* as it is not considered an internal.
*/
@Override
public void setOpaque(boolean opaque) {
super.setOpaque(opaque);
if (wrapper != null) {
wrapper.setOpaque(opaque);
}
}
/**
* A collapsible pane always returns its preferred size for the minimum size
* to ensure that the collapsing happens correctly.
*
* To query the minimum size of the contents user {@code
* getContentPane().getMinimumSize()}.
*
* @return the preferred size of the component
*/
@Override
public Dimension getMinimumSize() {
return getPreferredSize();
}
/**
* Forwards to the content pane.
*
* @param minimumSize
* the size to set on the content pane
*/
@Override
public void setMinimumSize(Dimension minimumSize) {
getContentPane().setMinimumSize(minimumSize);
}
/**
* The critical part of the animation of this JXCollapsiblePane
* relies on the calculation of its preferred size. During the animation, its
* preferred size (specially its height) will change, when expanding, from 0
* to the preferred size of the content pane, and the reverse when collapsing.
*
* @return this component preferred size
*/
@Override
public Dimension getPreferredSize() {
/*
* The preferred size is calculated based on the current position of the
* component in its animation sequence. If the Component is expanded, then
* the preferred size will be the preferred size of the top component plus
* the preferred size of the embedded content container.
However, if the
* scroll up is in any state of animation, the height component of the
* preferred size will be the current height of the component (as contained
* in the currentDimension variable and when orientation is VERTICAL, otherwise
* the same applies to the width)
*/
Dimension dim = getContentPane().getPreferredSize();
if (currentDimension != -1) {
if (direction.isVertical()) {
dim.height = currentDimension;
} else {
dim.width = currentDimension;
}
} else if(wrapper.collapsedState) {
if (direction.isVertical()) {
dim.height = 0;
} else {
dim.width = 0;
}
}
return dim;
}
@Override
public void setPreferredSize(Dimension preferredSize) {
getContentPane().setPreferredSize(preferredSize);
}
/**
* Sets the parameters controlling the animation
*
* @param params
* @throws IllegalArgumentException
* if params is null
*/
private void setAnimationParams(AnimationParams params) {
if (params == null) { throw new IllegalArgumentException(
"params can't be null"); }
if (animateTimer != null) {
animateTimer.stop();
}
animationParams = params;
animateTimer = new Timer(animationParams.waitTime, animator);
animateTimer.setInitialDelay(0);
}
/**
* Tagging interface for containers in a JXCollapsiblePane hierarchy who needs
* to be revalidated (invalidate/validate/repaint) when the pane is expanding
* or collapsing. Usually validating only the parent of the JXCollapsiblePane
* is enough but there might be cases where the parent's parent must be
* validated.
*/
public static interface CollapsiblePaneContainer {
Container getValidatingContainer();
}
/**
* Parameters controlling the animations
*/
private static class AnimationParams {
final int waitTime;
final int delta;
final float alphaStart;
final float alphaEnd;
/**
* @param waitTime
* the amount of time in milliseconds to wait between calls to the
* animation thread
* @param delta
* the delta, in the direction as specified by the orientation,
* to inc/dec the size of the scroll up by
* @param alphaStart
* the starting alpha transparency level
* @param alphaEnd
* the ending alpha transparency level
*/
public AnimationParams(int waitTime, int delta, float alphaStart,
float alphaEnd) {
this.waitTime = waitTime;
this.delta = delta;
this.alphaStart = alphaStart;
this.alphaEnd = alphaEnd;
}
}
/**
* This class actual provides the animation support for scrolling up/down this
* component. This listener is called whenever the animateTimer fires off. It
* fires off in response to scroll up/down requests. This listener is
* responsible for modifying the size of the content container and causing it
* to be repainted.
*
* @author Richard Bair
*/
private final class AnimationListener implements ActionListener {
/**
* Mutex used to ensure that the startDimension/finalDimension are not changed
* during a repaint operation.
*/
private final Object ANIMATION_MUTEX = "Animation Synchronization Mutex";
/**
* This is the starting dimension when animating. If > finalDimension, then the
* animation is going to be to scroll up the component. If it is less than
* finalDimension, then the animation will scroll down the component.
*/
private int startDimension = 0;
/**
* This is the final dimension that the content container is going to be when
* scrolling is finished.
*/
private int finalDimension = 0;
/**
* The current alpha setting used during "animation" (fade-in/fade-out)
*/
@SuppressWarnings({"FieldCanBeLocal"})
private float animateAlpha = 1.0f;
@Override
public void actionPerformed(ActionEvent e) {
/*
* Pre-1) If startDimension == finalDimension, then we're done so stop the timer
* 1) Calculate whether we're contracting or expanding. 2) Calculate the
* delta (which is either positive or negative, depending on the results
* of (1)) 3) Calculate the alpha value 4) Resize the ContentContainer 5)
* Revalidate/Repaint the content container
*/
synchronized (ANIMATION_MUTEX) {
if (startDimension == finalDimension) {
animateTimer.stop();
animateAlpha = animationParams.alphaEnd;
// keep the content pane hidden when it is collapsed, other it may
// still receive focus.
if (finalDimension > 0) {
currentDimension = -1;
wrapper.collapsedState = false;
validate();
JXCollapsiblePane.this.firePropertyChange("collapsed", collapseFiringState, false);
return;
} else {
wrapper.collapsedState = true;
wrapper.getView().setVisible(false);
JXCollapsiblePane.this.firePropertyChange("collapsed", collapseFiringState, true);
}
}
final boolean contracting = startDimension > finalDimension;
final int delta = contracting?-1 * animationParams.delta
:animationParams.delta;
int newDimension;
if (direction.isVertical()) {
newDimension = wrapper.getHeight() + delta;
} else {
newDimension = wrapper.getWidth() + delta;
}
if (contracting) {
if (newDimension < finalDimension) {
newDimension = finalDimension;
}
} else {
if (newDimension > finalDimension) {
newDimension = finalDimension;
}
}
int dimension;
if (direction.isVertical()) {
dimension = wrapper.getView().getPreferredSize().height;
} else {
dimension = wrapper.getView().getPreferredSize().width;
}
animateAlpha = (float)newDimension / (float)dimension;
Rectangle bounds = wrapper.getBounds();
if (direction.isVertical()) {
int oldHeight = bounds.height;
bounds.height = newDimension;
wrapper.setBounds(bounds);
if (direction.getFixedDirection(getComponentOrientation()) == Direction.DOWN) {
wrapper.setViewPosition(new Point(0, wrapper.getView().getPreferredSize().height - newDimension));
} else {
wrapper.setViewPosition(new Point(0, newDimension));
}
bounds = getBounds();
bounds.height = (bounds.height - oldHeight) + newDimension;
currentDimension = bounds.height;
} else {
int oldWidth = bounds.width;
bounds.width = newDimension;
wrapper.setBounds(bounds);
if (direction.getFixedDirection(getComponentOrientation()) == Direction.RIGHT) {
wrapper.setViewPosition(new Point(wrapper.getView().getPreferredSize().width - newDimension, 0));
} else {
wrapper.setViewPosition(new Point(newDimension, 0));
}
bounds = getBounds();
bounds.width = (bounds.width - oldWidth) + newDimension;
currentDimension = bounds.width;
}
setBounds(bounds);
startDimension = newDimension;
// it happens the animateAlpha goes over the alphaStart/alphaEnd range
// this code ensures it stays in bounds. This behavior is seen when
// component such as JTextComponents are used in the container.
if (contracting) {
// alphaStart > animateAlpha > alphaEnd
if (animateAlpha < animationParams.alphaEnd) {
animateAlpha = animationParams.alphaEnd;
}
if (animateAlpha > animationParams.alphaStart) {
animateAlpha = animationParams.alphaStart;
}
} else {
// alphaStart < animateAlpha < alphaEnd
if (animateAlpha > animationParams.alphaEnd) {
animateAlpha = animationParams.alphaEnd;
}
if (animateAlpha < animationParams.alphaStart) {
animateAlpha = animationParams.alphaStart;
}
}
wrapper.setAlpha(animateAlpha);
validate();
}
}
void validate() {
Container parent = SwingUtilities.getAncestorOfClass(
CollapsiblePaneContainer.class, JXCollapsiblePane.this);
if (parent != null) {
parent = ((CollapsiblePaneContainer)parent).getValidatingContainer();
} else {
parent = getParent();
}
if (parent != null) {
if (parent instanceof JComponent) {
((JComponent)parent).revalidate();
} else {
parent.invalidate();
}
parent.doLayout();
parent.repaint();
}
}
/**
* Reinitializes the timer for scrolling up/down the component. This method
* is properly synchronized, so you may make this call regardless of whether
* the timer is currently executing or not.
*
* @param startDimension
* @param stopDimension
*/
public void reinit(int startDimension, int stopDimension) {
synchronized (ANIMATION_MUTEX) {
this.startDimension = startDimension;
this.finalDimension = stopDimension;
animateAlpha = animationParams.alphaStart;
currentDimension = -1;
}
}
}
private final class WrapperContainer extends JViewport implements AlphaPaintable {
boolean collapsedState;
private volatile float alpha;
private boolean oldOpaque;
public WrapperContainer(Container c) {
alpha = 1.0f;
collapsedState = false;
setView(c);
// we must ensure the container is opaque. It is not opaque it introduces
// painting glitches specially on Linux with JDK 1.5 and GTK look and feel.
// GTK look and feel calls setOpaque(false)
if (c instanceof JComponent && !c.isOpaque()) {
((JComponent) c).setOpaque(true);
}
}
/**
* {@inheritDoc}
*
* Overridden to not have JViewPort behaviour (that is scroll the view)
* but delegate to parent scrollRectToVisible just a JComponent does.
*/
@Override
public void scrollRectToVisible(Rectangle aRect) {
//avoids JViewport's implementation
//by using JXCollapsiblePane's it will delegate upward
//getting any core fixes, by avoiding c&p
JXCollapsiblePane.this.scrollRectToVisible(aRect);
}
@Override
public float getAlpha() {
return alpha;
}
@Override
public void setAlpha(float alpha) {
if (alpha < 0f || alpha > 1f) {
throw new IllegalArgumentException("invalid alpha value " + alpha);
}
float oldValue = getAlpha();
this.alpha = alpha;
if (getAlpha() < 1f) {
if (oldValue == 1) {
//it used to be 1, but now is not. Save the oldOpaque
oldOpaque = isOpaque();
setOpaque(false);
}
} else {
//restore the oldOpaque if it was true (since opaque is false now)
if (oldOpaque) {
setOpaque(true);
}
}
firePropertyChange("alpha", oldValue, getAlpha());
repaint();
}
@Override
public boolean isInheritAlpha() {
return false;
}
@Override
public void setInheritAlpha(boolean inheritAlpha) {
//does nothing; always false;
}
@Override
public float getEffectiveAlpha() {
return getAlpha();
}
//support for Java 7 painting improvements
protected boolean isPaintingOrigin() {
return getAlpha() < 1f;
}
/**
* Overridden paint method to take into account the alpha setting.
*
* @param g
* the Graphics
context in which to paint
*/
@Override
public void paint(Graphics g) {
//short circuit painting if no transparency
if (getAlpha() == 1f) {
super.paint(g);
} else {
//the component is translucent, so we need to render to
//an intermediate image before painting
// TODO should we cache this image? repaint to same image unless size changes?
BufferedImage img = GraphicsUtilities.createCompatibleTranslucentImage(getWidth(), getHeight());
Graphics2D gfx = img.createGraphics();
try {
super.paint(gfx);
} finally {
gfx.dispose();
}
Graphics2D g2d = (Graphics2D) g;
Composite oldComp = g2d.getComposite();
try {
Composite alphaComp = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, getEffectiveAlpha());
g2d.setComposite(alphaComp);
//TODO should we cache the image?
g2d.drawImage(img, null, 0, 0);
} finally {
g2d.setComposite(oldComp);
}
}
}
}
// TEST CASE
// public static void main(String[] args) {
// SwingUtilities.invokeLater(new Runnable() {
// public void run() {
// JFrame f = new JFrame("Test Oriented Collapsible Pane");
//
// f.add(new JLabel("Press Ctrl+F or Ctrl+G to collapse panes."),
// BorderLayout.NORTH);
//
// JTree tree1 = new JTree();
// tree1.setBorder(BorderFactory.createEtchedBorder());
// f.add(tree1);
//
// JXCollapsiblePane pane = new JXCollapsiblePane(Orientation.VERTICAL);
// pane.setCollapsed(true);
// JTree tree2 = new JTree();
// tree2.setBorder(BorderFactory.createEtchedBorder());
// pane.add(tree2);
// f.add(pane, BorderLayout.SOUTH);
//
// pane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
// KeyStroke.getKeyStroke("ctrl F"),
// JXCollapsiblePane.TOGGLE_ACTION);
//
// pane = new JXCollapsiblePane(Orientation.HORIZONTAL);
// JTree tree3 = new JTree();
// pane.add(tree3);
// tree3.setBorder(BorderFactory.createEtchedBorder());
// f.add(pane, BorderLayout.WEST);
//
// pane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
// KeyStroke.getKeyStroke("ctrl G"),
// JXCollapsiblePane.TOGGLE_ACTION);
//
// f.setSize(640, 480);
// f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// f.setVisible(true);
// }
// });
// }
}