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
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() {} } }
Interesting.
ReplyDelete