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}