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