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