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