Cover | Table of Contents | Colophon
JButton to the full
Look and Feel API. I am still amazed at the power and
flexibility of Swing, and quite aware of its complexity. Some of the
more esoteric parts can take years to master. However, you don't need to
go straight into the JTree or Look
and Feel APIs just to do something cool. There are still a lot of fun
things waiting in the standard components we don't always think
about.JButton to the full
Look and Feel API. I am still amazed at the power and
flexibility of Swing, and quite aware of its complexity. Some of the
more esoteric parts can take years to master. However, you don't need to
go straight into the JTree or Look
and Feel APIs just to do something cool. There are still a lot of fun
things waiting in the standard components we don't always think
about.
JPanel called ImagePanel, shown in Example 1-1. public class ImagePanel extends JPanel {
private Image img;
public ImagePanel(Image img) {
this.img = img;
Dimension size = new Dimension(img.getWidth(null),
img.getHeight(null));setSize(size);
setPreferredSize(size);
setMinimumSize(size);
setMaximumSize(size);
setLayout(null);
}
}img variable. Then it calls
setSize() and setPreferredSize() with the size of the image.
This ensures that the panel will be the size of the image exactly. I had
to set the preferred, maximum, and minimum sizes as well—this is because the panel's
parent and children may not be using absolute layouts.JLabel. You can change the font,
size, color, and even add an icon. By using HTML in your components
[Hack #52] , you can even add
things like underline and bullets. This is fine for most jobs, but
sometimes you need more. What if you want a drop shadow or an embossed
effect? The JLabel is simply
inadequate for richer interfaces. Fortunately, the Swing Team made it
very easy to extend the JLabel and
add these features yourself.JLabel, which of
course calls for a subclass; see Example 1-5 for
details. public class RichJLabel extends JLabel {
private int tracking;
public RichJLabel(Stringtext, int tracking) {
super(text);
this.tracking = tracking;
}
private int left_x, left_y, right_x, right_y;
private Color left_color, right_color;
public void setLeftShadow(int x, int y, Color color) {
left_x = x;
left_y = y;
left_color = color;
}
public void setRightShadow(int x, int y, Color color) {
right_x = x;
right_y = y;
right_color = color;
}RichJLabel extends the
standard javax.swing.JLabel and adds a tracking
argument to the constructor. Next, it adds two methods for the right and
left shadow. These are called shadows because they will be drawn below
the main text, but whether they actually look like shadows depends on
the color, as well as the x- and y-offsets passed into each
method.MatteBorder, which can accept an image in its
constructor. For simple tiled backgrounds, such as a checkerboard
pattern, this works fine. However, if you want to have particular images
in each corner, creating a fully resizable image border, then you'll need
something more powerful. Fortunately, Swing makes it very easy to create
custom border classes. The image border in this hack will produce a
border that looks like Figure
1-12.
AbstractBorder and implement the paintBorder() method. The class will take
eight images in the constructor, one for each corner and each side; all
the code is shown in Example
1-6. public class ImageBorder extends AbstractBorder {
Image top_center, top_left, top_right;
Image left_center, right_center;
Image bottom_center, bottom_left, bottom_right;
Insets insets;
public ImageBorder(Image top_left, Image top_center, Image top_right,
Image left_center, Image right_center,
Image bottom_left, Image bottom_center, Image bottom_right) {
this.top_left = top_left;
this.top_center = top_center;
this.top_right = top_right;
this.left_center = left_center;
this.right_center = right_center;
this.bottom_left = bottom_left;
this.bottom_center = bottom_center;
this.bottom_right = bottom_right;
}
public voidsetInsets(Insets insets) {
this.insets = insets;
}
public Insets getBorderInsets(Component c) {
if(insets != null) {
return insets;
} else {
return new Insets(top_center.getHeight(null),
left_center.getWidth(null),
bottom_center.getHeight(null), right_center.getWidth(null));
}
}
java.util.Calendar and a few images.setDate() method, so that MVC frameworks can
play well with your calendar. Let's get started.
JPanel and override the paintComponent() method, as shown in Example 1-9. public class CalendarHack extends JPanel {
protected Image background, highlight, day_img;
protected SimpleDateFormat month = new SimpleDateFormat("MMMM");
protected SimpleDateFormat year = new SimpleDateFormat("yyyy");
protected SimpleDateFormat day = new SimpleDateFormat("d");
protected Date date = new Date();
public void setDate(Date date) {
this.date = date;
}
publicCalendarHack() {
background = new ImageIcon("calendar.png").getImage();
highlight = new ImageIcon("highlight.png").getImage();
day_img = new ImageIcon("day.png").getImage();
this.setPreferredSize(new Dimension(300,280));
}
public void paintComponent(Graphics g) {
((Graphics2D)g).setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g.drawImage(background,0,0,null);
g.setColor(Color.black);
g.setFont(new Font("SansSerif",Font.PLAIN,18));
g.drawString(month.format(date),34,36);
g.setColor(Color.white);
g.drawString(year.format(date),235,36);
}
} JTextField, a complex Swing component that does
not already support backgrounds or icons by default.JList and
JTable, use renderers to customize
their look. To put a background in a JTextField, however, requires more. The plan
is to subclass JTextField, prepare
the resources for drawing a background (loading the image, etc.), and
then draw a new background while preserving the normal JTextField drawing code for the text and
cursor.TexturePaint. Java2D allows you to fill any
area with instances of the Paint
interface. Typically you use a color, which is an implementation of
Paint, but it is possible to use
something else, such as a texture or gradient. This class will use a
TexturePaint to tile an image across
the component's background.JTextField subclass (shown in Example 1-10). public class WatermarkTextField extends JTextField {
BufferedImage img;
TexturePaint texture;
public WatermarkTextField(File file) throws IOException {
super();
img = ImageIO.read(file);
Rectangle rect = new Rectangle(0,0,
img.getWidth(null),img.getHeight(null));
texture = new TexturePaint(img, rect);
setOpaque(false);
}
}WatermarkTextField. It is a subclass of
JTextField with a custom constructor
that accepts a File object containing
an image. It also defines two member variables: img and texture. After the obligatory call to super(), the constructor reads the file into
the JScrollPane. Its nested composite design allows
developers to create some stunning effects.JScrollPane. A JScrollPane is not a single Swing
component—it's actually a wrapper around two scrollbars and the
component that does the real scrolling is a
JViewport. This viewport is the actual target
component; you will subclass it to draw both above and below the
View component (as seen in Example 1-12). The View is the Swing widget being scrolled; in
this case, it is a JTextArea. public class ScrollPaneWatermark extends JViewport {
BufferedImage fgimage, bgimage;
TexturePaint texture;
public void setBackgroundTexture(URL url) throws IOException {
bgimage = ImageIO.read(url);
Rectangle rect = new Rectangle(0,0,
bgimage.getWidth(null),bgimage.getHeight(null));
texture = new TexturePaint(bgimage, rect);
}
public void setForegroundBadge(URL url) throws IOException {
fgimage = ImageIO.read(url);
}ScrollPaneWatermark class
inherits from JViewport, adding two
methods: setBackgroundTexture() and
setForegroundBadge(). Each takes a
URL instead of a File to allow for
images loaded from places other than the local disk, such as a web
server or JAR file.ScrollPaneWatermark. This hack will pull a
photo down from the Web and reuse that class to put the photo in the
background. The photo itself comes from NASA's "Astronomy Picture of the
Day" page: http://antwrp.gsfc.nasa.gov/apod/. The URL to
the image changes each day, but the page itself does not. To pull the
image down you will load the page, find the image URL, then load the
image itself and put it into the ScrollPaneWatermark. Depending on the day, it
may look something like Figure
1-22.
BackgroundLoader, which implements Runnable so it can be placed on its own
thread. The constructor takes as an argument the ScrollPaneWatermark, which the loader will put the image into. The run() method contains a loop that will run every
two hours, loading the page, finding the SRC URL, then loading the image into the
watermark. public class BackgroundLoader implements Runnable {
private ScrollPaneWatermark watermark;
public BackgroundLoader(ScrollPaneWatermark watermark) {
this.watermark = watermark;
}
public void run() {
while(true) {
try {String base_url = "http://antwrp.gsfc.nasa.gov/apod/";
URL url = new URL(base_url);
Reader input = new InputStreamReader(url.openStream());
char buf[] = new char[1024];
StringBuffer page_buffer = new StringBuffer();
while(true) {
int n = input.read(buf);
if(n < 0) { break; }
page_buffer.append(buf,0,n);
}
// Locate the Image URL (see next section)
} catch (Exception ex) {
System.out.println("exception: " + ex);
ex.printStackTrace();
}
}
}
} JTabbedPane.JTabbedPane, except for the
actual animation drawing, which will be delegated to a further
subclass. By putting all of the heavy lifting into the parent class,
you will be able to create new animations easily. public class TransitionTabbedPane extends JTabbedPane
implements ChangeListener, Runnable {
protected int animation_length = 20;
public TransitionTabbedPane() {
super();this.addChangeListener(this);
}
public int getAnimationLength() {
return this.animation_length;
}
public void setAnimationLength(int length) {
this.animation_length = length;
}TransitionTabbedPane extends
the standard JTabbedPane and also
implements ChangeListener and
Runnable. ChangeListener allows you to learn when the
user has switched between tabs. Since the event is propagated
before the new tab is painted, inserting the
animation is very easy. paintComponent() method. This is true even for
components that offload the actual drawing to Look and Feel UI objects.
Because all drawing goes through the
paintComponent() method at some point, this
point is where you can do some interesting things by manipulating the
graphics object during the paint process.Graphics object passed in through the paintComponent() method. This means that if
you replace the Graphics object with
a custom version, you can capture a component's drawing into a bitmap instead of going straight to the
screen. public classBlurJButton extends JButton {
public BlurJButton(String text) {
super(text);
}
public void paintComponent(Graphics g) {
if(isEnabled()) {
super.paintComponent(g);
return;
}
BufferedImage buf = new BufferedImage(getWidth(),getHeight(),
BufferedImage.TYPE_INT_RGB);
super.paintComponent(buf.getGraphics());
// Blur the buffered image (see next section)
}
}BlurJButton class extends a
normal JButton and overrides the
paintComponent() method. If the
button is enabled (neither disabled nor grayed out), then it calls the superclass's
normal version of paintComponent()
and returns. If the button is disabled, however, then JComboBox but without the extension headaches of
Sun's version of the class.javax.swing. This works fine most of the time,
but every now and then you need to build something where there is no
easy standard component to start with. Even worse, sometimes the obvious
choice for your starting point is a component so convoluted that you
can't figure out where to start. Still, you'd rather not reimplement the
wheel. No, I'm not talking about JTree or JTable—I'm referring to the JComboBox. It seems like such a simple
component, but the implementation is fiendishly complex.JComboBox, but do something entirely
different, like select a color or show a history list. A quick search
through the JComboBox API doesn't
turn up any obvious extension points. You could customize it with some
cell renderers, but if you need a component that doesn't show a list of
data, you are pretty much out of luck. The source to JComboBox is not very helpful either. The work
is spread out over several UI classes in the various Look and Feel
(L&F) packages. If you did customize one of those, your component
would look out of place when used in a different L&F. The only real
option is to write your own combo box, which is pretty easy except for
the actual drop-down part. You need to show a component on top of the
others, poking out of the frame occasionally, but without any
decorations of its own. It should be just a borderless floating box.
Digging through Swing's source code reveals the secret ingredient: a
JWindow.JWindow is a subclass of
Window but not of Frame. This means it has no decorations on the
side, and it is hidden from the Dock and Taskbar. This is exactly what
you want from a pop up. Care must be taken when creating it, however, as
you must ensure the window appears only on top of the existing
components, and that it disappears when something else gains focus or
the window moves. Fortunately, you can do all of this with one composite
component and a few event listeners. BasicPopupMenuUI in the javax. swing.plaf.basic package and created a
subclass called CustomPopupMenuUI
(shown in Example
1-25). It only does two things special: adds a custom border to
the pop up's parent panel and sets the panel to be transparent. public class CustomPopupMenuUI extends BasicPopupMenuUI {
public static ComponentUI createUI(JComponent c) {
return new CustomPopupMenuUI();
}
public Popup getPopup(JPopupMenu popup, int x, int y) {
Popup pp = super.getPopup(popup,x,y);
JPanel panel = (JPanel)popup.getParent();
panel.setBorder(newShadowBorder(3,3));
panel.setOpaque(false);
return pp;
}
}paint() method of a parent
component and then rendering the children into a buffer [Hack
#9] . It would be nice to do the same thing here, but
there's just one small problem. Overriding the paint() method of the JMenu wouldn't do any good because the
JMenu doesn't draw what we think of as a menu—a
list of menu items that pop up when you click on the menu's title. The
JMenu actually only draws the title
at the top of a menu. The rest of the menu is drawn by a JPopupMenu created as a member of the JMenu. Unfortunately this member is marked
private, which means you can't
substitute your own JPopupMenu
subclass for the standard version.javax.swing.plaf package. If you override the
right plaf classes for the menu items
and pop-up menu, then you should be able to create the desired
translucent effect. It just takes a little subclassing.MenuItems are implemented
by some form of the javax.swing.plaf.
MenuItemUI class. When creating custom UI classes, it is always best to start by subclassing
something in the javax.swing.plaf.basic package (in this
case, BasicMenuItemUI) because it handles most of
the heavy lifting for you, as shown in Example 1-28. public class CustomMenuItemUI extends BasicMenuItemUI {
public static ComponentUI createUI(JComponent c) {
return new CustomMenuItemUI();
}
public void paint(Graphics g, JComponent comp) {
// paint to the buffered image
BufferedImage bufimg = new BufferedImage(
comp.getWidth(),
comp.getHeight(),
BufferedImage.TYPE_INT_ARGB);
Graphics2D g2 = bufimg.createGraphics();
// restore the foreground color in case the superclass needs it
g2.setColor(g.getColor());
super.paint(g2,comp);
// do an alpha composite
Graphics2D gx = (Graphics2D) g;
gx.setComposite(AlphaComposite.getInstance(
AlphaComposite.SRC_OVER,0.8f));
gx.drawImage(bufimg,0,0,null);
}
}
FilteredJList, is a single class with two inner
subclasses: FilterModel and FilterField. The list owns the field, so a
caller can create the JList fairly
typically and then just ask for the field and add it wherever it makes
sense in the layout.FilteredJList as a subclass of JList, and provide a constructor and some
convenience methods, as seen in Example 2-1.public classFilteredJList extends JList {
private FilterField filterField;
private int DEFAULT_FIELD_WIDTH = 20;
public FilteredJList() {
super();
setModel (new FilterModel());
filterField = new FilterField (DEFAULT_FIELD_WIDTH);
}
public void setModel (ListModel m) {
if (! (m instanceof FilterModel))
throw new IllegalArgumentException();
super.setModel (m);
}
public void addItem (Object o) {
(FilterModel)getModel()).addElement (o);
}
public JTextField getFilterField() {
return filterField;
}
FilteredJList, is a single class with two inner
subclasses: FilterModel and FilterField. The list owns the field, so a
caller can create the JList fairly
typically and then just ask for the field and add it wherever it makes
sense in the layout.FilteredJList as a subclass of JList, and provide a constructor and some
convenience methods, as seen in Example 2-1.public classFilteredJList extends JList {
private FilterField filterField;
private int DEFAULT_FIELD_WIDTH = 20;
public FilteredJList() {
super();
setModel (new FilterModel());
filterField = new FilterField (DEFAULT_FIELD_WIDTH);
}
public void setModel (ListModel m) {
if (! (m instanceof FilterModel))
throw new IllegalArgumentException();
super.setModel (m);
}
public void addItem (Object o) {
(FilterModel)getModel()).addElement (o);
}
public JTextField getFilterField() {
return filterField;
}FilterField, the JList also creates its own FilterModel in the constructor, and overrides
setModel() to ensure that you can't
push in an incompatible model. It also contains an addItem() method, which really just delegates
to the FilterModel.FilterModel, shown in Example 2-2, is where the
magic happens.JList. Now a JButton needs to be attached to the text
field, so the two are bundled together in the inner class FilterField. This class is responsible
for:JTextField, as before.JTextField's contents as a saved search
anytime the Return or Enter key is pressed.JButton and popping up a menu with
previous searches.JTextField
with a previous search when one is selected from the list. It
doesn't need to explicitly tell the model to refilter because
changing the text area will fire a DocumentEvent that is already accounted
for by the JTextField's DocumentListener.FilterField
class.class FilterField extends JComponent
implements DocumentListener, ActionListener {
LinkedList prevSearches;
JTextField textField;
JButton prevSearchButton;
JPopupMenu prevSearchMenu;
public FilterField (int width) {
super();
setLayout(new BorderLayout());
textField = new JTextField (width);
textField.getDocument().addDocumentListener (this);
textField.addActionListener (this);
prevSearchButton =
new JButton (new ImageIcon ("mag-glass.png"));
prevSearchButton.setBorder(null);prevSearchButton.addMouseListener (new MouseAdapter() {
public void mousePressed (MouseEvent me) { popMenu (me.getX(), me.getY());
}
});
add (prevSearchButton, BorderLayout.WEST);
add (textField, BorderLayout.CENTER);
prevSearches = new LinkedList ();
}
public void popMenu (int x, int y) {
prevSearchMenu = new JPopupMenu();
Iterator it = prevSearches.iterator();
while (it.hasNext())
prevSearchMenu.add (
new PrevSearchAction(it.next().toString()));
prevSearchMenu.show (prevSearchButton, x, y);
}
public void actionPerformed (ActionEvent e) {
// called on return/enter, adds term to prevSearches
if (e.getSource() == textField) {
prevSearches.addFirst (textField.getText());
if (prevSearches.size() > 10)
prevSearches.removeLast();
}
}
public void changedUpdate (DocumentEvent e) {
((FilterModel)getModel()).refilter();
}
public void insertUpdate (DocumentEvent e) {
((FilterModel)getModel()).refilter();
}
public void removeUpdate (DocumentEvent e) {
((FilterModel)getModel()).refilter();
}
}JList, creating my own
scrolling layout of JPanels and
faking the list behavior. It turned up a funny Swing bug because I was
using GridBagLayout for the fake
list, and it started totally bombing out after about 500 items were
added to the list. This was because GridBagLayout has a bug where it can't have
more than 512 rows. Considering the bug (number 4254022 on the Java
Bug Parade) was filed in 1999 and is still open, I'm figuring it won't
get fixed by the time you read this.JList. The tricky part here is that there
isn't a way (that I've found) to steal the mouse clicks from the
JList and consume them before the
normal calls to the ListSelectionModel are made. Instead, the
strategy is to set up a ListSelectionListener and just fix everything
after JList has done its
thing.JList and give it a custom ListSelectionListener and a ListCellRenderer. Acomplete listing is shown in
Example 2-6.JComponent
when you write a ListCellRenderer.
Instead, delegate the getListCellRendererComponent( ) call to one of several components, choosing whichever
best represents the item to be rendered.JComponent for ListCellRenderers is a pretty hateful practice
because they're not really used as Components anyway! They're certainly not added
to the JList. Instead, a list cell is rendered off screen and those
pixels are blitted to the JList. So,
provided that what you return in getListCellRendererComponent is what you want
the cell to look like, it really doesn't matter how you get
there.JList, you basically just have to implement
the full set of AWT drag-and-drop interfaces because the list will be
both the source of the drag and the target of the drop. The other thing
you need to do is to use some cell rendering tricks to provide a visual
cue as to where the drop will occur.ReorderableJList, shown in
Example 2-12, is a
JList that uses a DefaultListModel,
which is mutable for the obvious reason that it will need to change in
response to drag-and-drops. The bulk of it is concerned with
implementing the drag-and-drop interfaces DragSourceListener, DropTargetListener, and DragGestureListener. It has an inner class
implementing Tranferable to hold the
item being dropped, although this isn't absolutely necessary. I could
have just held the dragged item in an instance variable and nulled the Transferable in the drag-and-drop calls, but
it doesn't hurt to do it the nice way.public class ReorderableJList extends JList
implements DragSourceListener, DropTargetListener, DragGestureListener {
static DataFlavor localObjectFlavor;
static {
try {
localObjectFlavor =
new DataFlavor (DataFlavor.javaJVMLocalObjectMimeType);
} catch (ClassNotFoundException cnfe) { cnfe.printStackTrace(); }
}
static DataFlavor[] supportedFlavors = { localObjectFlavor };
DragSource dragSource;
DropTarget dropTarget;
Object dropTargetCell;
int draggedIndex = -1;
public ReorderableJList () {
super();
setCellRenderer (new ReorderableListCellRenderer());
setModel (new DefaultListModel());
dragSource = new DragSource();
DragGestureRecognizer dgr =
dragSource.createDefaultDragGestureRecognizer (this,
DnDConstants.ACTION_MOVE,
this);
dropTarget = new DropTarget (this, this);
}
// DragGestureListener
public void dragGestureRecognized (DragGestureEvent dge) {
System.out.println ("dragGestureRecognized");
// find object at this x,y
Point clickPoint = dge.getDragOrigin();
int index = locationToIndex(clickPoint);
if (index == -1)
return;
Object target = getModel().getElementAt(index);
Transferable trans = new RJLTransferable (target);
draggedIndex = index;
dragSource.startDrag (dge,Cursor.getDefaultCursor(),
trans, this);
}
// DragSourceListener events
public void dragDropEnd (DragSourceDropEvent dsde) {
System.out.println ("dragDropEnd()");
dropTargetCell = null;
draggedIndex = -1;
repaint();
}
public void dragEnter (DragSourceDragEvent dsde) {}
public void dragExit (DragSourceEvent dse) {}
public void dragOver (DragSourceDragEvent dsde) {}
public void dropActionChanged (DragSourceDragEvent dsde) {}
// DropTargetListener events
public void dragEnter (DropTargetDragEvent dtde) {
System.out.println ("dragEnter");
if (dtde.getSource() != dropTarget)
dtde.rejectDrag();
else {
dtde.acceptDrag(DnDConstants.ACTION_COPY_OR_MOVE);
System.out.println ("accepted dragEnter");
}
}
public void dragExit (DropTargetEvent dte) {}
// dragOver() listed below
// drop() listed below
public void dropActionChanged (DropTargetDragEvent dtde) {}
// main() method to test - listed below
// RJLTransferable listing below
// ReorderableListCellRendering listing below
}repaint(). The cell renderer can then use the
updated highlight color as it redraws the cells in the list. Example 2-18 shows this
technique.import java.awt.*;
import javax.swing.*;
import javax.swing.event.*;
import java.util.*;
public classAnimatedJList extends JList
implements ListSelectionListener {
static java.util.Random rand = new java.util.Random();
static Color listForeground, listBackground,
listSelectionForeground, listSelectionBackground;
static float[] foregroundComps, backgroundComps,
foregroundSelectionComps, backgroundSelectionComps;
static {
UIDefaults uid = UIManager.getLookAndFeel().getDefaults();
listForeground = uid.getColor ("List.foreground");
listBackground = uid.getColor ("List.background");
listSelectionForeground = uid.getColor ("List.selectionForeground");
listSelectionBackground = uid.getColor ("List.selectionBackground");
foregroundComps =
listForeground.getRGBColorComponents(null);
foregroundSelectionComps =
listSelectionForeground.getRGBColorComponents(null);
backgroundComps =
listBackground.getRGBColorComponents(null);
backgroundSelectionComps =
listSelectionBackground.getRGBColorComponents(null);
}
public Color colorizedSelectionForeground,
colorizedSelectionBackground;
public static final int ANIMATION_DURATION = 1000;
public static final int ANIMATION_REFRESH = 50;
public AnimatedJList() {
super();
addListSelectionListener (this);
setCellRenderer (new AnimatedCellRenderer());
}
public void valueChanged (ListSelectionEvent lse) {
if (! lse.getValueIsAdjusting()) {
HashSet selections = new HashSet();
for (int i=0; i < getModel().getSize(); i++) {
if (getSelectionModel().isSelectedIndex(i))
selections.add (new Integer(i));
}
CellAnimator animator = new CellAnimator (selections.toArray());
animator.start();
}
}
public static void main (String[] args) {
JList list = new AnimatedJList ();
DefaultListModel defModel = new DefaultListModel();
list.setModel (defModel);
String[] listItems = {
"Chris", "Joshua", "Daniel", "Michael",
"Don", "Kimi", "Kelly", "Keagan"
};
Iterator it = Arrays.asList(listItems).iterator();
while (it.hasNext())
defModel.addElement (it.next());
// show list
JScrollPane scroller =
new JScrollPane (list,
ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS,
ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
JFrame frame = new JFrame ("Checkbox JList");
frame.getContentPane().add (scroller);
frame.pack();
frame.setVisible(true);
}
class CellAnimator extends Thread {
Object[] selections;
long startTime;
long stopTime;
public CellAnimator (Object[] s) {
selections = s;
}
public void run() {
startTime = System.currentTimeMillis();
stopTime = startTime + ANIMATION_DURATION;
while (System.currentTimeMillis() < stopTime) {
colorizeSelections();
repaint();
try { Thread.sleep (ANIMATION_REFRESH); }
catch (InterruptedException ie) {}
}
// one more, at 100% selected color
colorizeSelections();
repaint();
}
// colorizeSelections() listing below
// AnimatedCellRenderer listing below
}