com.l2fprod.common.swing.JCollapsiblePane Maven / Gradle / Ivy
Show all versions of orbit-image-analysis Show documentation
/**
* $ $ License.
*
* Copyright $ L2FProd.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.l2fprod.common.swing;
import java.awt.AlphaComposite;
import java.awt.BorderLayout;
import java.awt.Component;
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.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import javax.swing.AbstractAction;
import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
/**
* JCollapsiblePane
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.
*
*
* In this example, the JCollapsiblePane
is used to build
* a Search pane which can be shown and hidden on demand.
*
*
*
* JCollapsiblePane cp = new JCollapsiblePane();
*
* // JCollapsiblePane 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);
*
* JFrame frame = new JFrame();
* 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("toggle"));
* frame.add("South", toggle);
*
* frame.pack();
* frame.setVisible(true);
*
*
*
*
* Note: JCollapsiblePane
requires its parent container to have a
* {@link java.awt.LayoutManager} using {@link #getPreferredSize()} when
* calculating its layout (example {@link com.l2fprod.common.swing.PercentLayout},
* {@link java.awt.BorderLayout}).
*
* @javabean.attribute
* name="isContainer"
* value="Boolean.TRUE"
* rtexpr="true"
*
* @javabean.attribute
* name="containerDelegate"
* value="getContentPane"
*
* @javabean.class
* name="JCollapsiblePane"
* shortDescription="A pane which hides its content with an animation."
* stopClass="java.awt.Component"
*
* @author rbair (from the JDNC project)
* @author Frederic Lavigne
*/
public class JCollapsiblePane extends JPanel {
/**
* Used when generating PropertyChangeEvents for the "animationState" property
*/
public final static String ANIMATION_STATE_KEY = "animationState";
/**
* Indicates whether the component is collapsed or expanded
*/
private boolean collapsed = false;
/**
* Timer used for doing the transparency animation (fade-in)
*/
private Timer animateTimer;
private AnimationListener animator;
private int currentHeight = -1;
private WrapperContainer wrapper;
private boolean useAnimation = true;
private AnimationParams animationParams;
/**
* Constructs a new JCollapsiblePane with a {@link JPanel} as content pane and
* a vertical {@link PercentLayout} with a gap of 2 pixels as layout manager.
*/
public JCollapsiblePane() {
super.setLayout(new BorderLayout(0, 0));
JPanel panel = new JPanel();
panel.setLayout(new PercentLayout(PercentLayout.VERTICAL, 2));
setContentPane(panel);
animator = new AnimationListener();
setAnimationParams(new AnimationParams(30, 8, 0.01f, 1.0f));
// add an action to automatically toggle the state of the pane
getActionMap().put("toggle", new AbstractAction("Toggle") {
public void actionPerformed(ActionEvent e) {
setCollapsed(!isCollapsed());
}
});
}
/**
* Sets the content pane of this JCollapsiblePane. Components must be added
* to this content pane, not to the JCollapsiblePane.
*
* @param contentPanel
* @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) {
super.remove(wrapper);
}
wrapper = new WrapperContainer(contentPanel);
super.addImpl(wrapper, BorderLayout.CENTER, -1);
}
/**
* @return the content pane
*/
public Container getContentPane() {
return wrapper.c;
}
/**
* Overriden to redirect call to the content pane.
*/
public void setLayout(LayoutManager mgr) {
// wrapper can be null when setLayout is called by "super()" constructor
if (wrapper != null) {
getContentPane().setLayout(mgr);
}
}
/**
* Overriden to redirect call to the content pane.
*/
protected void addImpl(Component comp, Object constraints, int index) {
getContentPane().add(comp, constraints, index);
}
/**
* Overriden to redirect call to the content pane
*/
public void remove(Component comp) {
getContentPane().remove(comp);
}
/**
* Overriden to redirect call to the content pane.
*/
public void remove(int index) {
getContentPane().remove(index);
}
/**
* Overriden to redirect call to the content pane.
*/
public void removeAll() {
getContentPane().removeAll();
}
/**
* If true, enables the animation when pane is collapsed/expanded. If false,
* animation is turned off.
*
*
* When animated, the JCollapsiblePane
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 JCollapsiblePane
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;
firePropertyChange("animated", !useAnimation, useAnimation);
}
}
/**
* @return true if the pane is animated, false otherwise
* @see #setAnimated(boolean)
*/
public boolean isAnimated() {
return useAnimation;
}
/**
* @return true if the pane is collapsed, false if expanded
*/
public boolean isCollapsed() {
return collapsed;
}
/**
* Expands or collapses this JCollapsiblePane
.
*
*
* If the component is collapsed and val
is false, then this
* call expands the JCollapsiblePane, such that the entire JCollapsiblePane
* 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 JCollapsiblePane, such that the entire
* JCollapsiblePane will be invisible. If {@link #isAnimated()} returns true,
* the collapse will be accompanied by an animation.
*
* @see #isAnimated()
* @see #setAnimated(boolean)
* @javabean.property
* bound="true"
* preferred="true"
*/
public void setCollapsed(boolean val) {
if (collapsed != val) {
collapsed = val;
if (isAnimated()) {
if (collapsed) {
setAnimationParams(new AnimationParams(30, Math.max(8, wrapper
.getHeight() / 10), 1.0f, 0.01f));
animator.reinit(wrapper.getHeight(), 0);
animateTimer.start();
} else {
setAnimationParams(new AnimationParams(30, Math.max(8,
getContentPane().getPreferredSize().height / 10), 0.01f, 1.0f));
animator.reinit(wrapper.getHeight(), getContentPane()
.getPreferredSize().height);
animateTimer.start();
}
} else {
wrapper.c.setVisible(!collapsed);
invalidate();
doLayout();
}
repaint();
firePropertyChange("collapsed", !collapsed, collapsed);
}
}
public Dimension getMinimumSize() {
return getPreferredSize();
}
/**
* The critical part of the animation of this JCollapsiblePane
* 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
*/
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 currentHeight variable)
*/
Dimension dim;
if (!isAnimated()) {
if (getContentPane().isVisible()) {
dim = getContentPane().getPreferredSize();
} else {
dim = super.getPreferredSize();
}
} else {
dim = new Dimension(getContentPane().getPreferredSize());
if (!getContentPane().isVisible() && currentHeight != -1) {
dim.height = currentHeight;
}
}
return dim;
}
/**
* 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 JCollapsiblePane hierarchy who needs
* to be revalidated (invalidate/validate/repaint) when the pane is expanding
* or collapsing. Usually validating only the parent of the JCollapsiblePane
* is enough but there might be cases where the parent parent must be
* validated.
*/
public static interface JCollapsiblePaneContainer {
Container getValidatingContainer();
}
/**
* Parameters controlling the animations
*/
private static class AnimationParams {
final int waitTime;
final int deltaY;
final float alphaStart;
final float alphaEnd;
/**
* @param waitTime
* the amount of time in milliseconds to wait between calls to the
* animation thread
* @param deltaY
* the delta in the Y direction 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 deltaY, float alphaStart,
float alphaEnd) {
this.waitTime = waitTime;
this.deltaY = deltaY;
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 startHeight/finalHeight are not changed
* during a repaint operation.
*/
private final Object ANIMATION_MUTEX = "Animation Synchronization Mutex";
/**
* This is the starting height when animating. If > finalHeight, then the
* animation is going to be to scroll up the component. If it is < then
* finalHeight, then the animation will scroll down the component.
*/
private int startHeight = 0;
/**
* This is the final height that the content container is going to be when
* scrolling is finished.
*/
private int finalHeight = 0;
/**
* The current alpha setting used during "animation" (fade-in/fade-out)
*/
private float animateAlpha = 1.0f;
public void actionPerformed(ActionEvent e) {
/*
* Pre-1) If startHeight == finalHeight, 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 (startHeight == finalHeight) {
animateTimer.stop();
animateAlpha = animationParams.alphaEnd;
// keep the content pane hidden when it is collapsed, other it may
// still receive focus.
if (finalHeight > 0) {
wrapper.showContent();
validate();
JCollapsiblePane.this.firePropertyChange(ANIMATION_STATE_KEY, null,
"expanded");
return;
}
}
final boolean contracting = startHeight > finalHeight;
final int delta_y = contracting?-1 * animationParams.deltaY
:animationParams.deltaY;
int newHeight = wrapper.getHeight() + delta_y;
if (contracting) {
if (newHeight < finalHeight) {
newHeight = finalHeight;
}
} else {
if (newHeight > finalHeight) {
newHeight = finalHeight;
}
}
animateAlpha = (float)newHeight
/ (float)wrapper.c.getPreferredSize().height;
Rectangle bounds = wrapper.getBounds();
int oldHeight = bounds.height;
bounds.height = newHeight;
wrapper.setBounds(bounds);
bounds = getBounds();
bounds.height = (bounds.height - oldHeight) + newHeight;
currentHeight = bounds.height;
setBounds(bounds);
startHeight = newHeight;
// 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.alpha = animateAlpha;
validate();
}
}
void validate() {
Container parent = SwingUtilities.getAncestorOfClass(
JCollapsiblePaneContainer.class, JCollapsiblePane.this);
if (parent != null) {
parent = ((JCollapsiblePaneContainer)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 startHeight
* @param stopHeight
*/
public void reinit(int startHeight, int stopHeight) {
synchronized (ANIMATION_MUTEX) {
JCollapsiblePane.this.firePropertyChange(ANIMATION_STATE_KEY, null,
"reinit");
this.startHeight = startHeight;
this.finalHeight = stopHeight;
animateAlpha = animationParams.alphaStart;
currentHeight = -1;
wrapper.showImage();
}
}
}
private final class WrapperContainer extends JPanel {
private BufferedImage img;
private Container c;
float alpha = 1.0f;
public WrapperContainer(Container c) {
super(new BorderLayout());
this.c = c;
add(c, BorderLayout.CENTER);
// 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 && !((JComponent)c).isOpaque()) {
((JComponent)c).setOpaque(true);
}
}
public void showImage() {
// render c into the img
makeImage();
c.setVisible(false);
}
public void showContent() {
currentHeight = -1;
c.setVisible(true);
}
void makeImage() {
// if we have no image or if the image has changed
if (getGraphicsConfiguration() != null && getWidth() > 0) {
Dimension dim = c.getPreferredSize();
// width and height must be > 0 to be able to create an image
if (dim.height > 0) {
img = getGraphicsConfiguration().createCompatibleImage(getWidth(),
dim.height);
c.setSize(getWidth(), dim.height);
c.paint(img.getGraphics());
} else {
img = null;
}
}
}
public void paintComponent(Graphics g) {
if (!useAnimation || c.isVisible()) {
super.paintComponent(g);
} else {
// within netbeans, it happens we arrive here and the image has not been
// created yet. We ensure it is.
if (img == null) {
makeImage();
}
// and we paint it only if it has been created and only if we have a
// valid graphics
if (g != null && img != null) {
// draw the image with y being height - imageHeight
g.drawImage(img, 0, getHeight() - img.getHeight(), null);
}
}
}
public void paint(Graphics g) {
Graphics2D g2d = (Graphics2D)g;
Composite oldComp = g2d.getComposite();
Composite alphaComp = AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
alpha);
g2d.setComposite(alphaComp);
super.paint(g2d);
g2d.setComposite(oldComp);
}
}
}