Basically all the component does is extend JButton,
over-riding a few key methods. The first thing it
does is add something to hold the multiple actions,
and identify the selected action.
It then adds a certain amount on to the returned
minimum size etc to ensure it has enough space to
draw the down arrow on the right (which allows the
multiple selection), and of course overrides the
paint method to draw the triangle (this needs some
real work and cross L&F testing right now) and a
line between the triangle and the action when the
mouse is hovering.
That line tells the user that clicking on the left
will have a different effect to clicking on the
right. In order to deliver that we simple check to
see if they should fire the current action (on the
left) or show a menu of all of the actions (on the
right).
/**
* Overridden so that a check can be made to see if
the current action should be fired, or the
* list of available actions should be shown.
* @param actionEvent The action event
*/
protected void fireActionPerformed(ActionEvent
actionEvent) {
if (this.getMousePosition().x>boundary){
JPopupMenu popup = new JPopupMenu();
for (Action action : actions){
popup.add(new ActionOptionWrapper(action));
}
popup.show(this,0,getHeight());
} else {
super.fireActionPerformed(actionEvent);
}
}
And that's it. The ActionOptionWrapper just sets the
selected action as the current action, and then fires
it. Simple really! Any how, I'll develop this one and
I do have "Rich" plans for it, but given this one can
be useful out the box, and I haven't had a chance to
do cross platform testing (hint hint).
For those of you keeping track of the CVS, I've added a couple of new effects, including a neat one to get at a ListCell which you can now see updated in the demo. You'll also notice I've taken some of the older links out, they were just cluttering up the demo now, so I don't think they are needed anymore.
I've also taken out the need for the demo to use signed jars which simplifies things nicely. So, just a little house work and an update to the demo.
Update
OK, that was easier than I thought! You can view the demo source here.
Well done to the JSmooth developers.
http://jsmooth.sourceforge.net/index.php
Effects Panel
Update
Fixed a bug on Windows that made the
effects demo, well, dog ugly! Also updated the
acknowledgments to use some of the new effects
(subtle but not un-purty). Also updated the src.zip,
web-start demo, and the CVS is of course up-to-date.
Quite often you want to overlay (or indeed...
underlay) effects over the top (or underneath!) of a
normal swing window. You've probably seen examples of
these before, from shadowing you can find in the
SwingX library (I think that's where I saw it) to
icon "blooms" as the mouse moves over a button
(something Krill's added to substance) to
transitions. They are all great, but I wanted a way
to build a re-usable library of effects so that they
could be easily added to any application without too
much re-jigging.
This is where the EffectsPanel comes in, it is a
Component that can be set as the glass pane (as it is
in my example) and you can add as many effects to it
as you like. It manages the timing and repainting of,
well Effects. The EffectsPanel class itself is really
not very exciting, create on, set it as the GlassPane
and then addEffects. Here's the code and then we'll
move onto the Effects themselves.
EffectsPanel effectsPanel = new EffectsPanel();
setGlassPane(effectsPanel);
effectsPanel.setVisible(true);
...
effectsPanel.addEffect(new SomeEffect());
That's it. What's much more interesting is the Effect
interface.
Special Effects
The Effect interface has three methods
of interest, let's start with the most simple.
public void paintEffect(Graphics2D graphics);
The implementation of this method is given a graphics
object and the effect paints itself on. That's it, of
course we would like our effects to change over time
so there is an update method that needs implementing
as well.
public long update();
That looks pretty simple too huh? Well it is, when
called by an EffectPanel it's a signal that any
animation, movement, pre-calcs should be done. The
return value is important as well, if it's a number
above zero it will be called again after that number
of intervals (how often update is polled is
controlled by the update panel). There are also a
couple of special values you can return specified in
the interface.
EFFECT_FINISHED
Return this, and the effect will be removed from the
EffectsPanel, effectively finishing it, and then
rather interestingly...
EFFECT_INACTIVE
Return this and the EffectsPanel will stop calling
update, which means that the effect will still be
painted, but no further calls to update will be made.
Now we can animate, paint our animations, really
that's all that is needed, except there is one more
method that you'll see used in a later example.
public boolean isLocalEffect();
Basically a local effect is supplied it's own
Graphics object meaning that you can go crazy with
transforms, composites etc, and the next effect along
will be un-affected, a non-local effect will impact
every other that is drawn.
So all you have to do to add an effect is create an
EffectsPanel and implement some effects. It's that
easy.
In the example I put in the web-start demo has three
effects, one is a does the Krill "Alpha Burst" thing
when a mouse moves over a component (using a
mouseListener to check when that happens). Here's the
class, you'll love how short it is!
public class AlphaBurst extends
AbstractComponentEffect{
protected int width = 0;
protected int height = 0;
protected float alpha = 1.0f;
/** Creates a new instance of AlphaBurst */
public AlphaBurst(Component c) {
super(c);
width = c.getWidth();
height = c.getHeight();
}
public void paintEffect(Graphics2D graphics) {
graphics.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,alpha));
graphics.drawImage(componentImage,centre.x-width/2,centre.y-height/2,width,height,null);
}
public long update() {
alpha=alpha-(alpha/8.0f);
width=(int) ((float)width*1.2f);
height=(int) ((float)height*1.2f);
if (alpha>0.1){
return 20;
} else {
return Effect.EFFECT_FINISHED;
}
}
}
That's it, it renders the component into an image,
and then increases the width and height it's drawn at
reducing the alpha. when the alpha goes below 10% it
ends the animation. That's it. Cute.
If you tick the "Mouse Trails" button it will also
add some particle effects, each time a mouse moved or
mouse dragged event is received it randomly adds a
particle. The particle effect is also pretty simple.
It's just a dot that updates (with a minor nod
towards physics) and bounces if it goes below the
bottom third of the screen. Here it is
public class ParticleEffect implements Effect{
protected BufferedImage particle = null;
protected double x = 0.0;
protected double y = 0.0;
protected double vx = 0.0;
protected double vy = 0.0;
protected int life = 0;
protected float alpha = 1.0f;
protected double floor = 1000000.0f;
/**
* Creates a new instance of ParticleEffect
*
*/
public ParticleEffect(BufferedImage particle, int x,
int y,int age, int floor) {
this.particle = particle;
this.x = (double) x;
this.y = (double) y;
this.vx = 4.0-Math.random()*8.0;
this.vy = 6.0-Math.random()*8.0;
this.life = age;
this.floor = (double) floor;
}
public boolean isLocalEffect() {
return true;
}
public void paintEffect(Graphics2D graphics) {
if (alpha<1.0f){
float finalAlpha = alpha;
if (graphics.getComposite() instanceof
AlphaComposite){
AlphaComposite alphaComp = (AlphaComposite)
graphics.getComposite();
finalAlpha = alphaComp.getAlpha() * alpha;
}
graphics.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,finalAlpha));
}
graphics.drawImage(particle,(int)x, (int)y,null);
}
public long update() {
life--;
if (life<=0){
return Effect.EFFECT_FINISHED;
}
if ((y>floor) && (vy>0)) {
vy=0.0-vy*0.5;
}
x+=vx;
y+=vy;
vy+=0.1;
if (life<10){
alpha = (float) life / 10.0f;
}
return 20;
}
}
Not rocket science as you can see. But we can do some
really cute things, what if we want our particles to
be reflected into that bottom third of the screen?
Well how about we create an Effect, that reflects
other Effects? Tick the Reflect check box to make it
happen, but here's the Reflect Effect class....
public class ReflectEffect implements Effect{
protected Effect reflectThis;
protected EffectsPanel thePanel;
/** Creates a new instance of ReflectEffect */
public ReflectEffect(Effect reflectThis, EffectsPanel
panel) {
this.reflectThis = reflectThis;
this.thePanel = panel;
}
public boolean isLocalEffect() {
return true;
}
public void paintEffect(Graphics2D graphics) {
reflectThis.paintEffect(graphics);
graphics.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,0.5f));
graphics.translate(0,thePanel.getHeight());
graphics.scale(1.0,-0.3);
reflectThis.paintEffect(graphics);
}
public long update() {
return reflectThis.update();
}
}
This effect takes another effect as a parameter,
passes on it's update calls to the sub-summed effect
and then in paint, does something clever. First it
asks the effect to paint itself normally before
applying a couple of transforms to the graphics
context, and calling it again making it paint itself
upside and squashed. Easy-peasy, why not have a play
with the web-start demo or check out (no pun) the
code from CVS in the JavaDEV project.
To see the new Effects run "Effects - Alpha Burst"
Update
JavaDocs are now complete (see link to documentation above). Interestingly after writing them there are a few "TODO"s that I've spotted, but I think we have a reasonable base to start adding in some of the new things I've been working on over the last month or so.
Believe it or not, the list goes on! The filters are available as a jar and the source under the Apache 2.0 license, and are a real resource.
For now I'll get back to documenting!
I'm also going on a bit of a document and re-factor effort to try and get things in shape as I am getting more requests to use the components (open-source dudes, do what you like!) I feel I should at least make them more usable and, well, better documented. In the meantime I'm (as usual) fiddling with one or two ideas that will hopefully mature enough to be added into the library.
Sweet ![]()
There is a magical method for each container, that can cause it to NOT hog mouse events. It simply dictates whether or not there is a component at a particular x,y co-ordinate. If there's not, Swing doesn't pass the mouse event on. After all, there's nothing there. Here it is (you can over-ride it)
public boolean contains(int x, int y);
That's it. If it returns false, then no events are passed on and the clicks fall through. I had started fiddling with this, but then stopped hitting a recursive dead end when I called getComponentAt(int x,int y) inside it. Of course, all I should have done is a little bit of work to implement my own getComponentAt() and I would have had my solution. Find out if there is something in the dock panel that isn't a spacer that's at that co-ordinate... if there's not return false and pretend it's got nothing to do with me.
Here's the implementation:
public boolean contains(int x, int y) {
Rectangle rect=new Rectangle();
//Have to implement our own componentAt here
for (Component comp : getComponents()){
rect=comp.getBounds(rect);
if (rect.contains(x,y)){
if (comp instanceof JSpacer){
mouseMoved(new Point(-1,-1));
return false;
}
return true;
}
}
//A little non-intuitive. If it's not over a normal component or a spacer, it's in the dock area
return true;
}
The final return true is a little bit tricky (it's after all like saying, I didn't find anything but yes send me the message). This stops the mouse going over little gaps causing a problem, in general, if it's over a spacer pretend I'm not there, otherwise consume the event. Doing this enabled me to remove the GlassPaneDock class entirely, and a WHOLE load of complexity went with it. It's also enabled me to do something else I wanted to, add the dock into a Frame's layered pane. This is useful because it means I can make things like combo-box drop downs and pop-up menus appear over it (play with the demo to make it happen if you wish). Which might be more appropriate in some cases. In the screen shot below see how the combo-box appears OVER the dock.
This "perfect" message handling has one problem, the
mouse can still fall between the cracks of dock icons
from time to time, but that's part of another tweak
I'm working towards. For now, the usual web-start and
source code update has been done!
![]()
Quite a few changes have been made here and there, one of my favourites was Krill's idea to add mouse wheel functionality, now why didn't I think of that? All very simple in the code as well....
public void mouseWheelMoved(MouseWheelEvent mouseWheelEvent) {
if (mouseWheelEvent.getScrollType() == MouseWheelEvent.WHEEL_UNIT_SCROLL) {
int amount = mouseWheelEvent.getWheelRotation();
if (lastWheeledTo==null){
lastWheeledTo=getFrontmost();
}
int lastPosition = layout.getComponentIndex(lastWheeledTo);
int frontMostPosition = layout.getComponentIndex(getComponent(0));
//Don't over spin
if (Math.abs(lastPosition-frontMostPosition)>layout.getComponentCount()/4){
return;
}
if (amount > 0) {
lastWheeledTo=layout.getPreviousComponent(lastWheeledTo);
} else {
lastWheeledTo=layout.getNextComponent(lastWheeledTo);
}
bringToFront(lastWheeledTo,true);
}
}
Basically all the code does is look for the mouse wheel to be rolled and try and bring the next component along to the front. I bet your are wondering about the over-spin bit? Well, the carousel always looks for the shortest route to be bringing a component to the front, so racing too far ahead suddenly spins it backwards, not what I wanted, but if we never let it go further than a quarter of the way around, it's always quickest to carry on moving in the same direction.
The next item, and I think you'll see it to great effect in Krill's application, is to improve how text is rendered and labels generated. Nice alpha blended backdrop in a rounded rectangle. Once again, there were a few e-mails backwards and forwards before we hit on the right solution. Fun!
Finally (and what started it all) items further back
are now more transparent (something I know a few of
you had raised) not to mention a couple of bad
practice items Krill had highlighted (laziness on my
part, but when you realize someone else is doing
things with your code!)
I have updated the source, as well the web-start
demo....
I've also tweaked the carousel example following a suggestion from Romain Guy (his site has moved btw, follow updated link) that the jittering some people experienced is due to single pixel increments in size, so I've made it only size to even pixels and we'll see if that helps (Webstart updated).
Other bits of house keeping have also been done to help people find their way around the site, so I hope it's a little bit clearer now.
Finally, thanks so much for all of the messages (and comments) with encouragement and ideas. Every single one has been valuable and greatly appreciated.
Starting off, let's list out the tasks ahead of us:
1. We need to do a small amount of re-factoring on
the carousel layout to allow us to sub-class it and
over-ride a couple of pieces of functionality.
2. We need a new component that will contain a
carousel, and a menu (I'm going to use a JList, it
does the job!)
3. The menu needs a custom cell renderer to make the
selected cell look pretty
The re-factoring was pretty minimal, but I did add a
couple of methods, just before I start that, I need
to mention one of the existing methods which needed
to be over-ridden:
public void setFrontMostComponent(Component
component)
Previously this would spin the
specified component to the FRONT of the carousel.
This time, I wanted it to be on the right of the
carousel, so the new method (in the rather
imaginatively named OffsetCarouselLayout class). This
function just determines the target angle
differently.
protected boolean shouldHide(Component comp,
double angle)
One of the problems with the
over-riding of the above function is that you can
have component in front of the currently focused one.
This new function is used to determine if a component
should be hidden (based on where it is on the
carousel). It lets me stop things appearing in front
of the current component.
The next two are pretty obvious, they let me
manipulate the center of the carousel (in the
component) and the radius of the carousel (I didn't
want it quite as squashed).
That's it, not bad, gotta-love OO programming!
So, let's summarize what we have; a single component
with a JCarousel component (with a new layout
manager), and a JList in it, pretty straight
forward....
As you can imagine, Java makes most of this pretty
easy. But there are a couple more steps that you'll
find in the source code.
It turns out, JLists don't like wrapping the selected
object when the user is using the keys to cycle
through (it gets stuck at the top or the bottom). So
you'll see in the code of the Carousel Menu that it
has a keyboard listener to move the cursor to the top
or the bottom when it hits one of the ends. Also we
need to update which component is at the "front" of
the carousel based on which item is selected, and a
simple implementation of the list selection listener
achieves that.
And of course, we need a nice border around the
selected item. I've cheated a little and created a
simple border called "ImageBorder" which takes a
bounded image (like the one shown below) and a set of
insets, and slices and dices it to make it
resize-able.
I've added the code for this below, and of course,
once I'm back from my trip I'll update my project
file so that you can all download the source. And
rather wonderfully, that's it. Once again, Java can
really deliver on a modern UI and I think you'll
agree it makes my blog examples a little more usable!
![]()
The final implementation essentially uses a
GridBagLayout to control the dock, with spacers
"pushing" the icons in the dock into the center. The
mouse is tracked, and when it moves over one of the
Components (you can just add any Java component into
there) its preferred size is changed (therefore
increasing the size). Of course, like everything it's
not QUITE that simple. The diagram below shows the
basic strategy.
I'll update the source code for the Blog project
when I get back home after my trip, but I'd like to
focus on a couple of the interesting problems that
needed solving.
Scaling The Icons Based on Distance From
Pointer
The DockPanel sets the preferred size
of the icons to control their "zooming" in the dock.
It's implemented in a method called. This is
implemented in the getComponentPreferedSize method.
First it checks to see if the mouse is actually
hovering over the given icon, if it is, it returns
the enlarged size, otherwise it looks for the
distance from the mouse pointer to the center of the
icon, and linearly scales. The method is protected so
it could be over-ridden by a subsequent
implementation. I point this out because I'm pretty
sure Apple are using a Sigma curve or the like scale
theirs as the distance increases. I'll leave it to
you to try it yourself!
Watching for Change
Another over-used class of mine that I've added to
the Blog project is a MouseTracker which does a
couple of things, including tracking a mouse inside a
container, filtering out a lot of the events, and
just passing interesting ones across (when I publish
the source you'll see). One of the
MouseTrackerListener call-backs is mouseMoved, and
it's here that I update the parameters on the
GridBagLayout to make any changes. There's one other
place that the same thing is done. As usual we don't
want everything flicking from small to giant etc so I
have a timer that updates a tweened animation between
the target size for the icons in the dock, and the
size they currently are. Consequently actionPerformed
also updates the layout.
Easy Animation
I've been pretty lazy here. If you look at the
updateLayout method (which updates all the grid bag
constraints) you'll see that the result of
getComponentPreferredSize is passed into the
tweenValue method. This is where the animation is
done. The tweenValue takes a current size, and a
target size and calculates a value between the two
sizes. If the two aren't already the same it lets the
animation timer know that the size being passed on
isn't the final size, and then returns an
intermediate size. Finally, it does a little bit of
"clipping" to make sure that the icons can always fit
in the space available.
Docking to the Different Sides
The code looks pretty lengthy, but a
lot is taken up by dealing with the various layout
wrinkles that come along with aligning the dock to
different sides, so as you read through, just pick a
compass point and worry about that.
Now speaking of the source code here it is, you can
launch the web-start demo (it creates an AWT has been
updated with a dock tab too to keep you going until
all the source is available.
Note: I have fixed the problem that
stopped the web-start working (it uses an
AWTEventListener which requires ALL permissions, so
the jar needed to be signed).
Dock Panel Source Code
/*
* DockPanel.java
*
* Created on January 9, 2007, 6:22 AM
*
* Re-implementation of the dock to use a
grid-bag-laid-out panel instead
* of the very complicated code I was developing.
*
*/
package com.blogofbug.swing.components;
import com.blogofbug.swing.delegates.MouseTracker;
import
com.blogofbug.swing.delegates.MouseTrackerListener;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.util.Hashtable;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import java.awt.Insets;
/**
*
* @author nigel
*/
public class DockPanel extends JPanel implements
MouseTrackerListener, ActionListener{
/**
* Enumeration object to contain the side of the
screen/window the dock is against
*
*/
public enum Side {NORTH, SOUTH, EAST, WEST};
//The enlarged size of the icons
private int enlargedSize = 96;
//The normal size of the icons
private int normalSize = 32;
//Insets around each dock icon
private int insets = 2;
//The side the dock is docked to
private Side dockedTo = Side.SOUTH;
//Autohide?
private boolean autoHide = false;
//Auto-hiding!
private boolean hiding = false;
//Layout and standard constraints
private GridBagLayout layout = new GridBagLayout();
private GridBagConstraints filler = new
GridBagConstraints();
private GridBagConstraints title = new
GridBagConstraints();
private GridBagConstraints icon = new
GridBagConstraints();
//Spacer Panels
protected JSpacer spacer; ;
protected JSpacer firstSpacer; ;
protected JSpacer lastSpacer;
//Hold index of components and their titles
private Hashtable iconLabels = new Hashtable();
//Track mouse movements (will be over the entire
component)
private MouseTracker mouseTracker;
//Animation timer
private Timer timer = new Timer(0,this);
private boolean animating = false;
//Mouse location (last known)
private Point lastKnownMouse = new Point(0,0);
/** Creates a new instance of DockPanel */
public DockPanel(int normalSize, int enlargedSize,
Side dockedTo) {
//Set up the basics
this.dockedTo = dockedTo;
this.enlargedSize = enlargedSize;
this.normalSize = normalSize;
//Prepare the timer
timer.setCoalesce(true);
timer.setDelay(20);
timer.setRepeats(true);
timer.start();
//make me transparent
setBackground(null);
setOpaque(false);
//Set up the mouse tracker
mouseTracker = new MouseTracker(this,this);
//Set up the spacers
spacer = new JSpacer();
firstSpacer = new JSpacer();
lastSpacer = new JSpacer();
//Initial setup
setLayout(layout);
initializeLayout();
add(spacer,filler);
add(firstSpacer,icon);
add(lastSpacer,icon);
//Perform initial layout
updateLayout(null);
}
public boolean isAutoHiding(){
return autoHide;
}
public void setAutoHiding(boolean autoHide){
this.autoHide = autoHide;
mouseMoved(lastKnownMouse);
}
public DockPanel(int normalSize, int enlargedSize){
this(normalSize,enlargedSize,Side.SOUTH);
}
public void setNormalSize(int normalSize){
this.normalSize = normalSize;
updateLayout(null);
layout.layoutContainer(this);
}
public void setEnlargedSize(int enlargedSize){
this.enlargedSize = enlargedSize;
updateLayout(null);
layout.layoutContainer(this);
}
/**
* Sets up the basic layout and then calls
updateLayout to finalize the
* individual dock elements
*/
private void initializeLayout(){
//Standard settings for the filler panel
filler.fill = GridBagConstraints.BOTH;
filler.weightx = 1.0;
filler.weighty = 1.0;
//Standard settings for the title components
title.anchor = GridBagConstraints.CENTER;
//Standard settings for the icon components
icon.anchor = GridBagConstraints.CENTER;
icon.weightx = 0.0;
icon.weighty = 0.0;
icon.insets = new
Insets(insets/2,insets/2,insets/2,insets/2);
//Do various bits of specific constraints layout
switch (dockedTo){
case SOUTH:
filler.gridx = 0;
filler.gridy = 0;
filler.gridheight =1;
filler.gridwidth = GridBagConstraints.REMAINDER;
title.gridx = 0;
title.gridy = 1;
icon.gridx = 0;
icon.gridy = 2;
icon.anchor = GridBagConstraints.SOUTH;
break;
case NORTH:
filler.gridx = 0;
filler.gridy = 2;
filler.gridheight =1;
filler.gridwidth = GridBagConstraints.REMAINDER;
title.gridx = 0;
title.gridy = 1;
icon.gridx = 0;
icon.gridy = 0;
icon.anchor = GridBagConstraints.NORTH;
break;
case WEST:
filler.gridx = 2;
filler.gridy = 0;
filler.gridheight =GridBagConstraints.REMAINDER;
filler.gridwidth = 1;
title.gridx = 1;
title.gridy = 0;
icon.gridx = 0;
icon.gridy = 0;
icon.anchor = GridBagConstraints.WEST;
break;
case EAST:
filler.gridx = 0;
filler.gridy = 0;
filler.gridheight =GridBagConstraints.REMAINDER;
filler.gridwidth = 1;
title.gridx = 1;
title.gridy = 0;
icon.gridx = 2;
icon.gridy = 0;
icon.anchor = GridBagConstraints.EAST;
break;
}
}
/**
* Changes the side of the panel the dock is attached
to
*/
public void setDockedTo(DockPanel.Side dockedTo) {
this.dockedTo = dockedTo;
initializeLayout();
layout.setConstraints(spacer,filler);
updateLayout(null);
layout.layoutContainer(this);
}
/**
* Determines the appropriate gridbag weight for the
"book-end" spacers
* at either end of the dock
*
* @return The weight
*/
private double spacerWeightX(){
switch (dockedTo){
case NORTH:
case SOUTH:
return 1.0;
case EAST:
case WEST:
return 0.0;
}
return 0.0;
}
/**
* Determines the appropriate gridbag weight for the
"book-end" spacers
* at either end of the dock
*
* @return The weight
*/
private double spacerWeightY(){
return 1.0-spacerWeightX();
}
/**
* Determines the appropriate size for the component
based on the current "mouse over"
* component, the location in the dock area relative
to the overall dock panel, and a given component
* This is protected so it can be over-ridden to apply
a better alogrithm .
*
* @param p The location of the mouse relative to the
overall dock
* @param compoment The component to be sized
* @param highlightedComponent The current component
with the mouse over it
* @return A Dimension object with the recommened size
of the component
*/
protected Dimension getComponentPreferedSize(Point p,
Component component, Component highlightedComponent){
//If we are hiding it should be small
if (hiding){
return new Dimension(insets*3,insets*3);
}
//determine it's distance based on half the size of
the biggest component
double compSize = component.getSize().height;
double distanceFromCenter = 0;
double delta = (double) (enlargedSize-normalSize);
if ((highlightedComponent!=null)){
if (highlightedComponent==component){
return new Dimension(enlargedSize,enlargedSize);
}
switch (dockedTo){
case NORTH:
case SOUTH:
distanceFromCenter = Math.abs((double)
((component.getLocation().x + compSize/2 )- p.x));
break;
case EAST:
case WEST:
distanceFromCenter = Math.abs((double)
((component.getLocation().y + compSize/2 )- p.y));
break;
}
double cdy = 1.0 - distanceFromCenter/(double)
(enlargedSize*2);
double tSize = Math.max((double) normalSize + cdy *
delta,(double) normalSize);
return new Dimension((int)tSize,(int) tSize);
} else {
return new Dimension(normalSize,normalSize);
}
}
/**
* Essentially removes the spacer that forces the dock
to the bottom of the
* panel, neat if you would like to use the panel as
something like a
* special tab top.
*
* @param fillSpace true if the panel should fill up
any empty space above the dock,
* false if it should not
*/
public void fillSpace(boolean fillSpace){
spacer.setVisible(fillSpace);
}
/**
* Determines a value between the current and target
including easing. Can
* be over-ridden if desired to have different sizing
behavior. Implementers
* should note that if the size is not clipped to
ensure that the icons will
* all fit in the dock, gridbag does Bad Things (tm).
*
* @param current The current value
* @param target The target value
* @return A value equal to or between target and
current
*/
protected double tweenValue(double current, double
target){
//Just make sure we do nothing if they are the same
if (current==target){
return target;
}
//Determine the difference
double delta = (target-current)/4.0;
//Tween in the right direction, but always by at
least one pixel
if (delta>0){
delta = Math.max(1.0,delta);
} else if (delta<0){
delta = Math.min(-1.0,delta);
}
switch (dockedTo){
case SOUTH:
case NORTH:
return Math.min(current+delta,(
getWidth()-insets*iconLabels.size()*2)/iconLabels.size()-10);
default:
return
Math.min(current+delta,(getHeight()-insets*iconLabels.size()*2)/iconLabels.size());
}
}
/**
* Determines if a component is in the dock
*
*/
public boolean dockContains(Component component){
if (iconLabels.get(component)!=null){
return true;
}
return false;
}
/**
* Takes a current size, and determines a new size
setting up an animation
* if needed.
*
* @param currentSize The current size of the
component
* @param newSize The new size of the component
* @return The in-between size
*/
private Dimension tweenSize(Dimension currentSize,
Dimension newSize){
if ((newSize.width != currentSize.width) ||
(newSize.height != currentSize.height)){
animating = true;
newSize.width = (int)
tweenValue(currentSize.width,newSize.width);
newSize.height = (int)
tweenValue(currentSize.height,newSize.height);
}
return newSize;
}
/**
* Determines the new settings for the gridbag layout,
and then applies them
* to all components
*
*/
private void updateLayout(Point p){
Component highlightedComponent = null;
animating = false;
if (p!=null){
highlightedComponent = getComponentAt(p);
//Make sure it's at least in the dock zone
switch (dockedTo){
case NORTH:
if (p.y>spacer.getY()){
highlightedComponent=null;
}
break;
case SOUTH:
if (p.y
highlightedComponent=null;
}
break;
case WEST:
if (p.x>spacer.getX()){
highlightedComponent=null;
}
break;
case EAST:
if (p.x
highlightedComponent=null;
}
break;
}
if ((highlightedComponent==firstSpacer) ||
(highlightedComponent==lastSpacer)){
highlightedComponent = null;
}
}
//Variables used for incrementing grid-x's and
grid-y's
int dx=0,dy=0;
switch (dockedTo){
case NORTH:
case SOUTH:
dx=1;
break;
case EAST:
case WEST:
dy=1;
break;
}
//Set everything up as it should be
initializeLayout();
//Add first spacer and move to next position
icon.weightx = spacerWeightX();
icon.weighty = spacerWeightY();
icon.fill = GridBagConstraints.BOTH;
layout.setConstraints(firstSpacer,icon);
icon.fill = GridBagConstraints.NONE;
icon.gridx += dx;
icon.gridy += dy;
title.gridx += dx;
title.gridy += dy;
//Iterate through the components updating everything
Component[] components = getComponents();
for (Component component : components){
//Make sure it's not a filler or a title
if (!((component == spacer) || (component instanceof
DockLabel) || (component == firstSpacer) ||
(component == lastSpacer))){
//Set the weight
icon.weightx = 0.0;
icon.weighty = 0.0;
//Icon first
layout.setConstraints(component, icon);
//Set the prefered size of the component
component.setPreferredSize(tweenSize(component.getSize(),getComponentPreferedSize(p,component,highlightedComponent)));
//Set the visibility of its label
if (highlightedComponent==component){
iconLabels.get(component).setVisible(true);
} else {
iconLabels.get(component).setVisible(false);
}
//Title next
layout.setConstraints(iconLabels.get(component),title);
//Move to next position
icon.gridx += dx;
icon.gridy += dy;
title.gridx += dx;
title.gridy += dy;
}
}
//Add the last spacer
icon.weightx = spacerWeightX();
icon.weighty = spacerWeightY();
icon.fill = GridBagConstraints.BOTH;
layout.setConstraints(lastSpacer,icon);
}
/**
* Adds a new item to the dock
*
*/
public void addDockElement(Component component,
String label){
if (iconLabels.get(component)!=null){
return;
}
DockLabel newLabel = new DockLabel(label);
//We would like it to do something funcky as it adds
them and make them grow
component.setPreferredSize(new Dimension(0,0));
iconLabels.put(component,newLabel);
add(component);
add(newLabel);
updateLayout(mouseTracker.getPosition());
}
/**
* Not interested
*/
public void mouseCrossThreshold(boolean mouseEntered)
{
}
/**
* When the mouse moves, update the layout. Should be
optimized when the
* highlighted component is the spacer to not do
anything
*/
public void mouseMoved(Point position) {
if (autoHide){
Component mouseOver = getComponentAt(position);
if ((mouseOver == firstSpacer) || (mouseOver ==
lastSpacer) || (spacer==mouseOver)){
hiding = true;
} else {
hiding = false;
}
} else {
hiding = false;
}
lastKnownMouse = position;
updateLayout(position);
layout.layoutContainer(this);
}
public void paint(Graphics graphics) {
graphics.setColor(new Color(200,200,220,127));
Rectangle dockSize = null;
int oldNormalSize=normalSize;
if (hiding){
normalSize = Math.min(normalSize,
((dockedTo==Side.NORTH) || (dockedTo==Side.SOUTH)) ?
firstSpacer.getHeight(): firstSpacer.getWidth());
}
//Draw the main dock background
switch (dockedTo){
case NORTH:
dockSize = new
Rectangle(firstSpacer.getX()+firstSpacer.getWidth(),0,getWidth()-(lastSpacer.getWidth()+firstSpacer.getWidth()),normalSize);
break;
case SOUTH:
dockSize = new
Rectangle(firstSpacer.getX()+firstSpacer.getWidth(),getHeight()-normalSize,getWidth()-(lastSpacer.getWidth()+firstSpacer.getWidth()),normalSize);
break;
case WEST:
dockSize = new
Rectangle(0,firstSpacer.getHeight(),normalSize,getHeight()-(lastSpacer.getHeight()+firstSpacer.getHeight()));
break;
case EAST:
dockSize = new
Rectangle(getWidth()-normalSize,firstSpacer.getHeight(),normalSize,getHeight()-(lastSpacer.getHeight()+firstSpacer.getHeight()));
break;
}
if (hiding){
normalSize = oldNormalSize;
}
dockSize.x-=insets;
dockSize.y-=insets;
dockSize.width+=insets*2;
dockSize.height+=insets*2;
graphics.fillRect(dockSize.x,dockSize.y,dockSize.width,dockSize.height);
//Draw the outline
graphics.setColor(new Color(255,255,255,127));
switch (dockedTo){
case SOUTH:
graphics.drawRect(dockSize.x,dockSize.y,dockSize.width-1,dockSize.height);
break;
}
super.paint(graphics);
}
public void actionPerformed(ActionEvent actionEvent)
{
if (animating){
updateLayout(lastKnownMouse);
layout.layoutContainer(this);
}
}
//For convenience
public class JSpacer extends JComponent {
public JSpacer(){
setBackground(null);
setOpaque(false);
}
}
//Temporary until I do something else
public class DockLabel extends StrokedLabel {
public DockLabel(String title){
super(title);
}
};
}
Anyhow, there is something coming along, and here's a little teaser....

Sleep patterns have been knocked all over the place
with the return to work and the bustle of the new
year. Oh, and there's a matter of the Ashes which is
on during the middle of the night giving me an excuse
to get out of bed rather than trying to go to sleep.
So I decided to use the early hours to optimize.
A few notes first:
1. This has been optimized for dual-or-more-core
processors (more later)
2. I'm not claiming it's the fastest or best or
anything. This is intellectual mastication (ahem) and
hopefully a hint of "oh shiny!"
About 1. As I've said, once upon a time I used to
write games, back in the days when 20Mhz was pretty
exciting and SVGA was bleeding edge. I've built up
some opinions about how I would go about making use
of dual cores, and this demo implements some of them.
The simulation runs in one thread, and is considered
more important than drawing. The second thread does
the painting (javax.swing.Timer anyone) and run's
every 20th of a second (here in the UK on the Amiga,
50Hz was the frame-rate we were aiming for, so that's
what I was going for).
I have a MacBook Pro and an
almost-exactly-the-same-spec Dell 620 to test on so
they are both Dual-Core, I'd love to read your
comments on performance on single core (in fact
whatever machine you have) machines. But that brings
me to something else I really need your help with.
I'm running on Java SE 6 on both the Mac and Windows.
Windows has always been a bit faster (since I've been
writing this), but when I split up the main thread,
Windows suddenly started to claim it was A LOT
faster. As in it's claiming 0ms on the Dell for the
simulation, and often 0ms for the re-draw. Hum. Can't
see why the instrumentation would suddenly change,
but hot-damn it does seem to be running without slow
down (on the dell at least) barely chewing any (or
either) of the cores. So again, any comments on your
experiences would be great.
The web-startable app below needs Java 5, but please
try on Java 6. Please post your results... and don't
forget to click on "Tap On" when you are ready to
start the water!
![]()
As you can see, 400 drops is no longer a problem inside the "20ms" limit required to meet the Amiga's performance, in-fact you can crank it up to over 100,000 on my MBP before it starts to sweat. No great surprise there, the processor is massively faster. What has surprised me is how easy it is to implement in Java, how well Java2D works. In addition, the Java implementation is doing a whole lot more than the original version. I'm not sure if I'm going to post the full source or not, but here are some of the problems that have been solved with the old algorithm.
Vertical Columns
The old routine didn't have any idea of the inertia a particle of water had as it flowed over the scene, so when the water could go down it would, creating very artificial looking columns of falling water.
Easy to address, give the water some inertia, immediately much nicer diffusing water falls.
Self-Leveling
This was a big one, the levels of the game had to be very carefully designed to ensure that no part of a level would highlight the fact that the water would not self-level (pressure equalizing the water level). It was a real pain, and subtly caused other minor problems. Have a look at the U-bends in the screen shot below. The water should be pushing down equalizing the pressure in the u-bend.
Well in the spirit of the original code (that is that it looks as much like water as possible, without ever really simulating what water does), I've managed to solve this problem too, although it is currently impacting performance (3-4 times slower in regions where water self-levels) but there are lots of ways that can be improved it just the raw solution at the moment, plugged into the inner loop. As you can see, it's working perfectly...
So what's the point of the teasing post? Well I'm not sure what I'm going to do with this code yet, it's partly an exercise for walking down memory lane for me, in which case I'll just post the code so people can do a better job than me! However, it's also an interesting look at writing dynamic graphics algorithms in Java, in which case I'm going to publish my findings so that they can be exploited by others. Anyhow, let me know if you find it interesting, I'll make it web-startable soon so you can all play with the routine (it's kind of hypnotic).
Update: The web-start demo has been implemented, and I would really like your help with getting some results from a range of machines: Click Here
1. Gradient painting was being done very inefficiently (and he suggested looking at this thread)
2. I should stop using ImageIcon to do the loading, and use ImageIO instead. I've been putting off looking at ImageIO for a long time, seems like a good excuse to have a fiddle
3. The BICUBIC rendering hint gives little improvement over BILINEAR but costs a LOT more
4. Check out SwingX and use their compatible BufferedImage loader for best possible performance
As this is the first time I've fiddled with ImageIO, I'm not going to use SwingX I like to figure things out myself the first time so that I fully understand why the right solution is better. I thought it would be good to perform some performance metrics and see just how much we could speed things up, here are the results. If you missed the original posts on the carousel then just look back in the blog (hum, it was only yesterday!)
The first step is to put some performance metrics in. This required a little bit of thinking as we have a couple of "painters" to see. My first thought was to look at the paint method and time that, it seemed to work well so I dug out an old metric recording class I wrote a long time ago to measure server request performance. It's a useful little class.
/*
* PerformanceMonitor.java
*
* Created on June 12, 2003, 7:36 PM
*
*/
package com.blogofbug.utility;
/**
*
* @author buggles
*/
public class PerformanceMonitor {
private long operationTimes[];
private int nextSlot=-1;
private String monitorName = "Unset";
private long operationStartTime = -1;
/** Creates a new instance of PerformanceMonitor */
public PerformanceMonitor(String monitorName, int samples) {
operationTimes = new long[samples];
nextSlot = 0;
this.monitorName = monitorName;
for (int i = 0;i
operationTimes[i]=-1;
}
}
/**
* Call when an operation started
*/
public void operationStarted(){
operationStartTime = System.currentTimeMillis();
}
/**
* Call when an operation is stopped
*/
public void operationStopped(){
operationTimes[nextSlot++]=System.currentTimeMillis() - operationStartTime;
if (nextSlot == operationTimes.length){
nextSlot = 0;
}
}
/**
* Produces a string containing the key metrics for the operation
*/
public String generateMetrics(){
long totalTime = 0;
long tally = 0;
long fastest = 1000000;
long slowest = 0;
for (int i=0;i
if (operationTimes[i]>-1){
totalTime+=operationTimes[i];
tally++;
fastest = Math.min(fastest, operationTimes[i]);
slowest = Math.max(slowest, operationTimes[i]);
} else{
break;
}
}
int lastSlot = nextSlot - 1;
if (lastSlot == -1){
lastSlot=operationTimes.length-1;
}
return monitorName+" "+tally+" operations - Range: "+fastest+"ms to "+slowest+"ms Total: "+totalTime+"ms Average: "+(totalTime/tally)+"ms Last: "+operationTimes[lastSlot]+"ms";
}
}
It's been trimmed down a little here, I just don't need some of the complexity for this. I plugged it into the paint method of JCarosel
public void paint(Graphics g){
performanceMonitor.operationStarted();
super.paint(g);
performanceMonitor.operationStopped();
System.out.println(performanceMonitor.generateMetrics());
}
and got the following results:
JCarousel.paint() 78 operations - Range: 21ms to 86ms Total: 2442ms Average: 31ms Last: 24ms
So we have a peak performance of 21ms (i.e. slower than 60 fps) and a criminaly slow 86ms performance. Average still very bad. Romain Guy was right, something is badly wrong!
Let's do some easy stuff first, changing the rendering hint from BICUBIC to BILINEAR...
JCarousel.paint() 78 operations - Range: 20ms to 86ms Total: 2348ms Average: 30ms Last: 23ms
Hum, doesn't look like a huge change, but remember that a millisecond we've trimmed off of peak and average performance. Certainly a start and not bad for changing one constant. Let's tackle the gradient next. There's a couple of inefficiencies here, one is I am creating a new gradient paint each time, and the second is how I'm rendering the gradient itself. I'm going to change the code to create and store the gradient painter first.
JCarousel.paint() 78 operations - Range: 20ms to 86ms Total: 2357ms Average: 30ms Last: 23ms
Hum, that looks slightly slower! Given that I'm already not using cyclic gradients (read the blog of Romains... All the way down) it would seem like a good idea to try the other recommended methodology using ImagePaint and stretching the image.
if (cache == null || cache.getHeight() != getHeight()) {
cache = new BufferedImage(2, getHeight(),
BufferedImage.TYPE_INT_RGB);
Graphics2D g2d = cache.createGraphics();
GradientPaint paint = new GradientPaint(0, 0, start,
0, getHeight(), end);
g2d.setPaint(paint);
g2d.fillRect(0, 0, 2, getHeight());
g2d.dispose();
}
g2.drawImage(cache, 0, 0, getWidth(), getHeight(), null);
The results came as a bit of a shock
JCarousel.paint() 78 operations - Range: 26ms to 89ms Total: 3081ms Average: 39ms Last: 35ms
Almost a second slower for just 78 draws, big negative impact. OK, let's try another technique listed on the site using a texture paint....
JCarousel.paint() 78 operations - Range: 20ms to 85ms Total: 2705ms Average: 34ms Last: 28ms
No great improvement, and certainly not faster for this size gradient (and that's probably key). Rather than change the size to suit the results, I'm going to stick with the standard gradient paint, but leave the texture code there, what I would really like to do in the future is some decent instrumentation to see the cross-over point between the two techniques and make it auto-switch. For now, a little disappointed, onto looking at ImageIO and my use of ImageIcon etc.
One of the other points Romain had made was that there was really no need to specify an ImageObserver, so I took that out, not gain, but no pain either
JCarousel.paint() 78 operations - Range: 20ms to 86ms Total: 2357ms Average: 30ms Last: 24ms
So onto ImageIO and creating an appropriately compatible image (with the actual graphics device being used by the OS). Well the ImageIO part turned out to be very very easy, the real trick was digging through the various device and graphics configurations to get that compatible image. To encapsulate this for the various constructors I created a setImage() method that takes a string URL and performs the various steps.
private void setupImage(String imageURL){
Image image = null;
try {
image = ImageIO.read(new URL(imageURL));
} catch (MalformedURLException ex) {
ex.printStackTrace();
} catch (IOException ex) {
ex.printStackTrace();
}
if (image==null){
return;
}
GraphicsConfiguration configuration = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration();
//Create a buffered image which is the right (translucent) format for the current graphics device, this
//should ensure the fastest possible performance.
BufferedImage originalImage = configuration.createCompatibleImage(image.getWidth(null),image.getHeight(null), Transparency.TRANSLUCENT);
//Blit the loaded image onto the optimized surface by creating a graphics context for the new image
Graphics g = originalImage.getGraphics();
g.drawImage(image, 0, 0, null);
g.dispose();
//First hack, just sets the bufferedIMage to the one loaded, don't cache any rendering
bufferedImage = originalImage;