Monday, January 2, 2012

Java/Swing: JTable Filter Highlighting

When you have a large table of textual data it is often useful to have a way to filter it and since Java 6 this is possible without third-party libraries using the RowFilter API. But when you search through multiple columns it is often not immediately apparent which cell contains the match.

One obvious solution to this problem is to highlight the matching text fragments. In principle this can easily be achieved by implementing a custom cell renderer that is aware of the filter string. Following the implementation of the DefaultCellRenderer we would just need a label where you can specify individual background colors for arbitrary substrings which you could then use as a basis for your custom cell renderer.

Fortunately JIDE already did the hard work for us with their StyledLabel implementation which is part of the open sourced JIDE Common Layer. With StyledLabel as a base for our custom cell renderer the implementation becomes rather trivial.

So to make it more interesting here are the requirements for our example:
  • must match in arbitrary user-specified columns
  • the filter string must be split into multiple terms at whitespace boundaries and a row must only match if each of those terms is a substring of at least one cell
  • the substring matching must be case-insensitive
  • the renderer must support alternating colors for different terms but must highlight same terms with same colors
  • the colors used for highlighting must be customizable
Seems we've got our work cut out for us so let's do it.

First of all we need a simple table:
static JTable createTable() {
    Object[][] data = new Object[][] {
        { "foo bar baz", "lorem ipsum dolor", "quux fizz buzz" },
        { "java scala clojure", "lisp haskell ml", "ruby python php" },
        { "blue green red", "orange yellow magenta", "pink cyan black" }
    };
    JTable tbl = new JTable(data, new Object[] {"1", "2", "3"});
    tbl.setAutoCreateRowSorter(true);
    return tbl;
}
Then we create a simple frame to test-drive our table:
public static void main(String[] args) throws Exception {
    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());

    final Integer[] cols = new Integer[] { 0, 1, 2 };
    final JTable tbl = createTable();
    final TableRowSorter<TableModel> sort =
        (TableRowSorter<TableModel>)tbl.getRowSorter();
    final JTextField filter = new JTextField();
    filter.addKeyListener(new KeyAdapter() {
        @Override public void keyReleased(KeyEvent e) {
            String text = filter.getText().trim();
            if (text.length() > 0) {
                final List<String> tokens = split(text.toLowerCase());
                sort.setRowFilter(makeRowFilter(tokens, cols));
            } else {
                sort.setRowFilter(null);
            }
        }
    });

    JScrollPane scroll = new JScrollPane(tbl);
    scroll.setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0,
        UIManager.getColor("Panel.background").darker()));
    scroll.setVerticalScrollBarPolicy(
        JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);

    final JPanel content = new JPanel(new MigLayout("fill, ins 0"));
    content.add(new JLabel("Filter:"), "gapleft 4");
    content.add(filter, "wrap, growx, pushx, gap 4 4 4");
    content.add(scroll, "grow, pushy, span");

    final JFrame f = new JFrame("JTable Filter Highlighting");
    f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    f.setContentPane(content);
    f.setSize(400, 300);
    f.setLocationRelativeTo(null);
    f.setVisible(true);
}

For the above to compile we also need to declare the makeRowFilter method:
static RowFilter<TableModel,Integer>
makeRowFilter(final List<String> tokens, final Integer... cols) {
    return new RowFilter<TableModel,Integer>() {
        @Override public boolean
        include(Entry<? extends TableModel,? extends Integer> en) {
            for (String tok : tokens) {
                for (int i = 0; i < cols.length; ++i) {
                    String val = en.getStringValue(cols[i]).toLowerCase();
                    if (val.contains(tok)) {
                        break;
                    } else if (i == cols.length - 1) {
                        return false;
                    }
                }
            }
            return true;
        }
    };
}
So now we have a table with all the filtering features that we specified. The only thing missing is the highlighting. As I already said, this is trivial to implement on top of StyledLabel. Here's the complete renderer implementation:
static class HighlightCellRenderer
    extends StyledLabel implements TableCellRenderer {
    private List<String> terms = new ArrayList<String>();
    private Color[] colors = new Color[] {
        Color.yellow, Color.pink, Color.cyan };
    private final List<Integer> cols;
    public HighlightCellRenderer(List<Integer> cols) {
        this.cols = cols;
        setBorder(new EmptyBorder(1, 1, 1, 1));
        setOpaque(true);
    }
    public void setTerms(List<String> terms) {
        this.terms = terms;
    }
    public void clearTerms() {
        terms.clear();
    }
    public void setColors(Color... colors) {
        this.colors = colors;
    }
    public Component getTableCellRendererComponent(JTable tbl, Object val,
        boolean sel, boolean hasFocus, int row, int col) {
        String text = val != null ? val.toString() : "";
        setText(text);
        clearStyleRanges();
        final Color fg = sel ?
            tbl.getSelectionForeground() : tbl.getForeground();
        final Color bg = sel ?
            tbl.getSelectionBackground() : tbl.getBackground();
        setForeground(fg);
        setBackground(bg);
        if (cols.contains(col)) {
            String textLc = text.toLowerCase();
            for (int i = 0; i < terms.size(); ++i) {
                String term = terms.get(i);
                int offset = textLc.indexOf(term);
                while (offset != -1) {
                    addStyleRange(new StyleRange(offset, term.length(),
                        getFont().getStyle(), sel ? bg : getForeground(),
                        sel ? fg : colors[i % colors.length], 0));
                    offset = textLc.indexOf(term, offset + 1);
                }
            }
        }

        configureBorder(tbl, sel, hasFocus, row, col);

        return this;
    }

    private void configureBorder(
        JTable tbl, boolean sel, boolean hasFocus,int row, int col) {
        // copied from DefaultTableCellRenderer
        if (hasFocus) {
            Border border = null;
            if (sel) {
                border = DefaultLookup.getBorder(
                    this, ui, "Table.focusSelectedCellHighlightBorder");
            }
            if (border == null) {
                border = DefaultLookup.getBorder(
                    this, ui, "Table.focusCellHighlightBorder");
            }
            setBorder(border);
            if (!sel && tbl.isCellEditable(row, col)) {
                Color color;
                color = DefaultLookup.getColor(
                    this, ui, "Table.focusCellForeground");
                if (color != null) {
                    super.setForeground(color);
                }
                color = DefaultLookup.getColor(
                    this, ui, "Table.focusCellBackground");
                if (color != null) {
                    super.setBackground(color);
                }
            }
        } else {
            setBorder(getNoFocusBorder());
        }
    }

    // copied from DefaultTableCellRenderer
    static final Border SAFE_NO_FOCUS_BORDER = new EmptyBorder(1, 1, 1, 1);
    static final Border DEFAULT_NO_FOCUS_BORDER = new EmptyBorder(1, 1, 1, 1);
    protected static Border noFocusBorder = DEFAULT_NO_FOCUS_BORDER;
    private Border getNoFocusBorder() {
        Border border = DefaultLookup.getBorder(
            this, ui, "Table.cellNoFocusBorder");
        if (System.getSecurityManager() != null) {
            if (border != null) return border;
            return SAFE_NO_FOCUS_BORDER;
        } else if (border != null) {
            if (noFocusBorder == null
                || noFocusBorder == DEFAULT_NO_FOCUS_BORDER) {
                return border;
            }
        }
        return noFocusBorder;
    }

    // overridden for performance reasons
    @Override public void invalidate() {}
    @Override public void validate() {}
    @Override public void revalidate() {}
    @Override public void repaint(long tm, int x, int y, int w, int h) {}
    @Override public void repaint(Rectangle r) { }
    @Override public void repaint() {}
}
To use it we just need to change a few lines in our test code:
final HighlightCellRenderer renderer = new HighlightCellRenderer(
    Arrays.asList(cols));
tbl.setDefaultRenderer(Object.class, renderer);
final JTextField filter = new JTextField();
filter.addKeyListener(new KeyAdapter() {
    @Override public void keyReleased(KeyEvent e) {
        String text = filter.getText().trim();
        if (text.length() > 0) {
            final List<String> tokens = split(text.toLowerCase());
            sort.setRowFilter(makeRowFilter(tokens, cols));
            renderer.setTerms(tokens);
        } else {
            sort.setRowFilter(null);
            renderer.clearTerms();
        }
    }
});

And that's it:
































Here's the complete code for this example (coded against com.jidesoft:jide-oss:2.10.2 and com.miglayout:miglayout-swing:4.1):
package sendtehcodez;

import java.awt.Color;
import java.awt.Component;
import java.awt.Rectangle;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.StringTokenizer;

import javax.swing.BorderFactory;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.RowFilter;
import javax.swing.UIManager;
import javax.swing.border.Border;
import javax.swing.border.EmptyBorder;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableModel;
import javax.swing.table.TableRowSorter;

import net.miginfocom.swing.MigLayout;
import sun.swing.DefaultLookup;

import com.jidesoft.swing.StyleRange;
import com.jidesoft.swing.StyledLabel;

public class JTableFilterHighlighting {
    public static void main(String[] args) throws Exception {
        UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());

        final Integer[] cols = new Integer[] { 0, 1, 2 };
        final JTable tbl = createTable();
        final TableRowSorter<TableModel> sort =
            (TableRowSorter<TableModel>)tbl.getRowSorter();
        final HighlightCellRenderer renderer = new HighlightCellRenderer(
            Arrays.asList(cols));
        tbl.setDefaultRenderer(Object.class, renderer);
        final JTextField filter = new JTextField();
        filter.addKeyListener(new KeyAdapter() {
            @Override public void keyReleased(KeyEvent e) {
                String text = filter.getText().trim();
                if (text.length() > 0) {
                    final List<String> tokens = split(text.toLowerCase());
                    sort.setRowFilter(makeRowFilter(tokens, cols));
                    renderer.setTerms(tokens);
                } else {
                    sort.setRowFilter(null);
                    renderer.clearTerms();
                }
            }
        });

        JScrollPane scroll = new JScrollPane(tbl);
        scroll.setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0,
            UIManager.getColor("Panel.background").darker()));
        scroll.setVerticalScrollBarPolicy(
            JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);

        final JPanel content = new JPanel(new MigLayout("fill, ins 0"));
        content.add(new JLabel("Filter:"), "gapleft 4");
        content.add(filter, "wrap, growx, pushx, gap 4 4 4");
        content.add(scroll, "grow, pushy, span");

        final JFrame f = new JFrame("JTable Filter Highlighting");
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.setContentPane(content);
        f.setSize(400, 300);
        f.setLocationRelativeTo(null);
        f.setVisible(true);
    }

    static RowFilter<TableModel,Integer>
    makeRowFilter(final List<String> tokens, final Integer... cols) {
        return new RowFilter<TableModel,Integer>() {
            @Override public boolean
            include(Entry<? extends TableModel,? extends Integer> en) {
                for (String tok : tokens) {
                    for (int i = 0; i < cols.length; ++i) {
                        String val = en.getStringValue(cols[i]).toLowerCase();
                        if (val.contains(tok)) {
                            break;
                        } else if (i == cols.length - 1) {
                            return false;
                        }
                    }
                }
                return true;
            }
        };
    }

    static List<String> split(String str) {
        List<String> tokens = new ArrayList<String>();
        StringTokenizer tok = new StringTokenizer(str);
        while (tok.hasMoreTokens()) {
            String token = tok.nextToken().trim();
            if (token.length() > 0) {
                tokens.add(token);
            }
        }
        return tokens;
    }

    static JTable createTable() {
        Object[][] data = new Object[][] {
            { "foo bar baz", "lorem ipsum dolor", "quux fizz buzz" },
            { "java scala clojure", "lisp haskell ml", "ruby python php" },
            { "blue green red", "orange yellow magenta", "pink cyan black" }
        };
        JTable tbl = new JTable(data, new Object[] {"1", "2", "3"});
        tbl.setAutoCreateRowSorter(true);
        return tbl;
    }

    static class HighlightCellRenderer
        extends StyledLabel implements TableCellRenderer {
        private List<String> terms = new ArrayList<String>();
        private Color[] colors = new Color[] {
            Color.yellow, Color.pink, Color.cyan };
        private final List<Integer> cols;
        public HighlightCellRenderer(List<Integer> cols) {
            this.cols = cols;
            setBorder(new EmptyBorder(1, 1, 1, 1));
            setOpaque(true);
        }
        public void setTerms(List<String> terms) {
            this.terms = terms;
        }
        public void clearTerms() {
            terms.clear();
        }
        public void setColors(Color... colors) {
            this.colors = colors;
        }
        public Component getTableCellRendererComponent(JTable tbl, Object val,
            boolean sel, boolean hasFocus, int row, int col) {
            String text = val != null ? val.toString() : "";
            setText(text);
            clearStyleRanges();
            final Color fg = sel ?
                tbl.getSelectionForeground() : tbl.getForeground();
            final Color bg = sel ?
                tbl.getSelectionBackground() : tbl.getBackground();
            setForeground(fg);
            setBackground(bg);
            if (cols.contains(col)) {
                String textLc = text.toLowerCase();
                for (int i = 0; i < terms.size(); ++i) {
                    String term = terms.get(i);
                    int offset = textLc.indexOf(term);
                    while (offset != -1) {
                        addStyleRange(new StyleRange(offset, term.length(),
                            getFont().getStyle(), sel ? bg : getForeground(),
                            sel ? fg : colors[i % colors.length], 0));
                        offset = textLc.indexOf(term, offset + 1);
                    }
                }
            }

            configureBorder(tbl, sel, hasFocus, row, col);

            return this;
        }

        private void configureBorder(
            JTable tbl, boolean sel, boolean hasFocus,int row, int col) {
            // copied from DefaultTableCellRenderer
            if (hasFocus) {
                Border border = null;
                if (sel) {
                    border = DefaultLookup.getBorder(
                        this, ui, "Table.focusSelectedCellHighlightBorder");
                }
                if (border == null) {
                    border = DefaultLookup.getBorder(
                        this, ui, "Table.focusCellHighlightBorder");
                }
                setBorder(border);
                if (!sel && tbl.isCellEditable(row, col)) {
                    Color color;
                    color = DefaultLookup.getColor(
                        this, ui, "Table.focusCellForeground");
                    if (color != null) {
                        super.setForeground(color);
                    }
                    color = DefaultLookup.getColor(
                        this, ui, "Table.focusCellBackground");
                    if (color != null) {
                        super.setBackground(color);
                    }
                }
            } else {
                setBorder(getNoFocusBorder());
            }
        }

        // copied from DefaultTableCellRenderer
        static final Border SAFE_NO_FOCUS_BORDER = new EmptyBorder(1, 1, 1, 1);
        static final Border DEFAULT_NO_FOCUS_BORDER = new EmptyBorder(1, 1, 1, 1);
        protected static Border noFocusBorder = DEFAULT_NO_FOCUS_BORDER;
        private Border getNoFocusBorder() {
            Border border = DefaultLookup.getBorder(
                this, ui, "Table.cellNoFocusBorder");
            if (System.getSecurityManager() != null) {
                if (border != null) return border;
                return SAFE_NO_FOCUS_BORDER;
            } else if (border != null) {
                if (noFocusBorder == null
                    || noFocusBorder == DEFAULT_NO_FOCUS_BORDER) {
                    return border;
                }
            }
            return noFocusBorder;
        }

        // overridden for performance reasons
        @Override public void invalidate() {}
        @Override public void validate() {}
        @Override public void revalidate() {}
        @Override public void repaint(long tm, int x, int y, int w, int h) {}
        @Override public void repaint(Rectangle r) { }
        @Override public void repaint() {}
    }
}

Saturday, December 24, 2011

Java/SwingX: Better UX for Tables with Hyperlinks

In my first post I want to show you how you can improve the user experience of JXHyperlink's in JXTable's. Both of those classes are part of SwingX which, although apparently dead, is still a very useful collection of Swing components that should really be in Swing itself. Out of the box you can use the mentioned classes to display hyperlinks in tables with the HyperlinkProvider and the DefaultTableRenderer implementation with just a few lines of code:
JXTable table = new JXTable(model);
table.setEditable(false);
AbstractHyperlinkAction<URI> act = new AbstractHyperlinkAction<URI>() {
    public void actionPerformed(ActionEvent ev) {
        try {
            Desktop.getDesktop().browse(target);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
};
table.setDefaultRenderer(URI.class,
    new DefaultTableRenderer(new HyperlinkProvider(act)));

This is nice but if you hover with your mouse over a cell which contains a hyperlink it will be rendered in rollover state even if the mouse is not really above the link text. That's because the rollover support in SwingX works on a cell basis. This also has the unfortunate effect of triggering the associated link action as soon as you click inside the cell even if you didn't click on the link itself. This is counterintuitive because that's not how links usually work. Fortunately we can modify this behavior by overriding the relevant parts in HyperlinkProvider and JXTable.

First of all we need a way to determine if the mouse is hovering above the actual text of a cell or not. For this purpose we implement the utility function isRolloverText:
static private boolean isRolloverText(JXTable tbl, int row, int col) {
    int mCol = tbl.convertColumnIndexToModel(col);
    int mRow = tbl.convertRowIndexToModel(row);
    if (tbl.getColumnClass(col) != URI.class) {
        return false;
    }
    Point pos = tbl.getMousePosition();
    if (pos == null) {
        return false;
    }
    AbstractRenderer ren = (AbstractRenderer)tbl.getCellRenderer(row, col);
    Object value = tbl.getModel().getValueAt(mRow, mCol);
    ComponentProvider<? extends JComponent> prov = ren
        .getComponentProvider();
    String text = prov.getString(value);
    if (text.length() == 0) {
        return false;
    }
    JComponent com = prov.getRendererComponent(null);
    int textWidth = com.getFontMetrics(com.getFont())
        .stringWidth(text);
    Rectangle cellBounds = tbl.getCellRect(row, col, true);
    Rectangle textBounds = new Rectangle(
        cellBounds.x, cellBounds.y, textWidth, cellBounds.height);
    return textBounds.contains(pos);
}
Note that this implementation assumes that the whole cell is used for rendering the text. For example, if you customize the hyperlink cell renderer to render an icon on the left side of the text this code wouldn't work properly anymore.

Now that we have a way to tell if the mouse is above the cell text we can override the HyperlinkProvider to only set the rollover state if the cursor is really above the text. In this example I just copied the implementation from the superclass and added the highlighted line:
@Override protected void configureState(CellContext context) {
    if (context.getComponent() == null) {
        return;
    }
    Point p = (Point) context.getComponent()
        .getClientProperty(RolloverProducer.ROLLOVER_KEY);
    if (p != null && (p.x >= 0)
        && (p.x == context.getColumn()) && (p.y == context.getRow())
        && isRolloverText((JXTable)context.getComponent(), p.y, p.x)
        && !rendererComponent.getModel().isRollover()) {
        rendererComponent.getModel().setRollover(true);
    }
    else if (rendererComponent.getModel().isRollover()) {
        rendererComponent.getModel().setRollover(false);
    }
}
Then we also need to modify the JXTable's rollover support:
@Override protected RolloverProducer createRolloverProducer() {
    return new TableRolloverProducer() {
        @Override public void mouseMoved(MouseEvent ev) {
            updateRollover(ev, ROLLOVER_KEY, true);
        }
    };
}
@Override protected TableRolloverController<JXTable>
createLinkController() {
    return new TableRolloverController<JXTable>() {
        @Override protected RolloverRenderer getRolloverRenderer(
            Point loc, boolean prep) {
            if (getColumnClass(loc.x) == URI.class
                && !isRolloverText(component, loc.y, loc.x)) {
                return null;
            }
            return super.getRolloverRenderer(loc, prep);
        }
    };
}
And that's it:





Here's the complete example (coded against org.swinglabs:swingx-core:1.6.2-2):
package sendtehcodez;

import java.awt.Desktop;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.MouseEvent;
import java.net.URI;
import java.net.URISyntaxException;

import javax.swing.BorderFactory;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.UIManager;
import javax.swing.table.DefaultTableModel;

import org.jdesktop.swingx.JXTable;
import org.jdesktop.swingx.hyperlink.AbstractHyperlinkAction;
import org.jdesktop.swingx.renderer.AbstractRenderer;
import org.jdesktop.swingx.renderer.CellContext;
import org.jdesktop.swingx.renderer.ComponentProvider;
import org.jdesktop.swingx.renderer.DefaultTableRenderer;
import org.jdesktop.swingx.renderer.HyperlinkProvider;
import org.jdesktop.swingx.rollover.RolloverProducer;
import org.jdesktop.swingx.rollover.RolloverRenderer;
import org.jdesktop.swingx.rollover.TableRolloverController;
import org.jdesktop.swingx.rollover.TableRolloverProducer;

public class TablesWithHyperlinks {
    public static void main(String[] args) throws Exception {
        UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());

        JScrollPane scroll = new JScrollPane(createTable());
        scroll.setBorder(BorderFactory.createEmptyBorder());

        final JFrame f = new JFrame("Tables with Hyperlinks");
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.getContentPane().add(scroll);
        f.setSize(400, 300);
        f.setLocationRelativeTo(null);
        f.setVisible(true);
    }

    static private boolean isRolloverText(JXTable tbl, int row, int col) {
        int mCol = tbl.convertColumnIndexToModel(col);
        int mRow = tbl.convertRowIndexToModel(row);
        if (tbl.getColumnClass(col) != URI.class) {
            return false;
        }
        Point pos = tbl.getMousePosition();
        if (pos == null) {
            return false;
        }
        AbstractRenderer ren = (AbstractRenderer)tbl.getCellRenderer(row, col);
        Object value = tbl.getModel().getValueAt(mRow, mCol);
        ComponentProvider<? extends JComponent> prov = ren
            .getComponentProvider();
        String text = prov.getString(value);
        if (text.length() == 0) {
            return false;
        }
        JComponent com = prov.getRendererComponent(null);
        int textWidth = com.getFontMetrics(com.getFont())
            .stringWidth(text);
        Rectangle cellBounds = tbl.getCellRect(row, col, true);
        Rectangle textBounds = new Rectangle(
            cellBounds.x, cellBounds.y, textWidth, cellBounds.height);
        return textBounds.contains(pos);
    }

    static JXTable createTable() throws URISyntaxException {
        Object[][] data = new Object[][] {
            { new URI("http://java.oracle.com"), "Java" },
            { new URI("http://clojure.org"), "Clojure" },
            { new URI("http://haskell.org"), "Haskell" },
            { new URI("http://erlang.org"), "Erlang" },
            { new URI("http://python.org"), "Python" },
            { new URI("http://ruby-lang.org"), "Ruby" }
        };
        DefaultTableModel model = new DefaultTableModel(data,
            new Object[] { "URI", "Language" }) {
            @Override public Class<?> getColumnClass(int col) {
                if (col == 0) {
                    return URI.class;
                }
                return super.getColumnClass(col);
            }
        };
        JXTable table = new JXTable(model) {
            @Override protected RolloverProducer createRolloverProducer() {
                return new TableRolloverProducer() {
                    @Override public void mouseMoved(MouseEvent ev) {
                        updateRollover(ev, ROLLOVER_KEY, true);
                    }
                };
            }
            @Override protected TableRolloverController<JXTable>
            createLinkController() {
                return new TableRolloverController<JXTable>() {
                    @Override protected RolloverRenderer getRolloverRenderer(
                        Point loc, boolean prep) {
                        if (getColumnClass(loc.x) == URI.class
                            && !isRolloverText(component, loc.y, loc.x)) {
                            return null;
                        }
                        return super.getRolloverRenderer(loc, prep);
                    }
                };
            }
        };

        AbstractHyperlinkAction<URI> act = new AbstractHyperlinkAction<URI>() {
            public void actionPerformed(ActionEvent ev) {
                try {
                    Desktop.getDesktop().browse(target);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        };
        HyperlinkProvider hp = new HyperlinkProvider(act) {
            @Override protected void configureState(CellContext context) {
                if (context.getComponent() == null) {
                    return;
                }
                Point p = (Point) context.getComponent()
                    .getClientProperty(RolloverProducer.ROLLOVER_KEY);
                if (p != null && (p.x >= 0)
                    && (p.x == context.getColumn()) && (p.y == context.getRow())
                    && isRolloverText((JXTable)context.getComponent(), p.y, p.x)
                    && !rendererComponent.getModel().isRollover()) {
                    rendererComponent.getModel().setRollover(true);
                }
                else if (rendererComponent.getModel().isRollover()) {
                    rendererComponent.getModel().setRollover(false);
                }
            }
        };
        table.setDefaultRenderer(URI.class, new DefaultTableRenderer(hp));
        table.setEditable(false);
        table.setColumnControlVisible(true);

        return table;
    }
}

About

I'm some random guy screwing around mostly with Java nowadays and sometimes C or C++. I used to work a lot with PHP but I don't really use it anymore. I'm madly in love with functional programming and Lisp in general and Clojure1 in particular. Also I think Haskell is the bomb and the most awesomest thing since sliced bread :-) 

I want to use this blog to collect cookbook-style code snippets that I needed at some point which I think could be useful to other people.
At the time of this writing I'm currently developing a Java/Swing application so my first posts will be about stuff I needed to do in that application that I think could be useful for someone else. We'll see how it goes from there.

1 Rich Hickey is my hero :-)