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.Main;
028import org.openstreetmap.josm.actions.mapmode.MapMode;
029import org.openstreetmap.josm.actions.search.SearchAction;
030import org.openstreetmap.josm.data.osm.Filter;
031import org.openstreetmap.josm.data.osm.FilterModel;
032import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent;
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();
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            Main.platform.makeTooltip(tr("Enable filter"), ENABLE_FILTER_SHORTCUT),
106            Main.platform.makeTooltip(tr("Hiding filter"), HIDING_FILTER_SHORTCUT),
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            FilterTableModel model = (FilterTableModel) table.getModel();
280            Component cell = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
281            cell.setEnabled(model.isCellEnabled(row, column));
282            return cell;
283        }
284    }
285
286    static class BooleanRenderer extends JCheckBox implements TableCellRenderer {
287        @Override
288        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
289            FilterTableModel model = (FilterTableModel) table.getModel();
290            setSelected(value != null && (Boolean) value);
291            setEnabled(model.isCellEnabled(row, column));
292            setHorizontalAlignment(javax.swing.SwingConstants.CENTER);
293            return this;
294        }
295    }
296
297    /**
298     * Updates the headline of this dialog to display the number of active filters.
299     */
300    public void updateDialogHeader() {
301        SwingUtilities.invokeLater(() -> setTitle(
302                tr("Filter Hidden:{0} Disabled:{1}",
303                        filterModel.model.getDisabledAndHiddenCount(), filterModel.model.getDisabledCount())));
304    }
305
306    /**
307     * Draws a text on the map display that indicates that filters are active.
308     * @param g The graphics to draw that text on.
309     */
310    public void drawOSDText(Graphics2D g) {
311        filterModel.drawOSDText(g);
312    }
313
314    @Override
315    public void dataChanged(DataChangedEvent event) {
316        filterModel.executeFilters();
317    }
318
319    @Override
320    public void nodeMoved(NodeMovedEvent event) {
321        filterModel.executeFilters();
322    }
323
324    @Override
325    public void otherDatasetChange(AbstractDatasetChangedEvent event) {
326        filterModel.executeFilters();
327    }
328
329    @Override
330    public void primitivesAdded(PrimitivesAddedEvent event) {
331        filterModel.executeFilters(event.getPrimitives());
332    }
333
334    @Override
335    public void primitivesRemoved(PrimitivesRemovedEvent event) {
336        filterModel.executeFilters();
337    }
338
339    @Override
340    public void relationMembersChanged(RelationMembersChangedEvent event) {
341        filterModel.executeFilters(FilterModel.getAffectedPrimitives(event.getPrimitives()));
342    }
343
344    @Override
345    public void tagsChanged(TagsChangedEvent event) {
346        filterModel.executeFilters(FilterModel.getAffectedPrimitives(event.getPrimitives()));
347    }
348
349    @Override
350    public void wayNodesChanged(WayNodesChangedEvent event) {
351        filterModel.executeFilters(FilterModel.getAffectedPrimitives(event.getPrimitives()));
352    }
353
354    @Override
355    public void mapModeChange(MapMode oldMapMode, MapMode newMapMode) {
356        filterModel.executeFilters();
357    }
358
359    /**
360     * This method is intended for Plugins getting the filtermodel and using .addFilter() to
361     * add a new filter.
362     * @return the filtermodel
363     */
364    public FilterTableModel getFilterModel() {
365        return filterModel;
366    }
367
368    abstract class AbstractFilterAction extends AbstractAction implements MultikeyShortcutAction {
369
370        protected transient Filter lastFilter;
371
372        @Override
373        public void actionPerformed(ActionEvent e) {
374            throw new UnsupportedOperationException();
375        }
376
377        @Override
378        public List<MultikeyInfo> getMultikeyCombinations() {
379            List<MultikeyInfo> result = new ArrayList<>();
380
381            for (int i = 0; i < filterModel.getRowCount(); i++) {
382                Filter filter = filterModel.getFilter(i);
383                MultikeyInfo info = new MultikeyInfo(i, filter.text);
384                result.add(info);
385            }
386
387            return result;
388        }
389
390        protected final boolean isLastFilterValid() {
391            return lastFilter != null && filterModel.getFilters().contains(lastFilter);
392        }
393
394        @Override
395        public MultikeyInfo getLastMultikeyAction() {
396            if (isLastFilterValid())
397                return new MultikeyInfo(-1, lastFilter.text);
398            else
399                return null;
400        }
401    }
402
403    private class EnableFilterAction extends AbstractFilterAction {
404
405        EnableFilterAction() {
406            putValue(SHORT_DESCRIPTION, tr("Enable filter"));
407            ENABLE_FILTER_SHORTCUT.setAccelerator(this);
408        }
409
410        @Override
411        public Shortcut getMultikeyShortcut() {
412            return ENABLE_FILTER_SHORTCUT;
413        }
414
415        @Override
416        public void executeMultikeyAction(int index, boolean repeatLastAction) {
417            if (index >= 0 && index < filterModel.getRowCount()) {
418                Filter filter = filterModel.getFilter(index);
419                filterModel.setValueAt(!filter.enable, index, FilterTableModel.COL_ENABLED);
420                lastFilter = filter;
421            } else if (repeatLastAction && isLastFilterValid()) {
422                filterModel.setValueAt(!lastFilter.enable, filterModel.getFilters().indexOf(lastFilter), FilterTableModel.COL_ENABLED);
423            }
424        }
425    }
426
427    private class HidingFilterAction extends AbstractFilterAction {
428
429        HidingFilterAction() {
430            putValue(SHORT_DESCRIPTION, tr("Hiding filter"));
431            HIDING_FILTER_SHORTCUT.setAccelerator(this);
432        }
433
434        @Override
435        public Shortcut getMultikeyShortcut() {
436            return HIDING_FILTER_SHORTCUT;
437        }
438
439        @Override
440        public void executeMultikeyAction(int index, boolean repeatLastAction) {
441            if (index >= 0 && index < filterModel.getRowCount()) {
442                Filter filter = filterModel.getFilter(index);
443                filterModel.setValueAt(!filter.hiding, index, FilterTableModel.COL_HIDING);
444                lastFilter = filter;
445            } else if (repeatLastAction && isLastFilterValid()) {
446                filterModel.setValueAt(!lastFilter.hiding, filterModel.getFilters().indexOf(lastFilter), FilterTableModel.COL_HIDING);
447            }
448        }
449    }
450}