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