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