package gui;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import javax.swing.JComponent;
import javax.swing.JTree;
import javax.swing.JViewport;
import javax.swing.Timer;
import javax.swing.plaf.basic.BasicTreeUI;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.TreePath;
import util.StaticIcon;
public class AnimatedTreeUI extends BasicTreeUI {
private static BufferedImage topImage;
private static BufferedImage bottomImage;
private static BufferedImage compositeImage;
// amount to offset bottom image position during animation
private static int offsetY;
// height of newly exposed subtree
private static int subTreeHeight;
private static Timer timer;
private static ActionListener timerListener;
private static enum AnimationState {
EXPANDING, COLLAPSING, NOT_ANIMATING
};
private static AnimationState animationState;
// animation progresses 1f -> 0f
private static float animationComplete = 0f;
// 0f = faster, 1f = slower
private static float ANIMATION_SPEED = 0.8f;
private static Color colorLeaf = new Color( 0xB7, 0xF5, 0xC6, 180 );
private static Color colorLeafBorder = new Color( 0x9E, 0xDA, 0xAD, 180 );
private static Color colorLeafSel = new Color( 0x29, 0x97, 0xF8, 180 );
private static Color colorLeafSelBorder = new Color( 0x11, 0x64, 0xAC, 180 );
private static Color colorLeafPause = new Color( 0xEB, 0xB0, 0x30, 180 );
private static Color colorLeafPauseBorder = new Color( 0xB4, 0x85, 0x1E, 180 );
private static Color colorLeafFaulty = new Color( 0xDB, 0x38, 0x38, 180 );
private static Color colorLeafFaultyBorder = new Color( 0xB0, 0x1F, 0x1F, 180 );
private static StaticIcon play = new StaticIcon( StaticIcon.miniPlayIcon );
private static StaticIcon playGrey = new StaticIcon( StaticIcon.miniPlayGreyIcon );
private static StaticIcon pause = new StaticIcon( StaticIcon.miniPauseIcon );
private static StaticIcon pauseGrey = new StaticIcon( StaticIcon.miniPauseGreyIcon );
private static StaticIcon stop = new StaticIcon( StaticIcon.miniStopIcon );
private static StaticIcon stopGrey = new StaticIcon( StaticIcon.miniStopGreyIcon );
private static StaticIcon del = new StaticIcon( StaticIcon.miniDelIcon );
private static StaticIcon delGrey = new StaticIcon( StaticIcon.miniDelGreyIcon );
private static StaticIcon retry = new StaticIcon( StaticIcon.miniRetryIcon );
private static StaticIcon retryGrey = new StaticIcon( StaticIcon.miniRetryGreyIcon );
public AnimatedTreeUI() {
super();
animationState = AnimatedTreeUI.AnimationState.NOT_ANIMATING;
timerListener = new TimerListener();
timer = new Timer( 1000 / 90, timerListener );
}
public static void drawCell( Graphics g, Rectangle bounds, JTree tree, TreePath path, int row ) {
boolean isRowSelected = tree.isRowSelected( row );
DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent();
if ( node instanceof LeafNode ) {
Color colorTmp = colorLeaf;
Color colorBorderTmp = colorLeafBorder;
if ( isRowSelected ) {
colorTmp = colorLeafSel;
colorBorderTmp = colorLeafSelBorder;
}
else if ( ( (LeafNode) node ).getDownload() != null && ( (LeafNode) node ).getDownload().isPaused() ) {
colorTmp = colorLeafPause;
colorBorderTmp = colorLeafPauseBorder;
}
else if ( ( (LeafNode) node ).getDownload() != null && ( (LeafNode) node ).getDownload().isFaulty() ) {
colorTmp = colorLeafFaulty;
colorBorderTmp = colorLeafFaultyBorder;
}
Graphics2D g2 = (Graphics2D) g.create();
g2.setColor( colorTmp );
// Download bar
g2.fillRect( tree.getVisibleRect().x + 4, bounds.y + 2, Math.round( ( ( tree.getVisibleRect().width - 10 )
* ( (LeafNode) node ).getDownPerc() * 100 ) / 100 ), bounds.height - 5 );
g2.setColor( colorBorderTmp );
g2.drawRect( tree.getVisibleRect().x + 4, bounds.y + 2, tree.getVisibleRect().width - 10, bounds.height - 5 );
g2.dispose();
}
}
public static void drawControls( Graphics g, Rectangle bounds, JTree tree, TreePath path, int row, int[] mousePos ) {
boolean isRowSelected = tree.isRowSelected( row );
if (isRowSelected) {
Object obj = tree.getLastSelectedPathComponent();
if (!(obj instanceof LeafNode))
return;
LeafNode node = (LeafNode) obj;
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
int xPos = ( tree.getVisibleRect().width + tree.getVisibleRect().x ) - 43;
// Background for controls
g2.setColor( new Color( 255, 255, 255, 170 ) );
g2.fillRect( xPos, bounds.y + 3, 37, 18 );
int selection = 0;
// Mouse over control
if ( mousePos[0] >= xPos && mousePos[0] < (xPos + 19) )
selection = 1;
else if ( mousePos[0] >= ( xPos + 19 ) && mousePos[0] < (xPos + 37 ) )
selection = 2;
int w;
int h;
BufferedImage bi;
Graphics g3;
if ( node.getDownload().isCompleted()
&& !node.getDownload().isFaulty() ) {
if ( selection == 1 ) {
w = play.getIconWidth();
h = play.getIconHeight();
bi = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
g3 = bi.getGraphics();
play.paintIcon(null, g3, 0, 0);
g2.drawImage(bi, null, xPos + 2, bounds.y + 4 );
tree.setToolTipText( "Launch file" );
}
else {
w = playGrey.getIconWidth();
h = playGrey.getIconHeight();
bi = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
g3 = bi.getGraphics();
playGrey.paintIcon(null, g3, 0, 0);
g2.drawImage(bi, null, xPos + 2, bounds.y + 4 );
}
if ( selection == 2 ) {
w = del.getIconWidth();
h = del.getIconHeight();
bi = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
g3 = bi.getGraphics();
del.paintIcon(null, g3, 0, 0);
g2.drawImage(bi, null, xPos + 19, bounds.y + 4 );
tree.setToolTipText( "Remove from downloads" );
}
else {
w = delGrey.getIconWidth();
h = delGrey.getIconHeight();
bi = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
g3 = bi.getGraphics();
delGrey.paintIcon(null, g3, 0, 0);
g2.drawImage(bi, null, xPos + 19, bounds.y + 4 );
}
}
else {
if ( node.getDownload().isCanceled()
|| node.getDownload().isFaulty() ) {
if ( selection == 1 ) {
w = retry.getIconWidth();
h = retry.getIconHeight();
bi = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
g3 = bi.getGraphics();
retry.paintIcon(null, g3, 0, 0);
g2.drawImage(bi, null, xPos + 2, bounds.y + 4 );
tree.setToolTipText( "Retry download" );
}
else {
w = retryGrey.getIconWidth();
h = retryGrey.getIconHeight();
bi = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
g3 = bi.getGraphics();
retryGrey.paintIcon(null, g3, 0, 0);
g2.drawImage(bi, null, xPos + 2, bounds.y + 4 );
}
}
else if ( node.getDownload().isPaused() ) {
if ( selection == 1 ) {
w = play.getIconWidth();
h = play.getIconHeight();
bi = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
g3 = bi.getGraphics();
play.paintIcon(null, g3, 0, 0);
g2.drawImage(bi, null, xPos + 2, bounds.y + 4 );
tree.setToolTipText( "Resume download" );
}
else {
w = playGrey.getIconWidth();
h = playGrey.getIconHeight();
bi = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
g3 = bi.getGraphics();
playGrey.paintIcon(null, g3, 0, 0);
g2.drawImage(bi, null, xPos + 2, bounds.y + 4 );
}
}
else {
if ( selection == 1 ) {
w = pause.getIconWidth();
h = pause.getIconHeight();
bi = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
g3 = bi.getGraphics();
pause.paintIcon(null, g3, 0, 0);
g2.drawImage(bi, null, xPos + 2, bounds.y + 4 );
tree.setToolTipText( "Pause download" );
}
else {
w = pauseGrey.getIconWidth();
h = pauseGrey.getIconHeight();
bi = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
g3 = bi.getGraphics();
pauseGrey.paintIcon(null, g3, 0, 0);
g2.drawImage(bi, null, xPos + 2, bounds.y + 4 );
}
}
if ( node.getDownload().isCanceled()
|| node.getDownload().isFaulty() ) {
if ( selection == 2 ) {
w = del.getIconWidth();
h = del.getIconHeight();
bi = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
g3 = bi.getGraphics();
del.paintIcon(null, g3, 0, 0);
g2.drawImage(bi, null, xPos + 19, bounds.y + 4 );
tree.setToolTipText( "Remove from downloads" );
}
else {
w = delGrey.getIconWidth();
h = delGrey.getIconHeight();
bi = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
g3 = bi.getGraphics();
delGrey.paintIcon(null, g3, 0, 0);
g2.drawImage(bi, null, xPos + 19, bounds.y + 4 );
}
}
else {
if ( selection == 2 ) {
w = stop.getIconWidth();
h = stop.getIconHeight();
bi = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
g3 = bi.getGraphics();
stop.paintIcon(null, g3, 0, 0);
g2.drawImage(bi, null, xPos + 19, bounds.y + 4 );
tree.setToolTipText( "Cancel download" );
}
else {
w = stopGrey.getIconWidth();
h = stopGrey.getIconHeight();
bi = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
g3 = bi.getGraphics();
stopGrey.paintIcon(null, g3, 0, 0);
g2.drawImage(bi, null, xPos + 19, bounds.y + 4 );
}
}
}
if ( selection == 0 )
tree.setToolTipText( "Select item" );
g2.dispose();
}
}
@Override
protected void paintRow( Graphics g, Rectangle clipBounds, Insets insets, Rectangle bounds,
TreePath path, int row, boolean isExpanded, boolean hasBeenExpanded, boolean isLeaf ) {
drawCell( g, bounds, tree, path, row );
super.paintRow( g, clipBounds, insets, bounds, path, row, isExpanded, hasBeenExpanded, isLeaf );
}
/**
* All expand and collapsing done by the UI comes through here. Use it to
* trigger the start of animation
*
* @param path
*/
protected void toggleExpandState( TreePath path ) {
if ( animationState != AnimatedTreeUI.AnimationState.NOT_ANIMATING )
return;
DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent();
if ( node instanceof LeafNode )
return;
if ( node == treeModel.getRoot() )
return;
if ( node.isLeaf() )
return;
animationComplete = 1f;
boolean state = !tree.isExpanded( path );
if ( state ) {
super.toggleExpandState( path );
subTreeHeight = getSubTreeHeight( path );
createImages( path );
animationState = AnimatedTreeUI.AnimationState.EXPANDING;
}
else {
subTreeHeight = getSubTreeHeight( path );
createImages( path );
super.toggleExpandState( path );
animationState = AnimatedTreeUI.AnimationState.COLLAPSING;
}
updateCompositeImage();
timer.restart();
}
/**
* Grab two images from the tree. The bit below the expanded node
* (bottomImage), and the rest above it (topImage)
*/
private void createImages( TreePath path ) {
int h = tree.getHeight();
int w = tree.getWidth();
BufferedImage baseImage = new BufferedImage( w, h, BufferedImage.TYPE_INT_RGB );
Graphics g = baseImage.getGraphics();
tree.paint( g );
// find the next row, this is where we take the overlay image from
int row = tree.getRowForPath( path ) + 1;
offsetY = tree.getRowBounds( row ).y;
topImage = new BufferedImage( tree.getWidth(), offsetY, BufferedImage.TYPE_INT_RGB );
Graphics topG = topImage.getGraphics();
topG.drawImage( baseImage, 0, 0, w, offsetY, // destination
0, 0, w, offsetY, // source
null );
bottomImage = new BufferedImage( w, baseImage.getHeight() - offsetY, BufferedImage.TYPE_INT_RGB );
Graphics bottomG = bottomImage.getGraphics();
bottomG.drawImage( baseImage, 0, 0, w, baseImage.getHeight() - offsetY, // destination
0, offsetY, w, baseImage.getHeight(), // source
null );
compositeImage = new BufferedImage( w, h, BufferedImage.TYPE_INT_RGB );
g.dispose();
topG.dispose();
bottomG.dispose();
}
/**
* create image to paint when hijacked, by painting the lower half of the
* image offset by an amount determined by the animation. Then paint the top
* part of the tree over the top, so some of the bottom peeks out.
*/
private void updateCompositeImage() {
Graphics g = compositeImage.getGraphics();
g.setColor( tree.getBackground() );
g.fillRect( 0, 0, compositeImage.getWidth(), compositeImage.getHeight() );
int yOff = (int) ( ( (float) subTreeHeight ) * ( animationComplete ) );
if ( animationState == AnimatedTreeUI.AnimationState.COLLAPSING )
yOff = subTreeHeight - yOff;
int dy1 = offsetY - yOff;
g.drawImage( bottomImage, 0, dy1, null );
g.drawImage( topImage, 0, 0, null );
g.dispose();
}
private boolean isAnimationComplete() {
switch ( animationState ) {
case COLLAPSING:
case EXPANDING:
return animationComplete * offsetY < 1.3f;
default:
return true;
}
}
/**
* get the height of the sub tree by measuring from the location of the
* first child in the subtree to the bottom of its last sibling The sub tree
* should be expanded for this to work correctly.
*/
public int getSubTreeHeight( TreePath path ) {
if ( path.getParentPath() == null )
return 0;
Object origObj = path.getLastPathComponent();
if ( getModel().getChildCount( origObj ) == 0 )
return 0;
Object firstChild = getModel().getChild( origObj, 0 );
Object lastChild = getModel().getChild( origObj, getModel().getChildCount( origObj ) - 1 );
TreePath firstPath = path.pathByAddingChild( firstChild );
TreePath lastPath = path.pathByAddingChild( lastChild );
int topFirst = getPathBounds( tree, firstPath ).y;
int bottomLast = getPathBounds( tree, lastPath ).y + getPathBounds( tree, lastPath ).height;
int height = bottomLast - topFirst;
return height;
}
private class TimerListener implements ActionListener {
public void actionPerformed( ActionEvent actionEvent ) {
animationComplete *= ANIMATION_SPEED;
if ( isAnimationComplete() ) {
animationState = AnimatedTreeUI.AnimationState.NOT_ANIMATING;
timer.stop();
}
else {
updateCompositeImage();
}
tree.repaint();
}
}
// overridden because the default clipping routine gives a NPE
// when the painting is hijacked.
@Override
public void update( Graphics g, JComponent c ) {
if ( c.isOpaque() ) {
g.setColor( c.getBackground() );
g.fillRect( 0, 0, c.getWidth(), c.getHeight() );
}
if ( animationState != AnimatedTreeUI.AnimationState.NOT_ANIMATING ) {
if ( c.getParent() instanceof JViewport ) {
JViewport vp = (JViewport) c.getParent();
Rectangle visibleR = vp.getViewRect();
g.setClip( visibleR.x, visibleR.y, visibleR.width, visibleR.height );
}
else {
g.setClip( 0, 0, compositeImage.getWidth(), compositeImage.getHeight() );
}
}
paint( g, c );
}
// Hijack painting when animating
@Override
public void paint( Graphics g, JComponent c ) {
if ( animationState != AnimatedTreeUI.AnimationState.NOT_ANIMATING ) {
g.drawImage( compositeImage, 0, 0, null );
return;
}
else {
try {
super.paint( g, c );
}
catch ( Exception e ) {
try {
Thread.sleep( 500 );
super.paint( g, c );
}
catch ( Exception e1 ) {
e1.printStackTrace();
}
}
}
}
}