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