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