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