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