2004-09-27

Alternating row colors in JTable

[An updated post Making JTable look more at home on Mac OS and GNOME is available, describing fixed bugs and added features.]

If you look at iTunes, the rows in the table are striped, and the stripes fill the whole table area even when there isn't enough data to fill the table.

If you look at a Java application on Mac OS, there are no row stripes in its tables.

You can add your own cell renderer to a JTable to get stripes. The cell renderer sets the row background based on the row number modulo 2. This is a well-known technique. The trouble with this implementation is that if the model doesn't contain enough rows to fill the table's area, there's no reason for the cell renderer to be called on the implied empty rows.

This means you get a big empty space at the bottom of the table, instead of having the alternating row pattern go all the way to the bottom. In fact, JTable doesn't even track the viewport height (though JList does), so there's no table there. That gap isn't the table painting background; it's the component behind painting it's background.

As usual, the UI delegate system makes it inconvenient to mess with the details of JTable painting, but overriding JTable.paint to paint the empty rows afterwards works out fine, and means we work whatever LAF is installed.

The only other thing we need to do is behave more like JList in tracking the viewport height (so we've actually got some space to paint into).

package e.gui;

import java.awt.*;
import javax.swing.*;
import javax.swing.table.*;
import e.util.*;

/**
* A better-looking table than JTable. In particular, on Mac OS this looks
* more like a Cocoa table than the default Aqua LAF manages.
*
* @author Elliott Hughes
*/
public class ETable extends JTable {
public ETable() {
}

/**
* Paints empty rows too, after letting the UI delegate do
* its painting.
*/
public void paint(Graphics g) {
super.paint(g);
paintEmptyRows(g);
}

/**
* Paints the backgrounds of the implied empty rows when the
* table model is insufficient to fill all the visible area
* available to us. We don't involve cell renderers, because
* we have no data.
*/
protected void paintEmptyRows(Graphics g) {
final int rowCount = getRowCount();
final Rectangle clip = g.getClipBounds();
if (rowCount * rowHeight < clip.height) {
for (int i = rowCount; i <= clip.height/rowHeight; ++i) {
g.setColor(colorForRow(i));
g.fillRect(clip.x, i * rowHeight, clip.width, rowHeight);
}
}
}

/**
* Changes the behavior of a table in a JScrollPane to be more like
* the behavior of JList, which expands to fill the available space.
* JTable normally restricts its size to just what's needed by its
* model.
*/
public boolean getScrollableTracksViewportHeight() {
if (getParent() instanceof JViewport) {
JViewport parent = (JViewport) getParent();
return (parent.getHeight() > getPreferredSize().height);
}
return false;
}

/**
* Returns the appropriate background color for the given row.
*/
protected Color colorForRow(int row) {
return (row % 2 == 0) ? GuiUtilities.ALTERNATE_ROW_COLOR : getBackground();
}

/**
* Shades alternate rows in different colors.
*/
public Component prepareRenderer(TableCellRenderer renderer, int row, int column) {
Component c = super.prepareRenderer(renderer, row, column);
if (isCellSelected(row, column) == false) {
c.setBackground(colorForRow(row));
c.setForeground(UIManager.getColor("Table.foreground"));
} else {
c.setBackground(UIManager.getColor("Table.selectionBackground"));
c.setForeground(UIManager.getColor("Table.selectionForeground"));
}
return c;
}
}

I wasn't looking for a solution that requires you to use a subclass of JTable; I'd have liked to be able to fix all JTables, but this will have to do for now.

I have an implementation of the equivalent feature for JList, too, but it's not as nice because of the support for variable-height rows in JList. I'm still trying to think if there's a better implementation.

Funnily enough, I thought JList would be the easy one!

2004-09-26

Darwen and Date on nulls

It's pretty common to hear arguments against null values in databases, but it's less common in my experience to hear about ways of getting rid of them. The trouble being that people do use nulls, and they use them to mean a variety of things, often a variety of things in the same context (such as both "unknown salary" and "no salary").

My argument against them would be that they're a bad smell because tests against null aren't intention-revealing.

A recent post on jikes-dev mentioned The Third Manifesto by Hugh Darwen and C.J. Date. I'd never heard of Darwen, but then everything you learn at university is 15 years out of date. (Despite the subject matter, and solely thanks to the brilliance of the lecturer in question, Derek Bridge, the database course at York was one of the best I took. If you're in the market for a university, you might want to try his. I learned things answering his exam questions, which isn't something I can say for many exams.)

Anyway, Darwen wrote a presentation How to Handle Missing Information without Using Nulls, and also a defense of the criticism that having a separate table for each meaning of null hides information in table names. The latter ends:

[We] don't propose to have a separate table for each meaning of Nulls. There's no such thing as a Null. We cannot entertain the idea of having a table for any meaning of something that doesn't exist! We would rather characterise [sic] our design (a trifle loosely) as involving a relvar for every distinct kind of statement [predicate] that we wish to be represented in the database.

So, to paraphrase their loose characterization, they insist on intention-revealing replacements for the hacks people are committing with nulls.

This brings to mind the most common argument against item 27 of "Effective Java", the item about returning a zero-length array instead of null, to avoid special-case code in the caller. The argument goes that there's a distinction between an empty array and null, something along the lines of the difference between an environment variable set to the empty string and an unset environment variable. Or, if you prefer, the difference between having a basket containing no apples and having no basket.

The counter-argument is exactly that made by Darwen and Date: that there's an extra predicate to be represented. In the case in the example above, that we should have boolean Environment.contains(String name) alongside String Environment.get(String name).

An alternative implementation is to return a maybe type (data Maybe a = Just a | Nothing), or – eschewing generics as one should – a more intention-revealing equivalent.

But somehow these are never very popular, because the code looks the same as it would if we just returned null: in each case there's the method invocation, the test, and the block to handle the failure case. The main difference is that these alternatives are slightly less idiomatic.

Sometimes, as is probably true for the environment variables case, we can get away with a getter method that takes a default value to return when there's no better answer. Only in languages like Ruby do we have a really convincing alternative: that of passing a block to handle the failure case directly.

You could throw an exception, but nobody likes exceptions. They're like return codes, only much messier to deal with because they make you pay (syntactically) even in the normal case. (This is partly tounge-in-cheek; there are cases where unchecked exceptions are the best solution, such as iterations that can fail, but this isn't one of them.)

And that's all I have to say. No simple answer like the database guys give you. Just a bunch of choices, none of which I'd suggest for all applications.

2004-09-25

"The Forgotten"

Julianne Moore follows Nicole Kidman's lead by starring alongside a 17" PowerBook. Other roles taken by a PowerMac with Cinema display, and an iBook.

But, yes, the main character is called "Telly", and her female friend is called "Eliot". Strange.

Nothing like as disturbing as Catherine Zeta Jones' accent on those cellphone ads. And what happened to her hair?

2004-09-23

Quick, call the SEC!

Trying a new online banking system today, I was impressed by their solid understanding of how to scare and frighten their users. If you go away from the browser window, do something else, and come back, you're told that you've been logged out due to inactivity or –they're not sure; how could they know? – "an illegal operation".

It's a good job they preface this with "Alert!" or it might not sound as serious as it obviously is.

"Illegal" is a word I'd definitely want to use if I were writing software for a bank. I suppose people writing software for hospitals don't talk about memory leaks; they have heap hemorrhages instead.

2004-09-20

JDC Tech Tips: don't try this at home, kids

Going through old mail, I came across Using GridBagLayout in a JDC Tech Tips mailing. The only reason I haven't unsubscribed from that list is that it's the modern equivalent of those PC-Lint adverts that would show you a few lines of C/C++ and ask "what's wrong with this code?"

The major difference being that the problems with the JDC Tech Tips example code are higher-level, and not so easily detected by static analysis.

The quality of code in the JDC Tech Tips is usually pretty low, as if no-one realizes or cares that it will be copy and pasted into projects the world over. The GridBagSample code, though, was impressively dense. Everything about it says "don't do this".

The best bit is that there's no clear way of fixing the immediate problems with the code (follow the link and look at it to see what I mean), because the whole approach is wrong. GridBagLayout? It might be okay for a computer, but it's not suitable for human consumption.

The other tip in the same mail, "Updating Jar Files", is also pretty ugly. The code isn't that bad, if you ignore all the exception handling. But you can't ignore the exception handling: there's so much of it. The real code is completely drowned out by it.

Making IOException a checked exception and offering finally clauses was not a good replacement for C++ destructors or Smalltalk blocks. No language since C does file handling with as little grace as Java. File handling is definitely Java's most monstrous carbuncle.

The question I have is this: wouldn't JDC Tech Tips be more useful if there were actually some technical tips, rather than short examples of using random bits of API – which would surely be better included in the JavaDoc itself, preferably with some new JavaDoc mechanism for actually extracting and running these examples (because nothing suffers from code rot like example code in comments) – and wouldn't it be a better idea to showcase good solutions to problems rather than just hack something together?

2004-09-12

Not answering the question

Reading the FAQ for harman/kardon's SoundSticks II, I was amused by their answer to "Why does the blue light stay on all the time?"

These speakers were designed to stay on all the time. There is no On/Off switch.

Fair enough. Who needs an on/off switch? But: why does the blue light stay on all the time? What does the blue light mean, beyond the fact that blue LEDs are now cheap?

I bought a Macally PowerBook power supply the other day (because I don't have an adapter to use my UK Apple one in the US, and because I was sick of the badly-made Apple connector coming apart because there's no grip on it and it takes quite a bit of force to plug/unplug), and it has a blue LED too. A very bright one. That's on all the time. Why would you do that?

If I wanted a night-light, I'd buy one.

I wonder if I can get one that also has a clock? I need another unsynchronized clock. Another remote would be good, too.

It's not just the software industry that's plagued by checklist features, you know.

I still can't decide what kind of speakers to get to replace the Denon kit I left in the UK. I really liked my Denon stuff (if you ignore the "HELLO" when you turn it on, the clock, and the slight crosstalk from the tuner), but I don't want to acquire any more stuff than absolutely necessary.

I can't spend the rest of my life wearing these iPod earphones, though.

2004-09-04

Handling selections when filtering a JList

I wrote code to provide iTunes-like filtering of a JList. The only interesting part (ignoring the UI, which I've mentioned already in A Cocoa-like search field for Java) was notifying listeners.

The premature optimizer might think they can get away with fireContentsChanged, but JList interprets that as meaning that the structure of the list hasn't changed. It accepts that it needs to redraw, but it won't realize that its selection model has been invalidated.

To fix this, I invoke fireIntervalRemoved for the interval [0, original model size) and then fireIntervalAdded for the interval [0, filtered model size). Which is okay, because you don't now have the wrong items selected, but it's not right, because you don't have any items selected.

iTunes does the right thing, so an item that was selected before filtering remains selected if it survives filtering. What I dislike about this is that you lose the ability to make the round trip: if you cancel the filter, you don't get the old selection back. You still just have the selected items that survived the filtering.

So while I wonder about what the right behavior is, and even wonder if there is a "right" behavior, I'm going to do nothing and wait until the current behavior becomes a problem in one of the applications I use FilteredListModel in.

If you want iTunes-like search functionality on a JList in your Swing application, and can live with the GPL, you can download the code as part of the oddly-named "salma-hayek" library. It doesn't yet have a web page of its own, but it's linked to from the page for SCM, for example.

JTable "coming soon", and JTree when I work out how to do it.

Blind programmers, user interface, and spam

I'm not blind, and I don't know anyone who is, so it's unusual that I should find myself thinking of the blind twice in one day.

Constructing user interfaces
The first thing was the realization that ways of constructing user interfaces without absolute positioning are even more important if you're a blind programmer. Automatic support for accessibility technologies is often listed as a reason to let the computer construct the UI from a high-level description, but I'd always seen that as an advantage to the end-user. Not something that would be significant to the developer, were they blind.

This (taken from Edit) is how I construct dialogs, not because of the various benefits to the end-users, or the automatic adjustments for other platforms, but simply because it's the easiest way for me to build a dialog. Worrying about the exact layout is the computer's concern. If I were blind, I'd be even more glad of this:

FormPanel formPanel = new FormPanel();
formPanel.addRow("Find:", patternField);
formPanel.addRow("Replace With:", replacementField);
formPanel.addRow("", statusLabel);
formPanel.addRow("Matches:", matchPane);
formPanel.addRow("Replacements:", replacementsPane);


Spam
Another thing I'd never thought of was how annoying spam must be to the blind. It's bad enough getting animated GIFs of animal pr0n and losing real mail because a brain-dead spam filter (MIMEsweeper) deletes more real mail than spam. But imagine having your text-to-speech system shouting spam titles across the room. And what do all those l33t-spelled words meant to defeat spam filters sound like anyway? I shudder to think what that much intra-word punctuation does to the computer's pronunciation.

How long until this becomes my most-read post because Google turns it up in searches for "animal pr0n"?

2004-09-03

A Cocoa-like search field for Java

If you've used a Mac, you'll have used NSSearchField, though you may not know it as such unless you're a Cocoa programmer. For those who can't guess, it's the little text field at the top of the iTunes window.

Having an always-visible search field to filter what you're looking at (often as-you-type) is a big part of what makes Mac OS applications feel like they're there to help, rather than get in your way (yes, Outlook, I'm talking about you, king of slow and awkward).

iTunes has one. Mail has one. Address Book has one. iCal has one.

You get the idea.

Even applications that don't offer a filterable view of some database use the same GUI component for their search. Safari, for example, has an NSSearchField next to the location bar that does a Google search. For those freaks who don't have Google as their home page, I guess.

Here are some of the most obvious features of NSSearchField:

  • Rounded corners to distinguish them from normal text fields.
  • Gray placeholder text (saying "Search" or "Google", say) when the field is empty and unfocused.
  • A cancel button when the field is non-empty.
  • Optional sending of notifications on each keystroke, rather than waiting for the user to hit Enter.
  • An optional menu of recent searches.
  • Optional automatic persistence of recent searches ("just add filename").


Some time ago, I wrote a JTextField subclass called SearchField that implemented the placeholder text. That was cunning, but easy: a FocusListener changed the foreground color and set the text to the placeholder string on losing the focus, and put the original string and color back when the focus returned. This is far simpler than trying to draw over the JTextArea by either subclassing or adding a Border. Sadly, those dead-ends were the first two ideas I had.

I had a go at the corners and cancel button, but gave up after getting nowhere. I'd planned on using BorderLayout to combine a JTextField and a JButton either side (one for the menu, one for the cancel button). Even pulling borders out of one component and giving them to another, I couldn't get this to look anything but stupid.

So for a while, I gave up and went away and did other things.

Then the other day I got sick of messing about with jar(1) and javap(1) on the command line whenever I wanted to poke about in a JAR file, and knocked up a quick front-end. And this was crying out for iTunes-like filtering on its list of JAR contents. (Filtering a JList is a story for another day.)

Anyway, this time I thought I'd see what I could do with a CompoundBorder on the JTextField. And it all just sort of came together. EmptyBorder is a good border to subclass, because it lets you say how much space you're going to need, but won't do any drawing. You can override paintBorder and do what you like. I conditionally draw a little gray circle with a white cross on it.

An anti-aliased circle, of course, because any other kind just doesn't look like a circle unless it's huge.

To put some behavior on my "button", I add a MouseInputAdapter (in two stages, because there's no addMouseInputListener, even though a custom component almost always needs both). The only tricky part there was remembering that mouseExited isn't invoked if a mouse button is down: you have to watch for the MouseEvent passed to mouseDragged going outside your component instead.

Getting notification on each keystroke is just a matter of adding a DocumentListener. This is made slightly more tricky by the placeholder text. [Update: I later switched to using a KeyListener so I could cancel if the user hit Esc. This implementation turns out to be nicer anyway, and slightly shorter too. As usual, the latest code is in the 'salma-hayek' nightly builds a few clicks away from my home page.]

Anyway, with the realization that I should get proper hit-testing sorted out for the "button", and that my experiences adding the recent searches menu may cause me to rethink the design so far, here's the current code:

package e.gui;

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.border.*;

/**
* A text field for search/filter interfaces. The extra functionality includes
* a placeholder string (when the user hasn't yet typed anything), and a button
* to clear the currently-entered text.
*
* @author Elliott Hughes
*/

//
// TODO: add a menu of recent searches.
// TODO: make recent searches persistent.
// TODO: use rounded corners, at least on Mac OS X.
//

public class SearchField extends JTextField {
private static final Border CANCEL_BORDER = new CancelBorder();

private boolean sendsNotificationForEachKeystroke = false;
private boolean showingPlaceholderText = false;
private boolean armed = false;

public SearchField(String placeholderText) {
super(15);
addFocusListener(new PlaceholderText(placeholderText));
initBorder();
initKeyListener();
}

public SearchField() {
this("Search");
}

private void initBorder() {
setBorder(new CompoundBorder(getBorder(), CANCEL_BORDER));

MouseInputListener mouseInputListener = new CancelListener();
addMouseListener(mouseInputListener);
addMouseMotionListener(mouseInputListener);
}

private void initKeyListener() {
addKeyListener(new KeyAdapter() {
public void keyReleased(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
cancel();
} else if (sendsNotificationForEachKeystroke) {
maybeNotify();
}
}
});
}

private void cancel() {
setText("");
postActionEvent();
}

private void maybeNotify() {
if (showingPlaceholderText) {
return;
}
postActionEvent();
}

public void setSendsNotificationForEachKeystroke(boolean eachKeystroke) {
this.sendsNotificationForEachKeystroke = eachKeystroke;
}

/**
* Draws the cancel button as a gray circle with a white cross inside.
*/
static class CancelBorder extends EmptyBorder {
private static final Color GRAY = new Color(0.7f, 0.7f, 0.7f);

CancelBorder() {
super(0, 0, 0, 15);
}

public void paintBorder(Component c, Graphics oldGraphics, int x, int y, int width, int height) {
SearchField field = (SearchField) c;
if (field.showingPlaceholderText || field.getText().length() == 0) {
return;
}

Graphics2D g = (Graphics2D) oldGraphics;
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

final int circleL = 14;
final int circleX = x + width - circleL;
final int circleY = y + (height - 1 - circleL)/2;
g.setColor(field.armed ? Color.GRAY : GRAY);
g.fillOval(circleX, circleY, circleL, circleL);

final int lineL = circleL - 8;
final int lineX = circleX + 4;
final int lineY = circleY + 4;
g.setColor(Color.WHITE);
g.drawLine(lineX, lineY, lineX + lineL, lineY + lineL);
g.drawLine(lineX, lineY + lineL, lineX + lineL, lineY);
}
}

/**
* Handles a click on the cancel button by clearing the text and notifying
* any ActionListeners.
*/
class CancelListener extends MouseInputAdapter {
private boolean isOverButton(MouseEvent e) {
// If the button is down, we might be outside the component
// without having had mouseExited invoked.
if (contains(e.getPoint()) == false) {
return false;
}

// In lieu of proper hit-testing for the circle, check that
// the mouse is somewhere in the border.
Rectangle innerArea = SwingUtilities.calculateInnerArea(SearchField.this, null);
return (innerArea.contains(e.getPoint()) == false);
}

public void mouseDragged(MouseEvent e) {
arm(e);
}

public void mouseEntered(MouseEvent e) {
arm(e);
}

public void mouseExited(MouseEvent e) {
disarm();
}

public void mousePressed(MouseEvent e) {
arm(e);
}

public void mouseReleased(MouseEvent e) {
if (armed) {
cancel();
}
disarm();
}

private void arm(MouseEvent e) {
armed = (isOverButton(e) && SwingUtilities.isLeftMouseButton(e));
repaint();
}

private void disarm() {
armed = false;
repaint();
}
}

/**
* Replaces the entered text with a gray placeholder string when the
* search field doesn't have the focus. The entered text returns when
* we get the focus back.
*/
class PlaceholderText implements FocusListener {
private String placeholderText;
private String previousText = "";
private Color previousColor;

PlaceholderText(String placeholderText) {
this.placeholderText = placeholderText;
focusLost(null);
}

public void focusGained(FocusEvent e) {
setForeground(previousColor);
setText(previousText);
showingPlaceholderText = false;
}

public void focusLost(FocusEvent e) {
previousText = getText();
previousColor = getForeground();
if (previousText.length() == 0) {
showingPlaceholderText = true;
setForeground(Color.GRAY);
setText(placeholderText);
}
}
}
}