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