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}