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