001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.dialogs;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.Component;
007import java.awt.Graphics2D;
008import java.awt.event.ActionEvent;
009import java.awt.event.KeyEvent;
010import java.awt.event.MouseEvent;
011import java.util.ArrayList;
012import java.util.Arrays;
013import java.util.List;
014
015import javax.swing.AbstractAction;
016import javax.swing.DefaultCellEditor;
017import javax.swing.JCheckBox;
018import javax.swing.JTable;
019import javax.swing.ListSelectionModel;
020import javax.swing.SwingUtilities;
021import javax.swing.table.DefaultTableCellRenderer;
022import javax.swing.table.JTableHeader;
023import javax.swing.table.TableCellRenderer;
024import javax.swing.table.TableColumnModel;
025import javax.swing.table.TableModel;
026
027import org.openstreetmap.josm.actions.mapmode.MapMode;
028import org.openstreetmap.josm.actions.search.SearchAction;
029import org.openstreetmap.josm.data.osm.Filter;
030import org.openstreetmap.josm.data.osm.FilterModel;
031import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent;
032import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent.DatasetEventType;
033import org.openstreetmap.josm.data.osm.event.DataChangedEvent;
034import org.openstreetmap.josm.data.osm.event.DataSetListener;
035import org.openstreetmap.josm.data.osm.event.DatasetEventManager;
036import org.openstreetmap.josm.data.osm.event.DatasetEventManager.FireMode;
037import org.openstreetmap.josm.data.osm.event.NodeMovedEvent;
038import org.openstreetmap.josm.data.osm.event.PrimitivesAddedEvent;
039import org.openstreetmap.josm.data.osm.event.PrimitivesRemovedEvent;
040import org.openstreetmap.josm.data.osm.event.RelationMembersChangedEvent;
041import org.openstreetmap.josm.data.osm.event.TagsChangedEvent;
042import org.openstreetmap.josm.data.osm.event.WayNodesChangedEvent;
043import org.openstreetmap.josm.data.osm.search.SearchSetting;
044import org.openstreetmap.josm.gui.MainApplication;
045import org.openstreetmap.josm.gui.MapFrame;
046import org.openstreetmap.josm.gui.MapFrame.MapModeChangeListener;
047import org.openstreetmap.josm.gui.SideButton;
048import org.openstreetmap.josm.gui.util.MultikeyActionsHandler;
049import org.openstreetmap.josm.gui.util.MultikeyShortcutAction;
050import org.openstreetmap.josm.gui.widgets.DisableShortcutsOnFocusGainedTextField;
051import org.openstreetmap.josm.tools.ImageProvider;
052import org.openstreetmap.josm.tools.InputMapUtils;
053import org.openstreetmap.josm.tools.Shortcut;
054
055/**
056 * The filter dialog displays a list of filters that are active on the current edit layer.
057 *
058 * @author Petr_DlouhĂ˝
059 */
060public class FilterDialog extends ToggleDialog implements DataSetListener, MapModeChangeListener {
061
062    private JTable userTable;
063    private final FilterTableModel filterModel = new FilterTableModel();
064
065    private final EnableFilterAction enableFilterAction;
066    private final HidingFilterAction hidingFilterAction;
067
068    /**
069     * Constructs a new {@code FilterDialog}
070     */
071    public FilterDialog() {
072        super(tr("Filter"), "filter", tr("Filter objects and hide/disable them."),
073                Shortcut.registerShortcut("subwindow:filter", tr("Toggle: {0}", tr("Filter")),
074                        KeyEvent.VK_F, Shortcut.ALT_SHIFT), 162);
075        build();
076        enableFilterAction = new EnableFilterAction();
077        hidingFilterAction = new HidingFilterAction();
078        MultikeyActionsHandler.getInstance().addAction(enableFilterAction);
079        MultikeyActionsHandler.getInstance().addAction(hidingFilterAction);
080    }
081
082    @Override
083    public void showNotify() {
084        DatasetEventManager.getInstance().addDatasetListener(this, FireMode.IN_EDT_CONSOLIDATED);
085        MapFrame.addMapModeChangeListener(this);
086        filterModel.executeFilters(true);
087    }
088
089    @Override
090    public void hideNotify() {
091        DatasetEventManager.getInstance().removeDatasetListener(this);
092        MapFrame.removeMapModeChangeListener(this);
093        filterModel.model.clearFilterFlags();
094        MainApplication.getLayerManager().invalidateEditLayer();
095    }
096
097    private static final Shortcut ENABLE_FILTER_SHORTCUT
098    = Shortcut.registerShortcut("core_multikey:enableFilter", tr("Multikey: {0}", tr("Enable filter")),
099            KeyEvent.VK_E, Shortcut.ALT_CTRL);
100
101    private static final Shortcut HIDING_FILTER_SHORTCUT
102    = Shortcut.registerShortcut("core_multikey:hidingFilter", tr("Multikey: {0}", tr("Hide filter")),
103            KeyEvent.VK_H, Shortcut.ALT_CTRL);
104
105    private static final String[] COLUMN_TOOLTIPS = {
106            Shortcut.makeTooltip(tr("Enable filter"), ENABLE_FILTER_SHORTCUT.getKeyStroke()),
107            Shortcut.makeTooltip(tr("Hiding filter"), HIDING_FILTER_SHORTCUT.getKeyStroke()),
108            null,
109            tr("Inverse filter"),
110            tr("Filter mode")
111    };
112
113    /**
114     * Builds the GUI.
115     */
116    protected void build() {
117        userTable = new UserTable(filterModel);
118
119        userTable.putClientProperty("terminateEditOnFocusLost", Boolean.TRUE);
120        userTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
121
122        userTable.getColumnModel().getColumn(0).setMaxWidth(1);
123        userTable.getColumnModel().getColumn(1).setMaxWidth(1);
124        userTable.getColumnModel().getColumn(3).setMaxWidth(1);
125        userTable.getColumnModel().getColumn(4).setMaxWidth(1);
126
127        userTable.getColumnModel().getColumn(0).setResizable(false);
128        userTable.getColumnModel().getColumn(1).setResizable(false);
129        userTable.getColumnModel().getColumn(3).setResizable(false);
130        userTable.getColumnModel().getColumn(4).setResizable(false);
131
132        userTable.setDefaultRenderer(Boolean.class, new BooleanRenderer());
133        userTable.setDefaultRenderer(String.class, new StringRenderer());
134        userTable.setDefaultEditor(String.class, new DefaultCellEditor(new DisableShortcutsOnFocusGainedTextField()));
135
136        SideButton addButton = new SideButton(new AbstractAction() {
137            {
138                putValue(NAME, tr("Add"));
139                putValue(SHORT_DESCRIPTION, tr("Add filter."));
140                new ImageProvider("dialogs", "add").getResource().attachImageIcon(this, true);
141            }
142
143            @Override
144            public void actionPerformed(ActionEvent e) {
145                SearchSetting searchSetting = SearchAction.showSearchDialog(new Filter());
146                if (searchSetting != null) {
147                    filterModel.addFilter(new Filter(searchSetting));
148                }
149            }
150        });
151        SideButton editButton = new SideButton(new AbstractAction() {
152            {
153                putValue(NAME, tr("Edit"));
154                putValue(SHORT_DESCRIPTION, tr("Edit filter."));
155                new ImageProvider("dialogs", "edit").getResource().attachImageIcon(this, true);
156            }
157
158            @Override
159            public void actionPerformed(ActionEvent e) {
160                int index = userTable.getSelectionModel().getMinSelectionIndex();
161                if (index < 0) return;
162                Filter f = filterModel.getFilter(index);
163                SearchSetting searchSetting = SearchAction.showSearchDialog(f);
164                if (searchSetting != null) {
165                    filterModel.setFilter(index, new Filter(searchSetting));
166                }
167            }
168        });
169        SideButton deleteButton = new SideButton(new AbstractAction() {
170            {
171                putValue(NAME, tr("Delete"));
172                putValue(SHORT_DESCRIPTION, tr("Delete filter."));
173                new ImageProvider("dialogs", "delete").getResource().attachImageIcon(this, true);
174            }
175
176            @Override
177            public void actionPerformed(ActionEvent e) {
178                int index = userTable.getSelectionModel().getMinSelectionIndex();
179                if (index >= 0) {
180                    filterModel.removeFilter(index);
181                }
182            }
183        });
184        SideButton upButton = new SideButton(new AbstractAction() {
185            {
186                putValue(NAME, tr("Up"));
187                putValue(SHORT_DESCRIPTION, tr("Move filter up."));
188                new ImageProvider("dialogs", "up").getResource().attachImageIcon(this, true);
189            }
190
191            @Override
192            public void actionPerformed(ActionEvent e) {
193                int index = userTable.getSelectionModel().getMinSelectionIndex();
194                if (index >= 0) {
195                    filterModel.moveUpFilter(index);
196                    userTable.getSelectionModel().setSelectionInterval(index-1, index-1);
197                }
198            }
199        });
200        SideButton downButton = new SideButton(new AbstractAction() {
201            {
202                putValue(NAME, tr("Down"));
203                putValue(SHORT_DESCRIPTION, tr("Move filter down."));
204                new ImageProvider("dialogs", "down").getResource().attachImageIcon(this, true);
205            }
206
207            @Override
208            public void actionPerformed(ActionEvent e) {
209                int index = userTable.getSelectionModel().getMinSelectionIndex();
210                if (index >= 0) {
211                    filterModel.moveDownFilter(index);
212                    userTable.getSelectionModel().setSelectionInterval(index+1, index+1);
213                }
214            }
215        });
216
217        // Toggle filter "enabled" on Enter
218        InputMapUtils.addEnterAction(userTable, new AbstractAction() {
219            @Override
220            public void actionPerformed(ActionEvent e) {
221                int index = userTable.getSelectedRow();
222                if (index >= 0) {
223                    Filter filter = filterModel.getFilter(index);
224                    filterModel.setValueAt(!filter.enable, index, FilterTableModel.COL_ENABLED);
225                }
226            }
227        });
228
229        // Toggle filter "hiding" on Spacebar
230        InputMapUtils.addSpacebarAction(userTable, new AbstractAction() {
231            @Override
232            public void actionPerformed(ActionEvent e) {
233                int index = userTable.getSelectedRow();
234                if (index >= 0) {
235                    Filter filter = filterModel.getFilter(index);
236                    filterModel.setValueAt(!filter.hiding, index, FilterTableModel.COL_HIDING);
237                }
238            }
239        });
240
241        createLayout(userTable, true, Arrays.asList(
242                addButton, editButton, deleteButton, upButton, downButton
243        ));
244    }
245
246    @Override
247    public void destroy() {
248        MultikeyActionsHandler.getInstance().removeAction(enableFilterAction);
249        MultikeyActionsHandler.getInstance().removeAction(hidingFilterAction);
250        super.destroy();
251    }
252
253    static final class UserTable extends JTable {
254        static final class UserTableHeader extends JTableHeader {
255            UserTableHeader(TableColumnModel cm) {
256                super(cm);
257            }
258
259            @Override
260            public String getToolTipText(MouseEvent e) {
261                int index = columnModel.getColumnIndexAtX(e.getPoint().x);
262                if (index == -1)
263                    return null;
264                int realIndex = columnModel.getColumn(index).getModelIndex();
265                return COLUMN_TOOLTIPS[realIndex];
266            }
267        }
268
269        UserTable(TableModel dm) {
270            super(dm);
271        }
272
273        @Override
274        protected JTableHeader createDefaultTableHeader() {
275            return new UserTableHeader(columnModel);
276        }
277    }
278
279    static class StringRenderer extends DefaultTableCellRenderer {
280        @Override
281        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
282            Component cell = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
283            TableModel model = table.getModel();
284            if (model instanceof FilterTableModel) {
285                cell.setEnabled(((FilterTableModel) model).isCellEnabled(row, column));
286            }
287            return cell;
288        }
289    }
290
291    static class BooleanRenderer extends JCheckBox implements TableCellRenderer {
292        @Override
293        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
294            FilterTableModel model = (FilterTableModel) table.getModel();
295            setSelected(value != null && (Boolean) value);
296            setEnabled(model.isCellEnabled(row, column));
297            setHorizontalAlignment(javax.swing.SwingConstants.CENTER);
298            return this;
299        }
300    }
301
302    /**
303     * Updates the headline of this dialog to display the number of active filters.
304     */
305    public void updateDialogHeader() {
306        SwingUtilities.invokeLater(() -> setTitle(
307                tr("Filter Hidden:{0} Disabled:{1}",
308                        filterModel.model.getDisabledAndHiddenCount(), filterModel.model.getDisabledCount())));
309    }
310
311    /**
312     * Draws a text on the map display that indicates that filters are active.
313     * @param g The graphics to draw that text on.
314     */
315    public void drawOSDText(Graphics2D g) {
316        filterModel.drawOSDText(g);
317    }
318
319    @Override
320    public void dataChanged(DataChangedEvent event) {
321        filterModel.executeFilters();
322    }
323
324    @Override
325    public void nodeMoved(NodeMovedEvent event) {
326        filterModel.executeFilters();
327    }
328
329    @Override
330    public void otherDatasetChange(AbstractDatasetChangedEvent event) {
331        if (DatasetEventType.FILTERS_CHANGED != event.getType()) {
332            filterModel.executeFilters();
333        }
334    }
335
336    @Override
337    public void primitivesAdded(PrimitivesAddedEvent event) {
338        filterModel.executeFilters(event.getPrimitives());
339    }
340
341    @Override
342    public void primitivesRemoved(PrimitivesRemovedEvent event) {
343        filterModel.executeFilters();
344    }
345
346    @Override
347    public void relationMembersChanged(RelationMembersChangedEvent event) {
348        filterModel.executeFilters(FilterModel.getAffectedPrimitives(event.getPrimitives()));
349    }
350
351    @Override
352    public void tagsChanged(TagsChangedEvent event) {
353        filterModel.executeFilters(FilterModel.getAffectedPrimitives(event.getPrimitives()));
354    }
355
356    @Override
357    public void wayNodesChanged(WayNodesChangedEvent event) {
358        filterModel.executeFilters(FilterModel.getAffectedPrimitives(event.getPrimitives()));
359    }
360
361    @Override
362    public void mapModeChange(MapMode oldMapMode, MapMode newMapMode) {
363        filterModel.executeFilters();
364    }
365
366    /**
367     * This method is intended for Plugins getting the filtermodel and using .addFilter() to
368     * add a new filter.
369     * @return the filtermodel
370     */
371    public FilterTableModel getFilterModel() {
372        return filterModel;
373    }
374
375    abstract class AbstractFilterAction extends AbstractAction implements MultikeyShortcutAction {
376
377        protected transient Filter lastFilter;
378
379        @Override
380        public void actionPerformed(ActionEvent e) {
381            throw new UnsupportedOperationException();
382        }
383
384        @Override
385        public List<MultikeyInfo> getMultikeyCombinations() {
386            List<MultikeyInfo> result = new ArrayList<>();
387
388            for (int i = 0; i < filterModel.getRowCount(); i++) {
389                Filter filter = filterModel.getFilter(i);
390                MultikeyInfo info = new MultikeyInfo(i, filter.text);
391                result.add(info);
392            }
393
394            return result;
395        }
396
397        protected final boolean isLastFilterValid() {
398            return lastFilter != null && filterModel.getFilters().contains(lastFilter);
399        }
400
401        @Override
402        public MultikeyInfo getLastMultikeyAction() {
403            if (isLastFilterValid())
404                return new MultikeyInfo(-1, lastFilter.text);
405            else
406                return null;
407        }
408    }
409
410    private class EnableFilterAction extends AbstractFilterAction {
411
412        EnableFilterAction() {
413            putValue(SHORT_DESCRIPTION, tr("Enable filter"));
414            ENABLE_FILTER_SHORTCUT.setAccelerator(this);
415        }
416
417        @Override
418        public Shortcut getMultikeyShortcut() {
419            return ENABLE_FILTER_SHORTCUT;
420        }
421
422        @Override
423        public void executeMultikeyAction(int index, boolean repeatLastAction) {
424            if (index >= 0 && index < filterModel.getRowCount()) {
425                Filter filter = filterModel.getFilter(index);
426                filterModel.setValueAt(!filter.enable, index, FilterTableModel.COL_ENABLED);
427                lastFilter = filter;
428            } else if (repeatLastAction && isLastFilterValid()) {
429                filterModel.setValueAt(!lastFilter.enable, filterModel.getFilters().indexOf(lastFilter), FilterTableModel.COL_ENABLED);
430            }
431        }
432    }
433
434    private class HidingFilterAction extends AbstractFilterAction {
435
436        HidingFilterAction() {
437            putValue(SHORT_DESCRIPTION, tr("Hiding filter"));
438            HIDING_FILTER_SHORTCUT.setAccelerator(this);
439        }
440
441        @Override
442        public Shortcut getMultikeyShortcut() {
443            return HIDING_FILTER_SHORTCUT;
444        }
445
446        @Override
447        public void executeMultikeyAction(int index, boolean repeatLastAction) {
448            if (index >= 0 && index < filterModel.getRowCount()) {
449                Filter filter = filterModel.getFilter(index);
450                filterModel.setValueAt(!filter.hiding, index, FilterTableModel.COL_HIDING);
451                lastFilter = filter;
452            } else if (repeatLastAction && isLastFilterValid()) {
453                filterModel.setValueAt(!lastFilter.hiding, filterModel.getFilters().indexOf(lastFilter), FilterTableModel.COL_HIDING);
454            }
455        }
456    }
457}