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