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

Last change on this file since 14673 was 14673, checked in by GerdP, 6 weeks ago

fix #17197 (regression from r14509)

Only reset selection when last edit layer is closed

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