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