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

Last change on this file since 13453 was 13453, checked in by Don-vip, 6 years ago

fix #8039, fix #10456: final fixes for the read-only/locked layers:

  • rename "read-only" to "locked" (in XML and Java classes/interfaces)
  • add a new download policy (true/never) to allow private layers forbidding only to download data, but allowing everything else

This leads to:

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