/*
* Copyright 2008 the original author or authors.
*
* 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 org.rioproject.tools.ui.progresspanel;
import javax.swing.*;
import javax.swing.event.AncestorEvent;
import javax.swing.event.AncestorListener;
import java.awt.*;
import java.awt.event.*;
import java.awt.font.FontRenderContext;
import java.awt.font.TextLayout;
import java.awt.geom.*;
import java.awt.image.BufferedImage;
/**
* A InfiniteProgressPanel-like component, but more efficient. This is the
* preferred class to use unless you need the total control over the appearance
* that InfiniteProgressPanel gives you.
*
* <p>An infinite progress panel
* displays a rotating figure and a message to notice the user of a long,
* duration unknown task. The shape and the text are drawn upon a white veil
* which alpha level (or shield value) lets the underlying component shine
* through. This panel is meant to be used as a <i>glass pane</i> in the window
* performing the long operation.
*
* <p>Calling setVisible(true) makes
* the component visible and starts the animation. Calling setVisible(false)
* halts the animation and makes the component invisible. Once you've started
* the animation all the mouse events are intercepted by this panel, preventing
* them from being forwared to the underlying components.
*
* <p>The panel
* can be controlled by the <code>setVisible()</code>, method.
*
* <p>This version of the infinite progress panel does not display any fade
* in/out when
* the animation is started/stopped.
*
*/
public class SingleComponentInfiniteProgress extends JComponent
implements ActionListener {
private static final double UNSCALED_BAR_SIZE = 45d;
public static final int DEFAULT_NUMBER_OF_BARS = 12;
public static final int DEFAULT_FPS = 10;
public static final double NO_AUTOMATIC_RESIZING = -1d;
public static final double NO_MAX_BAR_SIZE = -1d;
private int numBars;
private double dScale = 1.2d;
private double resizeRatio = NO_AUTOMATIC_RESIZING;
private double maxBarSize = 64d;
private double minBarSize = 4;
private boolean useBackBuffer;
private String text;
private MouseAdapter mouseAdapter = new MouseAdapter() {
};
private MouseMotionAdapter mouseMotionAdapter = new MouseMotionAdapter() {
};
private KeyAdapter keyAdapter = new KeyAdapter() {
};
private ComponentAdapter componentAdapter = new ComponentAdapter() {
public void componentResized(final ComponentEvent e) {
if (useBackBuffer) {
setOpaque(false);
imageBuf = null;
iterate = 3;
}
}
};
private BufferedImage imageBuf = null;
private Area[] bars;
private Rectangle barsBounds = null;
private Rectangle barsScreenBounds = null;
private AffineTransform centerAndScaleTransform = null;
private Timer timer;
private Color[] colors = null;
private int colorOffset = 0;
private boolean tempHide = false;
public SingleComponentInfiniteProgress() {
this(true);
}
public SingleComponentInfiniteProgress(boolean i_bUseBackBuffer) {
this(i_bUseBackBuffer, DEFAULT_NUMBER_OF_BARS);
}
public SingleComponentInfiniteProgress(int numBars) {
this(true, numBars, DEFAULT_FPS, NO_AUTOMATIC_RESIZING, NO_MAX_BAR_SIZE);
}
public SingleComponentInfiniteProgress(boolean i_bUseBackBuffer,
int numBars) {
this(i_bUseBackBuffer,
numBars,
DEFAULT_FPS,
NO_AUTOMATIC_RESIZING,
NO_MAX_BAR_SIZE);
}
public SingleComponentInfiniteProgress(boolean i_bUseBackBuffer,
int numBars,
int fps,
double resizeRatio,
double maxBarSize) {
this.useBackBuffer = i_bUseBackBuffer;
this.numBars = numBars;
this.resizeRatio = resizeRatio;
this.maxBarSize = maxBarSize;
this.timer = new Timer(1000 / fps, this);
colors = new Color[numBars * 2];
// build bars
bars = buildTicker(numBars);
// calculate bars bounding rectangle
barsBounds = new Rectangle();
for (Area bar : bars) {
barsBounds = barsBounds.union(bar.getBounds());
}
// create colors
for (int i = 0; i < bars.length; i++) {
int channel = 224 - 128 / (i + 1);
colors[i] = new Color(channel, channel, channel);
colors[numBars + i] = colors[i];
}
// set cursor
setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
// set opaque
setOpaque(useBackBuffer);
}
/**
* Show/Hide the pane, starting and stopping the animation as you go
*/
public void setVisible(boolean i_bIsVisible) {
final boolean old = isVisible();
setOpaque(false);
// capture
if (i_bIsVisible) {
if (useBackBuffer) {
// add window resize listener
Window w = SwingUtilities.getWindowAncestor(this);
if (w != null) {
w.addComponentListener(componentAdapter);
} else {
addAncestorListener(new AncestorListener() {
public void ancestorAdded(AncestorEvent event) {
Window w = SwingUtilities.getWindowAncestor(
SingleComponentInfiniteProgress.this);
if (w != null) {
w.addComponentListener(componentAdapter);
}
}
public void ancestorRemoved(AncestorEvent event) {
}
public void ancestorMoved(AncestorEvent event) {
}
});
}
iterate = 3;
}
// capture events
addMouseListener(mouseAdapter);
addMouseMotionListener(mouseMotionAdapter);
addKeyListener(keyAdapter);
timer.start();
} else {
// stop anim
timer.stop();
/// free back buffer
imageBuf = null;
// stop capturing events
removeMouseListener(mouseAdapter);
removeMouseMotionListener(mouseMotionAdapter);
removeKeyListener(keyAdapter);
// remove window resize listener
Window oWindow = SwingUtilities.getWindowAncestor(this);
if (oWindow != null) {
oWindow.removeComponentListener(componentAdapter);
}
}
super.setVisible(i_bIsVisible);
firePropertyChange("running", old, i_bIsVisible);
}
/**
* Recalc bars based on changes in size
*/
public void setBounds(int x, int y, int width, int height) {
super.setBounds(x, y, width, height);
recalcSize();
}
@Override
public void setSize(int i, int i1) {
super.setSize(i, i1);
recalcSize();
}
private void recalcSize() {
calcBarsForBounds(getWidth(), getHeight(), true);
// Now, see if the text fits...
final double maxY = getTextMaxY(getText(),
getFont(),
(Graphics2D) getGraphics(),
barsScreenBounds.getMaxY());
final int bottom = getY() + getHeight();
if (maxY >= bottom) {
final int neededSpace = (int) Math.ceil(maxY) - bottom;
calcBarsForBounds(getWidth(),
Math.max(1,
getHeight() - neededSpace),
false);
}
}
/**
* paint background dimed and bars over top
*/
protected void paintComponent(Graphics g) {
if (!tempHide) {
paintBars(g, true);
}
}
public void start() {
setVisible(true);
}
public void stop() {
setVisible(false);
}
public void setText(String text) {
final String old = this.text;
this.text = text;
repaint();
firePropertyChange("text", old, text);
}
public JComponent getComponent() {
return this;
}
//
// METHODS FROM INTERFACE ActionListener
//
int iterate; //we draw use transparency to draw a number of iterations before making a snapshot
/**
* Called to animate the rotation of the bar's colors
*/
public void actionPerformed(ActionEvent e) {
// rotate colors
if (colorOffset == (numBars - 1)) {
colorOffset = 0;
} else {
colorOffset++;
}
// repaint
if (barsScreenBounds != null) {
repaint(barsScreenBounds);
} else {
repaint();
}
if (useBackBuffer && imageBuf == null) {
if (iterate < 0) {
try {
makeSnapshot();
setOpaque(true);
} catch (AWTException e1) {
e1.printStackTrace();
}
} else {
iterate--;
}
}
}
public String getText() {
return text;
}
public double getResizeRatio() {
return resizeRatio;
}
public void setResizeRatio(final double resizeRatio) {
final double old = this.resizeRatio;
this.resizeRatio = resizeRatio;
setBounds(getBounds());
repaint();
firePropertyChange("resizeRatio", old, resizeRatio);
}
public double getMaxBarSize() {
return maxBarSize;
}
public void setMaxBarSize(final double maxBarSize) {
final double old = this.maxBarSize;
this.maxBarSize = maxBarSize;
setBounds(getBounds());
repaint();
firePropertyChange("maxBarSize", old, maxBarSize);
}
public int getNumBars() {
return numBars;
}
public boolean getUseBackBuffer() {
return useBackBuffer;
}
public boolean isRunning() {
return isVisible();
}
private void makeSnapshot() throws AWTException {
final Rectangle bounds = getBounds();
final Point upperLeft = new Point(bounds.x, bounds.y);
SwingUtilities.convertPointToScreen(upperLeft, this);
final Rectangle screenRect = new Rectangle(upperLeft.x,
upperLeft.y,
bounds.width,
bounds.height);
Insets insets = getInsets();
screenRect.x += insets.left;
screenRect.y += insets.top;
screenRect.width -= insets.left + insets.right;
screenRect.height -= insets.top + insets.bottom;
// capture window contents
imageBuf = new Robot().createScreenCapture(screenRect);
//no need to fade because we are allready using an image that is showing through
}
protected void calcBarsForBounds(final int width,
final int height,
final boolean honorMinBarSize) {
// update centering transform
centerAndScaleTransform = new AffineTransform();
centerAndScaleTransform.translate((double) width / 2d,
(double) height / 2d);
double scale = dScale;
if (resizeRatio != NO_AUTOMATIC_RESIZING) {
final int minSpace = Math.min(width, height);
scale = (minSpace * resizeRatio) / UNSCALED_BAR_SIZE;
if (maxBarSize != NO_MAX_BAR_SIZE &&
(UNSCALED_BAR_SIZE * scale) >= maxBarSize) {
scale = maxBarSize / UNSCALED_BAR_SIZE;
}
if (honorMinBarSize && (UNSCALED_BAR_SIZE * scale < minBarSize)) {
scale = minBarSize / UNSCALED_BAR_SIZE;
}
}
centerAndScaleTransform.scale(scale, scale);
calcNewBarsBounds();
}
private void calcNewBarsBounds() {
if (barsBounds != null) {
Area oBounds = new Area(barsBounds);
oBounds.transform(centerAndScaleTransform);
barsScreenBounds = oBounds.getBounds();
}
}
protected double paintBars(final Graphics g,
final boolean paintBackground) {
Rectangle oClip = g.getClipBounds();
if (paintBackground) {
if (imageBuf != null) {
// draw background image
g.drawImage(imageBuf,
oClip.x,
oClip.y,
oClip.x + oClip.width,
oClip.y + oClip.height,
oClip.x,
oClip.y,
oClip.x + oClip.width,
oClip.y + oClip.height,
null);
g.drawImage(imageBuf, 0, 0, null);
} else {
g.setColor(new Color(255, 255, 255, 180));
g.fillRect(oClip.x, oClip.y, oClip.width, oClip.height);
}
}
// move to center
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g2.transform(centerAndScaleTransform);
// draw ticker
for (int i = 0; i < bars.length; i++) {
g2.setColor(colors[i + colorOffset]);
g2.fill(bars[i]);
}
// NOTE: we pass in g and not g2 so that the transformation is not
// applied.
// NOTE: this will not contain the size of the sub components, since the
// paintSubComponents(...) method does not provide this information, and
// I don't feel like patching the adapter since I don't use it right
// now. :)
return drawTextAt(text,
getFont(),
(Graphics2D) g,
getWidth(),
barsScreenBounds.getMaxY(),
getForeground());
}
/**
* Draw text in a Graphics2D.
*
* @param text the text to draw
* @param font the font to use
* @param g2 the graphics context to draw in
* @param width the width of the parent, so it can be centered
* @param y the height at which to draw
* @param foreGround the foreground color to draw in
* @return the y value that is the y param + the text height.
*/
public static double drawTextAt(String text, Font font, Graphics2D g2,
int width, final double y, Color foreGround) {
final TextLayout layout = getTextLayout(text, font, g2);
if (layout != null) {
Rectangle2D bounds = layout.getBounds();
g2.setColor(foreGround);
float textX = (float) (width - bounds.getWidth()) / 2;
float horizontal = (float) (y + layout.getLeading() + 2 * layout.getAscent());
layout.draw(g2, textX, horizontal);
return y + bounds.getHeight();
} else {
return 0d;
}
}
public static double getTextMaxY(final String text, final Font font,
final Graphics2D g2, final double y) {
final TextLayout layout = getTextLayout(text, font, g2);
if (layout != null) {
return y + layout.getLeading() + (2 * layout.getAscent()) +
layout.getBounds().getHeight();
} else {
return 0d;
}
}
private static TextLayout getTextLayout(final String text, final Font font,
final Graphics2D g2) {
if (text != null && text.length() > 0) {
FontRenderContext context = g2.getFontRenderContext();
return new TextLayout(text, font, context);
} else {
return null;
}
}
/*
* Builds the circular shape and returns the result as an array of
* <code>Area</code>. Each <code>Area</code> is one of the bars composing the
* shape.
*/
private static Area[] buildTicker(int i_iBarCount) {
Area[] ticker = new Area[i_iBarCount];
Point2D.Double center = new Point2D.Double(0, 0);
double fixedAngle = 2.0 * Math.PI / ((double) i_iBarCount);
for (double i = 0.0; i < (double) i_iBarCount; i++) {
Area primitive = buildPrimitive();
AffineTransform toCenter = AffineTransform.getTranslateInstance(
center.getX(), center.getY());
AffineTransform toBorder = AffineTransform.getTranslateInstance(
UNSCALED_BAR_SIZE,
-6.0);
AffineTransform toCircle = AffineTransform.getRotateInstance(
-i * fixedAngle, center.getX(), center.getY());
AffineTransform toWheel = new AffineTransform();
toWheel.concatenate(toCenter);
toWheel.concatenate(toBorder);
primitive.transform(toWheel);
primitive.transform(toCircle);
ticker[(int) i] = primitive;
}
return ticker;
}
/*
* Builds a bar.
*/
private static Area buildPrimitive() {
Rectangle2D.Double body = new Rectangle2D.Double(6, 0, 30, 12);
Ellipse2D.Double head = new Ellipse2D.Double(0, 0, 12, 12);
Ellipse2D.Double tail = new Ellipse2D.Double(30, 0, 12, 12);
Area tick = new Area(body);
tick.add(new Area(head));
tick.add(new Area(tail));
return tick;
}
}