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