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