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