Tuesday, February 10, 2009

Embed a database in your desktop app with Apache Derby

This is not Swing related, but it could be useful to people writing desktop application. Derby is an apache project which is, quoting the official site :

Apache Derby, an Apache DB subproject, is an open source relational database implemented entirely in Java and available under the Apache License, Version 2.0. Some key advantages include:
  • Derby has a small footprint -- about 2 megabytes for the base engine and embedded JDBC driver.
  • Derby is based on the Java, JDBC, and SQL standards.
  • Derby provides an embedded JDBC driver that lets you embed Derby in any Java-based solution.
  • Derby also supports the more familiar client/server mode with the Derby Network Client JDBC driver and Derby Network Server.
  • Derby is easy to install, deploy, and use.

This mean you can easily have a database with your desktop application which will run locally on the PC, which is awesome for Swing application for example. So here is the 3 min / 5 step tutorial (I assume you already know JDBC) :
  • download this file and extract derby.jar
  • add derby.jar to the classpath of your project
  • load the driver and get a connection to the database :

create=true" will create the database if it not already exist.
  • do some JDBC stuff :

  • shutdown the database :


That's all ! There is also a "server" mode where the database is centralized if you want to do some "software as a service" or "data in the cloud" things.
Also, Derby is now also distributed with the JDK 6 (not the JRE!).

Monday, January 12, 2009

JRating, JTagCloud and GUI colorization

I have finally updated again my fake application. The look of the left list now use shadows, and there is also a very (too?) subtle radial gradient under the icon in the grid. There is also a cool "glow" effect when an icon is selected :



I have also integrated the customized toolbar from Jeremy (right click on the toolbar and choose "customize"). The implementation of Jeremy is very easy to use, the only downside is that you can't modify the way it look :



Also JTagCloud and JRating are now finished : you can add listeners to these components in order to detect user interaction. You can also create an UI for the JTagCloud in order to change the way tags are displayed.

 
the tags cloud component, darker = more elements
 
the rating component (double click an item to open this popup)


And finally I have also added a button which allow to change the color  of parts of the UI :



I use the great color picker from Jeremy (again :) ). When you have picked a color, click on a part of the main windows to change its color :

 
the footer, toolbar, breadcrumb bar and the main area are "colorizable"
the icons should be colorized too in order to look even better

 
red edition :)

GUI colorization is a subject I have already talked about (in fact it was my first post!). My previous solution used a filter to get the result, which is may be not the easiest way. This time I have used a JXLayer to catch all the user's click on the main frame (and also disable it at the same time). Then I loop over all the components under the cursor and set a "color" property. After this I just have to add property listeners to the components which have the ability to change their color. See the code of the popup with the color wheel :



Then, you just have to detect when the "color" property change :



I also wanted to change the cursor with a "fill" icon, but unfortunately the setCursor method does not seems to work when using translucent windows ...

Download the sources (as an Eclipse project).

Wednesday, November 19, 2008

Swing patterns

After reading the series of posts from Gregg Bolinger about Swing best practices, I thought I should share some of the patterns I use when writing Swing applications. I was going to post this as a comment in the original post, but it was too long so I'm writing a post instead ...


Usually we have only one main frame in a typical Swing application, so I implement this as a singleton named MainFrame :


public class MainFrame extends JFrame {

private static MainFrame INSTANCE;

public static MainFrame getInstance() {
if (INSTANCE == null)
INSTANCE = new MainFrame();

return INSTANCE;
}

private MainFrame() {
// ...
}

}


Each components in the frame has an accessor, where it is lazily loaded. For example :


private JPanel footer;
private JList menu;

public JPanel getFooter() {
if (footer == null) {
footer = new JPanel();
// ...
}

return footer;
}

public JList getMenu() {
if (menu == null) {
menu = new JList();
// ...
}

return menu;
}


So if somewhere in the application I have to do some stuff on a component all I have to do is :


MainFrame.getInstance().getMyComponent().doSomeStuff();


These components are added to the frame inside the constructor :


private MainFrame() {
// ...

this.add(getFooter());

// ...
}


If the various listeners are too complex to be created using an anonymous class, I use a private inner class named InnerListener :


private class InnerListener extends MouseAdapter implements MouseMotionListener, WindowListener {

public void mouseClicked(MouseEvent e) {
// ...
}

// ...

}


which is registered inside the constructor :


private InnerListener innerListener;

private MainFrame() {
// ...

innerListener = new InnerListener();
this.getFooter().addMouseListener(innerListener);

// ...
}


The idea here is to keep the various methods from the listeners interface private. See this example from my "fake app" :

before, no inner class

after, using a private inner class

The public API of the main frame is clean, no useless methods are accessible. Of course you can create multiple inner listeners if there is a lot of  code, for example MouseInnerListener and WindowInnerListener ...

The problem with these patterns is that the MainFrame class can become quite big (878 lines in my fake app, and still growing).

The complete skeleton for the MainFrame class :


public class MainFrame extends JFrame {


private static MainFrame INSTANCE;


private JPanel footer;

private JList menu;

private InnerListener innerListener;



public static MainFrame getInstance() {

if (INSTANCE == null)

INSTANCE = new MainFrame();



return INSTANCE;

}



private MainFrame() {

// ...


this.add(getFooter());


// ...


innerListener = new InnerListener();

this.getFooter().addMouseListener(innerListener);


// ...

}



public JPanel getFooter() {

if (footer == null) {

footer = new JPanel();

// ...

}


return footer;

}


public JList getMenu() {

if (menu == null) {

menu = new JList();

// ...

}



return menu;

}



private class InnerListener extends MouseAdapter implements MouseMotionListener, WindowListener {


public void mouseClicked(MouseEvent e) {

// ...

}



// ...



}


}


Of courses these are not "official" rules, just some practices I have found over the time, so I'd really like some feedback :)

Monday, October 27, 2008

Highlight search results with JXLayer

I have updated the "fake" app with a "find" feature similar to the one in firefox (ctrl + f). The search result matching the text in the "find" text field (at the bottom) are highlighted in yellow, in a "hand drawn" style :


The cells are highlighted as soon as you type (the search box at the top right work in this way too). This effect is made with JXLayer. A similar result can be found with the Rainbow app or at Curious creature.

Each highlighted cell look different because the drawing is "randomized". The problem was to make the effect work with different cell size :

A path is generated for each cell and size. I had to use a cache for all the generated paths, in order to keep the same drawing each time the cell is repaint, otherwise the result would look unnatural.

On a side note, there was already a JXLayer on the "folder" view, so if you interested in the different techniques which allow multiple layers on the same component, please see this thread on the JXLayer forum.

This "folder" view is a simple JList, so you can reuse the layer on another list :


import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.geom.GeneralPath;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;

import javax.swing.JList;
import javax.swing.JViewport;

import org.jdesktop.jxlayer.JXLayer;

public class ListSearchHighlightLayerUI extends AbstractLayerUI<JList> {

private List<Integer> highlights = new ArrayList<Integer>();
private Map<PathKey, GeneralPath> pathCache = new HashMap<PathKey, GeneralPath>();
private int strokeSize = 20;
private Color strokeColor = new Color(255, 255, 0, 150);

private static final Random RANDOM = new Random();


@Override
public void paintLayer(Graphics2D g, JXLayer<JList> layer) {
super.paintLayer(g, layer);

if (highlights != null & !highlights.isEmpty()) {
g.setColor(strokeColor);
g.setStroke(new BasicStroke(strokeSize, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL));

for (Integer index : highlights) {
if (index < layer.getView().getModel().getSize()) {
Rectangle r = layer.getView().getCellBounds(index, index);
g.draw(getPath(index, r));
}
}

g.setStroke(new BasicStroke());
}
}

/**
* @return the line which will be drawn for the cell at the given index and at the given size
*/
private GeneralPath getPath(int index, Rectangle bound) {
PathKey key = new PathKey(index, bound);
GeneralPath path = pathCache.get(key);

if (path == null) {
// if not in the cahcge, create the path :
path = new GeneralPath(GeneralPath.WIND_EVEN_ODD, bound.width / strokeSize);
path.moveTo (bound.x + getRandom(), bound.y + bound.height + getRandom());
int y, x = bound.x;
for (int i = 0; i < bound.width / strokeSize; i ++) {
if (i % 2 == 0) {
x = x + 2 * strokeSize;
y = bound.y;
} else {
y = bound.y + bound.height;
}
path.lineTo(x + getRandom(), y + getRandom());
}
pathCache.put(key, path);
}

return path;
}

/**
* @return a random int between -STROKE_SIZE/3 and STROKE_SIZE/3
*/
private int getRandom() {
return RANDOM.nextInt(strokeSize / 3) * (RANDOM.nextBoolean() ? 1 : -1);
}

public void addHighlightedIndex(int index) {
highlights.add(index);
setDirty(true);
}

public void removeHighlightedIndex(int index) {
highlights.remove(new Integer(index));
setDirty(true);
}

public void clearHighlightedIndex() {
highlights.clear();
setDirty(true);
}

public Color getStrokeColor() {
return strokeColor;
}

public void setStrokeColor(Color strokeColor) {
this.strokeColor = strokeColor;
this.setDirty(true);
}

public int getStrokeSize() {
return strokeSize;
}

public void setStrokeSize(int strokeSize) {
this.strokeSize = strokeSize;
this.setDirty(true);
}

// Scrollable implementation :

@Override
public boolean getScrollableTracksViewportHeight(JXLayer<JList> layer) {
if (layer.getView().getLayoutOrientation() == JList.VERTICAL_WRAP && layer.getView().getVisibleRowCount() <= 0)
return true;

if (layer.getParent() instanceof JViewport)
return layer.getParent().getHeight() > layer.getView().getPreferredSize().height;

return false;
}

@Override
public boolean getScrollableTracksViewportWidth(JXLayer<JList> layer) {
if (layer.getView().getLayoutOrientation() == JList.HORIZONTAL_WRAP && layer.getView().getVisibleRowCount() <= 0)
return true;

if (layer.getParent() instanceof JViewport)
return layer.getParent().getWidth() > layer.getView().getPreferredSize().width;

return false;
}


private static class PathKey {

private int index;
private Rectangle bound;

public PathKey(int index, Rectangle bound) {
this.index = index;
this.bound = bound;
}

@Override
public boolean equals(Object obj) {
return this.hashCode() == obj.hashCode();
}

@Override
public int hashCode() {
// see http://www.javapractices.com/topic/TopicAction.do?Id=28
int hash = 851 + index;
hash = 37 * hash + bound.height;
hash = 37 * hash + bound.width;
hash = 37 * hash + bound.x;
hash = 37 * hash + bound.y;

return hash;
}

}

}

Others updates to the app include :

- shadows on frame, defined as a border. The shadow is generated with a filter. See the class ilist.ui.generic.FrameShadowBorder in the sources for more informations.

- a very simple tag cloud component. The size and color of each tag depends from the number of element for each tag. This component is a simple panel with a FlowLayout. Each tag is a customized JToggleButton, which is then added to the panel. See the class ilist.ui.generic.tagcloud.JTagCloud for more information.

 
the bigger and brighter the tag is, the more elements it have

You can download the sources here.

Wednesday, October 1, 2008

Zune

PS : for the fastest readers, you may have missed a big update to my previous post.

Microsoft has recently released the third version of the Zune software. It's a media player (based on WMP), and you don't need a Zune to use it :)
It seems that Microsoft has spend a lot of time on the GUI :

The red thing at the bottom is the volume. It's best to tried it yourself, because they is a lot of subtle animations on each element. It ca also display pictures, video and podcast :


Not only the software download the album cover, it can also find online pictures of an artist and display it :


you can also display a nice view when playing music :


It can even download lyrics and show it with some cool animations but I have not yet managed to see it :


I'm sure the main windows could be done in Swing using the Substance look and feel :)
You may also be interrested by this post about creating a media player in java.

Tuesday, September 23, 2008

JGrid : a folder-like view, enhanced with JXLayer

I have finish coding this since 3 weeks and still haven't found the time to write a post, so here we go : I have updated my "sexy" swing app with a component which is a mix of a JTable and a windows folder :
The size of the cells is dynamic, so for example if you move the slider at the bottom the content will grow or shrink :


It look best when you play with the zoom in real time, so download it and have a look! (see the links at the end of the post)

I was searching for a way to implement the selection by drag and drop with the mouse, and JXLayer come to the rescue. The API is very simple, but yet very powerful.
With help of JXLayer you can easily decorate your compound components and catch all Mouse, Keyboard and FocusEvent for all its subcomponents.
Basically, you wrap your component into a layer which allow you to draw above or under the component. The "getting started" example will learn you all you need to know to start using JXLayer (it's really very simple and quick to learn).
So I used a ListSelectionModel for the logic and JXLayer for the drawing and events management. The result look like this :

I have used one layer on the whole grid component, to draw the selection rectangle when the user drag and drop to select cell.
JXLayer keep it simple!!
I can see plenty of different usage for this library, like a lightbox or a heat map over the UI.

About the JGrid component :
  • it's working like a JList (both display a list of values ...)
  • you can set renderer to customize each cell
  • the code need to be improved a little :) but you can still get it from here (in the ilist.ui.generic.grid package). There still some little problem with selection too.
  • it's scrollable, so you can embed it in a JScrollPane
Wow, big update. As pointed out in the comments,  it's possible to have the same thing with a simple JList :


JList grid = new JList(model); grid.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
// layout as a grid :
grid.setLayoutOrientation(JList.HORIZONTAL_WRAP);
grid.setVisibleRowCount(-1);
// cell size :
grid.setFixedCellHeight(100);
grid.setFixedCellWidth(100);
grid.setCellRenderer(myRenderer);

And if I wrap it with a JXLayer, i can have the "drag and drop" effect :


import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;

import javax.swing.JList;
import javax.swing.SwingUtilities;

import org.jdesktop.jxlayer.JXLayer;
import org.jdesktop.jxlayer.plaf.AbstractLayerUI;


public class GridLayerUI extends AbstractLayerUI {

private Point startDrag, endDrag;


@Override
protected void processMouseEvent(MouseEvent e, JXLayer l) {
super.processMouseEvent(e, l);

if (e.getID() == MouseEvent.MOUSE_PRESSED)
startDrag = SwingUtilities.convertPoint(e.getComponent(), e.getPoint(), l);

else if (e.getID() == MouseEvent.MOUSE_RELEASED) {
endDrag = null;
startDrag = null;
setDirty(true);
}
}

@Override
protected void processMouseMotionEvent(MouseEvent e, JXLayer l) {
super.processMouseMotionEvent(e, l);

if (e.getID() == MouseEvent.MOUSE_DRAGGED) {
endDrag = SwingUtilities.convertPoint(e.getComponent(), e.getPoint(), l);
if (startDrag != null) {
l.getView().getSelectionModel().clearSelection();
Rectangle dragZone = new Rectangle(Math.min(startDrag.x, endDrag.x), Math.min(startDrag.y, endDrag.y), Math.abs(endDrag.x - startDrag.x), Math.abs(endDrag.y - startDrag.y));
for (int i = 0; i < l.getView().getModel().getSize(); i++) { if (l.getView().getCellBounds(i, i).intersects(dragZone)) l.getView().getSelectionModel().addSelectionInterval(i, i); } } setDirty(true); } } @Override protected void paintLayer(Graphics2D g2, JXLayer l) {
super.paintLayer(g2, l);
if (endDrag != null && startDrag != null) {
// draw the selection rectangle when user drag & drop on the grid :
g2.setPaint(new Color(137, 177, 237, 128));
g2.fillRoundRect(Math.min(startDrag.x, endDrag.x), Math.min(startDrag.y, endDrag.y), Math.abs(endDrag.x - startDrag.x), Math.abs(endDrag.y - startDrag.y), 2, 2);
g2.setColor(new Color(19, 106, 197));
g2.drawRoundRect(Math.min(startDrag.x, endDrag.x), Math.min(startDrag.y, endDrag.y), Math.abs(endDrag.x - startDrag.x), Math.abs(endDrag.y - startDrag.y), 2, 2);
}
}
}


JXLayer gridLayer = new JXLayer(myList);
gridLayer.setUI(new GridLayerUI());
JScrollPane scrollPane = new JScrollPane(gridLayer);

The only problem is that by default JList select a cell in the list when you drag the mouse ... so you loose the selection done inside the layer :( The only solution I found is :


grid.putClientProperty("List.isFileList", Boolean.TRUE); // shame on me

which remove this feature. Yes it's a hack, and I'm currently searching for a best way to do this (may be by filtering the events with JXLayer ?).

About the "fake" application (see post #1 and post #2) :
  • the breadcrumb/search field/menu list/back and previous buttons work *cough*almost*cough*
  • the "new" button open a translucent panel, and also if you double click on a cell
  • there is a little bit of Vista, Mac OS and others things ... have to work on homogeneity :)
  • you can donwload it here (java -classpath ilist.jar;examples.jar;Filters.jar;jna.jar;plug-engine.jar;jxlayer.jar ilist.MainPlugin to start it, yes the jar is not runnable :( )
  • download sources here

Tuesday, August 26, 2008

Sexy desktop application (part 2)

(See the first part)

Some notes about the first part :
 - Rounded border : I wrote my own rounded border, but it seem that you can use LineBorder for this, using this constructor :

public LineBorder(Color color, int thickness, boolean roundedCorners)
But there is no way to specify the arc of the corner ...

- Custom UI : another way to customize component is to use Painter with the Nimbus look and feel, see this post for example.

I have added some features to my fake application (the search field and the badges for example) :


  • building a custom component :
If custom background, border or UI don't give you enough customization, may be you should build your own component. Before re-inventing the wheel, there is various library with cool Swing components, so have a look at it !
If you have to build your own component, you may have to write several classes/interfaces :
- a custom model
- events management (with may be your own listener + event)
- the component itself
- eventually a component UI

This post will give you all the information you need on the different parts of a swing component.

In this example I will show these various classes for the breadcrumb visible under the toolbar :

Speaking of not re-inventing the wheel, you can find a great breadcrumb bar at Flamingo. If you need this kind of component, you should probably use the one at Flamingo, which is far more complete/stable than mine (the reason I build my own implementation, apart from the joy of spreading my little knowledge here, is that I plan to make it evolved into a mix of a search field & breadcrumb).

So, the first classes here is BreadcrumbItem, which represent a single item from the bar :


import java.awt.Color;
import java.awt.Font;
import java.awt.image.BufferedImage;


public class BreadcrumbItem {

private T value;
private String label;
private BufferedImage icon;
private Font font;
private Color color;


public BreadcrumbItem(T value, String label, BufferedImage icon) {
this.label = label;
this.icon = icon;
this.value = value;
}

public BreadcrumbItem(T value, String label) {
this(value, label, null);
}


public T getValue() {
return value;
}

public String getLabel() {
return label;
}

public void setLabel(String label) {
this.label = label;
}

public BufferedImage getIcon() {
return icon;
}

public void setIcon(BufferedImage icon) {
this.icon = icon;
}

public Color getColor() {
return color;
}

public void setColor(Color color) {
this.color = color;
}

public Font getFont() {
return font;
}

public void setFont(Font font) {
this.font = font;
}

}

Next, the model. The only things we need to build a breadcrumb bar is the first item of the bar, and all the available choices for a given item :


import java.util.List;

public interface BreadcrumbModel {

public BreadcrumbItem<?> getRoot();

public List<BreadcrumbItem<?>> getChildrens(BreadcrumbItem<?> item);

}

Finally, the component itself, which is basically some button next to each others. The events are also managed here :


import ilist.ui.generic.GraphicsUtilities;

import java.awt.Color;
import java.awt.Cursor;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import javax.imageio.ImageIO;
import javax.swing.BoxLayout;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.border.EmptyBorder;
import javax.swing.event.ChangeEvent;

import com.jhlabs.image.GlowFilter;


@SuppressWarnings("serial")
public class JBreadcrumb extends JPanel {

private static GlowFilter glow = new GlowFilter();
private static BufferedImage arrowImg;
static {
glow.setAmount(0.08f);
try {
arrowImg = ImageIO.read(JBreadcrumb.class.getResource("/right.png"));
} catch (IOException e) {
e.printStackTrace();
}
}


private BreadcrumbModel model;
private List<BreadcrumbItem> path = new ArrayList<BreadcrumbItem>();
private BufferedImage arrowIcon = arrowImg;


public JBreadcrumb(BreadcrumbModel model) {
if (model == null)
throw new IllegalArgumentException("Model cannot be null");

this.model = model;
this.setOpaque(false);
this.setLayout(new BoxLayout(this, BoxLayout.LINE_AXIS));
this.addBreadcrumItem(model.getRoot());
}


protected JButton createItemButton(final BreadcrumbItem item) {
final JButton button = new JButton(item.getLabel());
if (item == model.getRoot() && item.getIcon() != null) {
button.setIcon(new ImageIcon(item.getIcon()));
button.setRolloverIcon(new ImageIcon(glow.filter(item.getIcon(), null)));
}
button.setContentAreaFilled(false);
button.setBorder(new EmptyBorder(0, 6, 0, 6));
button.setFocusPainted(false);
button.setBackground(GraphicsUtilities.TRANSPARENT_COLOR);
button.setForeground(item.getColor() == null ? Color.WHITE : item.getColor());
if (item.getFont() != null)
button.setFont(item.getFont());
button.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));

button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
goTo(item, button);
firePathChanged();
}
});

return button;
}

private void goTo(BreadcrumbItem<?> item, JButton button) {
for (int i = getComponentCount() - 1; i >= 0; i--) {
if (getComponent(i - 1) == button)
break;
else
remove(getComponent(i));
}
revalidate();

boolean remove = false;
for (Iterator i = path.iterator(); i.hasNext();) {
Object o = i.next();
if (remove)
i.remove();
if (o == item)
remove = true;
}
}

protected JButton createItemArrow(final JButton itemButton, final BreadcrumbItem<?> item) {
final JButton arrow = new JButton(new ImageIcon(arrowIcon));
arrow.setRolloverIcon(new ImageIcon(glow.filter(arrowIcon, null)));
arrow.setContentAreaFilled(false);
arrow.setBorder(null);
arrow.setFocusPainted(false);
arrow.setBackground(GraphicsUtilities.TRANSPARENT_COLOR);
arrow.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));

arrow.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
JPopupMenu menu = new JPopupMenu();
for (final BreadcrumbItem nextItem : model.getChildrens(item)) {
JMenuItem menuItem = new JMenuItem(nextItem.getLabel());
if (nextItem.getIcon() != null)
menuItem.setIcon(new ImageIcon(nextItem.getIcon()));
if (nextItem.getColor() != null)
menuItem.setForeground(nextItem.getColor());
if (nextItem.getFont() != null)
menuItem.setFont(nextItem.getFont());
menuItem.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
goTo(item, itemButton);
addBreadcrumItem(nextItem);
}
});
menu.add(menuItem);
}
menu.show(arrow, 0, arrow.getHeight());
}
});
return arrow;
}

public void addBreadcrumItem(BreadcrumbItem<?> item) {
JButton button = createItemButton(item);
this.add(button);
this.add(createItemArrow(button, item));
this.revalidate();
path.add(item);
this.firePathChanged();
}

public BreadcrumbModel getModel() {
return model;
}

public List getPath() {
return path;
}

public BufferedImage getArrowIcon() {
return arrowIcon;
}

public void setArrowIcon(BufferedImage arrowIcon) {
this.arrowIcon = arrowIcon;
}

protected void firePathChanged() {
ChangeEvent event = new ChangeEvent(this);
Object[] listeners = listenerList.getListenerList();
for (int i = listeners.length - 2; i >= 0; i -= 2) {
if (listeners[i] == BreadcrumbChangeListener.class) {
((BreadcrumbChangeListener) listeners[i + 1]).stateChanged(event);
}
}
}

public void addChangeListener(BreadcrumbChangeListener l) {
listenerList.add(BreadcrumbChangeListener.class, l);
}

public void removeChangeListener(BreadcrumbChangeListener l) {
listenerList.remove(BreadcrumbChangeListener.class, l);
}

}

An example of a model which display folders from the local drive :


import ilist.ui.generic.breadcrumb.BreadcrumbItem;
import ilist.ui.generic.breadcrumb.BreadcrumbModel;

import java.io.File;
import java.util.ArrayList;
import java.util.List;

public class FolderBreadcrumbModel implements BreadcrumbModel {

private BreadcrumbItem<File> root = new BreadcrumbItem<File>(File.listRoots()[0], "Root", null);

public BreadcrumbItem<?> getRoot() {
return root;
}

public List<BreadcrumbItem<?>> getChildrens(BreadcrumbItem<?> item) {
List<BreadcrumbItem<?>> list = new ArrayList<BreadcrumbItem<?>>();

File[] files = ((File)item.getValue()).listFiles();
for (File file : files) {
if (file.isDirectory())
list.add(new BreadcrumbItem<File>(file, file.getName(), null));
}

return list;
}

}

  • icons :
This is an important part of the look of your application, and you should choose unambiguous icons that clearly represent the underlying action.
There is a lot of icon pack (see www.eecs.wsu.edu/~djohnson/freeMedia) around the web.
Here a short list of great and clear icons :
- Silk : www.famfamfam.com/lab/icons/silk
- Sweetie : sweetie.sublink.ca
- Fugue & Diagona (1371 icons !!) : www.pinvoke.com
- Neu : art.gnome.org/themes/icon/1100
- Tango : tango.freedesktop.org/Tango_Icon_Library


If you want a cool glow effect on rollover, you can use a glow filter :


public JButton createToolbarButton(Action action, BufferedImage icon) {
JButton button = new JButton(action);
GlowFilter glow = new GlowFilter();
glow.setAmount(0.08f);
button.setIcon(new ImageIcon(icon));
button.setRolloverIcon(new ImageIcon(glow.filter(icon, null)));
button.setBorder(null);
button.setFocusPainted(false);
button.setForeground(GraphicsUtilities.TRANSPARENT_COLOR);
button.setBackground(GraphicsUtilities.TRANSPARENT_COLOR);
button.setContentAreaFilled(false);
button.setPreferredSize(new Dimension(32, 1));

return button;
}

The setContentAreaFilled method remove the background of a button and only leave the icon.
  • transparency + custom decoration :
The last step into GUI customization is to remove the default frame decoration (= the title bar and the close/reduce/maximize buttons) and to build our own. The setUndecorated method will do the trick. Then you can add your custom close/reduce/maximize buttons. Remember that you should not changed the way these buttons works, or your user will be lost, for example on Windows :
- keep the same order and position
- double click on the title bar maximize the frame
- maximize button turn into "de-maximize" when clicked
- when the frame is maximized, the top right pixel should be on the close button (remove any border/corner when maximized)

You also will have to implement the drag and drop of the tile bar to move the frame, and a way to resize it (see the source for more information).

If you want to have a different shape for your frame, or simply some translucent effect, see this post or, if you use the latest build from Java 6, look here and here. For some unknown reason, the setCursor method does not work when I use JNA to activate transparent windows ... (move your cursor over the breadcrumb bar for example, it should have turned into the "hand" pointer)

You can download :
- the sources
- the fake application (java -classpath ".";plug-engine.jar Launcher to run it (because I'm too lazy to do a jnlp version), in the "iList-Launcher" folder)