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.Toolkit; 017import java.awt.datatransfer.Clipboard; 018import java.awt.datatransfer.Transferable; 019import java.awt.event.ActionEvent; 020import java.awt.event.ActionListener; 021import java.awt.event.FocusAdapter; 022import java.awt.event.FocusEvent; 023import java.awt.event.InputEvent; 024import java.awt.event.KeyEvent; 025import java.awt.event.MouseAdapter; 026import java.awt.event.MouseEvent; 027import java.awt.event.WindowAdapter; 028import java.awt.event.WindowEvent; 029import java.awt.image.BufferedImage; 030import java.text.Normalizer; 031import java.util.ArrayList; 032import java.util.Arrays; 033import java.util.Collection; 034import java.util.Collections; 035import java.util.Comparator; 036import java.util.HashMap; 037import java.util.Iterator; 038import java.util.LinkedHashMap; 039import java.util.LinkedList; 040import java.util.List; 041import java.util.Map; 042 043import javax.swing.AbstractAction; 044import javax.swing.Action; 045import javax.swing.Box; 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.JOptionPane; 053import javax.swing.JPanel; 054import javax.swing.JPopupMenu; 055import javax.swing.KeyStroke; 056import javax.swing.ListCellRenderer; 057import javax.swing.table.DefaultTableModel; 058import javax.swing.text.JTextComponent; 059 060import org.openstreetmap.josm.Main; 061import org.openstreetmap.josm.actions.JosmAction; 062import org.openstreetmap.josm.command.ChangePropertyCommand; 063import org.openstreetmap.josm.command.Command; 064import org.openstreetmap.josm.command.SequenceCommand; 065import org.openstreetmap.josm.data.osm.OsmPrimitive; 066import org.openstreetmap.josm.data.osm.Tag; 067import org.openstreetmap.josm.data.preferences.BooleanProperty; 068import org.openstreetmap.josm.data.preferences.IntegerProperty; 069import org.openstreetmap.josm.gui.ExtendedDialog; 070import org.openstreetmap.josm.gui.mappaint.MapPaintStyles; 071import org.openstreetmap.josm.gui.tagging.ac.AutoCompletingComboBox; 072import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionListItem; 073import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionManager; 074import org.openstreetmap.josm.gui.tagging.presets.TaggingPreset; 075import org.openstreetmap.josm.gui.util.GuiHelper; 076import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher; 077import org.openstreetmap.josm.io.XmlWriter; 078import org.openstreetmap.josm.tools.GBC; 079import org.openstreetmap.josm.tools.Shortcut; 080import org.openstreetmap.josm.tools.Utils; 081import org.openstreetmap.josm.tools.WindowGeometry; 082 083/** 084 * Class that helps PropertiesDialog add and edit tag values. 085 * @since 5633 086 */ 087class TagEditHelper { 088 private final DefaultTableModel tagData; 089 private final Map<String, Map<String, Integer>> valueCount; 090 091 // Selection that we are editing by using both dialogs 092 private Collection<OsmPrimitive> sel; 093 094 private String changedKey; 095 private String objKey; 096 097 private Comparator<AutoCompletionListItem> defaultACItemComparator = new Comparator<AutoCompletionListItem>() { 098 @Override 099 public int compare(AutoCompletionListItem o1, AutoCompletionListItem o2) { 100 return String.CASE_INSENSITIVE_ORDER.compare(o1.getValue(), o2.getValue()); 101 } 102 }; 103 104 private String lastAddKey; 105 private String lastAddValue; 106 107 public static final int DEFAULT_LRU_TAGS_NUMBER = 5; 108 public static final int MAX_LRU_TAGS_NUMBER = 30; 109 110 // LRU cache for recently added tags (http://java-planet.blogspot.com/2005/08/how-to-set-up-simple-lru-cache-using.html) 111 private final Map<Tag, Void> recentTags = new LinkedHashMap<Tag, Void>(MAX_LRU_TAGS_NUMBER+1, 1.1f, true) { 112 @Override 113 protected boolean removeEldestEntry(Map.Entry<Tag, Void> eldest) { 114 return size() > MAX_LRU_TAGS_NUMBER; 115 } 116 }; 117 118 TagEditHelper(DefaultTableModel propertyData, Map<String, Map<String, Integer>> valueCount) { 119 this.tagData = propertyData; 120 this.valueCount = valueCount; 121 } 122 123 /** 124 * Open the add selection dialog and add a new key/value to the table (and 125 * to the dataset, of course). 126 */ 127 public void addTag() { 128 changedKey = null; 129 sel = Main.main.getInProgressSelection(); 130 if (sel == null || sel.isEmpty()) return; 131 132 final AddTagsDialog addDialog = new AddTagsDialog(); 133 134 addDialog.showDialog(); 135 136 addDialog.destroyActions(); 137 if (addDialog.getValue() == 1) 138 addDialog.performTagAdding(); 139 else 140 addDialog.undoAllTagsAdding(); 141 } 142 143 /** 144 * Edit the value in the tags table row. 145 * @param row The row of the table from which the value is edited. 146 * @param focusOnKey Determines if the initial focus should be set on key instead of value 147 * @since 5653 148 */ 149 public void editTag(final int row, boolean focusOnKey) { 150 changedKey = null; 151 sel = Main.main.getInProgressSelection(); 152 if (sel == null || sel.isEmpty()) return; 153 154 String key = tagData.getValueAt(row, 0).toString(); 155 objKey = key; 156 157 @SuppressWarnings("unchecked") 158 final EditTagDialog editDialog = new EditTagDialog(key, 159 (Map<String, Integer>) tagData.getValueAt(row, 1), focusOnKey); 160 editDialog.showDialog(); 161 if (editDialog.getValue() != 1) return; 162 editDialog.performTagEdit(); 163 } 164 165 /** 166 * If during last editProperty call user changed the key name, this key will be returned 167 * Elsewhere, returns null. 168 * @return The modified key, or {@code null} 169 */ 170 public String getChangedKey() { 171 return changedKey; 172 } 173 174 public void resetChangedKey() { 175 changedKey = null; 176 } 177 178 /** 179 * For a given key k, return a list of keys which are used as keys for 180 * auto-completing values to increase the search space. 181 * @param key the key k 182 * @return a list of keys 183 */ 184 private static List<String> getAutocompletionKeys(String key) { 185 if ("name".equals(key) || "addr:street".equals(key)) 186 return Arrays.asList("addr:street", "name"); 187 else 188 return Arrays.asList(key); 189 } 190 191 /** 192 * Load recently used tags from preferences if needed. 193 */ 194 public void loadTagsIfNeeded() { 195 if (PROPERTY_REMEMBER_TAGS.get() && recentTags.isEmpty()) { 196 recentTags.clear(); 197 Collection<String> c = Main.pref.getCollection("properties.recent-tags"); 198 Iterator<String> it = c.iterator(); 199 String key, value; 200 while (it.hasNext()) { 201 key = it.next(); 202 value = it.next(); 203 recentTags.put(new Tag(key, value), null); 204 } 205 } 206 } 207 208 /** 209 * Store recently used tags in preferences if needed. 210 */ 211 public void saveTagsIfNeeded() { 212 if (PROPERTY_REMEMBER_TAGS.get() && !recentTags.isEmpty()) { 213 List<String> c = new ArrayList<>(recentTags.size()*2); 214 for (Tag t: recentTags.keySet()) { 215 c.add(t.getKey()); 216 c.add(t.getValue()); 217 } 218 Main.pref.putCollection("properties.recent-tags", c); 219 } 220 } 221 222 /** 223 * Warns user about a key being overwritten. 224 * @param action The action done by the user. Must state what key is changed 225 * @param togglePref The preference to save the checkbox state to 226 * @return {@code true} if the user accepts to overwrite key, {@code false} otherwise 227 */ 228 private static boolean warnOverwriteKey(String action, String togglePref) { 229 ExtendedDialog ed = new ExtendedDialog( 230 Main.parent, 231 tr("Overwrite key"), 232 new String[]{tr("Replace"), tr("Cancel")}); 233 ed.setButtonIcons(new String[]{"purge", "cancel"}); 234 ed.setContent(action+'\n'+ tr("The new key is already used, overwrite values?")); 235 ed.setCancelButton(2); 236 ed.toggleEnable(togglePref); 237 ed.showDialog(); 238 239 return ed.getValue() == 1; 240 } 241 242 public final class EditTagDialog extends AbstractTagsDialog { 243 private final String key; 244 private final transient Map<String, Integer> m; 245 246 private transient Comparator<AutoCompletionListItem> usedValuesAwareComparator = new Comparator<AutoCompletionListItem>() { 247 @Override 248 public int compare(AutoCompletionListItem o1, AutoCompletionListItem o2) { 249 boolean c1 = m.containsKey(o1.getValue()); 250 boolean c2 = m.containsKey(o2.getValue()); 251 if (c1 == c2) 252 return String.CASE_INSENSITIVE_ORDER.compare(o1.getValue(), o2.getValue()); 253 else if (c1) 254 return -1; 255 else 256 return +1; 257 } 258 }; 259 260 private final transient ListCellRenderer<AutoCompletionListItem> cellRenderer = new ListCellRenderer<AutoCompletionListItem>() { 261 private final DefaultListCellRenderer def = new DefaultListCellRenderer(); 262 @Override 263 public Component getListCellRendererComponent(JList<? extends AutoCompletionListItem> list, 264 AutoCompletionListItem value, int index, boolean isSelected, boolean cellHasFocus) { 265 Component c = def.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); 266 if (c instanceof JLabel) { 267 String str = value.getValue(); 268 if (valueCount.containsKey(objKey)) { 269 Map<String, Integer> m = valueCount.get(objKey); 270 if (m.containsKey(str)) { 271 str = tr("{0} ({1})", str, m.get(str)); 272 c.setFont(c.getFont().deriveFont(Font.ITALIC + Font.BOLD)); 273 } 274 } 275 ((JLabel) c).setText(str); 276 } 277 return c; 278 } 279 }; 280 281 private EditTagDialog(String key, Map<String, Integer> map, final boolean initialFocusOnKey) { 282 super(Main.parent, trn("Change value?", "Change values?", map.size()), new String[] {tr("OK"), tr("Cancel")}); 283 setButtonIcons(new String[] {"ok", "cancel"}); 284 setCancelButton(2); 285 configureContextsensitiveHelp("/Dialog/EditValue", true /* show help button */); 286 this.key = key; 287 this.m = map; 288 289 JPanel mainPanel = new JPanel(new BorderLayout()); 290 291 String msg = "<html>"+trn("This will change {0} object.", 292 "This will change up to {0} objects.", sel.size(), sel.size()) 293 +"<br><br>("+tr("An empty value deletes the tag.", key)+")</html>"; 294 295 mainPanel.add(new JLabel(msg), BorderLayout.NORTH); 296 297 JPanel p = new JPanel(new GridBagLayout()); 298 mainPanel.add(p, BorderLayout.CENTER); 299 300 AutoCompletionManager autocomplete = Main.main.getEditLayer().data.getAutoCompletionManager(); 301 List<AutoCompletionListItem> keyList = autocomplete.getKeys(); 302 Collections.sort(keyList, defaultACItemComparator); 303 304 keys = new AutoCompletingComboBox(key); 305 keys.setPossibleACItems(keyList); 306 keys.setEditable(true); 307 keys.setSelectedItem(key); 308 309 p.add(Box.createVerticalStrut(5), GBC.eol()); 310 p.add(new JLabel(tr("Key")), GBC.std()); 311 p.add(Box.createHorizontalStrut(10), GBC.std()); 312 p.add(keys, GBC.eol().fill(GBC.HORIZONTAL)); 313 314 List<AutoCompletionListItem> valueList = autocomplete.getValues(getAutocompletionKeys(key)); 315 Collections.sort(valueList, usedValuesAwareComparator); 316 317 final String selection = m.size() != 1 ? tr("<different>") : m.entrySet().iterator().next().getKey(); 318 319 values = new AutoCompletingComboBox(selection); 320 values.setRenderer(cellRenderer); 321 322 values.setEditable(true); 323 values.setPossibleACItems(valueList); 324 values.setSelectedItem(selection); 325 values.getEditor().setItem(selection); 326 p.add(Box.createVerticalStrut(5), GBC.eol()); 327 p.add(new JLabel(tr("Value")), GBC.std()); 328 p.add(Box.createHorizontalStrut(10), GBC.std()); 329 p.add(values, GBC.eol().fill(GBC.HORIZONTAL)); 330 values.getEditor().addActionListener(new ActionListener() { 331 @Override 332 public void actionPerformed(ActionEvent e) { 333 buttonAction(0, null); // emulate OK button click 334 } 335 }); 336 addFocusAdapter(autocomplete, usedValuesAwareComparator); 337 338 setContent(mainPanel, false); 339 340 addWindowListener(new WindowAdapter() { 341 @Override 342 public void windowOpened(WindowEvent e) { 343 if (initialFocusOnKey) { 344 selectKeysComboBox(); 345 } else { 346 selectValuesCombobox(); 347 } 348 } 349 }); 350 } 351 352 /** 353 * Edit tags of multiple selected objects according to selected ComboBox values 354 * If value == "", tag will be deleted 355 * Confirmations may be needed. 356 */ 357 private void performTagEdit() { 358 String value = Tag.removeWhiteSpaces(values.getEditor().getItem().toString()); 359 value = Normalizer.normalize(value, java.text.Normalizer.Form.NFC); 360 if (value.isEmpty()) { 361 value = null; // delete the key 362 } 363 String newkey = Tag.removeWhiteSpaces(keys.getEditor().getItem().toString()); 364 newkey = Normalizer.normalize(newkey, java.text.Normalizer.Form.NFC); 365 if (newkey.isEmpty()) { 366 newkey = key; 367 value = null; // delete the key instead 368 } 369 if (key.equals(newkey) && tr("<different>").equals(value)) 370 return; 371 if (key.equals(newkey) || value == null) { 372 Main.main.undoRedo.add(new ChangePropertyCommand(sel, newkey, value)); 373 AutoCompletionManager.rememberUserInput(newkey, value, true); 374 } else { 375 for (OsmPrimitive osm: sel) { 376 if (osm.get(newkey) != null) { 377 if (!warnOverwriteKey(tr("You changed the key from ''{0}'' to ''{1}''.", key, newkey), 378 "overwriteEditKey")) 379 return; 380 break; 381 } 382 } 383 Collection<Command> commands = new ArrayList<>(); 384 commands.add(new ChangePropertyCommand(sel, key, null)); 385 if (value.equals(tr("<different>"))) { 386 Map<String, List<OsmPrimitive>> map = new HashMap<>(); 387 for (OsmPrimitive osm: sel) { 388 String val = osm.get(key); 389 if (val != null) { 390 if (map.containsKey(val)) { 391 map.get(val).add(osm); 392 } else { 393 List<OsmPrimitive> v = new ArrayList<>(); 394 v.add(osm); 395 map.put(val, v); 396 } 397 } 398 } 399 for (Map.Entry<String, List<OsmPrimitive>> e: map.entrySet()) { 400 commands.add(new ChangePropertyCommand(e.getValue(), newkey, e.getKey())); 401 } 402 } else { 403 commands.add(new ChangePropertyCommand(sel, newkey, value)); 404 AutoCompletionManager.rememberUserInput(newkey, value, false); 405 } 406 Main.main.undoRedo.add(new SequenceCommand( 407 trn("Change properties of up to {0} object", 408 "Change properties of up to {0} objects", sel.size(), sel.size()), 409 commands)); 410 } 411 412 changedKey = newkey; 413 } 414 } 415 416 public static final BooleanProperty PROPERTY_FIX_TAG_LOCALE = new BooleanProperty("properties.fix-tag-combobox-locale", false); 417 public static final BooleanProperty PROPERTY_REMEMBER_TAGS = new BooleanProperty("properties.remember-recently-added-tags", true); 418 public static final IntegerProperty PROPERTY_RECENT_TAGS_NUMBER = new IntegerProperty("properties.recently-added-tags", 419 DEFAULT_LRU_TAGS_NUMBER); 420 421 abstract class AbstractTagsDialog extends ExtendedDialog { 422 protected AutoCompletingComboBox keys; 423 protected AutoCompletingComboBox values; 424 protected Component componentUnderMouse; 425 426 AbstractTagsDialog(Component parent, String title, String[] buttonTexts) { 427 super(parent, title, buttonTexts); 428 addMouseListener(new PopupMenuLauncher(popupMenu)); 429 } 430 431 @Override 432 public void setupDialog() { 433 super.setupDialog(); 434 final Dimension size = getSize(); 435 // Set resizable only in width 436 setMinimumSize(size); 437 setPreferredSize(size); 438 // setMaximumSize does not work, and never worked, but still it seems not to bother Oracle to fix this 10-year-old bug 439 // https://bugs.openjdk.java.net/browse/JDK-6200438 440 // https://bugs.openjdk.java.net/browse/JDK-6464548 441 442 setRememberWindowGeometry(getClass().getName() + ".geometry", 443 WindowGeometry.centerInWindow(Main.parent, size)); 444 } 445 446 @Override 447 public void setVisible(boolean visible) { 448 // Do not want dialog to be resizable in height, as its size may increase each time because of the recently added tags 449 // So need to modify the stored geometry (size part only) in order to use the automatic positioning mechanism 450 if (visible) { 451 WindowGeometry geometry = initWindowGeometry(); 452 Dimension storedSize = geometry.getSize(); 453 Dimension size = getSize(); 454 if (!storedSize.equals(size)) { 455 if (storedSize.width < size.width) { 456 storedSize.width = size.width; 457 } 458 if (storedSize.height != size.height) { 459 storedSize.height = size.height; 460 } 461 rememberWindowGeometry(geometry); 462 } 463 keys.setFixedLocale(PROPERTY_FIX_TAG_LOCALE.get()); 464 } 465 super.setVisible(visible); 466 } 467 468 private void selectACComboBoxSavingUnixBuffer(AutoCompletingComboBox cb) { 469 // select combobox with saving unix system selection (middle mouse paste) 470 Clipboard sysSel = Toolkit.getDefaultToolkit().getSystemSelection(); 471 if (sysSel != null) { 472 Transferable old = Utils.getTransferableContent(sysSel); 473 cb.requestFocusInWindow(); 474 cb.getEditor().selectAll(); 475 sysSel.setContents(old, null); 476 } else { 477 cb.requestFocusInWindow(); 478 cb.getEditor().selectAll(); 479 } 480 } 481 482 public void selectKeysComboBox() { 483 selectACComboBoxSavingUnixBuffer(keys); 484 } 485 486 public void selectValuesCombobox() { 487 selectACComboBoxSavingUnixBuffer(values); 488 } 489 490 /** 491 * Create a focus handling adapter and apply in to the editor component of value 492 * autocompletion box. 493 * @param autocomplete Manager handling the autocompletion 494 * @param comparator Class to decide what values are offered on autocompletion 495 * @return The created adapter 496 */ 497 protected FocusAdapter addFocusAdapter(final AutoCompletionManager autocomplete, final Comparator<AutoCompletionListItem> comparator) { 498 // get the combo box' editor component 499 JTextComponent editor = (JTextComponent) values.getEditor().getEditorComponent(); 500 // Refresh the values model when focus is gained 501 FocusAdapter focus = new FocusAdapter() { 502 @Override 503 public void focusGained(FocusEvent e) { 504 String key = keys.getEditor().getItem().toString(); 505 506 List<AutoCompletionListItem> valueList = autocomplete.getValues(getAutocompletionKeys(key)); 507 Collections.sort(valueList, comparator); 508 if (Main.isDebugEnabled()) { 509 Main.debug("Focus gained by {0}, e={1}", values, e); 510 } 511 values.setPossibleACItems(valueList); 512 values.getEditor().selectAll(); 513 objKey = key; 514 } 515 }; 516 editor.addFocusListener(focus); 517 return focus; 518 } 519 520 protected JPopupMenu popupMenu = new JPopupMenu() { 521 private final JCheckBoxMenuItem fixTagLanguageCb = new JCheckBoxMenuItem( 522 new AbstractAction(tr("Use English language for tag by default")) { 523 @Override 524 public void actionPerformed(ActionEvent e) { 525 boolean use = ((JCheckBoxMenuItem) e.getSource()).getState(); 526 PROPERTY_FIX_TAG_LOCALE.put(use); 527 keys.setFixedLocale(use); 528 } 529 }); 530 { 531 add(fixTagLanguageCb); 532 fixTagLanguageCb.setState(PROPERTY_FIX_TAG_LOCALE.get()); 533 } 534 }; 535 } 536 537 class AddTagsDialog extends AbstractTagsDialog { 538 private List<JosmAction> recentTagsActions = new ArrayList<>(); 539 540 // Counter of added commands for possible undo 541 private int commandCount; 542 543 AddTagsDialog() { 544 super(Main.parent, tr("Add value?"), new String[] {tr("OK"), tr("Cancel")}); 545 setButtonIcons(new String[] {"ok", "cancel"}); 546 setCancelButton(2); 547 configureContextsensitiveHelp("/Dialog/AddValue", true /* show help button */); 548 549 JPanel mainPanel = new JPanel(new GridBagLayout()); 550 keys = new AutoCompletingComboBox(); 551 values = new AutoCompletingComboBox(); 552 553 mainPanel.add(new JLabel("<html>"+trn("This will change up to {0} object.", 554 "This will change up to {0} objects.", sel.size(), sel.size()) 555 +"<br><br>"+tr("Please select a key")), GBC.eol().fill(GBC.HORIZONTAL)); 556 557 AutoCompletionManager autocomplete = Main.main.getEditLayer().data.getAutoCompletionManager(); 558 List<AutoCompletionListItem> keyList = autocomplete.getKeys(); 559 560 AutoCompletionListItem itemToSelect = null; 561 // remove the object's tag keys from the list 562 Iterator<AutoCompletionListItem> iter = keyList.iterator(); 563 while (iter.hasNext()) { 564 AutoCompletionListItem item = iter.next(); 565 if (item.getValue().equals(lastAddKey)) { 566 itemToSelect = item; 567 } 568 for (int i = 0; i < tagData.getRowCount(); ++i) { 569 if (item.getValue().equals(tagData.getValueAt(i, 0))) { 570 if (itemToSelect == item) { 571 itemToSelect = null; 572 } 573 iter.remove(); 574 break; 575 } 576 } 577 } 578 579 Collections.sort(keyList, defaultACItemComparator); 580 keys.setPossibleACItems(keyList); 581 keys.setEditable(true); 582 583 mainPanel.add(keys, GBC.eop().fill(GBC.HORIZONTAL)); 584 585 mainPanel.add(new JLabel(tr("Please select a value")), GBC.eol()); 586 values.setEditable(true); 587 mainPanel.add(values, GBC.eop().fill(GBC.HORIZONTAL)); 588 if (itemToSelect != null) { 589 keys.setSelectedItem(itemToSelect); 590 if (lastAddValue != null) { 591 values.setSelectedItem(lastAddValue); 592 } 593 } 594 595 FocusAdapter focus = addFocusAdapter(autocomplete, defaultACItemComparator); 596 // fire focus event in advance or otherwise the popup list will be too small at first 597 focus.focusGained(null); 598 599 int recentTagsToShow = PROPERTY_RECENT_TAGS_NUMBER.get(); 600 if (recentTagsToShow > MAX_LRU_TAGS_NUMBER) { 601 recentTagsToShow = MAX_LRU_TAGS_NUMBER; 602 } 603 604 // Add tag on Shift-Enter 605 mainPanel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put( 606 KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, InputEvent.SHIFT_MASK), "addAndContinue"); 607 mainPanel.getActionMap().put("addAndContinue", new AbstractAction() { 608 @Override 609 public void actionPerformed(ActionEvent e) { 610 performTagAdding(); 611 selectKeysComboBox(); 612 } 613 }); 614 615 suggestRecentlyAddedTags(mainPanel, recentTagsToShow, focus); 616 617 mainPanel.add(Box.createVerticalGlue(), GBC.eop().fill()); 618 setContent(mainPanel, false); 619 620 selectKeysComboBox(); 621 622 popupMenu.add(new AbstractAction(tr("Set number of recently added tags")) { 623 @Override 624 public void actionPerformed(ActionEvent e) { 625 selectNumberOfTags(); 626 } 627 }); 628 JCheckBoxMenuItem rememberLastTags = new JCheckBoxMenuItem( 629 new AbstractAction(tr("Remember last used tags after a restart")) { 630 @Override 631 public void actionPerformed(ActionEvent e) { 632 boolean sel = ((JCheckBoxMenuItem) e.getSource()).getState(); 633 PROPERTY_REMEMBER_TAGS.put(sel); 634 if (sel) saveTagsIfNeeded(); 635 } 636 }); 637 rememberLastTags.setState(PROPERTY_REMEMBER_TAGS.get()); 638 popupMenu.add(rememberLastTags); 639 } 640 641 private String code(String text) { 642 return "<code>" + text + "</code> "; 643 } 644 645 @Override 646 public void setContentPane(Container contentPane) { 647 final int commandDownMask = GuiHelper.getMenuShortcutKeyMaskEx(); 648 List<String> lines = new ArrayList<>(); 649 Shortcut sc = Shortcut.findShortcut(KeyEvent.VK_1, commandDownMask); 650 if (sc != null) { 651 lines.add(code(sc.getKeyText()) + tr("to apply first suggestion")); 652 } 653 lines.add(code(KeyEvent.getKeyModifiersText(KeyEvent.SHIFT_MASK)+'+'+KeyEvent.getKeyText(KeyEvent.VK_ENTER)) 654 +tr("to add without closing the dialog")); 655 sc = Shortcut.findShortcut(KeyEvent.VK_1, commandDownMask | KeyEvent.SHIFT_DOWN_MASK); 656 if (sc != null) { 657 lines.add(code(sc.getKeyText()) + tr("to add first suggestion without closing the dialog")); 658 } 659 final JLabel helpLabel = new JLabel("<html>" + Utils.join("<br>", lines) + "</html>"); 660 helpLabel.setFont(helpLabel.getFont().deriveFont(Font.PLAIN)); 661 contentPane.add(helpLabel, GBC.eol().fill(GridBagConstraints.HORIZONTAL).insets(1, 2, 1, 2)); 662 super.setContentPane(contentPane); 663 } 664 665 private void selectNumberOfTags() { 666 String s = JOptionPane.showInputDialog(this, tr("Please enter the number of recently added tags to display")); 667 if (s == null) { 668 return; 669 } 670 try { 671 int v = Integer.parseInt(s); 672 if (v >= 0 && v <= MAX_LRU_TAGS_NUMBER) { 673 PROPERTY_RECENT_TAGS_NUMBER.put(v); 674 return; 675 } 676 } catch (NumberFormatException ex) { 677 Main.warn(ex); 678 } 679 JOptionPane.showMessageDialog(this, tr("Please enter integer number between 0 and {0}", MAX_LRU_TAGS_NUMBER)); 680 } 681 682 private void suggestRecentlyAddedTags(JPanel mainPanel, int tagsToShow, final FocusAdapter focus) { 683 if (!(tagsToShow > 0 && !recentTags.isEmpty())) 684 return; 685 686 mainPanel.add(new JLabel(tr("Recently added tags")), GBC.eol()); 687 688 int count = 1; 689 // We store the maximum number (9) of recent tags to allow dynamic change of number of tags shown in the preferences. 690 // This implies to iterate in descending order, as the oldest elements will only be removed after we reach the maximum 691 // number and not the number of tags to show. 692 // However, as Set does not allow to iterate in descending order, we need to copy its elements into a List we can access 693 // in reverse order. 694 List<Tag> tags = new LinkedList<>(recentTags.keySet()); 695 for (int i = tags.size()-1; i >= 0 && count <= tagsToShow; i--, count++) { 696 final Tag t = tags.get(i); 697 // Create action for reusing the tag, with keyboard shortcut Ctrl+(1-5) 698 String actionShortcutKey = "properties:recent:"+count; 699 String actionShortcutShiftKey = "properties:recent:shift:"+count; 700 // CHECKSTYLE.OFF: LineLength 701 Shortcut sc = Shortcut.registerShortcut(actionShortcutKey, tr("Choose recent tag {0}", count), KeyEvent.VK_0+count, Shortcut.CTRL); 702 // CHECKSTYLE.ON: LineLength 703 final JosmAction action = new JosmAction(actionShortcutKey, null, tr("Use this tag again"), sc, false) { 704 @Override 705 public void actionPerformed(ActionEvent e) { 706 keys.setSelectedItem(t.getKey()); 707 // fix #7951, #8298 - update list of values before setting value (?) 708 focus.focusGained(null); 709 values.setSelectedItem(t.getValue()); 710 selectValuesCombobox(); 711 } 712 }; 713 Shortcut scShift = Shortcut.registerShortcut(actionShortcutShiftKey, tr("Apply recent tag {0}", count), 714 KeyEvent.VK_0+count, Shortcut.CTRL_SHIFT); 715 final JosmAction actionShift = new JosmAction(actionShortcutShiftKey, null, tr("Use this tag again"), scShift, false) { 716 @Override 717 public void actionPerformed(ActionEvent e) { 718 action.actionPerformed(null); 719 performTagAdding(); 720 selectKeysComboBox(); 721 } 722 }; 723 recentTagsActions.add(action); 724 recentTagsActions.add(actionShift); 725 disableTagIfNeeded(t, action); 726 // Find and display icon 727 ImageIcon icon = MapPaintStyles.getNodeIcon(t, false); // Filters deprecated icon 728 if (icon == null) { 729 // If no icon found in map style look at presets 730 Map<String, String> map = new HashMap<>(); 731 map.put(t.getKey(), t.getValue()); 732 for (TaggingPreset tp : TaggingPreset.getMatchingPresets(null, map, false)) { 733 icon = tp.getIcon(); 734 if (icon != null) { 735 break; 736 } 737 } 738 // If still nothing display an empty icon 739 if (icon == null) { 740 icon = new ImageIcon(new BufferedImage(16, 16, BufferedImage.TYPE_INT_ARGB)); 741 } 742 } 743 GridBagConstraints gbc = new GridBagConstraints(); 744 gbc.ipadx = 5; 745 mainPanel.add(new JLabel(action.isEnabled() ? icon : GuiHelper.getDisabledIcon(icon)), gbc); 746 // Create tag label 747 final String color = action.isEnabled() ? "" : "; color:gray"; 748 final JLabel tagLabel = new JLabel("<html>" 749 + "<style>td{" + color + "}</style>" 750 + "<table><tr>" 751 + "<td>" + count + ".</td>" 752 + "<td style='border:1px solid gray'>" + XmlWriter.encode(t.toString(), true) + '<' + 753 "/td></tr></table></html>"); 754 tagLabel.setFont(tagLabel.getFont().deriveFont(Font.PLAIN)); 755 if (action.isEnabled()) { 756 // Register action 757 mainPanel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(sc.getKeyStroke(), actionShortcutKey); 758 mainPanel.getActionMap().put(actionShortcutKey, action); 759 mainPanel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(scShift.getKeyStroke(), actionShortcutShiftKey); 760 mainPanel.getActionMap().put(actionShortcutShiftKey, actionShift); 761 // Make the tag label clickable and set tooltip to the action description (this displays also the keyboard shortcut) 762 tagLabel.setToolTipText((String) action.getValue(Action.SHORT_DESCRIPTION)); 763 tagLabel.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); 764 tagLabel.addMouseListener(new MouseAdapter() { 765 @Override 766 public void mouseClicked(MouseEvent e) { 767 action.actionPerformed(null); 768 // add tags and close window on double-click 769 if (e.getClickCount() > 1) { 770 buttonAction(0, null); // emulate OK click and close the dialog 771 } 772 // add tags on Shift-Click 773 if (e.isShiftDown()) { 774 performTagAdding(); 775 selectKeysComboBox(); 776 } 777 } 778 }); 779 } else { 780 // Disable tag label 781 tagLabel.setEnabled(false); 782 // Explain in the tooltip why 783 tagLabel.setToolTipText(tr("The key ''{0}'' is already used", t.getKey())); 784 } 785 // Finally add label to the resulting panel 786 JPanel tagPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0)); 787 tagPanel.add(tagLabel); 788 mainPanel.add(tagPanel, GBC.eol().fill(GBC.HORIZONTAL)); 789 } 790 } 791 792 public void destroyActions() { 793 for (JosmAction action : recentTagsActions) { 794 action.destroy(); 795 } 796 } 797 798 /** 799 * Read tags from comboboxes and add it to all selected objects 800 */ 801 public final void performTagAdding() { 802 String key = Tag.removeWhiteSpaces(keys.getEditor().getItem().toString()); 803 String value = Tag.removeWhiteSpaces(values.getEditor().getItem().toString()); 804 if (key.isEmpty() || value.isEmpty()) return; 805 for (OsmPrimitive osm: sel) { 806 String val = osm.get(key); 807 if (val != null && !val.equals(value)) { 808 if (!warnOverwriteKey(tr("You changed the value of ''{0}'' from ''{1}'' to ''{2}''.", key, val, value), 809 "overwriteAddKey")) 810 return; 811 break; 812 } 813 } 814 lastAddKey = key; 815 lastAddValue = value; 816 recentTags.put(new Tag(key, value), null); 817 AutoCompletionManager.rememberUserInput(key, value, false); 818 commandCount++; 819 Main.main.undoRedo.add(new ChangePropertyCommand(sel, key, value)); 820 changedKey = key; 821 keys.getEditor().setItem(""); 822 values.getEditor().setItem(""); 823 } 824 825 public void undoAllTagsAdding() { 826 Main.main.undoRedo.undo(commandCount); 827 } 828 829 private void disableTagIfNeeded(final Tag t, final JosmAction action) { 830 // Disable action if its key is already set on the object (the key being absent from the keys list for this reason 831 // performing this action leads to autocomplete to the next key (see #7671 comments) 832 for (int j = 0; j < tagData.getRowCount(); ++j) { 833 if (t.getKey().equals(tagData.getValueAt(j, 0))) { 834 action.setEnabled(false); 835 break; 836 } 837 } 838 } 839 } 840}