001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.widgets; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.util.ArrayList; 007import java.util.List; 008import java.util.Objects; 009import java.util.regex.PatternSyntaxException; 010 011import javax.swing.JTable; 012import javax.swing.RowFilter; 013import javax.swing.event.DocumentEvent; 014import javax.swing.event.DocumentListener; 015import javax.swing.table.AbstractTableModel; 016import javax.swing.table.TableModel; 017import javax.swing.table.TableRowSorter; 018 019import org.openstreetmap.josm.tools.Logging; 020import org.openstreetmap.josm.tools.Utils; 021 022/** 023 * Text field allowing to filter contents. 024 * @since 15116 025 */ 026public class FilterField extends JosmTextField { 027 028 /** 029 * Constructs a new {@code TableFilterField}. 030 */ 031 public FilterField() { 032 setToolTipText(tr("Enter a search expression")); 033 SelectAllOnFocusGainedDecorator.decorate(this); 034 } 035 036 /** 037 * Defines the filter behaviour. 038 */ 039 @FunctionalInterface 040 public interface FilterBehaviour { 041 /** 042 * Filters a component according to the given filter expression. 043 * @param expr filter expression 044 */ 045 void filter(String expr); 046 } 047 048 /** 049 * Enables filtering of given table/model. 050 * @param table table to filter 051 * @param model table model 052 * @return {@code this} for easy chaining 053 */ 054 public FilterField filter(JTable table, AbstractTableModel model) { 055 return filter(new TableFilterBehaviour(table, model)); 056 } 057 058 /** 059 * Enables generic filtering. 060 * @param behaviour filter behaviour 061 * @return {@code this} for easy chaining 062 */ 063 public FilterField filter(FilterBehaviour behaviour) { 064 getDocument().addDocumentListener(new FilterFieldAdapter(behaviour)); 065 return this; 066 } 067 068 private static class TableFilterBehaviour implements FilterBehaviour { 069 private final JTable table; 070 private final AbstractTableModel model; 071 072 TableFilterBehaviour(JTable table, AbstractTableModel model) { 073 this.table = Objects.requireNonNull(table, "table"); 074 this.model = Objects.requireNonNull(model, "model"); 075 Objects.requireNonNull(table.getRowSorter(), "table.rowSorter"); 076 } 077 078 @Override 079 public void filter(String expr) { 080 try { 081 final TableRowSorter<? extends TableModel> sorter = 082 (TableRowSorter<? extends TableModel>) table.getRowSorter(); 083 if (expr == null || expr.isEmpty()) { 084 sorter.setRowFilter(null); 085 } else { 086 expr = expr.replace("+", "\\+"); 087 // split search string on whitespace, do case-insensitive AND search 088 List<RowFilter<Object, Object>> andFilters = new ArrayList<>(); 089 for (String word : expr.split("\\s+")) { 090 andFilters.add(RowFilter.regexFilter("(?i)" + word)); 091 } 092 sorter.setRowFilter(RowFilter.andFilter(andFilters)); 093 } 094 model.fireTableDataChanged(); 095 } catch (PatternSyntaxException | ClassCastException ex) { 096 Logging.warn(ex); 097 } 098 } 099 } 100 101 private class FilterFieldAdapter implements DocumentListener { 102 private final FilterBehaviour behaviour; 103 104 FilterFieldAdapter(FilterBehaviour behaviour) { 105 this.behaviour = Objects.requireNonNull(behaviour); 106 } 107 108 private void filter() { 109 behaviour.filter(Utils.strip(getText())); 110 } 111 112 @Override 113 public void changedUpdate(DocumentEvent e) { 114 filter(); 115 } 116 117 @Override 118 public void insertUpdate(DocumentEvent e) { 119 filter(); 120 } 121 122 @Override 123 public void removeUpdate(DocumentEvent e) { 124 filter(); 125 } 126 } 127}