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