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