2006-09-03

Highlighting find matches in a JTextPane HTMLDocument

This is pretty obvious, but my first implementation was wrong, and my attempt to ask Google for a quick answer didn't work.

I was using JTextPane to display HTML, and I wanted to implement Firefox-like find functionality, where all the matches get highlighted. My preferred editor works like this, but it uses a custom text component (a kind of high-performance multi-color JTextArea). My favorite terminal emulator works like this too, but has a different kind of custom text component, suited to terminal emulation.

My first implementation simply invoked JTextPane.getText, found the matches, and added highlights at the offsets returned by the Matcher. The trouble was, these are offsets into the source HTML, but the Highlighter works in terms of offsets into the visible text.

What you need to do is get an HTMLDocument.Iterator that iterates over just the "content" text. You can use the offsets the iterator gives you to work out where the highlighter goes. The code looks like this:

Highlighter highlighter = textPane.getHighlighter();
HTMLDocument document = (HTMLDocument) textPane.getDocument();
for (HTMLDocument.Iterator it = document.getIterator(HTML.Tag.CONTENT);
it.isValid(); it.next()) {
try {
String fragment = document.getText(it.getStartOffset(),
it.getEndOffset() - it.getStartOffset());
Matcher matcher = pattern.matcher(fragment);
while (matcher.find()) {
highlighter.addHighlight(it.getStartOffset() + matcher.start(),
it.getStartOffset() + matcher.end(),
FIND_HIGHLIGHT_PAINTER);
++matchCount;
}
} catch (BadLocationException ex) {
}
}

If you need a find dialog and find/find previous/find next actions, you can find it all in salma-hayek, and just need one line in your code:

JTextComponentUtilities.addFindFunctionalityTo(textPane);





One other thing, while I'm on the subject of Highlighter... For some reason, most example code involves a useless subclass of DefaultHighlighter.DefaultHighlightPainter. Here's an example from the fount of all bad Java code, the Java Almanac:

// Don't do this! This is stupid!

// An instance of the private subclass of the default highlight painter
Highlighter.HighlightPainter myHighlightPainter = new MyHighlightPainter(Color.red);

// A private subclass of the default highlight painter
class MyHighlightPainter extends DefaultHighlighter.DefaultHighlightPainter {
public MyHighlightPainter(Color color) {
super(color);
}
}

All you need, as is obvious from the above, is something like:

Highlighter.HighlightPainter myHighlightPainter =
new DefaultHighlighter.DefaultHighlightPainter(Color.RED);

I've no idea why this cargo-cult nonsense persists. The Java Almanac version is a relatively mild form. I've seen more verbose variants, including cases where people subclass and replace the default Highlighter just to change the HighlightPainter, breaking selection highlighting for no obvious reason. Even when a custom HighlightPainter is necessary, a custom Highlighter probably isn't.

So if you come across that kind of thing and wonder what you're missing, don't worry: it's not you who's mad.