001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.dialogs.properties;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.Component;
007import java.awt.Container;
008import java.awt.Font;
009import java.awt.GridBagLayout;
010import java.awt.Point;
011import java.awt.event.ActionEvent;
012import java.awt.event.InputEvent;
013import java.awt.event.KeyEvent;
014import java.awt.event.MouseAdapter;
015import java.awt.event.MouseEvent;
016import java.util.ArrayList;
017import java.util.Arrays;
018import java.util.Collection;
019import java.util.EnumSet;
020import java.util.HashMap;
021import java.util.HashSet;
022import java.util.List;
023import java.util.Map;
024import java.util.Map.Entry;
025import java.util.Optional;
026import java.util.Set;
027import java.util.TreeMap;
028import java.util.TreeSet;
029import java.util.concurrent.atomic.AtomicBoolean;
030import java.util.stream.Collectors;
031
032import javax.swing.AbstractAction;
033import javax.swing.JComponent;
034import javax.swing.JLabel;
035import javax.swing.JMenuItem;
036import javax.swing.JPanel;
037import javax.swing.JPopupMenu;
038import javax.swing.JScrollPane;
039import javax.swing.JTable;
040import javax.swing.KeyStroke;
041import javax.swing.ListSelectionModel;
042import javax.swing.event.ListSelectionEvent;
043import javax.swing.event.ListSelectionListener;
044import javax.swing.event.PopupMenuEvent;
045import javax.swing.event.RowSorterEvent;
046import javax.swing.event.RowSorterListener;
047import javax.swing.table.DefaultTableCellRenderer;
048import javax.swing.table.DefaultTableModel;
049import javax.swing.table.TableCellRenderer;
050import javax.swing.table.TableColumnModel;
051import javax.swing.table.TableModel;
052import javax.swing.table.TableRowSorter;
053
054import org.openstreetmap.josm.actions.JosmAction;
055import org.openstreetmap.josm.actions.relation.DeleteRelationsAction;
056import org.openstreetmap.josm.actions.relation.EditRelationAction;
057import org.openstreetmap.josm.command.ChangeCommand;
058import org.openstreetmap.josm.command.ChangePropertyCommand;
059import org.openstreetmap.josm.command.Command;
060import org.openstreetmap.josm.data.UndoRedoHandler;
061import org.openstreetmap.josm.data.osm.AbstractPrimitive;
062import org.openstreetmap.josm.data.osm.DataSelectionListener;
063import org.openstreetmap.josm.data.osm.DataSet;
064import org.openstreetmap.josm.data.osm.DefaultNameFormatter;
065import org.openstreetmap.josm.data.osm.IPrimitive;
066import org.openstreetmap.josm.data.osm.IRelation;
067import org.openstreetmap.josm.data.osm.IRelationMember;
068import org.openstreetmap.josm.data.osm.KeyValueVisitor;
069import org.openstreetmap.josm.data.osm.Node;
070import org.openstreetmap.josm.data.osm.OsmData;
071import org.openstreetmap.josm.data.osm.OsmDataManager;
072import org.openstreetmap.josm.data.osm.OsmPrimitive;
073import org.openstreetmap.josm.data.osm.Relation;
074import org.openstreetmap.josm.data.osm.RelationMember;
075import org.openstreetmap.josm.data.osm.Tag;
076import org.openstreetmap.josm.data.osm.Tags;
077import org.openstreetmap.josm.data.osm.Way;
078import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent;
079import org.openstreetmap.josm.data.osm.event.DataSetListenerAdapter;
080import org.openstreetmap.josm.data.osm.event.DatasetEventManager;
081import org.openstreetmap.josm.data.osm.event.DatasetEventManager.FireMode;
082import org.openstreetmap.josm.data.osm.event.SelectionEventManager;
083import org.openstreetmap.josm.data.osm.search.SearchCompiler;
084import org.openstreetmap.josm.data.osm.search.SearchSetting;
085import org.openstreetmap.josm.data.preferences.BooleanProperty;
086import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil;
087import org.openstreetmap.josm.gui.ExtendedDialog;
088import org.openstreetmap.josm.gui.MainApplication;
089import org.openstreetmap.josm.gui.PopupMenuHandler;
090import org.openstreetmap.josm.gui.SideButton;
091import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils;
092import org.openstreetmap.josm.gui.dialogs.ToggleDialog;
093import org.openstreetmap.josm.gui.dialogs.relation.RelationEditor;
094import org.openstreetmap.josm.gui.dialogs.relation.RelationPopupMenus;
095import org.openstreetmap.josm.gui.help.HelpUtil;
096import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent;
097import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener;
098import org.openstreetmap.josm.gui.layer.OsmDataLayer;
099import org.openstreetmap.josm.gui.tagging.presets.TaggingPreset;
100import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetHandler;
101import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetType;
102import org.openstreetmap.josm.gui.util.AbstractTag2LinkPopupListener;
103import org.openstreetmap.josm.gui.util.HighlightHelper;
104import org.openstreetmap.josm.gui.util.TableHelper;
105import org.openstreetmap.josm.gui.widgets.CompileSearchTextDecorator;
106import org.openstreetmap.josm.gui.widgets.DisableShortcutsOnFocusGainedTextField;
107import org.openstreetmap.josm.gui.widgets.JosmTextField;
108import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher;
109import org.openstreetmap.josm.spi.preferences.Config;
110import org.openstreetmap.josm.spi.preferences.PreferenceChangedListener;
111import org.openstreetmap.josm.tools.AlphanumComparator;
112import org.openstreetmap.josm.tools.GBC;
113import org.openstreetmap.josm.tools.InputMapUtils;
114import org.openstreetmap.josm.tools.Logging;
115import org.openstreetmap.josm.tools.Shortcut;
116import org.openstreetmap.josm.tools.Territories;
117import org.openstreetmap.josm.tools.Utils;
118
119/**
120 * This dialog displays the tags of the current selected primitives.
121 *
122 * If no object is selected, the dialog list is empty.
123 * If only one is selected, all tags of this object are selected.
124 * If more than one object are selected, the sum of all tags are displayed. If the
125 * different objects share the same tag, the shared value is displayed. If they have
126 * different values, all of them are put in a combo box and the string "<different>"
127 * is displayed in italic.
128 *
129 * Below the list, the user can click on an add, modify and delete tag button to
130 * edit the table selection value.
131 *
132 * The command is applied to all selected entries.
133 *
134 * @author imi
135 */
136public class PropertiesDialog extends ToggleDialog
137implements DataSelectionListener, ActiveLayerChangeListener, DataSetListenerAdapter.Listener {
138
139    /**
140     * hook for roadsigns plugin to display a small button in the upper right corner of this dialog
141     */
142    public static final JPanel pluginHook = new JPanel();
143
144    /**
145     * The tag data of selected objects.
146     */
147    private final ReadOnlyTableModel tagData = new ReadOnlyTableModel();
148    private final PropertiesCellRenderer cellRenderer = new PropertiesCellRenderer();
149    private final transient TableRowSorter<ReadOnlyTableModel> tagRowSorter = new TableRowSorter<>(tagData);
150    private final JosmTextField tagTableFilter;
151
152    /**
153     * The membership data of selected objects.
154     */
155    private final DefaultTableModel membershipData = new ReadOnlyTableModel();
156
157    /**
158     * The tags table.
159     */
160    private final JTable tagTable = new JTable(tagData);
161
162    /**
163     * The membership table.
164     */
165    private final JTable membershipTable = new JTable(membershipData);
166
167    /** JPanel containing both previous tables */
168    private final JPanel bothTables = new JPanel(new GridBagLayout());
169
170    // Popup menus
171    private final JPopupMenu tagMenu = new JPopupMenu();
172    private final JPopupMenu membershipMenu = new JPopupMenu();
173    private final JPopupMenu blankSpaceMenu = new JPopupMenu();
174
175    // Popup menu handlers
176    private final transient PopupMenuHandler tagMenuHandler = new PopupMenuHandler(tagMenu);
177    private final transient PopupMenuHandler membershipMenuHandler = new PopupMenuHandler(membershipMenu);
178    private final transient PopupMenuHandler blankSpaceMenuHandler = new PopupMenuHandler(blankSpaceMenu);
179
180    private final List<JMenuItem> tagMenuTagInfoNatItems = new ArrayList<>();
181    private final List<JMenuItem> membershipMenuTagInfoNatItems = new ArrayList<>();
182
183    private final transient Map<String, Map<String, Integer>> valueCount = new TreeMap<>();
184    /**
185     * This sub-object is responsible for all adding and editing of tags
186     */
187    private final transient TagEditHelper editHelper = new TagEditHelper(tagTable, tagData, valueCount);
188
189    private final transient DataSetListenerAdapter dataChangedAdapter = new DataSetListenerAdapter(this);
190    private final HelpAction helpTagAction = new HelpTagAction(tagTable, editHelper::getDataKey, editHelper::getDataValues);
191    private final HelpAction helpRelAction = new HelpMembershipAction(membershipTable, x -> (IRelation<?>) membershipData.getValueAt(x, 0));
192    private final TaginfoAction taginfoAction = new TaginfoAction(tagTable, editHelper::getDataKey, editHelper::getDataValues,
193            membershipTable, x -> (IRelation<?>) membershipData.getValueAt(x, 0));
194    private final Collection<TaginfoAction> taginfoNationalActions = new ArrayList<>();
195    private final PasteValueAction pasteValueAction = new PasteValueAction();
196    private final CopyValueAction copyValueAction = new CopyValueAction(
197            tagTable, editHelper::getDataKey, OsmDataManager.getInstance()::getInProgressISelection);
198    private final CopyKeyValueAction copyKeyValueAction = new CopyKeyValueAction(
199            tagTable, editHelper::getDataKey, OsmDataManager.getInstance()::getInProgressISelection);
200    private final CopyAllKeyValueAction copyAllKeyValueAction = new CopyAllKeyValueAction(
201            tagTable, editHelper::getDataKey, OsmDataManager.getInstance()::getInProgressISelection);
202    private final SearchAction searchActionSame = new SearchAction(true);
203    private final SearchAction searchActionAny = new SearchAction(false);
204    private final AddAction addAction = new AddAction();
205    private final EditAction editAction = new EditAction();
206    private final DeleteAction deleteAction = new DeleteAction();
207    private final JosmAction[] josmActions = {addAction, editAction, deleteAction};
208
209    private final transient HighlightHelper highlightHelper = new HighlightHelper();
210
211    /**
212     * The Add button (needed to be able to disable it)
213     */
214    private final SideButton btnAdd = new SideButton(addAction);
215    /**
216     * The Edit button (needed to be able to disable it)
217     */
218    private final SideButton btnEdit = new SideButton(editAction);
219    /**
220     * The Delete button (needed to be able to disable it)
221     */
222    private final SideButton btnDel = new SideButton(deleteAction);
223    /**
224     * Matching preset display class
225     */
226    private final PresetListPanel presets = new PresetListPanel();
227
228    /**
229     * Text to display when nothing selected.
230     */
231    private final JLabel selectSth = new JLabel("<html><p>"
232            + tr("Select objects for which to change tags.") + "</p></html>");
233
234    private final PreferenceChangedListener preferenceListener = e -> {
235                if (MainApplication.getLayerManager().getActiveData() != null) {
236                    // Re-load data when display preference change
237                    updateSelection();
238                }
239            };
240
241    private final transient TaggingPresetHandler presetHandler = new TaggingPresetCommandHandler();
242
243    private static final BooleanProperty PROP_AUTORESIZE_TAGS_TABLE = new BooleanProperty("propertiesdialog.autoresizeTagsTable", false);
244
245    /**
246     * Create a new PropertiesDialog
247     */
248    public PropertiesDialog() {
249        super(tr("Tags/Memberships"), "propertiesdialog", tr("Tags for selected objects."),
250                Shortcut.registerShortcut("subwindow:properties", tr("Toggle: {0}", tr("Tags/Memberships")), KeyEvent.VK_P,
251                        Shortcut.ALT_SHIFT), 150, true);
252
253        setupTagsMenu();
254        buildTagsTable();
255
256        setupMembershipMenu();
257        buildMembershipTable();
258
259        tagTableFilter = setupFilter();
260
261        // combine both tables and wrap them in a scrollPane
262        boolean top = Config.getPref().getBoolean("properties.presets.top", true);
263        boolean presetsVisible = Config.getPref().getBoolean("properties.presets.visible", true);
264        if (presetsVisible && top) {
265            bothTables.add(presets, GBC.std().fill(GBC.HORIZONTAL).insets(5, 2, 5, 2).anchor(GBC.NORTHWEST));
266            double epsilon = Double.MIN_VALUE; // need to set a weight or else anchor value is ignored
267            bothTables.add(pluginHook, GBC.eol().insets(0, 1, 1, 1).anchor(GBC.NORTHEAST).weight(epsilon, epsilon));
268        }
269        bothTables.add(selectSth, GBC.eol().fill().insets(10, 10, 10, 10));
270        bothTables.add(tagTableFilter, GBC.eol().fill(GBC.HORIZONTAL));
271        bothTables.add(tagTable.getTableHeader(), GBC.eol().fill(GBC.HORIZONTAL));
272        bothTables.add(tagTable, GBC.eol().fill(GBC.BOTH));
273        bothTables.add(membershipTable.getTableHeader(), GBC.eol().fill(GBC.HORIZONTAL));
274        bothTables.add(membershipTable, GBC.eol().fill(GBC.BOTH));
275        if (presetsVisible && !top) {
276            bothTables.add(presets, GBC.eol().fill(GBC.HORIZONTAL).insets(5, 2, 5, 2));
277        }
278
279        setupBlankSpaceMenu();
280        setupKeyboardShortcuts();
281
282        // Let the actions know when selection in the tables change
283        tagTable.getSelectionModel().addListSelectionListener(editAction);
284        membershipTable.getSelectionModel().addListSelectionListener(editAction);
285        tagTable.getSelectionModel().addListSelectionListener(deleteAction);
286        membershipTable.getSelectionModel().addListSelectionListener(deleteAction);
287
288        JScrollPane scrollPane = (JScrollPane) createLayout(bothTables, true,
289                Arrays.asList(this.btnAdd, this.btnEdit, this.btnDel));
290
291        MouseClickWatch mouseClickWatch = new MouseClickWatch();
292        tagTable.addMouseListener(mouseClickWatch);
293        membershipTable.addMouseListener(mouseClickWatch);
294        scrollPane.addMouseListener(mouseClickWatch);
295
296        selectSth.setPreferredSize(scrollPane.getSize());
297        presets.setSize(scrollPane.getSize());
298
299        editHelper.loadTagsIfNeeded();
300
301        Config.getPref().addKeyPreferenceChangeListener("display.discardable-keys", preferenceListener);
302    }
303
304    @Override
305    public String helpTopic() {
306        return HelpUtil.ht("/Dialog/TagsMembership");
307    }
308
309    private void buildTagsTable() {
310        // setting up the tags table
311        tagData.setColumnIdentifiers(new String[]{tr("Key"), tr("Value")});
312        tagTable.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
313        tagTable.getTableHeader().setReorderingAllowed(false);
314
315        tagTable.getColumnModel().getColumn(0).setCellRenderer(cellRenderer);
316        tagTable.getColumnModel().getColumn(1).setCellRenderer(cellRenderer);
317        tagTable.setRowSorter(tagRowSorter);
318
319        final RemoveHiddenSelection removeHiddenSelection = new RemoveHiddenSelection();
320        tagTable.getSelectionModel().addListSelectionListener(removeHiddenSelection);
321        tagRowSorter.addRowSorterListener(removeHiddenSelection);
322        tagRowSorter.setComparator(0, AlphanumComparator.getInstance());
323        tagRowSorter.setComparator(1, (o1, o2) -> {
324            if (o1 instanceof Map && o2 instanceof Map) {
325                final String v1 = ((Map) o1).size() == 1 ? (String) ((Map) o1).keySet().iterator().next() : tr("<different>");
326                final String v2 = ((Map) o2).size() == 1 ? (String) ((Map) o2).keySet().iterator().next() : tr("<different>");
327                return AlphanumComparator.getInstance().compare(v1, v2);
328            } else {
329                return AlphanumComparator.getInstance().compare(String.valueOf(o1), String.valueOf(o2));
330            }
331        });
332    }
333
334    private void buildMembershipTable() {
335        membershipData.setColumnIdentifiers(new String[]{tr("Member Of"), tr("Role"), tr("Position")});
336        membershipTable.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
337
338        TableColumnModel mod = membershipTable.getColumnModel();
339        membershipTable.getTableHeader().setReorderingAllowed(false);
340        mod.getColumn(0).setCellRenderer(new MemberOfCellRenderer());
341        mod.getColumn(1).setCellRenderer(new RoleCellRenderer());
342        mod.getColumn(2).setCellRenderer(new PositionCellRenderer());
343        mod.getColumn(2).setPreferredWidth(20);
344        mod.getColumn(1).setPreferredWidth(40);
345        mod.getColumn(0).setPreferredWidth(200);
346    }
347
348    /**
349     * Creates the popup menu @field blankSpaceMenu and its launcher on main panel.
350     */
351    private void setupBlankSpaceMenu() {
352        if (Config.getPref().getBoolean("properties.menu.add_edit_delete", true)) {
353            blankSpaceMenuHandler.addAction(addAction);
354            PopupMenuLauncher launcher = new BlankSpaceMenuLauncher(blankSpaceMenu);
355            bothTables.addMouseListener(launcher);
356            tagTable.addMouseListener(launcher);
357        }
358    }
359
360    private void destroyTaginfoNationalActions() {
361        membershipMenuTagInfoNatItems.forEach(membershipMenu::remove);
362        membershipMenuTagInfoNatItems.clear();
363        tagMenuTagInfoNatItems.forEach(tagMenu::remove);
364        tagMenuTagInfoNatItems.clear();
365        taginfoNationalActions.forEach(JosmAction::destroy);
366        taginfoNationalActions.clear();
367    }
368
369    private void setupTaginfoNationalActions(Collection<? extends IPrimitive> newSel) {
370        destroyTaginfoNationalActions();
371        if (!newSel.isEmpty()) {
372            for (Entry<String, String> e : Territories.getNationalTaginfoUrls(
373                    newSel.iterator().next().getBBox().getCenter()).entrySet()) {
374                taginfoNationalActions.add(new TaginfoAction(tagTable, editHelper::getDataKey, editHelper::getDataValues,
375                        membershipTable, x -> (IRelation<?>) membershipData.getValueAt(x, 0), e.getValue(), e.getKey()));
376            }
377            taginfoNationalActions.stream().map(membershipMenu::add).forEach(membershipMenuTagInfoNatItems::add);
378            taginfoNationalActions.stream().map(tagMenu::add).forEach(tagMenuTagInfoNatItems::add);
379        }
380    }
381
382    /**
383     * Creates the popup menu @field membershipMenu and its launcher on membership table.
384     */
385    private void setupMembershipMenu() {
386        // setting up the membership table
387        if (Config.getPref().getBoolean("properties.menu.add_edit_delete", true)) {
388            membershipMenuHandler.addAction(editAction);
389            membershipMenuHandler.addAction(deleteAction);
390            membershipMenu.addSeparator();
391        }
392        RelationPopupMenus.setupHandler(membershipMenuHandler, EditRelationAction.class, DeleteRelationsAction.class);
393        membershipMenu.addSeparator();
394        membershipMenu.add(helpRelAction);
395        membershipMenu.add(taginfoAction);
396
397        membershipMenu.addPopupMenuListener(new AbstractTag2LinkPopupListener() {
398            @Override
399            public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
400                getSelectedMembershipRelations().forEach(relation ->
401                        relation.visitKeys((primitive, key, value) -> addLinks(membershipMenu, key, value)));
402            }
403        });
404
405        membershipTable.addMouseListener(new PopupMenuLauncher(membershipMenu) {
406            @Override
407            protected int checkTableSelection(JTable table, Point p) {
408                int row = super.checkTableSelection(table, p);
409                List<IRelation<?>> rels = new ArrayList<>();
410                for (int i: table.getSelectedRows()) {
411                    rels.add((IRelation<?>) table.getValueAt(i, 0));
412                }
413                membershipMenuHandler.setPrimitives(rels);
414                return row;
415            }
416
417            @Override
418            public void mouseClicked(MouseEvent e) {
419                //update highlights
420                if (MainApplication.isDisplayingMapView()) {
421                    int row = membershipTable.rowAtPoint(e.getPoint());
422                    if (row >= 0 && highlightHelper.highlightOnly((Relation) membershipTable.getValueAt(row, 0))) {
423                        MainApplication.getMap().mapView.repaint();
424                    }
425                }
426                super.mouseClicked(e);
427            }
428
429            @Override
430            public void mouseExited(MouseEvent me) {
431                highlightHelper.clear();
432            }
433        });
434    }
435
436    /**
437     * Creates the popup menu @field tagMenu and its launcher on tag table.
438     */
439    private void setupTagsMenu() {
440        if (Config.getPref().getBoolean("properties.menu.add_edit_delete", true)) {
441            tagMenu.add(addAction);
442            tagMenu.add(editAction);
443            tagMenu.add(deleteAction);
444            tagMenu.addSeparator();
445        }
446        tagMenu.add(pasteValueAction);
447        tagMenu.add(copyValueAction);
448        tagMenu.add(copyKeyValueAction);
449        tagMenu.addPopupMenuListener(copyKeyValueAction);
450        tagMenu.add(copyAllKeyValueAction);
451        tagMenu.addSeparator();
452        tagMenu.add(searchActionAny);
453        tagMenu.add(searchActionSame);
454        tagMenu.addSeparator();
455        tagMenu.add(helpTagAction);
456        tagMenu.add(taginfoAction);
457        tagMenu.addPopupMenuListener(new AbstractTag2LinkPopupListener() {
458            @Override
459            public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
460                visitSelectedProperties((primitive, key, value) -> addLinks(tagMenu, key, value));
461            }
462        });
463
464        tagTable.addMouseListener(new PopupMenuLauncher(tagMenu));
465    }
466
467    /**
468     * Sets a filter to restrict the displayed properties.
469     * @param filter the filter
470     * @since 8980
471     */
472    public void setFilter(final SearchCompiler.Match filter) {
473        this.tagRowSorter.setRowFilter(new SearchBasedRowFilter(filter));
474    }
475
476    /**
477     * Assigns all needed keys like Enter and Spacebar to most important actions.
478     */
479    private void setupKeyboardShortcuts() {
480
481        // ENTER = editAction, open "edit" dialog
482        InputMapUtils.addEnterActionWhenAncestor(tagTable, editAction);
483        InputMapUtils.addEnterActionWhenAncestor(membershipTable, editAction);
484
485        // INSERT button = addAction, open "add tag" dialog
486        tagTable.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
487                .put(KeyStroke.getKeyStroke(KeyEvent.VK_INSERT, 0), "onTableInsert");
488        tagTable.getActionMap().put("onTableInsert", addAction);
489
490        // unassign some standard shortcuts for JTable to allow upload / download / image browsing
491        InputMapUtils.unassignCtrlShiftUpDown(tagTable, JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
492        InputMapUtils.unassignPageUpDown(tagTable, JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
493
494        // unassign some standard shortcuts for correct copy-pasting, fix #8508
495        tagTable.setTransferHandler(null);
496
497        tagTable.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
498                .put(KeyStroke.getKeyStroke(KeyEvent.VK_C, InputEvent.CTRL_DOWN_MASK), "onCopy");
499        tagTable.getActionMap().put("onCopy", copyKeyValueAction);
500
501        // allow using enter to add tags for all look&feel configurations
502        InputMapUtils.enableEnter(this.btnAdd);
503
504        // DEL button = deleteAction
505        getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(
506                KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), "delete"
507                );
508        getActionMap().put("delete", deleteAction);
509
510        // F1 button = custom help action
511        getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(
512                HelpAction.getKeyStroke(), "onHelp");
513        getActionMap().put("onHelp", new AbstractAction() {
514            @Override
515            public void actionPerformed(ActionEvent e) {
516                if (membershipTable.getSelectedRowCount() == 1) {
517                    helpRelAction.actionPerformed(e);
518                } else {
519                    helpTagAction.actionPerformed(e);
520                }
521            }
522        });
523    }
524
525    private JosmTextField setupFilter() {
526        final JosmTextField f = new DisableShortcutsOnFocusGainedTextField();
527        f.setToolTipText(tr("Tag filter"));
528        final CompileSearchTextDecorator decorator = CompileSearchTextDecorator.decorate(f);
529        f.addPropertyChangeListener("filter", evt -> setFilter(decorator.getMatch()));
530        return f;
531    }
532
533    /**
534     * This simply fires up an {@link RelationEditor} for the relation shown; everything else
535     * is the editor's business.
536     *
537     * @param row position
538     */
539    private void editMembership(int row) {
540        Relation relation = (Relation) membershipData.getValueAt(row, 0);
541        MainApplication.getMap().relationListDialog.selectRelation(relation);
542        OsmDataLayer layer = MainApplication.getLayerManager().getActiveDataLayer();
543        if (!layer.isLocked()) {
544            List<RelationMember> members = new ArrayList<>();
545            for (IRelationMember<?> rm : ((MemberInfo) membershipData.getValueAt(row, 1)).role) {
546                if (rm instanceof RelationMember) {
547                    members.add((RelationMember) rm);
548                }
549            }
550            RelationEditor.getEditor(layer, relation, members).setVisible(true);
551        }
552    }
553
554    private static int findViewRow(JTable table, TableModel model, Object value) {
555        for (int i = 0; i < model.getRowCount(); i++) {
556            if (model.getValueAt(i, 0).equals(value))
557                return table.convertRowIndexToView(i);
558        }
559        return -1;
560    }
561
562    /**
563     * Update selection status, call @{link #selectionChanged} function.
564     */
565    private void updateSelection() {
566        // Parameter is ignored in this class
567        selectionChanged(null);
568    }
569
570    @Override
571    public void showNotify() {
572        DatasetEventManager.getInstance().addDatasetListener(dataChangedAdapter, FireMode.IN_EDT_CONSOLIDATED);
573        SelectionEventManager.getInstance().addSelectionListenerForEdt(this);
574        MainApplication.getLayerManager().addActiveLayerChangeListener(this);
575        for (JosmAction action : josmActions) {
576            MainApplication.registerActionShortcut(action);
577        }
578        updateSelection();
579    }
580
581    @Override
582    public void hideNotify() {
583        DatasetEventManager.getInstance().removeDatasetListener(dataChangedAdapter);
584        SelectionEventManager.getInstance().removeSelectionListener(this);
585        MainApplication.getLayerManager().removeActiveLayerChangeListener(this);
586        for (JosmAction action : josmActions) {
587            MainApplication.unregisterActionShortcut(action);
588        }
589    }
590
591    @Override
592    public void setVisible(boolean b) {
593        super.setVisible(b);
594        if (b && MainApplication.getLayerManager().getActiveData() != null) {
595            updateSelection();
596        }
597    }
598
599    @Override
600    public void destroy() {
601        taginfoAction.destroy();
602        destroyTaginfoNationalActions();
603        super.destroy();
604        Config.getPref().removeKeyPreferenceChangeListener("display.discardable-keys", preferenceListener);
605        Container parent = pluginHook.getParent();
606        if (parent != null) {
607            parent.remove(pluginHook);
608        }
609    }
610
611    @Override
612    public void selectionChanged(SelectionChangeEvent event) {
613        if (!isVisible())
614            return;
615        if (tagTable == null)
616            return; // selection changed may be received in base class constructor before init
617        if (tagTable.getCellEditor() != null) {
618            tagTable.getCellEditor().cancelCellEditing();
619        }
620
621        // Ignore parameter as we do not want to operate always on real selection here, especially in draw mode
622        Collection<? extends IPrimitive> newSel = OsmDataManager.getInstance().getInProgressISelection();
623        int newSelSize = newSel.size();
624        IRelation<?> selectedRelation = null;
625        String selectedTag = editHelper.getChangedKey(); // select last added or last edited key by default
626        if (selectedTag == null && tagTable.getSelectedRowCount() == 1) {
627            selectedTag = editHelper.getDataKey(tagTable.getSelectedRow());
628        }
629        if (membershipTable.getSelectedRowCount() == 1) {
630            selectedRelation = (IRelation<?>) membershipData.getValueAt(membershipTable.getSelectedRow(), 0);
631        }
632
633        // re-load tag data
634        tagData.setRowCount(0);
635
636        final boolean displayDiscardableKeys = Config.getPref().getBoolean("display.discardable-keys", false);
637        final Map<String, Integer> keyCount = new HashMap<>();
638        final Map<String, String> tags = new HashMap<>();
639        valueCount.clear();
640        Set<TaggingPresetType> types = EnumSet.noneOf(TaggingPresetType.class);
641        for (IPrimitive osm : newSel) {
642            types.add(TaggingPresetType.forPrimitive(osm));
643            for (String key : osm.keySet()) {
644                if (displayDiscardableKeys || !AbstractPrimitive.getDiscardableKeys().contains(key)) {
645                    String value = osm.get(key);
646                    keyCount.put(key, keyCount.containsKey(key) ? keyCount.get(key) + 1 : 1);
647                    if (valueCount.containsKey(key)) {
648                        Map<String, Integer> v = valueCount.get(key);
649                        v.put(value, v.containsKey(value) ? v.get(value) + 1 : 1);
650                    } else {
651                        Map<String, Integer> v = new TreeMap<>();
652                        v.put(value, 1);
653                        valueCount.put(key, v);
654                    }
655                }
656            }
657        }
658        for (Entry<String, Map<String, Integer>> e : valueCount.entrySet()) {
659            int count = 0;
660            for (Entry<String, Integer> e1 : e.getValue().entrySet()) {
661                count += e1.getValue();
662            }
663            if (count < newSelSize) {
664                e.getValue().put("", newSelSize - count);
665            }
666            tagData.addRow(new Object[]{e.getKey(), e.getValue()});
667            tags.put(e.getKey(), e.getValue().size() == 1
668                    ? e.getValue().keySet().iterator().next() : tr("<different>"));
669        }
670
671        membershipData.setRowCount(0);
672
673        Map<IRelation<?>, MemberInfo> roles = new HashMap<>();
674        for (IPrimitive primitive: newSel) {
675            for (IPrimitive ref: primitive.getReferrers(true)) {
676                if (ref instanceof IRelation && !ref.isIncomplete() && !ref.isDeleted()) {
677                    IRelation<?> r = (IRelation<?>) ref;
678                    MemberInfo mi = Optional.ofNullable(roles.get(r)).orElseGet(() -> new MemberInfo(newSel));
679                    roles.put(r, mi);
680                    int i = 1;
681                    for (IRelationMember<?> m : r.getMembers()) {
682                        if (m.getMember() == primitive) {
683                            mi.add(m, i);
684                        }
685                        ++i;
686                    }
687                }
688            }
689        }
690
691        List<IRelation<?>> sortedRelations = new ArrayList<>(roles.keySet());
692        sortedRelations.sort((o1, o2) -> {
693            int comp = Boolean.compare(o1.isDisabledAndHidden(), o2.isDisabledAndHidden());
694            return comp != 0 ? comp : DefaultNameFormatter.getInstance().getRelationComparator().compare(o1, o2);
695        });
696
697        for (IRelation<?> r: sortedRelations) {
698            membershipData.addRow(new Object[]{r, roles.get(r)});
699        }
700
701        presets.updatePresets(types, tags, presetHandler);
702
703        membershipTable.getTableHeader().setVisible(membershipData.getRowCount() > 0);
704        membershipTable.setVisible(membershipData.getRowCount() > 0);
705
706        OsmData<?, ?, ?, ?> ds = MainApplication.getLayerManager().getActiveData();
707        boolean isReadOnly = ds != null && ds.isLocked();
708        boolean hasSelection = !newSel.isEmpty();
709        boolean hasTags = hasSelection && tagData.getRowCount() > 0;
710        boolean hasMemberships = hasSelection && membershipData.getRowCount() > 0;
711        addAction.setEnabled(!isReadOnly && hasSelection);
712        editAction.setEnabled(!isReadOnly && (hasTags || hasMemberships));
713        deleteAction.setEnabled(!isReadOnly && (hasTags || hasMemberships));
714        tagTable.setVisible(hasTags);
715        tagTable.getTableHeader().setVisible(hasTags);
716        tagTableFilter.setVisible(hasTags);
717        selectSth.setVisible(!hasSelection);
718        pluginHook.setVisible(hasSelection);
719
720        setupTaginfoNationalActions(newSel);
721        autoresizeTagTable();
722
723        int selectedIndex;
724        if (selectedTag != null && (selectedIndex = findViewRow(tagTable, tagData, selectedTag)) != -1) {
725            tagTable.changeSelection(selectedIndex, 0, false, false);
726        } else if (selectedRelation != null && (selectedIndex = findViewRow(membershipTable, membershipData, selectedRelation)) != -1) {
727            membershipTable.changeSelection(selectedIndex, 0, false, false);
728        } else if (hasTags) {
729            tagTable.changeSelection(0, 0, false, false);
730        } else if (hasMemberships) {
731            membershipTable.changeSelection(0, 0, false, false);
732        }
733
734        if (tagData.getRowCount() != 0 || membershipData.getRowCount() != 0) {
735            if (newSelSize > 1) {
736                setTitle(tr("Objects: {2} / Tags: {0} / Memberships: {1}",
737                    tagData.getRowCount(), membershipData.getRowCount(), newSelSize));
738            } else {
739                setTitle(tr("Tags: {0} / Memberships: {1}",
740                    tagData.getRowCount(), membershipData.getRowCount()));
741            }
742        } else {
743            setTitle(tr("Tags/Memberships"));
744        }
745    }
746
747    private void autoresizeTagTable() {
748        if (PROP_AUTORESIZE_TAGS_TABLE.get()) {
749            // resize table's columns to fit content
750            TableHelper.computeColumnsWidth(tagTable);
751        }
752    }
753
754    /* ---------------------------------------------------------------------------------- */
755    /* ActiveLayerChangeListener                                                          */
756    /* ---------------------------------------------------------------------------------- */
757    @Override
758    public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) {
759        if (e.getSource().getEditLayer() == null) {
760            editHelper.saveTagsIfNeeded();
761            editHelper.resetSelection();
762        }
763        // it is time to save history of tags
764        updateSelection();
765    }
766
767    @Override
768    public void processDatasetEvent(AbstractDatasetChangedEvent event) {
769        updateSelection();
770    }
771
772    /**
773     * Replies the tag popup menu handler.
774     * @return The tag popup menu handler
775     */
776    public PopupMenuHandler getPropertyPopupMenuHandler() {
777        return tagMenuHandler;
778    }
779
780    /**
781     * Returns the selected tag. Value is empty if several tags are selected for a given key.
782     * @return The current selected tag
783     */
784    public Tag getSelectedProperty() {
785        Tags tags = getSelectedProperties();
786        return tags == null ? null : new Tag(
787                tags.getKey(),
788                tags.getValues().size() > 1 ? "" : tags.getValues().iterator().next());
789    }
790
791    /**
792     * Returns the selected tags. Contains all values if several are selected for a given key.
793     * @return The current selected tags
794     * @since 15376
795     */
796    public Tags getSelectedProperties() {
797        int row = tagTable.getSelectedRow();
798        if (row == -1) return null;
799        Map<String, Integer> map = editHelper.getDataValues(row);
800        return new Tags(editHelper.getDataKey(row), map.keySet());
801    }
802
803    /**
804     * Visits all combinations of the selected keys/values.
805     * @param visitor the visitor
806     * @since 15707
807     */
808    public void visitSelectedProperties(KeyValueVisitor visitor) {
809        for (int row : tagTable.getSelectedRows()) {
810            final String key = editHelper.getDataKey(row);
811            Set<String> values = editHelper.getDataValues(row).keySet();
812            values.forEach(value -> visitor.visitKeyValue(null, key, value));
813        }
814    }
815
816    /**
817     * Replies the membership popup menu handler.
818     * @return The membership popup menu handler
819     */
820    public PopupMenuHandler getMembershipPopupMenuHandler() {
821        return membershipMenuHandler;
822    }
823
824    /**
825     * Returns the selected relation membership.
826     * @return The current selected relation membership
827     */
828    public IRelation<?> getSelectedMembershipRelation() {
829        int row = membershipTable.getSelectedRow();
830        return row > -1 ? (IRelation<?>) membershipData.getValueAt(row, 0) : null;
831    }
832
833    /**
834     * Returns all selected relation memberships.
835     * @return The selected relation memberships
836     * @since 15707
837     */
838    public Collection<IRelation<?>> getSelectedMembershipRelations() {
839        return Arrays.stream(membershipTable.getSelectedRows())
840                .mapToObj(row -> (IRelation<?>) membershipData.getValueAt(row, 0))
841                .collect(Collectors.toList());
842    }
843
844    /**
845     * Adds a custom table cell renderer to render cells of the tags table.
846     *
847     * If the renderer is not capable performing a {@link TableCellRenderer#getTableCellRendererComponent},
848     * it should return {@code null} to fall back to the
849     * {@link PropertiesCellRenderer#getTableCellRendererComponent default implementation}.
850     * @param renderer the renderer to add
851     * @since 9149
852     */
853    public void addCustomPropertiesCellRenderer(TableCellRenderer renderer) {
854        cellRenderer.addCustomRenderer(renderer);
855    }
856
857    /**
858     * Removes a custom table cell renderer.
859     * @param renderer the renderer to remove
860     * @since 9149
861     */
862    public void removeCustomPropertiesCellRenderer(TableCellRenderer renderer) {
863        cellRenderer.removeCustomRenderer(renderer);
864    }
865
866    static final class MemberOfCellRenderer extends DefaultTableCellRenderer {
867        @Override
868        public Component getTableCellRendererComponent(JTable table, Object value,
869                boolean isSelected, boolean hasFocus, int row, int column) {
870            Component c = super.getTableCellRendererComponent(table, value, isSelected, false, row, column);
871            if (value == null)
872                return this;
873            if (c instanceof JLabel) {
874                JLabel label = (JLabel) c;
875                IRelation<?> r = (IRelation<?>) value;
876                label.setText(r.getDisplayName(DefaultNameFormatter.getInstance()));
877                if (r.isDisabledAndHidden()) {
878                    label.setFont(label.getFont().deriveFont(Font.ITALIC));
879                }
880            }
881            return c;
882        }
883    }
884
885    static final class RoleCellRenderer extends DefaultTableCellRenderer {
886        @Override
887        public Component getTableCellRendererComponent(JTable table, Object value,
888                boolean isSelected, boolean hasFocus, int row, int column) {
889            if (value == null)
890                return this;
891            Component c = super.getTableCellRendererComponent(table, value, isSelected, false, row, column);
892            boolean isDisabledAndHidden = ((IRelation<?>) table.getValueAt(row, 0)).isDisabledAndHidden();
893            if (c instanceof JLabel) {
894                JLabel label = (JLabel) c;
895                label.setText(((MemberInfo) value).getRoleString());
896                if (isDisabledAndHidden) {
897                    label.setFont(label.getFont().deriveFont(Font.ITALIC));
898                }
899            }
900            return c;
901        }
902    }
903
904    static final class PositionCellRenderer extends DefaultTableCellRenderer {
905        @Override
906        public Component getTableCellRendererComponent(JTable table, Object value,
907                boolean isSelected, boolean hasFocus, int row, int column) {
908            Component c = super.getTableCellRendererComponent(table, value, isSelected, false, row, column);
909            IRelation<?> relation = (IRelation<?>) table.getValueAt(row, 0);
910            boolean isDisabledAndHidden = relation != null && relation.isDisabledAndHidden();
911            if (c instanceof JLabel) {
912                JLabel label = (JLabel) c;
913                MemberInfo member = (MemberInfo) table.getValueAt(row, 1);
914                if (member != null) {
915                    label.setText(member.getPositionString());
916                }
917                if (isDisabledAndHidden) {
918                    label.setFont(label.getFont().deriveFont(Font.ITALIC));
919                }
920            }
921            return c;
922        }
923    }
924
925    static final class BlankSpaceMenuLauncher extends PopupMenuLauncher {
926        BlankSpaceMenuLauncher(JPopupMenu menu) {
927            super(menu);
928        }
929
930        @Override
931        protected boolean checkSelection(Component component, Point p) {
932            if (component instanceof JTable) {
933                return ((JTable) component).rowAtPoint(p) == -1;
934            }
935            return true;
936        }
937    }
938
939    static final class TaggingPresetCommandHandler implements TaggingPresetHandler {
940        @Override
941        public void updateTags(List<Tag> tags) {
942            Command command = TaggingPreset.createCommand(getSelection(), tags);
943            if (command != null) {
944                UndoRedoHandler.getInstance().add(command);
945            }
946        }
947
948        @Override
949        public Collection<OsmPrimitive> getSelection() {
950            return OsmDataManager.getInstance().getInProgressSelection();
951        }
952    }
953
954    /**
955     * Class that watches for mouse clicks
956     * @author imi
957     */
958    public class MouseClickWatch extends MouseAdapter {
959        @Override
960        public void mouseClicked(MouseEvent e) {
961            if (e.getClickCount() < 2) {
962                // single click, clear selection in other table not clicked in
963                if (e.getSource() == tagTable) {
964                    membershipTable.clearSelection();
965                } else if (e.getSource() == membershipTable) {
966                    tagTable.clearSelection();
967                }
968            } else if (e.getSource() == tagTable) {
969                // double click, edit or add tag
970                int row = tagTable.rowAtPoint(e.getPoint());
971                if (row > -1) {
972                    boolean focusOnKey = tagTable.columnAtPoint(e.getPoint()) == 0;
973                    editHelper.editTag(row, focusOnKey);
974                } else {
975                    editHelper.addTag();
976                    btnAdd.requestFocusInWindow();
977                }
978            } else if (e.getSource() == membershipTable) {
979                int row = membershipTable.rowAtPoint(e.getPoint());
980                if (row > -1) {
981                    editMembership(row);
982                }
983            } else {
984                editHelper.addTag();
985                btnAdd.requestFocusInWindow();
986            }
987        }
988
989        @Override
990        public void mousePressed(MouseEvent e) {
991            if (e.getSource() == tagTable) {
992                membershipTable.clearSelection();
993            } else if (e.getSource() == membershipTable) {
994                tagTable.clearSelection();
995            }
996        }
997    }
998
999    static class MemberInfo {
1000        private final List<IRelationMember<?>> role = new ArrayList<>();
1001        private Set<IPrimitive> members = new HashSet<>();
1002        private List<Integer> position = new ArrayList<>();
1003        private Collection<? extends IPrimitive> selection;
1004        private String positionString;
1005        private String roleString;
1006
1007        MemberInfo(Collection<? extends IPrimitive> selection) {
1008            this.selection = selection;
1009        }
1010
1011        void add(IRelationMember<?> r, Integer p) {
1012            role.add(r);
1013            members.add(r.getMember());
1014            position.add(p);
1015        }
1016
1017        String getPositionString() {
1018            if (positionString == null) {
1019                positionString = Utils.getPositionListString(position);
1020                // if not all objects from the selection are member of this relation
1021                if (selection.stream().anyMatch(p -> !members.contains(p))) {
1022                    positionString += ",\u2717";
1023                }
1024                members = null;
1025                position = null;
1026                selection = null;
1027            }
1028            return Utils.shortenString(positionString, 20);
1029        }
1030
1031        String getRoleString() {
1032            if (roleString == null) {
1033                for (IRelationMember<?> r : role) {
1034                    if (roleString == null) {
1035                        roleString = r.getRole();
1036                    } else if (!roleString.equals(r.getRole())) {
1037                        roleString = tr("<different>");
1038                        break;
1039                    }
1040                }
1041            }
1042            return roleString;
1043        }
1044
1045        @Override
1046        public String toString() {
1047            return "MemberInfo{" +
1048                    "roles='" + roleString + '\'' +
1049                    ", positions='" + positionString + '\'' +
1050                    '}';
1051        }
1052    }
1053
1054    /**
1055     * Class that allows fast creation of read-only table model with String columns
1056     */
1057    public static class ReadOnlyTableModel extends DefaultTableModel {
1058        @Override
1059        public boolean isCellEditable(int row, int column) {
1060            return false;
1061        }
1062
1063        @Override
1064        public Class<?> getColumnClass(int columnIndex) {
1065            return String.class;
1066        }
1067    }
1068
1069    /**
1070     * Action handling delete button press in properties dialog.
1071     */
1072    class DeleteAction extends JosmAction implements ListSelectionListener {
1073
1074        private static final String DELETE_FROM_RELATION_PREF = "delete_from_relation";
1075
1076        DeleteAction() {
1077            super(tr("Delete"), /* ICON() */ "dialogs/delete", tr("Delete the selected key in all objects"),
1078                    Shortcut.registerShortcut("properties:delete", tr("Delete Tags"), KeyEvent.VK_D,
1079                            Shortcut.ALT_CTRL_SHIFT), false);
1080            updateEnabledState();
1081        }
1082
1083        protected void deleteTags(int... rows) {
1084            // convert list of rows to HashMap (and find gap for nextKey)
1085            Map<String, String> tags = new HashMap<>(rows.length);
1086            int nextKeyIndex = rows[0];
1087            for (int row : rows) {
1088                String key = editHelper.getDataKey(row);
1089                if (row == nextKeyIndex + 1) {
1090                    nextKeyIndex = row; // no gap yet
1091                }
1092                tags.put(key, null);
1093            }
1094
1095            // find key to select after deleting other tags
1096            String nextKey = null;
1097            int rowCount = tagData.getRowCount();
1098            if (rowCount > rows.length) {
1099                if (nextKeyIndex == rows[rows.length-1]) {
1100                    // no gap found, pick next or previous key in list
1101                    nextKeyIndex = nextKeyIndex + 1 < rowCount ? nextKeyIndex + 1 : rows[0] - 1;
1102                } else {
1103                    // gap found
1104                    nextKeyIndex++;
1105                }
1106                // We use unfiltered indexes here. So don't use getDataKey()
1107                nextKey = (String) tagData.getValueAt(nextKeyIndex, 0);
1108            }
1109
1110            Collection<OsmPrimitive> sel = OsmDataManager.getInstance().getInProgressSelection();
1111            UndoRedoHandler.getInstance().add(new ChangePropertyCommand(sel, tags));
1112
1113            membershipTable.clearSelection();
1114            if (nextKey != null) {
1115                tagTable.changeSelection(findViewRow(tagTable, tagData, nextKey), 0, false, false);
1116            }
1117        }
1118
1119        protected void deleteFromRelation(int row) {
1120            Relation cur = (Relation) membershipData.getValueAt(row, 0);
1121
1122            Relation nextRelation = null;
1123            int rowCount = membershipTable.getRowCount();
1124            if (rowCount > 1) {
1125                nextRelation = (Relation) membershipData.getValueAt(row + 1 < rowCount ? row + 1 : row - 1, 0);
1126            }
1127
1128            ExtendedDialog ed = new ExtendedDialog(MainApplication.getMainFrame(),
1129                    tr("Change relation"),
1130                    tr("Delete from relation"), tr("Cancel"));
1131            ed.setButtonIcons("dialogs/delete", "cancel");
1132            ed.setContent(tr("Really delete selection from relation {0}?", cur.getDisplayName(DefaultNameFormatter.getInstance())));
1133            ed.toggleEnable(DELETE_FROM_RELATION_PREF);
1134
1135            if (ed.showDialog().getValue() != 1)
1136                return;
1137
1138            Relation rel = new Relation(cur);
1139            for (OsmPrimitive primitive: OsmDataManager.getInstance().getInProgressSelection()) {
1140                rel.removeMembersFor(primitive);
1141            }
1142            UndoRedoHandler.getInstance().add(new ChangeCommand(cur, rel));
1143
1144            tagTable.clearSelection();
1145            if (nextRelation != null) {
1146                membershipTable.changeSelection(findViewRow(membershipTable, membershipData, nextRelation), 0, false, false);
1147            }
1148        }
1149
1150        @Override
1151        public void actionPerformed(ActionEvent e) {
1152            if (tagTable.getSelectedRowCount() > 0) {
1153                int[] rows = tagTable.getSelectedRows();
1154                deleteTags(rows);
1155            } else if (membershipTable.getSelectedRowCount() > 0) {
1156                ConditionalOptionPaneUtil.startBulkOperation(DELETE_FROM_RELATION_PREF);
1157                int[] rows = membershipTable.getSelectedRows();
1158                // delete from last relation to conserve row numbers in the table
1159                for (int i = rows.length-1; i >= 0; i--) {
1160                    deleteFromRelation(rows[i]);
1161                }
1162                ConditionalOptionPaneUtil.endBulkOperation(DELETE_FROM_RELATION_PREF);
1163            }
1164        }
1165
1166        @Override
1167        protected final void updateEnabledState() {
1168            DataSet ds = OsmDataManager.getInstance().getActiveDataSet();
1169            setEnabled(ds != null && !ds.isLocked() &&
1170                    ((tagTable != null && tagTable.getSelectedRowCount() >= 1)
1171                    || (membershipTable != null && membershipTable.getSelectedRowCount() > 0)
1172                    ));
1173        }
1174
1175        @Override
1176        public void valueChanged(ListSelectionEvent e) {
1177            updateEnabledState();
1178        }
1179    }
1180
1181    /**
1182     * Action handling add button press in properties dialog.
1183     */
1184    class AddAction extends JosmAction {
1185        AtomicBoolean isPerforming = new AtomicBoolean(false);
1186        AddAction() {
1187            super(tr("Add"), /* ICON() */ "dialogs/add", tr("Add a new key/value pair to all objects"),
1188                    Shortcut.registerShortcut("properties:add", tr("Add Tag"), KeyEvent.VK_A,
1189                            Shortcut.ALT), false);
1190        }
1191
1192        @Override
1193        public void actionPerformed(ActionEvent e) {
1194            if (!/*successful*/isPerforming.compareAndSet(false, true)) {
1195                return;
1196            }
1197            try {
1198                editHelper.addTag();
1199                btnAdd.requestFocusInWindow();
1200            } finally {
1201                isPerforming.set(false);
1202            }
1203        }
1204    }
1205
1206    /**
1207     * Action handling edit button press in properties dialog.
1208     */
1209    class EditAction extends JosmAction implements ListSelectionListener {
1210        AtomicBoolean isPerforming = new AtomicBoolean(false);
1211        EditAction() {
1212            super(tr("Edit"), /* ICON() */ "dialogs/edit", tr("Edit the value of the selected key for all objects"),
1213                    Shortcut.registerShortcut("properties:edit", tr("Edit Tags"), KeyEvent.VK_S,
1214                            Shortcut.ALT), false);
1215            updateEnabledState();
1216        }
1217
1218        @Override
1219        public void actionPerformed(ActionEvent e) {
1220            if (!/*successful*/isPerforming.compareAndSet(false, true)) {
1221                return;
1222            }
1223            try {
1224                if (tagTable.getSelectedRowCount() == 1) {
1225                    int row = tagTable.getSelectedRow();
1226                    editHelper.editTag(row, false);
1227                } else if (membershipTable.getSelectedRowCount() == 1) {
1228                    int row = membershipTable.getSelectedRow();
1229                    editMembership(row);
1230                }
1231            } finally {
1232                isPerforming.set(false);
1233            }
1234        }
1235
1236        @Override
1237        protected void updateEnabledState() {
1238            DataSet ds = OsmDataManager.getInstance().getActiveDataSet();
1239            setEnabled(ds != null && !ds.isLocked() &&
1240                    ((tagTable != null && tagTable.getSelectedRowCount() == 1)
1241                    ^ (membershipTable != null && membershipTable.getSelectedRowCount() == 1)
1242                    ));
1243        }
1244
1245        @Override
1246        public void valueChanged(ListSelectionEvent e) {
1247            updateEnabledState();
1248        }
1249    }
1250
1251    class PasteValueAction extends AbstractAction {
1252        PasteValueAction() {
1253            putValue(NAME, tr("Paste Value"));
1254            putValue(SHORT_DESCRIPTION, tr("Paste the value of the selected tag from clipboard"));
1255        }
1256
1257        @Override
1258        public void actionPerformed(ActionEvent ae) {
1259            if (tagTable.getSelectedRowCount() != 1)
1260                return;
1261            String key = editHelper.getDataKey(tagTable.getSelectedRow());
1262            Collection<OsmPrimitive> sel = OsmDataManager.getInstance().getInProgressSelection();
1263            String clipboard = ClipboardUtils.getClipboardStringContent();
1264            if (sel.isEmpty() || clipboard == null || sel.iterator().next().getDataSet().isLocked())
1265                return;
1266            UndoRedoHandler.getInstance().add(new ChangePropertyCommand(sel, key, Utils.strip(clipboard)));
1267        }
1268    }
1269
1270    class SearchAction extends AbstractAction {
1271        private final boolean sameType;
1272
1273        SearchAction(boolean sameType) {
1274            this.sameType = sameType;
1275            if (sameType) {
1276                putValue(NAME, tr("Search Key/Value/Type"));
1277                putValue(SHORT_DESCRIPTION, tr("Search with the key and value of the selected tag, restrict to type (i.e., node/way/relation)"));
1278            } else {
1279                putValue(NAME, tr("Search Key/Value"));
1280                putValue(SHORT_DESCRIPTION, tr("Search with the key and value of the selected tag"));
1281            }
1282        }
1283
1284        @Override
1285        public void actionPerformed(ActionEvent e) {
1286            if (tagTable.getSelectedRowCount() != 1)
1287                return;
1288            String key = editHelper.getDataKey(tagTable.getSelectedRow());
1289            Collection<? extends IPrimitive> sel = OsmDataManager.getInstance().getInProgressISelection();
1290            if (sel.isEmpty())
1291                return;
1292            final SearchSetting ss = createSearchSetting(key, sel, sameType);
1293            org.openstreetmap.josm.actions.search.SearchAction.searchStateless(ss);
1294        }
1295    }
1296
1297    static SearchSetting createSearchSetting(String key, Collection<? extends IPrimitive> sel, boolean sameType) {
1298        String sep = "";
1299        StringBuilder s = new StringBuilder();
1300        Set<String> consideredTokens = new TreeSet<>();
1301        for (IPrimitive p : sel) {
1302            String val = p.get(key);
1303            if (val == null || (!sameType && consideredTokens.contains(val))) {
1304                continue;
1305            }
1306            String t = "";
1307            if (!sameType) {
1308                t = "";
1309            } else if (p instanceof Node) {
1310                t = "type:node ";
1311            } else if (p instanceof Way) {
1312                t = "type:way ";
1313            } else if (p instanceof Relation) {
1314                t = "type:relation ";
1315            }
1316            String token = new StringBuilder(t).append(val).toString();
1317            if (consideredTokens.add(token)) {
1318                s.append(sep).append('(').append(t).append(SearchCompiler.buildSearchStringForTag(key, val)).append(')');
1319                sep = " OR ";
1320            }
1321        }
1322
1323        final SearchSetting ss = new SearchSetting();
1324        ss.text = s.toString();
1325        ss.caseSensitive = true;
1326        return ss;
1327    }
1328
1329    /**
1330     * Clears the row selection when it is filtered away by the row sorter.
1331     */
1332    private class RemoveHiddenSelection implements ListSelectionListener, RowSorterListener {
1333
1334        void removeHiddenSelection() {
1335            try {
1336                tagRowSorter.convertRowIndexToModel(tagTable.getSelectedRow());
1337            } catch (IndexOutOfBoundsException ignore) {
1338                Logging.trace(ignore);
1339                Logging.trace("Clearing tagTable selection");
1340                tagTable.clearSelection();
1341            }
1342        }
1343
1344        @Override
1345        public void valueChanged(ListSelectionEvent event) {
1346            removeHiddenSelection();
1347        }
1348
1349        @Override
1350        public void sorterChanged(RowSorterEvent e) {
1351            removeHiddenSelection();
1352        }
1353    }
1354}