source: josm/trunk/src/org/openstreetmap/josm/gui/dialogs/properties/PropertiesDialog.java @ 13854

Last change on this file since 13854 was 13854, checked in by Don-vip, 9 months ago

fix #8939 - prevent add/edit tag dialog to be created twice when hitting simultaneously enter key and button click

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