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