Something for the weekend
Java
Introducing JMultiButton
So this isn't a very "Rich" component yet, but I've been working on a little helper app for myself and I've always needed one of these. It's basically a button that allows you to add multiple actions which can be selected from or the left side clicked to fire the current action. It saves space on a tool-bar. Although I've uploaded it to the cvs repository there is much more work to do, but it is useful right now, which is good!


multi_button

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).

|
Updated Demo, JavaDEV Project
I've broken out the examples from the library, because they really don't have any place in the library. I'm looking into how I can make the demo source neatly available in an easy to keep up to date way (anyone know of a good "convert source tree to html" tool, please let me know).

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.



|
Sun, are you paying attention?
JSmooth has just released what I consider to be the "important" missing part of Java on windows.... a way to launch the a jar, and if Java isn't installed to go get it and install it. That's all that's needed. If the download could be made smaller and modularized... fine... but the ground zero has to be a simple exe that can fire up the JRE whether it's there or not.

Well done to the JSmooth developers.

http://jsmooth.sourceforge.net/index.php

|
Introducing... the EffectsPanel class!
Now that I've documented and fixed up what was there, I've started moving a few of the new things I've been building into the library, and I wanted to start with the EffectPanel.


EffectsPanel
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"


webstart-small2

|
Changes to Carousel API commited to JavaDEV Project
Just a note to those who are keeping up to date, I've started delivering API changes (and some improved documentation) to the java dev project. I'm not going to dilly-dally with the methods I have deprecated so if you are using the library, please quickly update your code before you get a nasty shock! On the plus side, it's already getting easier to use the carousel as a result. More changes to come.

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.
|
Java Image Filters
Every so often you come across a site where someone has done loads of the things you are always meaning to get around to, and today was one of those days. Whilst writing an application to manipulate multiple images in a batch mode (I'll make it available, it's certainly been useful for me!). I wanted to add a couple more image operations to it before sharing it with you guys, and I stumbled across this site jhlabs.

java_image_filters

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!
|
What happens next...?
As I've hinted in one of the comments on a recent blog, I've been setting up a dev.java.net project for the blog code. It's proved more work than I expected, largely because I've had to set up one thing or another (CVS repositories, last time I did anything like this by had was writing sccs bash scripts to make it easy to manage a group project!). All that aside, once the project has been approved I'll post the link here.

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.
|
First real application of java carousel
I've mentioned a couple of times that I've spent a few days working with Krill on some bits and bobs for substance, and I have to say it's great to see the carousel (and for me the carousel menu) having a real (and useful) application. So head on over to Krill's blog and check out what he has done...

KrillPreview
Sweet Happy

|
Improved Dock Panel and Coder Stupidity
When I first publish my dock example, it was implemented in a glass pane. Which is fine. I also went through some hoops with AWT listeners to stop it sucking all the events away from the component below. I wrote some event forwarders. I added listeners. I did a lot of stuff. The whole time something was niggling away at the back of my mind. It just shouldn't be this hard. It wasn't.

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.

Layered Dock Pane

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!

webstart-small2

|
Updating Blog Jar and New Features to Carousel
What happens when bloggers team up? Interesting things!!! Kirill Grouchnikov recently asked if he could use JCarousel in one of his projects, and of course I was delighted to agree. During the day Krill would send me some updates, suggest improvements, and the end result is subtly but substantially improved.

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!

Updated Dock zoomed


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!)

Updated Dock

I have updated the source, as well the web-start demo....

webstart-small2

|
Updating source code and licensing
Well I'm back, and I'm very jet-lagged! As part of staying awake I've decided to release the source code under the Apache 2.0 license meaning you can all get it into which-ever projects you like pretty easily. Look in the downloads section for the updated Netbeans project including all of the latest content. You'll also need to download and link against the Cascade.jar or delete the references to the Demo file if you get linking errors, it's up to you!

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.
|
Carousel Menu (a la Apple TV)
While everyone was wrapped up in Steve's reality distortion field during his recent keynote, I was ogling the Apple TV interface. What a fantastic product (I'm getting one of those, just what I need). Anyhow, one of the interface features that I noticed was that they had re-used a carousel to produce a funky menu. Well, I have a carousel component how hard can it be to re-use it to create.... a carousel menu?

CarouselMenu

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....

JCarouselMenu Structure

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.

menu_highlight

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!

webstart-small2

|
Java Dock Component
As promised I've done some work on an OS X a-like Dock component for Swing. I've implemented a couple of different uses for it, a Glass Pane version that displays the dock over a window, but also kept it in a separate component (which could be useful for a funky tab-like-pane). After spending several evenings tweaking it, I threw it all away for a much simpler solution using GridBagLayout, for more details, read on.

Picture 1


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.


dock panel

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).

webstart-small2

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);
}

};
}

|
Recreating the Apple Dock in Java
I had hoped to keep up the momentum with a few more of my "rough hacks" being tweaked enough to be published, but the start of the business year has as always deflected me. To top it all I'm off on a couple of weeks of business trips so I'm unlikely to be in a position to publish. However, a couple of international flights can be great for giving you time to code in the flight. To pique your interest while I'm off, I thought you might like to see the next component I've been working up, the Dock. It's actually pretty close but I haven't done things like allowing it to be attached to the various compass points.

Anyhow, there is something coming along, and here's a little teaser....


Picture 2

|
Java Self-Leveling Fluid Simulation - Help Needed!
I blogged in the new year about a little hack-up of an old algorithm. Well I've been fiddling with it in the evenings and I've managed to optimize the self-leveling code so that it barely impacts performance. Great, I hear you say, that's wonderful now post a Web-Start demo. I'm going to, but first I would like you to read a bit, and then help!

Picture 1

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!

webstart-small2

|
Re-writing history implementing old game in Java
I have a confession, I was an Amiga fan-boy. More than that, I used to develop games for the platform. One game we were never able to find a publisher for was called Cascade. It used a really simple algorithm to simulate water a particle at a time, and by manipulating the environment a player would attempt to collect enough water before the time ran out. Well, with a really tightly engineered loop the Amiga version on a 1200 used to manage about 400 drops + the rest of the game engine every 50th of a second. I recently wondered how Java would handle the algorithm and whether a few of the limitations could be worked around. I have to admit, it's looking good!

Picture 2


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.

Picture 6

Easy to address, give the water some inertia, immediately much nicer diffusing water falls.

Picture 7

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.

Picture 10

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...

Picture 12

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
|
Improving the performance of the Carousel
I sent a quick e-mail to Romain guy to let him know how I was re-using his reflection code, and because I thought he might get a kick out of it. I was delighted to get a reply along with some suggestions for improving performance. The main points he raised were as follows:

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;