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

Last change on this file since 13130 was 13130, checked in by Don-vip, 16 months ago

fix #15572 - use ImageProvider attach API for all JOSM actions to ensure proper icon size everywhere

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