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 * @since 5633 084 */ 085class TagEditHelper { 086 private final DefaultTableModel tagData; 087 private final Map<String, Map<String, Integer>> valueCount; 088 089 // Selection that we are editing by using both dialogs 090 Collection<OsmPrimitive> sel; 091 092 private String changedKey; 093 private String objKey; 094 095 Comparator<AutoCompletionListItem> defaultACItemComparator = new Comparator<AutoCompletionListItem>() { 096 @Override 097 public int compare(AutoCompletionListItem o1, AutoCompletionListItem o2) { 098 return String.CASE_INSENSITIVE_ORDER.compare(o1.getValue(), o2.getValue()); 099 } 100 }; 101 102 private String lastAddKey = null; 103 private String lastAddValue = null; 104 105 public static final int DEFAULT_LRU_TAGS_NUMBER = 5; 106 public static final int MAX_LRU_TAGS_NUMBER = 30; 107 108 // LRU cache for recently added tags (http://java-planet.blogspot.com/2005/08/how-to-set-up-simple-lru-cache-using.html) 109 private final Map<Tag, Void> recentTags = new LinkedHashMap<Tag, Void>(MAX_LRU_TAGS_NUMBER+1, 1.1f, true) { 110 @Override 111 protected boolean removeEldestEntry(Map.Entry<Tag, Void> eldest) { 112 return size() > MAX_LRU_TAGS_NUMBER; 113 } 114 }; 115 116 TagEditHelper(DefaultTableModel propertyData, Map<String, Map<String, Integer>> valueCount) { 117 this.tagData = propertyData; 118 this.valueCount = valueCount; 119 } 120 121 /** 122 * Open the add selection dialog and add a new key/value to the table (and 123 * to the dataset, of course). 124 */ 125 public void addTag() { 126 changedKey = null; 127 sel = Main.main.getInProgressSelection(); 128 if (sel == null || sel.isEmpty()) return; 129 130 final AddTagsDialog addDialog = new AddTagsDialog(); 131 132 addDialog.showDialog(); 133 134 addDialog.destroyActions(); 135 if (addDialog.getValue() == 1) 136 addDialog.performTagAdding(); 137 else 138 addDialog.undoAllTagsAdding(); 139 } 140 141 /** 142 * Edit the value in the tags table row. 143 * @param row The row of the table from which the value is edited. 144 * @param focusOnKey Determines if the initial focus should be set on key instead of value 145 * @since 5653 146 */ 147 public void editTag(final int row, boolean focusOnKey) { 148 changedKey = null; 149 sel = Main.main.getInProgressSelection(); 150 if (sel == null || sel.isEmpty()) return; 151 152 String key = tagData.getValueAt(row, 0).toString(); 153 objKey=key; 154 155 @SuppressWarnings("unchecked") 156 final EditTagDialog editDialog = new EditTagDialog(key, row, 157 (Map<String, Integer>) tagData.getValueAt(row, 1), focusOnKey); 158 editDialog.showDialog(); 159 if (editDialog.getValue() !=1 ) return; 160 editDialog.performTagEdit(); 161 } 162 163 /** 164 * If during last editProperty call user changed the key name, this key will be returned 165 * Elsewhere, returns null. 166 * @return The modified key, or {@code null} 167 */ 168 public String getChangedKey() { 169 return changedKey; 170 } 171 172 public void resetChangedKey() { 173 changedKey = null; 174 } 175 176 /** 177 * For a given key k, return a list of keys which are used as keys for 178 * auto-completing values to increase the search space. 179 * @param key the key k 180 * @return a list of keys 181 */ 182 private static List<String> getAutocompletionKeys(String key) { 183 if ("name".equals(key) || "addr:street".equals(key)) 184 return Arrays.asList("addr:street", "name"); 185 else 186 return Arrays.asList(key); 187 } 188 189 /** 190 * Load recently used tags from preferences if needed. 191 */ 192 public void loadTagsIfNeeded() { 193 if (PROPERTY_REMEMBER_TAGS.get() && recentTags.isEmpty()) { 194 recentTags.clear(); 195 Collection<String> c = Main.pref.getCollection("properties.recent-tags"); 196 Iterator<String> it = c.iterator(); 197 String key, value; 198 while (it.hasNext()) { 199 key = it.next(); 200 value = it.next(); 201 recentTags.put(new Tag(key, value), null); 202 } 203 } 204 } 205 206 /** 207 * Store recently used tags in preferences if needed. 208 */ 209 public void saveTagsIfNeeded() { 210 if (PROPERTY_REMEMBER_TAGS.get() && !recentTags.isEmpty()) { 211 List<String> c = new ArrayList<>( recentTags.size()*2 ); 212 for (Tag t: recentTags.keySet()) { 213 c.add(t.getKey()); 214 c.add(t.getValue()); 215 } 216 Main.pref.putCollection("properties.recent-tags", c); 217 } 218 } 219 220 /** 221 * Warns user about a key being overwritten. 222 * @param action The action done by the user. Must state what key is changed 223 * @param togglePref The preference to save the checkbox state to 224 * @return {@code true} if the user accepts to overwrite key, {@code false} otherwise 225 */ 226 private boolean warnOverwriteKey(String action, String togglePref) { 227 ExtendedDialog ed = new ExtendedDialog( 228 Main.parent, 229 tr("Overwrite key"), 230 new String[]{tr("Replace"), tr("Cancel")}); 231 ed.setButtonIcons(new String[]{"purge", "cancel"}); 232 ed.setContent(action+"\n"+ tr("The new key is already used, overwrite values?")); 233 ed.setCancelButton(2); 234 ed.toggleEnable(togglePref); 235 ed.showDialog(); 236 237 return ed.getValue() == 1; 238 } 239 240 public final class EditTagDialog extends AbstractTagsDialog { 241 final String key; 242 final Map<String, Integer> m; 243 final int row; 244 245 Comparator<AutoCompletionListItem> usedValuesAwareComparator = new Comparator<AutoCompletionListItem>() { 246 @Override 247 public int compare(AutoCompletionListItem o1, AutoCompletionListItem o2) { 248 boolean c1 = m.containsKey(o1.getValue()); 249 boolean c2 = m.containsKey(o2.getValue()); 250 if (c1 == c2) 251 return String.CASE_INSENSITIVE_ORDER.compare(o1.getValue(), o2.getValue()); 252 else if (c1) 253 return -1; 254 else 255 return +1; 256 } 257 }; 258 259 ListCellRenderer<AutoCompletionListItem> cellRenderer = new ListCellRenderer<AutoCompletionListItem>() { 260 final DefaultListCellRenderer def = new DefaultListCellRenderer(); 261 @Override 262 public Component getListCellRendererComponent(JList<? extends AutoCompletionListItem> list, 263 AutoCompletionListItem value, int index, boolean isSelected, boolean cellHasFocus){ 264 Component c = def.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); 265 if (c instanceof JLabel) { 266 String str = value.getValue(); 267 if (valueCount.containsKey(objKey)) { 268 Map<String, Integer> m = valueCount.get(objKey); 269 if (m.containsKey(str)) { 270 str = tr("{0} ({1})", str, m.get(str)); 271 c.setFont(c.getFont().deriveFont(Font.ITALIC + Font.BOLD)); 272 } 273 } 274 ((JLabel) c).setText(str); 275 } 276 return c; 277 } 278 }; 279 280 private EditTagDialog(String key, int row, Map<String, Integer> map, final boolean initialFocusOnKey) { 281 super(Main.parent, trn("Change value?", "Change values?", map.size()), new String[] {tr("OK"),tr("Cancel")}); 282 setButtonIcons(new String[] {"ok","cancel"}); 283 setCancelButton(2); 284 configureContextsensitiveHelp("/Dialog/EditValue", true /* show help button */); 285 this.key = key; 286 this.row = row; 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 } else { 374 for (OsmPrimitive osm: sel) { 375 if (osm.get(newkey) != null) { 376 if (!warnOverwriteKey(tr("You changed the key from ''{0}'' to ''{1}''.", key, newkey), 377 "overwriteEditKey")) 378 return; 379 break; 380 } 381 } 382 Collection<Command> commands = new ArrayList<>(); 383 commands.add(new ChangePropertyCommand(sel, key, null)); 384 if (value.equals(tr("<different>"))) { 385 Map<String, List<OsmPrimitive>> map = new HashMap<>(); 386 for (OsmPrimitive osm: sel) { 387 String val = osm.get(key); 388 if (val != null) { 389 if (map.containsKey(val)) { 390 map.get(val).add(osm); 391 } else { 392 List<OsmPrimitive> v = new ArrayList<>(); 393 v.add(osm); 394 map.put(val, v); 395 } 396 } 397 } 398 for (Map.Entry<String, List<OsmPrimitive>> e: map.entrySet()) { 399 commands.add(new ChangePropertyCommand(e.getValue(), newkey, e.getKey())); 400 } 401 } else { 402 commands.add(new ChangePropertyCommand(sel, newkey, value)); 403 } 404 Main.main.undoRedo.add(new SequenceCommand( 405 trn("Change properties of up to {0} object", 406 "Change properties of up to {0} objects", sel.size(), sel.size()), 407 commands)); 408 } 409 410 changedKey = newkey; 411 } 412 } 413 414 public static final BooleanProperty PROPERTY_FIX_TAG_LOCALE = new BooleanProperty("properties.fix-tag-combobox-locale", false); 415 public static final BooleanProperty PROPERTY_REMEMBER_TAGS = new BooleanProperty("properties.remember-recently-added-tags", false); 416 public static final IntegerProperty PROPERTY_RECENT_TAGS_NUMBER = new IntegerProperty("properties.recently-added-tags", DEFAULT_LRU_TAGS_NUMBER); 417 418 abstract class AbstractTagsDialog extends ExtendedDialog { 419 AutoCompletingComboBox keys; 420 AutoCompletingComboBox values; 421 Component componentUnderMouse; 422 423 public AbstractTagsDialog(Component parent, String title, String[] buttonTexts) { 424 super(parent, title, buttonTexts); 425 addMouseListener(new PopupMenuLauncher(popupMenu)); 426 } 427 428 @Override 429 public void setupDialog() { 430 super.setupDialog(); 431 final Dimension size = getSize(); 432 // Set resizable only in width 433 setMinimumSize(size); 434 setPreferredSize(size); 435 // setMaximumSize does not work, and never worked, but still it seems not to bother Oracle to fix this 10-year-old bug 436 // https://bugs.openjdk.java.net/browse/JDK-6200438 437 // https://bugs.openjdk.java.net/browse/JDK-6464548 438 439 setRememberWindowGeometry(getClass().getName() + ".geometry", 440 WindowGeometry.centerInWindow(Main.parent, size)); 441 } 442 443 @Override 444 public void setVisible(boolean visible) { 445 // Do not want dialog to be resizable in height, as its size may increase each time because of the recently added tags 446 // So need to modify the stored geometry (size part only) in order to use the automatic positioning mechanism 447 if (visible) { 448 WindowGeometry geometry = initWindowGeometry(); 449 Dimension storedSize = geometry.getSize(); 450 Dimension size = getSize(); 451 if (!storedSize.equals(size)) { 452 if (storedSize.width < size.width) { 453 storedSize.width = size.width; 454 } 455 if (storedSize.height != size.height) { 456 storedSize.height = size.height; 457 } 458 rememberWindowGeometry(geometry); 459 } 460 keys.setFixedLocale(PROPERTY_FIX_TAG_LOCALE.get()); 461 } 462 super.setVisible(visible); 463 } 464 465 private void selectACComboBoxSavingUnixBuffer(AutoCompletingComboBox cb) { 466 // select combobox with saving unix system selection (middle mouse paste) 467 Clipboard sysSel = Toolkit.getDefaultToolkit().getSystemSelection(); 468 if(sysSel != null) { 469 Transferable old = sysSel.getContents(null); 470 cb.requestFocusInWindow(); 471 cb.getEditor().selectAll(); 472 sysSel.setContents(old, null); 473 } else { 474 cb.requestFocusInWindow(); 475 cb.getEditor().selectAll(); 476 } 477 } 478 479 public void selectKeysComboBox() { 480 selectACComboBoxSavingUnixBuffer(keys); 481 } 482 483 public void selectValuesCombobox() { 484 selectACComboBoxSavingUnixBuffer(values); 485 } 486 487 /** 488 * Create a focus handling adapter and apply in to the editor component of value 489 * autocompletion box. 490 * @param autocomplete Manager handling the autocompletion 491 * @param comparator Class to decide what values are offered on autocompletion 492 * @return The created adapter 493 */ 494 protected FocusAdapter addFocusAdapter(final AutoCompletionManager autocomplete, final Comparator<AutoCompletionListItem> comparator) { 495 // get the combo box' editor component 496 JTextComponent editor = (JTextComponent)values.getEditor().getEditorComponent(); 497 // Refresh the values model when focus is gained 498 FocusAdapter focus = new FocusAdapter() { 499 @Override 500 public void focusGained(FocusEvent e) { 501 String key = keys.getEditor().getItem().toString(); 502 503 List<AutoCompletionListItem> valueList = autocomplete.getValues(getAutocompletionKeys(key)); 504 Collections.sort(valueList, comparator); 505 506 values.setPossibleACItems(valueList); 507 objKey=key; 508 } 509 }; 510 editor.addFocusListener(focus); 511 return focus; 512 } 513 514 protected JPopupMenu popupMenu = new JPopupMenu() { 515 JCheckBoxMenuItem fixTagLanguageCb = new JCheckBoxMenuItem( 516 new AbstractAction(tr("Use English language for tag by default")){ 517 @Override 518 public void actionPerformed(ActionEvent e) { 519 boolean sel=((JCheckBoxMenuItem) e.getSource()).getState(); 520 PROPERTY_FIX_TAG_LOCALE.put(sel); 521 } 522 }); 523 { 524 add(fixTagLanguageCb); 525 fixTagLanguageCb.setState(PROPERTY_FIX_TAG_LOCALE.get()); 526 } 527 }; 528 } 529 530 class AddTagsDialog extends AbstractTagsDialog { 531 List<JosmAction> recentTagsActions = new ArrayList<>(); 532 533 // Counter of added commands for possible undo 534 private int commandCount; 535 536 public AddTagsDialog() { 537 super(Main.parent, tr("Add value?"), new String[] {tr("OK"),tr("Cancel")}); 538 setButtonIcons(new String[] {"ok","cancel"}); 539 setCancelButton(2); 540 configureContextsensitiveHelp("/Dialog/AddValue", true /* show help button */); 541 542 JPanel mainPanel = new JPanel(new GridBagLayout()); 543 keys = new AutoCompletingComboBox(); 544 values = new AutoCompletingComboBox(); 545 546 mainPanel.add(new JLabel("<html>"+trn("This will change up to {0} object.", 547 "This will change up to {0} objects.", sel.size(),sel.size()) 548 +"<br><br>"+tr("Please select a key")), GBC.eol().fill(GBC.HORIZONTAL)); 549 550 AutoCompletionManager autocomplete = Main.main.getEditLayer().data.getAutoCompletionManager(); 551 List<AutoCompletionListItem> keyList = autocomplete.getKeys(); 552 553 AutoCompletionListItem itemToSelect = null; 554 // remove the object's tag keys from the list 555 Iterator<AutoCompletionListItem> iter = keyList.iterator(); 556 while (iter.hasNext()) { 557 AutoCompletionListItem item = iter.next(); 558 if (item.getValue().equals(lastAddKey)) { 559 itemToSelect = item; 560 } 561 for (int i = 0; i < tagData.getRowCount(); ++i) { 562 if (item.getValue().equals(tagData.getValueAt(i, 0))) { 563 if (itemToSelect == item) { 564 itemToSelect = null; 565 } 566 iter.remove(); 567 break; 568 } 569 } 570 } 571 572 Collections.sort(keyList, defaultACItemComparator); 573 keys.setPossibleACItems(keyList); 574 keys.setEditable(true); 575 576 mainPanel.add(keys, GBC.eop().fill()); 577 578 mainPanel.add(new JLabel(tr("Please select a value")), GBC.eol()); 579 values.setEditable(true); 580 mainPanel.add(values, GBC.eop().fill()); 581 if (itemToSelect != null) { 582 keys.setSelectedItem(itemToSelect); 583 if (lastAddValue != null) { 584 values.setSelectedItem(lastAddValue); 585 } 586 } 587 588 FocusAdapter focus = addFocusAdapter(autocomplete, defaultACItemComparator); 589 // fire focus event in advance or otherwise the popup list will be too small at first 590 focus.focusGained(null); 591 592 int recentTagsToShow = PROPERTY_RECENT_TAGS_NUMBER.get(); 593 if (recentTagsToShow > MAX_LRU_TAGS_NUMBER) { 594 recentTagsToShow = MAX_LRU_TAGS_NUMBER; 595 } 596 597 // Add tag on Shift-Enter 598 mainPanel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put( 599 KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, InputEvent.SHIFT_MASK), "addAndContinue"); 600 mainPanel.getActionMap().put("addAndContinue", new AbstractAction() { 601 @Override 602 public void actionPerformed(ActionEvent e) { 603 performTagAdding(); 604 selectKeysComboBox(); 605 } 606 }); 607 608 suggestRecentlyAddedTags(mainPanel, recentTagsToShow, focus); 609 610 setContent(mainPanel, false); 611 612 selectKeysComboBox(); 613 614 popupMenu.add(new AbstractAction(tr("Set number of recently added tags")) { 615 @Override 616 public void actionPerformed(ActionEvent e) { 617 selectNumberOfTags(); 618 } 619 }); 620 JCheckBoxMenuItem rememberLastTags = new JCheckBoxMenuItem( 621 new AbstractAction(tr("Remember last used tags")){ 622 @Override 623 public void actionPerformed(ActionEvent e) { 624 boolean sel=((JCheckBoxMenuItem) e.getSource()).getState(); 625 PROPERTY_REMEMBER_TAGS.put(sel); 626 if (sel) saveTagsIfNeeded(); 627 } 628 }); 629 rememberLastTags.setState(PROPERTY_REMEMBER_TAGS.get()); 630 popupMenu.add(rememberLastTags); 631 } 632 633 private void selectNumberOfTags() { 634 String s = JOptionPane.showInputDialog(this, tr("Please enter the number of recently added tags to display")); 635 if (s!=null) try { 636 int v = Integer.parseInt(s); 637 if (v>=0 && v<=MAX_LRU_TAGS_NUMBER) { 638 PROPERTY_RECENT_TAGS_NUMBER.put(v); 639 return; 640 } 641 } catch (NumberFormatException ex) { 642 Main.warn(ex); 643 } 644 JOptionPane.showMessageDialog(this, tr("Please enter integer number between 0 and {0}", MAX_LRU_TAGS_NUMBER)); 645 } 646 647 private void suggestRecentlyAddedTags(JPanel mainPanel, int tagsToShow, final FocusAdapter focus) { 648 if (!(tagsToShow > 0 && !recentTags.isEmpty())) 649 return; 650 651 mainPanel.add(new JLabel(tr("Recently added tags")), GBC.eol()); 652 653 int count = 1; 654 // We store the maximum number (9) of recent tags to allow dynamic change of number of tags shown in the preferences. 655 // 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. 656 // 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. 657 List<Tag> tags = new LinkedList<>(recentTags.keySet()); 658 for (int i = tags.size()-1; i >= 0 && count <= tagsToShow; i--, count++) { 659 final Tag t = tags.get(i); 660 // Create action for reusing the tag, with keyboard shortcut Ctrl+(1-5) 661 String actionShortcutKey = "properties:recent:"+count; 662 String actionShortcutShiftKey = "properties:recent:shift:"+count; 663 Shortcut sc = Shortcut.registerShortcut(actionShortcutKey, tr("Choose recent tag {0}", count), KeyEvent.VK_0+count, Shortcut.CTRL); 664 final JosmAction action = new JosmAction(actionShortcutKey, null, tr("Use this tag again"), sc, false) { 665 @Override 666 public void actionPerformed(ActionEvent e) { 667 keys.setSelectedItem(t.getKey()); 668 // fix #7951, #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}