001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.dialogs.properties; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005import static org.openstreetmap.josm.tools.I18n.trn; 006 007import java.awt.BorderLayout; 008import java.awt.Component; 009import java.awt.ComponentOrientation; 010import java.awt.Container; 011import java.awt.Cursor; 012import java.awt.Dimension; 013import java.awt.FlowLayout; 014import java.awt.Font; 015import java.awt.GridBagConstraints; 016import java.awt.GridBagLayout; 017import java.awt.event.ActionEvent; 018import java.awt.event.FocusEvent; 019import java.awt.event.FocusListener; 020import java.awt.event.InputEvent; 021import java.awt.event.KeyEvent; 022import java.awt.event.MouseAdapter; 023import java.awt.event.MouseEvent; 024import java.awt.event.WindowAdapter; 025import java.awt.event.WindowEvent; 026import java.awt.image.BufferedImage; 027import java.text.Normalizer; 028import java.util.ArrayList; 029import java.util.Arrays; 030import java.util.Collection; 031import java.util.Collections; 032import java.util.Comparator; 033import java.util.Iterator; 034import java.util.List; 035import java.util.Map; 036import java.util.Objects; 037import java.util.Optional; 038import java.util.TreeMap; 039import java.util.stream.Collectors; 040import java.util.stream.IntStream; 041 042import javax.swing.AbstractAction; 043import javax.swing.Action; 044import javax.swing.Box; 045import javax.swing.ButtonGroup; 046import javax.swing.ImageIcon; 047import javax.swing.JCheckBoxMenuItem; 048import javax.swing.JComponent; 049import javax.swing.JLabel; 050import javax.swing.JList; 051import javax.swing.JMenu; 052import javax.swing.JOptionPane; 053import javax.swing.JPanel; 054import javax.swing.JPopupMenu; 055import javax.swing.JRadioButtonMenuItem; 056import javax.swing.JTable; 057import javax.swing.KeyStroke; 058import javax.swing.ListCellRenderer; 059import javax.swing.SwingUtilities; 060import javax.swing.event.PopupMenuEvent; 061import javax.swing.event.PopupMenuListener; 062import javax.swing.table.DefaultTableModel; 063 064import org.openstreetmap.josm.actions.JosmAction; 065import org.openstreetmap.josm.actions.search.SearchAction; 066import org.openstreetmap.josm.command.ChangePropertyCommand; 067import org.openstreetmap.josm.command.Command; 068import org.openstreetmap.josm.command.SequenceCommand; 069import org.openstreetmap.josm.data.UndoRedoHandler; 070import org.openstreetmap.josm.data.osm.DataSet; 071import org.openstreetmap.josm.data.osm.OsmDataManager; 072import org.openstreetmap.josm.data.osm.OsmPrimitive; 073import org.openstreetmap.josm.data.osm.OsmPrimitiveType; 074import org.openstreetmap.josm.data.osm.Tag; 075import org.openstreetmap.josm.data.osm.search.SearchCompiler; 076import org.openstreetmap.josm.data.osm.search.SearchParseError; 077import org.openstreetmap.josm.data.osm.search.SearchSetting; 078import org.openstreetmap.josm.data.preferences.BooleanProperty; 079import org.openstreetmap.josm.data.preferences.EnumProperty; 080import org.openstreetmap.josm.data.preferences.IntegerProperty; 081import org.openstreetmap.josm.data.preferences.ListProperty; 082import org.openstreetmap.josm.data.preferences.StringProperty; 083import org.openstreetmap.josm.data.tagging.ac.AutoCompletionItem; 084import org.openstreetmap.josm.gui.ExtendedDialog; 085import org.openstreetmap.josm.gui.IExtendedDialog; 086import org.openstreetmap.josm.gui.MainApplication; 087import org.openstreetmap.josm.gui.tagging.ac.AutoCompComboBox; 088import org.openstreetmap.josm.gui.tagging.ac.AutoCompEvent; 089import org.openstreetmap.josm.gui.tagging.ac.AutoCompListener; 090import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionManager; 091import org.openstreetmap.josm.gui.util.GuiHelper; 092import org.openstreetmap.josm.gui.util.WindowGeometry; 093import org.openstreetmap.josm.gui.widgets.JosmListCellRenderer; 094import org.openstreetmap.josm.gui.widgets.OrientationAction; 095import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher; 096import org.openstreetmap.josm.io.XmlWriter; 097import org.openstreetmap.josm.tools.GBC; 098import org.openstreetmap.josm.tools.ImageProvider; 099import org.openstreetmap.josm.tools.Logging; 100import org.openstreetmap.josm.tools.OsmPrimitiveImageProvider; 101import org.openstreetmap.josm.tools.PlatformManager; 102import org.openstreetmap.josm.tools.Shortcut; 103import org.openstreetmap.josm.tools.Utils; 104 105/** 106 * Class that helps PropertiesDialog add and edit tag values. 107 * @since 5633 108 */ 109public class TagEditHelper { 110 111 private final JTable tagTable; 112 private final DefaultTableModel tagData; 113 private final Map<String, Map<String, Integer>> valueCount; 114 115 // Selection that we are editing by using both dialogs 116 protected Collection<OsmPrimitive> sel; 117 118 private String changedKey; 119 120 static final Comparator<AutoCompletionItem> DEFAULT_AC_ITEM_COMPARATOR = 121 (o1, o2) -> String.CASE_INSENSITIVE_ORDER.compare(o1.getValue(), o2.getValue()); 122 123 /** Default number of recent tags */ 124 public static final int DEFAULT_LRU_TAGS_NUMBER = 5; 125 /** Maximum number of recent tags */ 126 public static final int MAX_LRU_TAGS_NUMBER = 30; 127 /** Autocomplete keys by default */ 128 public static final BooleanProperty AUTOCOMPLETE_KEYS = new BooleanProperty("properties.autocomplete-keys", true); 129 /** Autocomplete values by default */ 130 public static final BooleanProperty AUTOCOMPLETE_VALUES = new BooleanProperty("properties.autocomplete-values", true); 131 /** Use English language for tag by default */ 132 public static final BooleanProperty PROPERTY_FIX_TAG_LOCALE = new BooleanProperty("properties.fix-tag-combobox-locale", false); 133 /** Whether recent tags must be remembered */ 134 public static final BooleanProperty PROPERTY_REMEMBER_TAGS = new BooleanProperty("properties.remember-recently-added-tags", true); 135 /** Number of recent tags */ 136 public static final IntegerProperty PROPERTY_RECENT_TAGS_NUMBER = new IntegerProperty("properties.recently-added-tags", 137 DEFAULT_LRU_TAGS_NUMBER); 138 /** The preference storage of recent tags */ 139 public static final ListProperty PROPERTY_RECENT_TAGS = new ListProperty("properties.recent-tags", 140 Collections.<String>emptyList()); 141 /** The preference list of tags which should not be remembered, since r9940 */ 142 public static final StringProperty PROPERTY_TAGS_TO_IGNORE = new StringProperty("properties.recent-tags.ignore", 143 new SearchSetting().writeToString()); 144 145 /** 146 * What to do with recent tags where keys already exist 147 */ 148 private enum RecentExisting { 149 ENABLE, 150 DISABLE, 151 HIDE 152 } 153 154 /** 155 * Preference setting for popup menu item "Recent tags with existing key" 156 */ 157 public static final EnumProperty<RecentExisting> PROPERTY_RECENT_EXISTING = new EnumProperty<>( 158 "properties.recently-added-tags-existing-key", RecentExisting.class, RecentExisting.DISABLE); 159 160 /** 161 * What to do after applying tag 162 */ 163 private enum RefreshRecent { 164 NO, 165 STATUS, 166 REFRESH 167 } 168 169 /** 170 * Preference setting for popup menu item "Refresh recent tags list after applying tag" 171 */ 172 public static final EnumProperty<RefreshRecent> PROPERTY_REFRESH_RECENT = new EnumProperty<>( 173 "properties.refresh-recently-added-tags", RefreshRecent.class, RefreshRecent.STATUS); 174 175 final RecentTagCollection recentTags = new RecentTagCollection(MAX_LRU_TAGS_NUMBER); 176 SearchSetting tagsToIgnore; 177 178 /** 179 * Copy of recently added tags in sorted from newest to oldest order. 180 * 181 * We store the maximum number of recent tags to allow dynamic change of number of tags shown in the preferences. 182 * Used to cache initial status. 183 */ 184 private List<Tag> tags; 185 186 static { 187 // init user input based on recent tags 188 final RecentTagCollection recentTags = new RecentTagCollection(MAX_LRU_TAGS_NUMBER); 189 recentTags.loadFromPreference(PROPERTY_RECENT_TAGS); 190 recentTags.toList().forEach(tag -> AutoCompletionManager.rememberUserInput(tag.getKey(), tag.getValue(), false)); 191 } 192 193 /** 194 * A custom list cell renderer that adds the value count to some items. 195 */ 196 static class TEHListCellRenderer extends JosmListCellRenderer<AutoCompletionItem> { 197 protected Map<String, Integer> map; 198 199 TEHListCellRenderer(Component component, ListCellRenderer<? super AutoCompletionItem> renderer, Map<String, Integer> map) { 200 super(component, renderer); 201 this.map = map; 202 } 203 204 @Override 205 public Component getListCellRendererComponent(JList<? extends AutoCompletionItem> list, AutoCompletionItem value, 206 int index, boolean isSelected, boolean cellHasFocus) { 207 Integer count = null; 208 // if there is a value count add it to the text 209 if (map != null) { 210 String text = value == null ? "" : value.toString(); 211 count = map.get(text); 212 if (count != null) { 213 value = new AutoCompletionItem(tr("{0} ({1})", text, count)); 214 } 215 } 216 Component l = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); 217 l.setComponentOrientation(component.getComponentOrientation()); 218 if (count != null) { 219 l.setFont(l.getFont().deriveFont(Font.ITALIC + Font.BOLD)); 220 } 221 return l; 222 } 223 } 224 225 /** 226 * Constructs a new {@code TagEditHelper}. 227 * @param tagTable tag table 228 * @param propertyData table model 229 * @param valueCount tag value count 230 */ 231 public TagEditHelper(JTable tagTable, DefaultTableModel propertyData, Map<String, Map<String, Integer>> valueCount) { 232 this.tagTable = tagTable; 233 this.tagData = propertyData; 234 this.valueCount = valueCount; 235 } 236 237 /** 238 * Finds the key from given row of tag editor. 239 * @param viewRow index of row 240 * @return key of tag 241 */ 242 public final String getDataKey(int viewRow) { 243 return tagData.getValueAt(tagTable.convertRowIndexToModel(viewRow), 0).toString(); 244 } 245 246 /** 247 * Determines if the given tag key is already used (by all selected primitives, not just some of them) 248 * @param key the key to check 249 * @return {@code true} if the key is used by all selected primitives (key not unset for at least one primitive) 250 */ 251 @SuppressWarnings("unchecked") 252 boolean containsDataKey(String key) { 253 return IntStream.range(0, tagData.getRowCount()) 254 .anyMatch(i -> key.equals(tagData.getValueAt(i, 0)) /* sic! do not use getDataKey*/ 255 && !((Map<String, Integer>) tagData.getValueAt(i, 1)).containsKey("") /* sic! do not use getDataValues*/); 256 } 257 258 /** 259 * Finds the values from given row of tag editor. 260 * @param viewRow index of row 261 * @return map of values and number of occurrences 262 */ 263 @SuppressWarnings("unchecked") 264 public final Map<String, Integer> getDataValues(int viewRow) { 265 return (Map<String, Integer>) tagData.getValueAt(tagTable.convertRowIndexToModel(viewRow), 1); 266 } 267 268 /** 269 * Open the add selection dialog and add a new key/value to the table (and 270 * to the dataset, of course). 271 */ 272 public void addTag() { 273 changedKey = null; 274 DataSet activeDataSet = OsmDataManager.getInstance().getActiveDataSet(); 275 if (activeDataSet == null) 276 return; 277 try { 278 activeDataSet.beginUpdate(); 279 sel = OsmDataManager.getInstance().getInProgressSelection(); 280 if (Utils.isEmpty(sel)) 281 return; 282 283 final AddTagsDialog addDialog = getAddTagsDialog(); 284 285 addDialog.showDialog(); 286 287 addDialog.destroyActions(); 288 if (addDialog.getValue() == 1) 289 addDialog.performTagAdding(); 290 else 291 addDialog.undoAllTagsAdding(); 292 } finally { 293 activeDataSet.endUpdate(); 294 } 295 } 296 297 /** 298 * Returns a new {@code AddTagsDialog}. 299 * @return a new {@code AddTagsDialog} 300 */ 301 protected AddTagsDialog getAddTagsDialog() { 302 return new AddTagsDialog(); 303 } 304 305 /** 306 * Edit the value in the tags table row. 307 * @param row The row of the table from which the value is edited. 308 * @param focusOnKey Determines if the initial focus should be set on key instead of value 309 * @since 5653 310 */ 311 public void editTag(final int row, boolean focusOnKey) { 312 changedKey = null; 313 sel = OsmDataManager.getInstance().getInProgressSelection(); 314 if (Utils.isEmpty(sel)) 315 return; 316 317 final IEditTagDialog editDialog = getEditTagDialog(row, focusOnKey, getDataKey(row)); 318 editDialog.showDialog(); 319 if (editDialog.getValue() != 1) 320 return; 321 editDialog.performTagEdit(); 322 } 323 324 /** 325 * Extracted interface of {@link EditTagDialog}. 326 */ 327 protected interface IEditTagDialog extends IExtendedDialog { 328 /** 329 * Edit tags of multiple selected objects according to selected ComboBox values 330 * If value == "", tag will be deleted 331 * Confirmations may be needed. 332 */ 333 void performTagEdit(); 334 } 335 336 protected IEditTagDialog getEditTagDialog(int row, boolean focusOnKey, String key) { 337 return new EditTagDialog(key, getDataValues(row), focusOnKey); 338 } 339 340 /** 341 * If during last editProperty call user changed the key name, this key will be returned 342 * Elsewhere, returns null. 343 * @return The modified key, or {@code null} 344 */ 345 public String getChangedKey() { 346 return changedKey; 347 } 348 349 /** 350 * Reset last changed key. 351 */ 352 public void resetChangedKey() { 353 changedKey = null; 354 } 355 356 /** 357 * For a given key k, return a list of keys which are used as keys for 358 * auto-completing values to increase the search space. 359 * @param key the key k 360 * @return a list of keys 361 */ 362 private static List<String> getAutocompletionKeys(String key) { 363 if ("name".equals(key) || "addr:street".equals(key)) 364 return Arrays.asList("addr:street", "name"); 365 else 366 return Arrays.asList(key); 367 } 368 369 /** 370 * Load recently used tags from preferences if needed. 371 */ 372 public void loadTagsIfNeeded() { 373 loadTagsToIgnore(); 374 if (PROPERTY_REMEMBER_TAGS.get() && recentTags.isEmpty()) { 375 recentTags.loadFromPreference(PROPERTY_RECENT_TAGS); 376 } 377 } 378 379 void loadTagsToIgnore() { 380 final SearchSetting searchSetting = Utils.firstNonNull( 381 SearchSetting.readFromString(PROPERTY_TAGS_TO_IGNORE.get()), new SearchSetting()); 382 if (!Objects.equals(tagsToIgnore, searchSetting)) { 383 try { 384 tagsToIgnore = searchSetting; 385 recentTags.setTagsToIgnore(tagsToIgnore); 386 } catch (SearchParseError parseError) { 387 warnAboutParseError(parseError); 388 tagsToIgnore = new SearchSetting(); 389 recentTags.setTagsToIgnore(SearchCompiler.Never.INSTANCE); 390 } 391 } 392 } 393 394 private static void warnAboutParseError(SearchParseError parseError) { 395 Logging.warn(parseError); 396 JOptionPane.showMessageDialog( 397 MainApplication.getMainFrame(), 398 parseError.getMessage(), 399 tr("Error"), 400 JOptionPane.ERROR_MESSAGE 401 ); 402 } 403 404 /** 405 * Store recently used tags in preferences if needed. 406 */ 407 public void saveTagsIfNeeded() { 408 if (PROPERTY_REMEMBER_TAGS.get() && !recentTags.isEmpty()) { 409 recentTags.saveToPreference(PROPERTY_RECENT_TAGS); 410 } 411 } 412 413 /** 414 * Forget recently selected primitives to allow GC. 415 * @since 14509 416 */ 417 public void resetSelection() { 418 sel = null; 419 } 420 421 /** 422 * Update cache of recent tags used for displaying tags. 423 */ 424 private void cacheRecentTags() { 425 tags = recentTags.toList(); 426 Collections.reverse(tags); 427 } 428 429 /** 430 * Returns the edited item with whitespaces removed 431 * @param cb the combobox 432 * @return the edited item with whitespaces removed 433 * @since 18173 434 */ 435 public static String getEditItem(AutoCompComboBox<AutoCompletionItem> cb) { 436 return Utils.removeWhiteSpaces(cb.getEditorItemAsString()); 437 } 438 439 /** 440 * Returns the selected item or the edited item as string 441 * @param cb the combobox 442 * @return the selected item or the edited item as string 443 * @since 18173 444 */ 445 public static String getSelectedOrEditItem(AutoCompComboBox<AutoCompletionItem> cb) { 446 final Object selectedItem = cb.getSelectedItem(); 447 if (selectedItem != null) 448 return selectedItem.toString(); 449 return getEditItem(cb); 450 } 451 452 /** 453 * Warns user about a key being overwritten. 454 * @param action The action done by the user. Must state what key is changed 455 * @param togglePref The preference to save the checkbox state to 456 * @return {@code true} if the user accepts to overwrite key, {@code false} otherwise 457 */ 458 private static boolean warnOverwriteKey(String action, String togglePref) { 459 return new ExtendedDialog( 460 MainApplication.getMainFrame(), 461 tr("Overwrite tag"), 462 tr("Overwrite"), tr("Cancel")) 463 .setButtonIcons("ok", "cancel") 464 .setContent(action) 465 .setCancelButton(2) 466 .toggleEnable(togglePref) 467 .showDialog().getValue() == 1; 468 } 469 470 protected class EditTagDialog extends AbstractTagsDialog implements IEditTagDialog { 471 private final String key; 472 private final transient Map<String, Integer> m; 473 private final transient Comparator<AutoCompletionItem> usedValuesAwareComparator; 474 private final transient AutoCompletionManager autocomplete; 475 476 protected EditTagDialog(String key, Map<String, Integer> map, boolean initialFocusOnKey) { 477 super(MainApplication.getMainFrame(), trn("Change value?", "Change values?", map.size()), tr("OK"), tr("Cancel")); 478 setButtonIcons("ok", "cancel"); 479 setCancelButton(2); 480 configureContextsensitiveHelp("/Dialog/EditValue", true /* show help button */); 481 this.key = key; 482 this.m = map; 483 this.initialFocusOnKey = initialFocusOnKey; 484 485 usedValuesAwareComparator = (o1, o2) -> { 486 boolean c1 = m.containsKey(o1.getValue()); 487 boolean c2 = m.containsKey(o2.getValue()); 488 if (c1 == c2) 489 return String.CASE_INSENSITIVE_ORDER.compare(o1.getValue(), o2.getValue()); 490 else if (c1) 491 return -1; 492 else 493 return +1; 494 }; 495 496 JPanel mainPanel = new JPanel(new BorderLayout()); 497 498 String msg = "<html>"+trn("This will change {0} object.", 499 "This will change up to {0} objects.", sel.size(), sel.size()) 500 +"<br><br>("+tr("An empty value deletes the tag.", key)+")</html>"; 501 502 mainPanel.add(new JLabel(msg), BorderLayout.NORTH); 503 504 JPanel p = new JPanel(new GridBagLayout()) { 505 /** 506 * This hack allows the comboboxes to have their own orientation. 507 * 508 * The problem is that 509 * {@link org.openstreetmap.josm.gui.ExtendedDialog#showDialog ExtendedDialog} calls 510 * {@code applyComponentOrientation} very late in the dialog construction process 511 * thus overwriting the orientation the components have chosen for themselves. 512 * 513 * This stops the propagation of {@code applyComponentOrientation}, thus all 514 * components may (and have to) set their own orientation. 515 */ 516 @Override 517 public void applyComponentOrientation(ComponentOrientation o) { 518 setComponentOrientation(o); 519 } 520 }; 521 mainPanel.add(p, BorderLayout.CENTER); 522 523 autocomplete = AutoCompletionManager.of(OsmDataManager.getInstance().getActiveDataSet()); 524 List<AutoCompletionItem> keyList = autocomplete.getTagKeys(DEFAULT_AC_ITEM_COMPARATOR); 525 526 keys = new AutoCompComboBox<>(); 527 keys.getModel().setComparator(Comparator.naturalOrder()); // according to Comparable 528 keys.setEditable(true); 529 keys.setPrototypeDisplayValue(new AutoCompletionItem("dummy")); 530 keys.getModel().addAllElements(keyList); 531 keys.setSelectedItemText(key); 532 533 p.add(Box.createVerticalStrut(5), GBC.eol()); 534 p.add(new JLabel(tr("Key")), GBC.std()); 535 p.add(Box.createHorizontalStrut(10), GBC.std()); 536 p.add(keys, GBC.eol().fill(GBC.HORIZONTAL)); 537 538 List<AutoCompletionItem> valueList = autocomplete.getTagValues(getAutocompletionKeys(key), usedValuesAwareComparator); 539 540 final String selection = m.size() != 1 ? tr("<different>") : m.entrySet().iterator().next().getKey(); 541 542 values = new AutoCompComboBox<>(); 543 values.getModel().setComparator(Comparator.naturalOrder()); 544 values.setRenderer(new TEHListCellRenderer(values, values.getRenderer(), valueCount.get(key))); 545 values.setEditable(true); 546 values.setPrototypeDisplayValue(new AutoCompletionItem("dummy")); 547 values.getModel().addAllElements(valueList); 548 values.setSelectedItemText(selection); 549 550 p.add(Box.createVerticalStrut(5), GBC.eol()); 551 p.add(new JLabel(tr("Value")), GBC.std()); 552 p.add(Box.createHorizontalStrut(10), GBC.std()); 553 p.add(values, GBC.eol().fill(GBC.HORIZONTAL)); 554 p.add(Box.createVerticalStrut(2), GBC.eol()); 555 556 p.applyComponentOrientation(OrientationAction.getDefaultComponentOrientation()); 557 keys.applyComponentOrientation(ComponentOrientation.LEFT_TO_RIGHT); 558 values.applyComponentOrientation(OrientationAction.getNamelikeOrientation(keys.getText())); 559 560 setContent(mainPanel, false); 561 562 addEventListeners(); 563 } 564 565 @Override 566 public void autoCompBefore(AutoCompEvent e) { 567 updateValueModel(autocomplete, usedValuesAwareComparator); 568 } 569 570 @Override 571 public void popupMenuWillBecomeVisible(PopupMenuEvent e) { 572 updateValueModel(autocomplete, usedValuesAwareComparator); 573 } 574 575 @Override 576 public void performTagEdit() { 577 String value = getEditItem(values); 578 value = Normalizer.normalize(value, Normalizer.Form.NFC); 579 if (value.isEmpty()) { 580 value = null; // delete the key 581 } 582 String newkey = getEditItem(keys); 583 newkey = Normalizer.normalize(newkey, Normalizer.Form.NFC); 584 if (newkey.isEmpty()) { 585 newkey = key; 586 value = null; // delete the key instead 587 } 588 if (key.equals(newkey) && tr("<different>").equals(value)) 589 return; 590 if (key.equals(newkey) || value == null) { 591 UndoRedoHandler.getInstance().add(new ChangePropertyCommand(sel, newkey, value)); 592 if (value != null) { 593 AutoCompletionManager.rememberUserInput(newkey, value, true); 594 recentTags.add(new Tag(key, value)); 595 } 596 } else { 597 for (OsmPrimitive osm: sel) { 598 if (osm.get(newkey) != null) { 599 if (!warnOverwriteKey(tr("You changed the key from ''{0}'' to ''{1}''.", key, newkey) 600 + "\n" + tr("The new key is already used, overwrite values?"), 601 "overwriteEditKey")) 602 return; 603 break; 604 } 605 } 606 Collection<Command> commands = new ArrayList<>(); 607 commands.add(new ChangePropertyCommand(sel, key, null)); 608 if (value.equals(tr("<different>"))) { 609 String newKey = newkey; 610 sel.stream() 611 .filter(osm -> osm.hasKey(key)) 612 .collect(Collectors.groupingBy(osm -> osm.get(key))) 613 .forEach((newValue, osmPrimitives) -> commands.add(new ChangePropertyCommand(osmPrimitives, newKey, newValue))); 614 } else { 615 commands.add(new ChangePropertyCommand(sel, newkey, value)); 616 AutoCompletionManager.rememberUserInput(newkey, value, false); 617 } 618 UndoRedoHandler.getInstance().add(new SequenceCommand( 619 trn("Change properties of up to {0} object", 620 "Change properties of up to {0} objects", sel.size(), sel.size()), 621 commands)); 622 } 623 624 changedKey = newkey; 625 } 626 } 627 628 protected abstract class AbstractTagsDialog extends ExtendedDialog implements AutoCompListener, FocusListener, PopupMenuListener { 629 protected AutoCompComboBox<AutoCompletionItem> keys; 630 protected AutoCompComboBox<AutoCompletionItem> values; 631 protected boolean initialFocusOnKey = true; 632 /** 633 * The 'values' model is currently holding values for this key. Used for lazy-loading of values. 634 */ 635 protected String currentValuesModelKey = ""; 636 637 AbstractTagsDialog(Component parent, String title, String... buttonTexts) { 638 super(parent, title, buttonTexts); 639 addMouseListener(new PopupMenuLauncher(popupMenu)); 640 } 641 642 @Override 643 public void setupDialog() { 644 super.setupDialog(); 645 buttons.get(0).setEnabled(!OsmDataManager.getInstance().getActiveDataSet().isLocked()); 646 final Dimension size = getSize(); 647 // Set resizable only in width 648 setMinimumSize(size); 649 setPreferredSize(size); 650 // setMaximumSize does not work, and never worked, but still it seems not to bother Oracle to fix this 10-year-old bug 651 // https://bugs.openjdk.java.net/browse/JDK-6200438 652 // https://bugs.openjdk.java.net/browse/JDK-6464548 653 654 setRememberWindowGeometry(getClass().getName() + ".geometry", 655 WindowGeometry.centerInWindow(MainApplication.getMainFrame(), size)); 656 keys.setFixedLocale(PROPERTY_FIX_TAG_LOCALE.get()); 657 } 658 659 @Override 660 public void setVisible(boolean visible) { 661 // Do not want dialog to be resizable in height, as its size may increase each time because of the recently added tags 662 // So need to modify the stored geometry (size part only) in order to use the automatic positioning mechanism 663 if (visible) { 664 WindowGeometry geometry = initWindowGeometry(); 665 Dimension storedSize = geometry.getSize(); 666 Dimension size = getSize(); 667 if (!storedSize.equals(size)) { 668 if (storedSize.width < size.width) { 669 storedSize.width = size.width; 670 } 671 if (storedSize.height != size.height) { 672 storedSize.height = size.height; 673 } 674 rememberWindowGeometry(geometry); 675 } 676 updateOkButtonIcon(); 677 } 678 super.setVisible(visible); 679 } 680 681 /** 682 * Updates the values model if the key has changed 683 * 684 * @param autocomplete the autocompletion manager 685 * @param comparator sorting order for the items in the combo dropdown 686 */ 687 protected void updateValueModel(AutoCompletionManager autocomplete, Comparator<AutoCompletionItem> comparator) { 688 String key = keys.getText(); 689 if (!key.equals(currentValuesModelKey)) { 690 Logging.debug("updateValueModel: lazy loading values for key ''{0}''", key); 691 // key has changed, reload model 692 String savedText = values.getText(); 693 values.getModel().removeAllElements(); 694 values.getModel().addAllElements(autocomplete.getTagValues(getAutocompletionKeys(key), comparator)); 695 values.applyComponentOrientation(OrientationAction.getNamelikeOrientation(key)); 696 values.setSelectedItemText(savedText); 697 values.getEditor().selectAll(); 698 currentValuesModelKey = key; 699 } 700 } 701 702 protected void addEventListeners() { 703 // OK on Enter in values 704 values.getEditor().addActionListener(e -> buttonAction(0, null)); 705 // update values orientation according to key 706 keys.getEditorComponent().addFocusListener(this); 707 // update the "values" data model before an autocomplete or list dropdown 708 values.getEditorComponent().addAutoCompListener(this); 709 values.addPopupMenuListener(this); 710 // set the initial focus to either combobox 711 addWindowListener(new WindowAdapter() { 712 @Override 713 public void windowOpened(WindowEvent e) { 714 if (initialFocusOnKey) { 715 keys.requestFocus(); 716 } else { 717 values.requestFocus(); 718 } 719 } 720 }); 721 } 722 723 @Override 724 public void autoCompPerformed(AutoCompEvent e) { 725 } 726 727 @Override 728 public void popupMenuWillBecomeInvisible(PopupMenuEvent e) { 729 } 730 731 @Override 732 public void popupMenuCanceled(PopupMenuEvent e) { 733 } 734 735 @Override 736 public void focusGained(FocusEvent e) { 737 } 738 739 @Override 740 public void focusLost(FocusEvent e) { 741 // update the values combobox orientation if the key changed 742 values.applyComponentOrientation(OrientationAction.getNamelikeOrientation(keys.getText())); 743 } 744 745 protected void updateOkButtonIcon() { 746 if (buttons.isEmpty()) { 747 return; 748 } 749 buttons.get(0).setIcon(findIcon(getSelectedOrEditItem(keys), getSelectedOrEditItem(values)) 750 .orElse(ImageProvider.get("ok", ImageProvider.ImageSizes.LARGEICON))); 751 } 752 753 protected Optional<ImageIcon> findIcon(String key, String value) { 754 final Iterator<OsmPrimitive> osmPrimitiveIterator = sel.iterator(); 755 final OsmPrimitiveType type = osmPrimitiveIterator.hasNext() ? osmPrimitiveIterator.next().getType() : OsmPrimitiveType.NODE; 756 return OsmPrimitiveImageProvider.getResource(key, value, type) 757 .map(resource -> resource.getPaddedIcon(ImageProvider.ImageSizes.LARGEICON.getImageDimension())); 758 } 759 760 protected JPopupMenu popupMenu = new JPopupMenu() { 761 private final JCheckBoxMenuItem fixTagLanguageCb = new JCheckBoxMenuItem( 762 new AbstractAction(tr("Use English language for tag by default")) { 763 @Override 764 public void actionPerformed(ActionEvent e) { 765 boolean use = ((JCheckBoxMenuItem) e.getSource()).getState(); 766 PROPERTY_FIX_TAG_LOCALE.put(use); 767 keys.setFixedLocale(use); 768 } 769 }); 770 { 771 add(fixTagLanguageCb); 772 fixTagLanguageCb.setState(PROPERTY_FIX_TAG_LOCALE.get()); 773 } 774 }; 775 } 776 777 protected class AddTagsDialog extends AbstractTagsDialog { 778 private final List<JosmAction> recentTagsActions = new ArrayList<>(); 779 private final JPanel mainPanel; 780 private JPanel recentTagsPanel; 781 782 // Counter of added commands for possible undo 783 private int commandCount; 784 private final transient AutoCompletionManager autocomplete; 785 786 protected AddTagsDialog() { 787 super(MainApplication.getMainFrame(), tr("Add tag"), tr("OK"), tr("Cancel")); 788 setButtonIcons("ok", "cancel"); 789 setCancelButton(2); 790 configureContextsensitiveHelp("/Dialog/AddValue", true /* show help button */); 791 792 mainPanel = new JPanel(new GridBagLayout()) { 793 /** 794 * This hack allows the comboboxes to have their own orientation. 795 * 796 * The problem is that 797 * {@link org.openstreetmap.josm.gui.ExtendedDialog#showDialog ExtendedDialog} calls 798 * {@code applyComponentOrientation} very late in the dialog construction process 799 * thus overwriting the orientation the components have chosen for themselves. 800 * 801 * This stops the propagation of {@code applyComponentOrientation}, thus all 802 * components may (and have to) set their own orientation. 803 */ 804 @Override 805 public void applyComponentOrientation(ComponentOrientation o) { 806 setComponentOrientation(o); 807 } 808 }; 809 mainPanel.add(new JLabel("<html>"+trn("This will change up to {0} object.", 810 "This will change up to {0} objects.", sel.size(), sel.size()) 811 +"<br><br>"+tr("Please select a key")), GBC.eol().fill(GBC.HORIZONTAL)); 812 813 keys = new AutoCompComboBox<>(); 814 keys.setPrototypeDisplayValue(new AutoCompletionItem("dummy")); 815 keys.setEditable(true); 816 keys.getModel().setComparator(Comparator.naturalOrder()); // according to Comparable 817 keys.setAutocompleteEnabled(AUTOCOMPLETE_KEYS.get()); 818 819 mainPanel.add(keys, GBC.eop().fill(GBC.HORIZONTAL)); 820 mainPanel.add(new JLabel(tr("Choose a value")), GBC.eol()); 821 822 values = new AutoCompComboBox<>(); 823 values.setPrototypeDisplayValue(new AutoCompletionItem("dummy")); 824 values.setEditable(true); 825 values.getModel().setComparator(Comparator.naturalOrder()); 826 values.setAutocompleteEnabled(AUTOCOMPLETE_VALUES.get()); 827 828 mainPanel.add(values, GBC.eop().fill(GBC.HORIZONTAL)); 829 830 cacheRecentTags(); 831 autocomplete = AutoCompletionManager.of(OsmDataManager.getInstance().getActiveDataSet()); 832 List<AutoCompletionItem> keyList = autocomplete.getTagKeys(DEFAULT_AC_ITEM_COMPARATOR); 833 834 // remove the object's tag keys from the list 835 keyList.removeIf(item -> containsDataKey(item.getValue())); 836 837 keys.getModel().addAllElements(keyList); 838 839 updateValueModel(autocomplete, DEFAULT_AC_ITEM_COMPARATOR); 840 841 // pre-fill first recent tag for which the key is not already present 842 tags.stream() 843 .filter(tag -> !containsDataKey(tag.getKey())) 844 .findFirst() 845 .ifPresent(tag -> { 846 keys.setSelectedItemText(tag.getKey()); 847 values.setSelectedItemText(tag.getValue()); 848 }); 849 850 851 keys.addActionListener(ignore -> updateOkButtonIcon()); 852 values.addActionListener(ignore -> updateOkButtonIcon()); 853 854 // Add tag on Shift-Enter 855 mainPanel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put( 856 KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, InputEvent.SHIFT_DOWN_MASK), "addAndContinue"); 857 mainPanel.getActionMap().put("addAndContinue", new AbstractAction() { 858 @Override 859 public void actionPerformed(ActionEvent e) { 860 performTagAdding(); 861 refreshRecentTags(); 862 keys.requestFocus(); 863 } 864 }); 865 866 suggestRecentlyAddedTags(); 867 868 mainPanel.add(Box.createVerticalGlue(), GBC.eop().fill()); 869 mainPanel.applyComponentOrientation(OrientationAction.getDefaultComponentOrientation()); 870 871 setContent(mainPanel, false); 872 873 addEventListeners(); 874 875 popupMenu.add(new AbstractAction(tr("Set number of recently added tags")) { 876 @Override 877 public void actionPerformed(ActionEvent e) { 878 selectNumberOfTags(); 879 suggestRecentlyAddedTags(); 880 } 881 }); 882 883 popupMenu.add(buildMenuRecentExisting()); 884 popupMenu.add(buildMenuRefreshRecent()); 885 886 JCheckBoxMenuItem rememberLastTags = new JCheckBoxMenuItem( 887 new AbstractAction(tr("Remember last used tags after a restart")) { 888 @Override 889 public void actionPerformed(ActionEvent e) { 890 boolean state = ((JCheckBoxMenuItem) e.getSource()).getState(); 891 PROPERTY_REMEMBER_TAGS.put(state); 892 if (state) 893 saveTagsIfNeeded(); 894 } 895 }); 896 rememberLastTags.setState(PROPERTY_REMEMBER_TAGS.get()); 897 popupMenu.add(rememberLastTags); 898 } 899 900 @Override 901 public void autoCompBefore(AutoCompEvent e) { 902 updateValueModel(autocomplete, DEFAULT_AC_ITEM_COMPARATOR); 903 } 904 905 @Override 906 public void popupMenuWillBecomeVisible(PopupMenuEvent e) { 907 updateValueModel(autocomplete, DEFAULT_AC_ITEM_COMPARATOR); 908 } 909 910 private JMenu buildMenuRecentExisting() { 911 JMenu menu = new JMenu(tr("Recent tags with existing key")); 912 TreeMap<RecentExisting, String> radios = new TreeMap<>(); 913 radios.put(RecentExisting.ENABLE, tr("Enable")); 914 radios.put(RecentExisting.DISABLE, tr("Disable")); 915 radios.put(RecentExisting.HIDE, tr("Hide")); 916 ButtonGroup buttonGroup = new ButtonGroup(); 917 for (final Map.Entry<RecentExisting, String> entry : radios.entrySet()) { 918 JRadioButtonMenuItem radio = new JRadioButtonMenuItem(new AbstractAction(entry.getValue()) { 919 @Override 920 public void actionPerformed(ActionEvent e) { 921 PROPERTY_RECENT_EXISTING.put(entry.getKey()); 922 suggestRecentlyAddedTags(); 923 } 924 }); 925 buttonGroup.add(radio); 926 radio.setSelected(PROPERTY_RECENT_EXISTING.get() == entry.getKey()); 927 menu.add(radio); 928 } 929 return menu; 930 } 931 932 private JMenu buildMenuRefreshRecent() { 933 JMenu menu = new JMenu(tr("Refresh recent tags list after applying tag")); 934 TreeMap<RefreshRecent, String> radios = new TreeMap<>(); 935 radios.put(RefreshRecent.NO, tr("No refresh")); 936 radios.put(RefreshRecent.STATUS, tr("Refresh tag status only (enabled / disabled)")); 937 radios.put(RefreshRecent.REFRESH, tr("Refresh tag status and list of recently added tags")); 938 ButtonGroup buttonGroup = new ButtonGroup(); 939 for (final Map.Entry<RefreshRecent, String> entry : radios.entrySet()) { 940 JRadioButtonMenuItem radio = new JRadioButtonMenuItem(new AbstractAction(entry.getValue()) { 941 @Override 942 public void actionPerformed(ActionEvent e) { 943 PROPERTY_REFRESH_RECENT.put(entry.getKey()); 944 } 945 }); 946 buttonGroup.add(radio); 947 radio.setSelected(PROPERTY_REFRESH_RECENT.get() == entry.getKey()); 948 menu.add(radio); 949 } 950 return menu; 951 } 952 953 @Override 954 public void setContentPane(Container contentPane) { 955 final int commandDownMask = PlatformManager.getPlatform().getMenuShortcutKeyMaskEx(); 956 List<String> lines = new ArrayList<>(); 957 Shortcut.findShortcut(KeyEvent.VK_1, commandDownMask).ifPresent(sc -> 958 lines.add(sc.getKeyText() + ' ' + tr("to apply first suggestion")) 959 ); 960 lines.add(Shortcut.getKeyText(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, KeyEvent.SHIFT_DOWN_MASK)) + ' ' 961 +tr("to add without closing the dialog")); 962 Shortcut.findShortcut(KeyEvent.VK_1, commandDownMask | KeyEvent.SHIFT_DOWN_MASK).ifPresent(sc -> 963 lines.add(sc.getKeyText() + ' ' + tr("to add first suggestion without closing the dialog")) 964 ); 965 final JLabel helpLabel = new JLabel("<html>" + String.join("<br>", lines) + "</html>"); 966 helpLabel.setFont(helpLabel.getFont().deriveFont(Font.PLAIN)); 967 contentPane.add(helpLabel, GBC.eol().fill(GridBagConstraints.HORIZONTAL).insets(5, 5, 5, 5)); 968 super.setContentPane(contentPane); 969 } 970 971 protected void selectNumberOfTags() { 972 String s = String.format("%d", PROPERTY_RECENT_TAGS_NUMBER.get()); 973 while (true) { 974 s = JOptionPane.showInputDialog(this, tr("Please enter the number of recently added tags to display"), s); 975 if (Utils.isEmpty(s)) { 976 return; 977 } 978 try { 979 int v = Integer.parseInt(s); 980 if (v >= 0 && v <= MAX_LRU_TAGS_NUMBER) { 981 PROPERTY_RECENT_TAGS_NUMBER.put(v); 982 return; 983 } 984 } catch (NumberFormatException ex) { 985 Logging.warn(ex); 986 } 987 JOptionPane.showMessageDialog(this, tr("Please enter integer number between 0 and {0}", MAX_LRU_TAGS_NUMBER)); 988 } 989 } 990 991 protected void suggestRecentlyAddedTags() { 992 if (recentTagsPanel == null) { 993 recentTagsPanel = new JPanel(new GridBagLayout()); 994 buildRecentTagsPanel(); 995 mainPanel.add(recentTagsPanel, GBC.eol().fill(GBC.HORIZONTAL)); 996 } else { 997 Dimension panelOldSize = recentTagsPanel.getPreferredSize(); 998 recentTagsPanel.removeAll(); 999 buildRecentTagsPanel(); 1000 Dimension panelNewSize = recentTagsPanel.getPreferredSize(); 1001 Dimension dialogOldSize = getMinimumSize(); 1002 Dimension dialogNewSize = new Dimension(dialogOldSize.width, dialogOldSize.height-panelOldSize.height+panelNewSize.height); 1003 setMinimumSize(dialogNewSize); 1004 setPreferredSize(dialogNewSize); 1005 setSize(dialogNewSize); 1006 revalidate(); 1007 repaint(); 1008 } 1009 } 1010 1011 protected void buildRecentTagsPanel() { 1012 final int tagsToShow = Math.min(PROPERTY_RECENT_TAGS_NUMBER.get(), MAX_LRU_TAGS_NUMBER); 1013 if (!(tagsToShow > 0 && !recentTags.isEmpty())) 1014 return; 1015 recentTagsPanel.add(new JLabel(tr("Recently added tags")), GBC.eol()); 1016 1017 int count = 0; 1018 destroyActions(); 1019 for (int i = 0; i < tags.size() && count < tagsToShow; i++) { 1020 final Tag t = tags.get(i); 1021 boolean keyExists = containsDataKey(t.getKey()); 1022 if (keyExists && PROPERTY_RECENT_EXISTING.get() == RecentExisting.HIDE) 1023 continue; 1024 count++; 1025 // Create action for reusing the tag, with keyboard shortcut 1026 /* POSSIBLE SHORTCUTS: 1,2,3,4,5,6,7,8,9,0=10 */ 1027 final Shortcut sc = count > 10 ? null : Shortcut.registerShortcut("properties:recent:" + count, 1028 tr("Choose recent tag {0}", count), KeyEvent.VK_0 + (count % 10), Shortcut.CTRL); 1029 final JosmAction action = new JosmAction( 1030 tr("Choose recent tag {0}", count), null, tr("Use this tag again"), sc, false) { 1031 @Override 1032 public void actionPerformed(ActionEvent e) { 1033 keys.setSelectedItemText(t.getKey()); 1034 // fix #7951, #8298 - update list of values before setting value (?) 1035 updateValueModel(autocomplete, DEFAULT_AC_ITEM_COMPARATOR); 1036 values.setSelectedItemText(t.getValue()); 1037 values.requestFocus(); 1038 } 1039 }; 1040 /* POSSIBLE SHORTCUTS: 1,2,3,4,5,6,7,8,9,0=10 */ 1041 final Shortcut scShift = count > 10 ? null : Shortcut.registerShortcut("properties:recent:apply:" + count, 1042 tr("Apply recent tag {0}", count), KeyEvent.VK_0 + (count % 10), Shortcut.CTRL_SHIFT); 1043 final JosmAction actionShift = new JosmAction( 1044 tr("Apply recent tag {0}", count), null, tr("Use this tag again"), scShift, false) { 1045 @Override 1046 public void actionPerformed(ActionEvent e) { 1047 action.actionPerformed(null); 1048 performTagAdding(); 1049 refreshRecentTags(); 1050 keys.requestFocus(); 1051 } 1052 }; 1053 recentTagsActions.add(action); 1054 recentTagsActions.add(actionShift); 1055 if (keyExists && PROPERTY_RECENT_EXISTING.get() == RecentExisting.DISABLE) { 1056 action.setEnabled(false); 1057 } 1058 ImageIcon icon = findIcon(t.getKey(), t.getValue()) 1059 // If still nothing display an empty icon 1060 1061 .orElseGet(() -> new ImageIcon(new BufferedImage(16, 16, BufferedImage.TYPE_INT_ARGB))); 1062 GridBagConstraints gbc = new GridBagConstraints(); 1063 gbc.ipadx = 5; 1064 recentTagsPanel.add(new JLabel(action.isEnabled() ? icon : GuiHelper.getDisabledIcon(icon)), gbc); 1065 // Create tag label 1066 final String color = action.isEnabled() ? "" : "; color:gray"; 1067 final JLabel tagLabel = new JLabel("<html>" 1068 + "<style>td{" + color + "}</style>" 1069 + "<table><tr>" 1070 + "<td>" + count + ".</td>" 1071 + "<td style='border:1px solid gray'>" + XmlWriter.encode(t.toString(), true) + '<' + 1072 "/td></tr></table></html>"); 1073 tagLabel.setFont(tagLabel.getFont().deriveFont(Font.PLAIN)); 1074 if (action.isEnabled() && sc != null && scShift != null) { 1075 // Register action 1076 recentTagsPanel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(sc.getKeyStroke(), "choose"+count); 1077 recentTagsPanel.getActionMap().put("choose"+count, action); 1078 recentTagsPanel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(scShift.getKeyStroke(), "apply"+count); 1079 recentTagsPanel.getActionMap().put("apply"+count, actionShift); 1080 } 1081 if (action.isEnabled()) { 1082 // Make the tag label clickable and set tooltip to the action description (this displays also the keyboard shortcut) 1083 tagLabel.setToolTipText((String) action.getValue(Action.SHORT_DESCRIPTION)); 1084 tagLabel.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); 1085 tagLabel.addMouseListener(new MouseAdapter() { 1086 @Override 1087 public void mouseClicked(MouseEvent e) { 1088 action.actionPerformed(null); 1089 if (SwingUtilities.isRightMouseButton(e)) { 1090 Component component = e.getComponent(); 1091 if (component.isShowing()) { 1092 new TagPopupMenu(t).show(component, e.getX(), e.getY()); 1093 } 1094 } else if (e.isShiftDown()) { 1095 // add tags on Shift-Click 1096 performTagAdding(); 1097 refreshRecentTags(); 1098 keys.requestFocus(); 1099 } else if (e.getClickCount() > 1) { 1100 // add tags and close window on double-click 1101 buttonAction(0, null); // emulate OK click and close the dialog 1102 } 1103 } 1104 }); 1105 } else { 1106 // Disable tag label 1107 tagLabel.setEnabled(false); 1108 // Explain in the tooltip why 1109 tagLabel.setToolTipText(tr("The key ''{0}'' is already used", t.getKey())); 1110 } 1111 // Finally add label to the resulting panel 1112 JPanel tagPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0)); 1113 tagPanel.add(tagLabel); 1114 recentTagsPanel.add(tagPanel, GBC.eol().fill(GBC.HORIZONTAL)); 1115 } 1116 // Clear label if no tags were added 1117 if (count == 0) { 1118 recentTagsPanel.removeAll(); 1119 } 1120 } 1121 1122 class TagPopupMenu extends JPopupMenu { 1123 1124 TagPopupMenu(Tag t) { 1125 add(new IgnoreTagAction(tr("Ignore key ''{0}''", t.getKey()), new Tag(t.getKey(), ""))); 1126 add(new IgnoreTagAction(tr("Ignore tag ''{0}''", t), t)); 1127 add(new EditIgnoreTagsAction()); 1128 } 1129 } 1130 1131 class IgnoreTagAction extends AbstractAction { 1132 final transient Tag tag; 1133 1134 IgnoreTagAction(String name, Tag tag) { 1135 super(name); 1136 this.tag = tag; 1137 } 1138 1139 @Override 1140 public void actionPerformed(ActionEvent e) { 1141 try { 1142 if (tagsToIgnore != null) { 1143 recentTags.ignoreTag(tag, tagsToIgnore); 1144 PROPERTY_TAGS_TO_IGNORE.put(tagsToIgnore.writeToString()); 1145 } 1146 } catch (SearchParseError parseError) { 1147 throw new IllegalStateException(parseError); 1148 } 1149 } 1150 } 1151 1152 class EditIgnoreTagsAction extends AbstractAction { 1153 1154 EditIgnoreTagsAction() { 1155 super(tr("Edit ignore list")); 1156 } 1157 1158 @Override 1159 public void actionPerformed(ActionEvent e) { 1160 final SearchSetting newTagsToIngore = SearchAction.showSearchDialog(tagsToIgnore); 1161 if (newTagsToIngore == null) { 1162 return; 1163 } 1164 try { 1165 tagsToIgnore = newTagsToIngore; 1166 recentTags.setTagsToIgnore(tagsToIgnore); 1167 PROPERTY_TAGS_TO_IGNORE.put(tagsToIgnore.writeToString()); 1168 } catch (SearchParseError parseError) { 1169 warnAboutParseError(parseError); 1170 } 1171 } 1172 } 1173 1174 /** 1175 * Destroy the recentTagsActions. 1176 */ 1177 public void destroyActions() { 1178 for (JosmAction action : recentTagsActions) { 1179 action.destroy(); 1180 } 1181 recentTagsActions.clear(); 1182 } 1183 1184 /** 1185 * Read tags from comboboxes and add it to all selected objects 1186 */ 1187 public final void performTagAdding() { 1188 String key = getEditItem(keys); 1189 String value = getEditItem(values); 1190 if (key.isEmpty() || value.isEmpty()) 1191 return; 1192 for (OsmPrimitive osm : sel) { 1193 String val = osm.get(key); 1194 if (val != null && !val.equals(value)) { 1195 String valueHtmlString = Utils.joinAsHtmlUnorderedList(Arrays.asList("<strike>" + val + "</strike>", value)); 1196 if (!warnOverwriteKey("<html>" 1197 + tr("You changed the value of ''{0}'': {1}", key, valueHtmlString) 1198 + tr("Overwrite?"), "overwriteAddKey")) 1199 return; 1200 break; 1201 } 1202 } 1203 recentTags.add(new Tag(key, value)); 1204 valueCount.put(key, new TreeMap<String, Integer>()); 1205 AutoCompletionManager.rememberUserInput(key, value, false); 1206 commandCount++; 1207 UndoRedoHandler.getInstance().add(new ChangePropertyCommand(sel, key, value)); 1208 changedKey = key; 1209 clearEntries(); 1210 } 1211 1212 protected void clearEntries() { 1213 keys.getEditor().setItem(""); 1214 values.getEditor().setItem(""); 1215 } 1216 1217 public void undoAllTagsAdding() { 1218 UndoRedoHandler.getInstance().undo(commandCount); 1219 } 1220 1221 private void refreshRecentTags() { 1222 switch (PROPERTY_REFRESH_RECENT.get()) { 1223 case REFRESH: 1224 cacheRecentTags(); 1225 suggestRecentlyAddedTags(); 1226 break; 1227 case STATUS: 1228 suggestRecentlyAddedTags(); 1229 break; 1230 default: // Do nothing 1231 } 1232 } 1233 } 1234}