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