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