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

Last change on this file since 14678 was 14678, checked in by simon04, 2 months ago

see #13160 - use atomic AtomicBoolean.compareAndSet

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