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