source: josm/trunk/src/org/openstreetmap/josm/gui/dialogs/properties/TagEditHelper.java@ 9743

Last change on this file since 9743 was 9743, checked in by bastiK, 8 years ago

applied #12438 - hide items from list of recently added tags (patch by kolesar; minor changes)

  • Property svn:eol-style set to native
File size: 43.3 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.dialogs.properties;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5import static org.openstreetmap.josm.tools.I18n.trn;
6
7import java.awt.BorderLayout;
8import java.awt.Component;
9import java.awt.Container;
10import java.awt.Cursor;
11import java.awt.Dimension;
12import java.awt.FlowLayout;
13import java.awt.Font;
14import java.awt.GridBagConstraints;
15import java.awt.GridBagLayout;
16import java.awt.datatransfer.Clipboard;
17import java.awt.datatransfer.Transferable;
18import java.awt.event.ActionEvent;
19import java.awt.event.ActionListener;
20import java.awt.event.FocusAdapter;
21import java.awt.event.FocusEvent;
22import java.awt.event.InputEvent;
23import java.awt.event.KeyEvent;
24import java.awt.event.MouseAdapter;
25import java.awt.event.MouseEvent;
26import java.awt.event.WindowAdapter;
27import java.awt.event.WindowEvent;
28import java.awt.image.BufferedImage;
29import java.text.Normalizer;
30import java.util.ArrayList;
31import java.util.Arrays;
32import java.util.Collection;
33import java.util.Collections;
34import java.util.Comparator;
35import java.util.HashMap;
36import java.util.Iterator;
37import java.util.LinkedHashMap;
38import java.util.LinkedList;
39import java.util.List;
40import java.util.Map;
41import java.util.TreeMap;
42
43import javax.swing.AbstractAction;
44import javax.swing.Action;
45import javax.swing.Box;
46import javax.swing.ButtonGroup;
47import javax.swing.DefaultListCellRenderer;
48import javax.swing.ImageIcon;
49import javax.swing.JCheckBoxMenuItem;
50import javax.swing.JComponent;
51import javax.swing.JLabel;
52import javax.swing.JList;
53import javax.swing.JMenu;
54import javax.swing.JOptionPane;
55import javax.swing.JPanel;
56import javax.swing.JPopupMenu;
57import javax.swing.JRadioButtonMenuItem;
58import javax.swing.JTable;
59import javax.swing.KeyStroke;
60import javax.swing.ListCellRenderer;
61import javax.swing.table.DefaultTableModel;
62import javax.swing.text.JTextComponent;
63
64import org.openstreetmap.josm.Main;
65import org.openstreetmap.josm.actions.JosmAction;
66import org.openstreetmap.josm.command.ChangePropertyCommand;
67import org.openstreetmap.josm.command.Command;
68import org.openstreetmap.josm.command.SequenceCommand;
69import org.openstreetmap.josm.data.osm.OsmPrimitive;
70import org.openstreetmap.josm.data.osm.Tag;
71import org.openstreetmap.josm.data.preferences.BooleanProperty;
72import org.openstreetmap.josm.data.preferences.EnumProperty;
73import org.openstreetmap.josm.data.preferences.IntegerProperty;
74import org.openstreetmap.josm.gui.ExtendedDialog;
75import org.openstreetmap.josm.gui.mappaint.MapPaintStyles;
76import org.openstreetmap.josm.gui.tagging.ac.AutoCompletingComboBox;
77import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionListItem;
78import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionManager;
79import org.openstreetmap.josm.gui.tagging.presets.TaggingPreset;
80import org.openstreetmap.josm.gui.tagging.presets.TaggingPresets;
81import org.openstreetmap.josm.gui.util.GuiHelper;
82import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher;
83import org.openstreetmap.josm.io.XmlWriter;
84import org.openstreetmap.josm.tools.GBC;
85import org.openstreetmap.josm.tools.Shortcut;
86import org.openstreetmap.josm.tools.Utils;
87import org.openstreetmap.josm.tools.WindowGeometry;
88
89/**
90 * Class that helps PropertiesDialog add and edit tag values.
91 * @since 5633
92 */
93public class TagEditHelper {
94
95 private final JTable tagTable;
96 private final DefaultTableModel tagData;
97 private final Map<String, Map<String, Integer>> valueCount;
98
99 // Selection that we are editing by using both dialogs
100 protected Collection<OsmPrimitive> sel;
101
102 private String changedKey;
103 private String objKey;
104
105 private final Comparator<AutoCompletionListItem> defaultACItemComparator = new Comparator<AutoCompletionListItem>() {
106 @Override
107 public int compare(AutoCompletionListItem o1, AutoCompletionListItem o2) {
108 return String.CASE_INSENSITIVE_ORDER.compare(o1.getValue(), o2.getValue());
109 }
110 };
111
112 private String lastAddKey;
113 private String lastAddValue;
114
115 /** Default number of recent tags */
116 public static final int DEFAULT_LRU_TAGS_NUMBER = 5;
117 /** Maximum number of recent tags */
118 public static final int MAX_LRU_TAGS_NUMBER = 30;
119
120 /** Use English language for tag by default */
121 public static final BooleanProperty PROPERTY_FIX_TAG_LOCALE = new BooleanProperty("properties.fix-tag-combobox-locale", false);
122 /** Whether recent tags must be remembered */
123 public static final BooleanProperty PROPERTY_REMEMBER_TAGS = new BooleanProperty("properties.remember-recently-added-tags", true);
124 /** Number of recent tags */
125 public static final IntegerProperty PROPERTY_RECENT_TAGS_NUMBER = new IntegerProperty("properties.recently-added-tags",
126 DEFAULT_LRU_TAGS_NUMBER);
127
128 /**
129 * What to do with recent tags where keys already exist
130 */
131 private enum RecentExisting {
132 ENABLE,
133 DISABLE,
134 HIDE
135 }
136
137 /**
138 * Preference setting for popup menu item "Recent tags with existing key"
139 */
140 public static final EnumProperty<RecentExisting> PROPERTY_RECENT_EXISTING = new EnumProperty<>(
141 "properties.recently-added-tags-existing-key", RecentExisting.class, RecentExisting.DISABLE);
142
143 /**
144 * What to do after applying tag
145 */
146 private enum RefreshRecent {
147 NO,
148 STATUS,
149 REFRESH
150 }
151
152 /**
153 * Preference setting for popup menu item "Refresh recent tags list after applying tag"
154 */
155 public static final EnumProperty<RefreshRecent> PROPERTY_REFRESH_RECENT = new EnumProperty<>(
156 "properties.refresh-recently-added-tags", RefreshRecent.class, RefreshRecent.STATUS);
157
158 // LRU cache for recently added tags (http://java-planet.blogspot.com/2005/08/how-to-set-up-simple-lru-cache-using.html)
159 private final Map<Tag, Void> recentTags = new LinkedHashMap<Tag, Void>(MAX_LRU_TAGS_NUMBER+1, 1.1f, true) {
160 @Override
161 protected boolean removeEldestEntry(Map.Entry<Tag, Void> eldest) {
162 return size() > MAX_LRU_TAGS_NUMBER;
163 }
164 };
165
166 // Copy of recently added tags, used to cache initial status
167 private List<Tag> tags;
168
169 /**
170 * Constructs a new {@code TagEditHelper}.
171 * @param tagTable
172 * @param propertyData
173 * @param valueCount
174 */
175 public TagEditHelper(JTable tagTable, DefaultTableModel propertyData, Map<String, Map<String, Integer>> valueCount) {
176 this.tagTable = tagTable;
177 this.tagData = propertyData;
178 this.valueCount = valueCount;
179 }
180
181 /**
182 * Finds the key from given row of tag editor.
183 * @param viewRow index of row
184 * @return key of tag
185 */
186 public final String getDataKey(int viewRow) {
187 return tagData.getValueAt(tagTable.convertRowIndexToModel(viewRow), 0).toString();
188 }
189
190 /**
191 * Finds the values from given row of tag editor.
192 * @param viewRow index of row
193 * @return map of values and number of occurrences
194 */
195 @SuppressWarnings("unchecked")
196 public final Map<String, Integer> getDataValues(int viewRow) {
197 return (Map<String, Integer>) tagData.getValueAt(tagTable.convertRowIndexToModel(viewRow), 1);
198 }
199
200 /**
201 * Open the add selection dialog and add a new key/value to the table (and
202 * to the dataset, of course).
203 */
204 public void addTag() {
205 changedKey = null;
206 sel = Main.main.getInProgressSelection();
207 if (sel == null || sel.isEmpty())
208 return;
209
210 final AddTagsDialog addDialog = getAddTagsDialog();
211
212 addDialog.showDialog();
213
214 addDialog.destroyActions();
215 if (addDialog.getValue() == 1)
216 addDialog.performTagAdding();
217 else
218 addDialog.undoAllTagsAdding();
219 }
220
221 protected AddTagsDialog getAddTagsDialog() {
222 return new AddTagsDialog();
223 }
224
225 /**
226 * Edit the value in the tags table row.
227 * @param row The row of the table from which the value is edited.
228 * @param focusOnKey Determines if the initial focus should be set on key instead of value
229 * @since 5653
230 */
231 public void editTag(final int row, boolean focusOnKey) {
232 changedKey = null;
233 sel = Main.main.getInProgressSelection();
234 if (sel == null || sel.isEmpty())
235 return;
236
237 String key = getDataKey(row);
238 objKey = key;
239
240 final IEditTagDialog editDialog = getEditTagDialog(row, focusOnKey, key);
241 editDialog.showDialog();
242 if (editDialog.getValue() != 1)
243 return;
244 editDialog.performTagEdit();
245 }
246
247 protected interface IEditTagDialog {
248 ExtendedDialog showDialog();
249
250 int getValue();
251
252 void performTagEdit();
253 }
254
255 protected IEditTagDialog getEditTagDialog(int row, boolean focusOnKey, String key) {
256 return new EditTagDialog(key, getDataValues(row), focusOnKey);
257 }
258
259 /**
260 * If during last editProperty call user changed the key name, this key will be returned
261 * Elsewhere, returns null.
262 * @return The modified key, or {@code null}
263 */
264 public String getChangedKey() {
265 return changedKey;
266 }
267
268 /**
269 * Reset last changed key.
270 */
271 public void resetChangedKey() {
272 changedKey = null;
273 }
274
275 /**
276 * For a given key k, return a list of keys which are used as keys for
277 * auto-completing values to increase the search space.
278 * @param key the key k
279 * @return a list of keys
280 */
281 private static List<String> getAutocompletionKeys(String key) {
282 if ("name".equals(key) || "addr:street".equals(key))
283 return Arrays.asList("addr:street", "name");
284 else
285 return Arrays.asList(key);
286 }
287
288 /**
289 * Load recently used tags from preferences if needed.
290 */
291 public void loadTagsIfNeeded() {
292 if (PROPERTY_REMEMBER_TAGS.get() && recentTags.isEmpty()) {
293 recentTags.clear();
294 Collection<String> c = Main.pref.getCollection("properties.recent-tags");
295 Iterator<String> it = c.iterator();
296 while (it.hasNext()) {
297 String key = it.next();
298 String value = it.next();
299 recentTags.put(new Tag(key, value), null);
300 }
301 }
302 }
303
304 /**
305 * Store recently used tags in preferences if needed.
306 */
307 public void saveTagsIfNeeded() {
308 if (PROPERTY_REMEMBER_TAGS.get() && !recentTags.isEmpty()) {
309 List<String> c = new ArrayList<>(recentTags.size()*2);
310 for (Tag t: recentTags.keySet()) {
311 c.add(t.getKey());
312 c.add(t.getValue());
313 }
314 Main.pref.putCollection("properties.recent-tags", c);
315 }
316 }
317
318 /**
319 * Update cache of recent tags used for displaying tags.
320 */
321 private void cacheRecentTags() {
322 tags = new LinkedList<>(recentTags.keySet());
323 }
324
325 /**
326 * Warns user about a key being overwritten.
327 * @param action The action done by the user. Must state what key is changed
328 * @param togglePref The preference to save the checkbox state to
329 * @return {@code true} if the user accepts to overwrite key, {@code false} otherwise
330 */
331 private static boolean warnOverwriteKey(String action, String togglePref) {
332 ExtendedDialog ed = new ExtendedDialog(
333 Main.parent,
334 tr("Overwrite key"),
335 new String[]{tr("Replace"), tr("Cancel")});
336 ed.setButtonIcons(new String[]{"purge", "cancel"});
337 ed.setContent(action+'\n'+ tr("The new key is already used, overwrite values?"));
338 ed.setCancelButton(2);
339 ed.toggleEnable(togglePref);
340 ed.showDialog();
341
342 return ed.getValue() == 1;
343 }
344
345 protected class EditTagDialog extends AbstractTagsDialog implements IEditTagDialog {
346 private final String key;
347 private final transient Map<String, Integer> m;
348
349 private final transient Comparator<AutoCompletionListItem> usedValuesAwareComparator = new Comparator<AutoCompletionListItem>() {
350 @Override
351 public int compare(AutoCompletionListItem o1, AutoCompletionListItem o2) {
352 boolean c1 = m.containsKey(o1.getValue());
353 boolean c2 = m.containsKey(o2.getValue());
354 if (c1 == c2)
355 return String.CASE_INSENSITIVE_ORDER.compare(o1.getValue(), o2.getValue());
356 else if (c1)
357 return -1;
358 else
359 return +1;
360 }
361 };
362
363 private final transient ListCellRenderer<AutoCompletionListItem> cellRenderer = new ListCellRenderer<AutoCompletionListItem>() {
364 private final DefaultListCellRenderer def = new DefaultListCellRenderer();
365 @Override
366 public Component getListCellRendererComponent(JList<? extends AutoCompletionListItem> list,
367 AutoCompletionListItem value, int index, boolean isSelected, boolean cellHasFocus) {
368 Component c = def.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
369 if (c instanceof JLabel) {
370 String str = value.getValue();
371 if (valueCount.containsKey(objKey)) {
372 Map<String, Integer> map = valueCount.get(objKey);
373 if (map.containsKey(str)) {
374 str = tr("{0} ({1})", str, map.get(str));
375 c.setFont(c.getFont().deriveFont(Font.ITALIC + Font.BOLD));
376 }
377 }
378 ((JLabel) c).setText(str);
379 }
380 return c;
381 }
382 };
383
384 protected EditTagDialog(String key, Map<String, Integer> map, final boolean initialFocusOnKey) {
385 super(Main.parent, trn("Change value?", "Change values?", map.size()), new String[] {tr("OK"), tr("Cancel")});
386 setButtonIcons(new String[] {"ok", "cancel"});
387 setCancelButton(2);
388 configureContextsensitiveHelp("/Dialog/EditValue", true /* show help button */);
389 this.key = key;
390 this.m = map;
391
392 JPanel mainPanel = new JPanel(new BorderLayout());
393
394 String msg = "<html>"+trn("This will change {0} object.",
395 "This will change up to {0} objects.", sel.size(), sel.size())
396 +"<br><br>("+tr("An empty value deletes the tag.", key)+")</html>";
397
398 mainPanel.add(new JLabel(msg), BorderLayout.NORTH);
399
400 JPanel p = new JPanel(new GridBagLayout());
401 mainPanel.add(p, BorderLayout.CENTER);
402
403 AutoCompletionManager autocomplete = Main.main.getEditLayer().data.getAutoCompletionManager();
404 List<AutoCompletionListItem> keyList = autocomplete.getKeys();
405 Collections.sort(keyList, defaultACItemComparator);
406
407 keys = new AutoCompletingComboBox(key);
408 keys.setPossibleACItems(keyList);
409 keys.setEditable(true);
410 keys.setSelectedItem(key);
411
412 p.add(Box.createVerticalStrut(5), GBC.eol());
413 p.add(new JLabel(tr("Key")), GBC.std());
414 p.add(Box.createHorizontalStrut(10), GBC.std());
415 p.add(keys, GBC.eol().fill(GBC.HORIZONTAL));
416
417 List<AutoCompletionListItem> valueList = autocomplete.getValues(getAutocompletionKeys(key));
418 Collections.sort(valueList, usedValuesAwareComparator);
419
420 final String selection = m.size() != 1 ? tr("<different>") : m.entrySet().iterator().next().getKey();
421
422 values = new AutoCompletingComboBox(selection);
423 values.setRenderer(cellRenderer);
424
425 values.setEditable(true);
426 values.setPossibleACItems(valueList);
427 values.setSelectedItem(selection);
428 values.getEditor().setItem(selection);
429 p.add(Box.createVerticalStrut(5), GBC.eol());
430 p.add(new JLabel(tr("Value")), GBC.std());
431 p.add(Box.createHorizontalStrut(10), GBC.std());
432 p.add(values, GBC.eol().fill(GBC.HORIZONTAL));
433 values.getEditor().addActionListener(new ActionListener() {
434 @Override
435 public void actionPerformed(ActionEvent e) {
436 buttonAction(0, null); // emulate OK button click
437 }
438 });
439 addFocusAdapter(autocomplete, usedValuesAwareComparator);
440
441 setContent(mainPanel, false);
442
443 addWindowListener(new WindowAdapter() {
444 @Override
445 public void windowOpened(WindowEvent e) {
446 if (initialFocusOnKey) {
447 selectKeysComboBox();
448 } else {
449 selectValuesCombobox();
450 }
451 }
452 });
453 }
454
455 /**
456 * Edit tags of multiple selected objects according to selected ComboBox values
457 * If value == "", tag will be deleted
458 * Confirmations may be needed.
459 */
460 @Override
461 public void performTagEdit() {
462 String value = Tag.removeWhiteSpaces(values.getEditor().getItem().toString());
463 value = Normalizer.normalize(value, java.text.Normalizer.Form.NFC);
464 if (value.isEmpty()) {
465 value = null; // delete the key
466 }
467 String newkey = Tag.removeWhiteSpaces(keys.getEditor().getItem().toString());
468 newkey = Normalizer.normalize(newkey, java.text.Normalizer.Form.NFC);
469 if (newkey.isEmpty()) {
470 newkey = key;
471 value = null; // delete the key instead
472 }
473 if (key.equals(newkey) && tr("<different>").equals(value))
474 return;
475 if (key.equals(newkey) || value == null) {
476 Main.main.undoRedo.add(new ChangePropertyCommand(sel, newkey, value));
477 AutoCompletionManager.rememberUserInput(newkey, value, true);
478 } else {
479 for (OsmPrimitive osm: sel) {
480 if (osm.get(newkey) != null) {
481 if (!warnOverwriteKey(tr("You changed the key from ''{0}'' to ''{1}''.", key, newkey),
482 "overwriteEditKey"))
483 return;
484 break;
485 }
486 }
487 Collection<Command> commands = new ArrayList<>();
488 commands.add(new ChangePropertyCommand(sel, key, null));
489 if (value.equals(tr("<different>"))) {
490 Map<String, List<OsmPrimitive>> map = new HashMap<>();
491 for (OsmPrimitive osm: sel) {
492 String val = osm.get(key);
493 if (val != null) {
494 if (map.containsKey(val)) {
495 map.get(val).add(osm);
496 } else {
497 List<OsmPrimitive> v = new ArrayList<>();
498 v.add(osm);
499 map.put(val, v);
500 }
501 }
502 }
503 for (Map.Entry<String, List<OsmPrimitive>> e: map.entrySet()) {
504 commands.add(new ChangePropertyCommand(e.getValue(), newkey, e.getKey()));
505 }
506 } else {
507 commands.add(new ChangePropertyCommand(sel, newkey, value));
508 AutoCompletionManager.rememberUserInput(newkey, value, false);
509 }
510 Main.main.undoRedo.add(new SequenceCommand(
511 trn("Change properties of up to {0} object",
512 "Change properties of up to {0} objects", sel.size(), sel.size()),
513 commands));
514 }
515
516 changedKey = newkey;
517 }
518 }
519
520 protected abstract class AbstractTagsDialog extends ExtendedDialog {
521 protected AutoCompletingComboBox keys;
522 protected AutoCompletingComboBox values;
523 protected Component componentUnderMouse;
524
525 AbstractTagsDialog(Component parent, String title, String[] buttonTexts) {
526 super(parent, title, buttonTexts);
527 addMouseListener(new PopupMenuLauncher(popupMenu));
528 }
529
530 @Override
531 public void setupDialog() {
532 super.setupDialog();
533 final Dimension size = getSize();
534 // Set resizable only in width
535 setMinimumSize(size);
536 setPreferredSize(size);
537 // setMaximumSize does not work, and never worked, but still it seems not to bother Oracle to fix this 10-year-old bug
538 // https://bugs.openjdk.java.net/browse/JDK-6200438
539 // https://bugs.openjdk.java.net/browse/JDK-6464548
540
541 setRememberWindowGeometry(getClass().getName() + ".geometry",
542 WindowGeometry.centerInWindow(Main.parent, size));
543 }
544
545 @Override
546 public void setVisible(boolean visible) {
547 // Do not want dialog to be resizable in height, as its size may increase each time because of the recently added tags
548 // So need to modify the stored geometry (size part only) in order to use the automatic positioning mechanism
549 if (visible) {
550 WindowGeometry geometry = initWindowGeometry();
551 Dimension storedSize = geometry.getSize();
552 Dimension size = getSize();
553 if (!storedSize.equals(size)) {
554 if (storedSize.width < size.width) {
555 storedSize.width = size.width;
556 }
557 if (storedSize.height != size.height) {
558 storedSize.height = size.height;
559 }
560 rememberWindowGeometry(geometry);
561 }
562 keys.setFixedLocale(PROPERTY_FIX_TAG_LOCALE.get());
563 }
564 super.setVisible(visible);
565 }
566
567 private void selectACComboBoxSavingUnixBuffer(AutoCompletingComboBox cb) {
568 // select combobox with saving unix system selection (middle mouse paste)
569 Clipboard sysSel = GuiHelper.getSystemSelection();
570 if (sysSel != null) {
571 Transferable old = Utils.getTransferableContent(sysSel);
572 cb.requestFocusInWindow();
573 cb.getEditor().selectAll();
574 sysSel.setContents(old, null);
575 } else {
576 cb.requestFocusInWindow();
577 cb.getEditor().selectAll();
578 }
579 }
580
581 public void selectKeysComboBox() {
582 selectACComboBoxSavingUnixBuffer(keys);
583 }
584
585 public void selectValuesCombobox() {
586 selectACComboBoxSavingUnixBuffer(values);
587 }
588
589 /**
590 * Create a focus handling adapter and apply in to the editor component of value
591 * autocompletion box.
592 * @param autocomplete Manager handling the autocompletion
593 * @param comparator Class to decide what values are offered on autocompletion
594 * @return The created adapter
595 */
596 protected FocusAdapter addFocusAdapter(final AutoCompletionManager autocomplete, final Comparator<AutoCompletionListItem> comparator) {
597 // get the combo box' editor component
598 final JTextComponent editor = values.getEditorComponent();
599 // Refresh the values model when focus is gained
600 FocusAdapter focus = new FocusAdapter() {
601 @Override
602 public void focusGained(FocusEvent e) {
603 String key = keys.getEditor().getItem().toString();
604
605 List<AutoCompletionListItem> valueList = autocomplete.getValues(getAutocompletionKeys(key));
606 Collections.sort(valueList, comparator);
607 if (Main.isTraceEnabled()) {
608 Main.trace("Focus gained by {0}, e={1}", values, e);
609 }
610 values.setPossibleACItems(valueList);
611 values.getEditor().selectAll();
612 objKey = key;
613 }
614 };
615 editor.addFocusListener(focus);
616 return focus;
617 }
618
619 protected JPopupMenu popupMenu = new JPopupMenu() {
620 private final JCheckBoxMenuItem fixTagLanguageCb = new JCheckBoxMenuItem(
621 new AbstractAction(tr("Use English language for tag by default")) {
622 @Override
623 public void actionPerformed(ActionEvent e) {
624 boolean use = ((JCheckBoxMenuItem) e.getSource()).getState();
625 PROPERTY_FIX_TAG_LOCALE.put(use);
626 keys.setFixedLocale(use);
627 }
628 });
629 {
630 add(fixTagLanguageCb);
631 fixTagLanguageCb.setState(PROPERTY_FIX_TAG_LOCALE.get());
632 }
633 };
634 }
635
636 protected class AddTagsDialog extends AbstractTagsDialog {
637 private final List<JosmAction> recentTagsActions = new ArrayList<>();
638 protected final transient FocusAdapter focus;
639 private JPanel mainPanel;
640 private JPanel recentTagsPanel;
641
642 // Counter of added commands for possible undo
643 private int commandCount;
644
645 protected AddTagsDialog() {
646 super(Main.parent, tr("Add value?"), new String[] {tr("OK"), tr("Cancel")});
647 setButtonIcons(new String[] {"ok", "cancel"});
648 setCancelButton(2);
649 configureContextsensitiveHelp("/Dialog/AddValue", true /* show help button */);
650
651 mainPanel = new JPanel(new GridBagLayout());
652 keys = new AutoCompletingComboBox();
653 values = new AutoCompletingComboBox();
654
655 mainPanel.add(new JLabel("<html>"+trn("This will change up to {0} object.",
656 "This will change up to {0} objects.", sel.size(), sel.size())
657 +"<br><br>"+tr("Please select a key")), GBC.eol().fill(GBC.HORIZONTAL));
658
659 AutoCompletionManager autocomplete = Main.main.getEditLayer().data.getAutoCompletionManager();
660 List<AutoCompletionListItem> keyList = autocomplete.getKeys();
661
662 AutoCompletionListItem itemToSelect = null;
663 // remove the object's tag keys from the list
664 Iterator<AutoCompletionListItem> iter = keyList.iterator();
665 while (iter.hasNext()) {
666 AutoCompletionListItem item = iter.next();
667 if (item.getValue().equals(lastAddKey)) {
668 itemToSelect = item;
669 }
670 for (int i = 0; i < tagData.getRowCount(); ++i) {
671 if (item.getValue().equals(getDataKey(i))) {
672 if (itemToSelect == item) {
673 itemToSelect = null;
674 }
675 iter.remove();
676 break;
677 }
678 }
679 }
680
681 Collections.sort(keyList, defaultACItemComparator);
682 keys.setPossibleACItems(keyList);
683 keys.setEditable(true);
684
685 mainPanel.add(keys, GBC.eop().fill(GBC.HORIZONTAL));
686
687 mainPanel.add(new JLabel(tr("Please select a value")), GBC.eol());
688 values.setEditable(true);
689 mainPanel.add(values, GBC.eop().fill(GBC.HORIZONTAL));
690 if (itemToSelect != null) {
691 keys.setSelectedItem(itemToSelect);
692 if (lastAddValue != null) {
693 values.setSelectedItem(lastAddValue);
694 }
695 }
696
697 focus = addFocusAdapter(autocomplete, defaultACItemComparator);
698 // fire focus event in advance or otherwise the popup list will be too small at first
699 focus.focusGained(null);
700
701 // Add tag on Shift-Enter
702 mainPanel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
703 KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, InputEvent.SHIFT_MASK), "addAndContinue");
704 mainPanel.getActionMap().put("addAndContinue", new AbstractAction() {
705 @Override
706 public void actionPerformed(ActionEvent e) {
707 performTagAdding();
708 refreshRecentTags();
709 selectKeysComboBox();
710 }
711 });
712
713 cacheRecentTags();
714 suggestRecentlyAddedTags();
715
716 mainPanel.add(Box.createVerticalGlue(), GBC.eop().fill());
717 setContent(mainPanel, false);
718
719 selectKeysComboBox();
720
721 popupMenu.add(new AbstractAction(tr("Set number of recently added tags")) {
722 @Override
723 public void actionPerformed(ActionEvent e) {
724 selectNumberOfTags();
725 suggestRecentlyAddedTags();
726 }
727 });
728
729 popupMenu.add(buildMenuRecentExisting());
730 popupMenu.add(buildMenuRefreshRecent());
731
732 JCheckBoxMenuItem rememberLastTags = new JCheckBoxMenuItem(
733 new AbstractAction(tr("Remember last used tags after a restart")) {
734 @Override
735 public void actionPerformed(ActionEvent e) {
736 boolean state = ((JCheckBoxMenuItem) e.getSource()).getState();
737 PROPERTY_REMEMBER_TAGS.put(state);
738 if (state)
739 saveTagsIfNeeded();
740 }
741 });
742 rememberLastTags.setState(PROPERTY_REMEMBER_TAGS.get());
743 popupMenu.add(rememberLastTags);
744 }
745
746 private JMenu buildMenuRecentExisting() {
747 JMenu menu = new JMenu(tr("Recent tags with existing key"));
748 TreeMap<RecentExisting, String> radios = new TreeMap<>();
749 radios.put(RecentExisting.ENABLE, tr("Enable"));
750 radios.put(RecentExisting.DISABLE, tr("Disable"));
751 radios.put(RecentExisting.HIDE, tr("Hide"));
752 ButtonGroup buttonGroup = new ButtonGroup();
753 for (final Map.Entry<RecentExisting, String> entry : radios.entrySet()) {
754 JRadioButtonMenuItem radio = new JRadioButtonMenuItem(new AbstractAction(entry.getValue()) {
755 @Override
756 public void actionPerformed(ActionEvent e) {
757 PROPERTY_RECENT_EXISTING.put(entry.getKey());
758 suggestRecentlyAddedTags();
759 }
760 });
761 buttonGroup.add(radio);
762 radio.setSelected(PROPERTY_RECENT_EXISTING.get() == entry.getKey());
763 menu.add(radio);
764 }
765 return menu;
766 }
767
768 private JMenu buildMenuRefreshRecent() {
769 JMenu menu = new JMenu(tr("Refresh recent tags list after applying tag"));
770 TreeMap<RefreshRecent, String> radios = new TreeMap<>();
771 radios.put(RefreshRecent.NO, tr("No refresh"));
772 radios.put(RefreshRecent.STATUS, tr("Refresh tag status only (enabled / disabled)"));
773 radios.put(RefreshRecent.REFRESH, tr("Refresh tag status and list of recently added tags"));
774 ButtonGroup buttonGroup = new ButtonGroup();
775 for (final Map.Entry<RefreshRecent, String> entry : radios.entrySet()) {
776 JRadioButtonMenuItem radio = new JRadioButtonMenuItem(new AbstractAction(entry.getValue()) {
777 @Override
778 public void actionPerformed(ActionEvent e) {
779 PROPERTY_REFRESH_RECENT.put(entry.getKey());
780 }
781 });
782 buttonGroup.add(radio);
783 radio.setSelected(PROPERTY_REFRESH_RECENT.get() == entry.getKey());
784 menu.add(radio);
785 }
786 return menu;
787 }
788
789 @Override
790 public void setContentPane(Container contentPane) {
791 final int commandDownMask = GuiHelper.getMenuShortcutKeyMaskEx();
792 List<String> lines = new ArrayList<>();
793 Shortcut sc = Shortcut.findShortcut(KeyEvent.VK_1, commandDownMask);
794 if (sc != null) {
795 lines.add(sc.getKeyText() + " " + tr("to apply first suggestion"));
796 }
797 lines.add(KeyEvent.getKeyModifiersText(KeyEvent.SHIFT_MASK)+'+'+KeyEvent.getKeyText(KeyEvent.VK_ENTER) + " "
798 +tr("to add without closing the dialog"));
799 sc = Shortcut.findShortcut(KeyEvent.VK_1, commandDownMask | KeyEvent.SHIFT_DOWN_MASK);
800 if (sc != null) {
801 lines.add(sc.getKeyText() + " " + tr("to add first suggestion without closing the dialog"));
802 }
803 final JLabel helpLabel = new JLabel("<html>" + Utils.join("<br>", lines) + "</html>");
804 helpLabel.setFont(helpLabel.getFont().deriveFont(Font.PLAIN));
805 contentPane.add(helpLabel, GBC.eol().fill(GridBagConstraints.HORIZONTAL).insets(5, 5, 5, 5));
806 super.setContentPane(contentPane);
807 }
808
809 protected void selectNumberOfTags() {
810 String s = String.format("%d", PROPERTY_RECENT_TAGS_NUMBER.get());
811 while (true) {
812 s = JOptionPane.showInputDialog(this, tr("Please enter the number of recently added tags to display"), s);
813 if (s == null) {
814 return;
815 }
816 try {
817 int v = Integer.parseInt(s);
818 if (v >= 0 && v <= MAX_LRU_TAGS_NUMBER) {
819 PROPERTY_RECENT_TAGS_NUMBER.put(v);
820 return;
821 }
822 } catch (NumberFormatException ex) {
823 Main.warn(ex);
824 }
825 JOptionPane.showMessageDialog(this, tr("Please enter integer number between 0 and {0}", MAX_LRU_TAGS_NUMBER));
826 }
827 }
828
829 protected void suggestRecentlyAddedTags() {
830 if (recentTagsPanel == null) {
831 recentTagsPanel = new JPanel(new GridBagLayout());
832 buildRecentTagsPanel();
833 mainPanel.add(recentTagsPanel, GBC.eol().fill(GBC.HORIZONTAL));
834 } else {
835 Dimension panelOldSize = recentTagsPanel.getPreferredSize();
836 recentTagsPanel.removeAll();
837 buildRecentTagsPanel();
838 Dimension panelNewSize = recentTagsPanel.getPreferredSize();
839 Dimension dialogOldSize = getMinimumSize();
840 Dimension dialogNewSize = new Dimension(dialogOldSize.width, dialogOldSize.height-panelOldSize.height+panelNewSize.height);
841 setMinimumSize(dialogNewSize);
842 setPreferredSize(dialogNewSize);
843 setSize(dialogNewSize);
844 revalidate();
845 repaint();
846 }
847 }
848
849 protected void buildRecentTagsPanel() {
850 final int tagsToShow = Math.min(PROPERTY_RECENT_TAGS_NUMBER.get(), MAX_LRU_TAGS_NUMBER);
851 if (!(tagsToShow > 0 && !recentTags.isEmpty()))
852 return;
853 recentTagsPanel.add(new JLabel(tr("Recently added tags")), GBC.eol());
854
855 int count = 0;
856 destroyActions();
857 // We store the maximum number of recent tags to allow dynamic change of number of tags shown in the preferences.
858 // This implies to iterate in descending order, as the oldest elements will only be removed after we reach the maximum
859 // number and not the number of tags to show.
860 // However, as Set does not allow to iterate in descending order, we need to copy its elements into a List we can access
861 // in reverse order.
862 for (int i = tags.size()-1; i >= 0 && count < tagsToShow; i--) {
863 final Tag t = tags.get(i);
864 boolean keyExists = keyExists(t);
865 if (keyExists && PROPERTY_RECENT_EXISTING.get() == RecentExisting.HIDE)
866 continue;
867 count++;
868 // Create action for reusing the tag, with keyboard shortcut
869 /* POSSIBLE SHORTCUTS: 1,2,3,4,5,6,7,8,9,0=10 */
870 final Shortcut sc = count > 10 ? null : Shortcut.registerShortcut("properties:recent:" + count,
871 tr("Choose recent tag {0}", count), KeyEvent.VK_0 + (count % 10), Shortcut.CTRL);
872 final JosmAction action = new JosmAction(
873 tr("Choose recent tag {0}", count), null, tr("Use this tag again"), sc, false) {
874 @Override
875 public void actionPerformed(ActionEvent e) {
876 keys.setSelectedItem(t.getKey());
877 // fix #7951, #8298 - update list of values before setting value (?)
878 focus.focusGained(null);
879 values.setSelectedItem(t.getValue());
880 selectValuesCombobox();
881 }
882 };
883 /* POSSIBLE SHORTCUTS: 1,2,3,4,5,6,7,8,9,0=10 */
884 final Shortcut scShift = count > 10 ? null : Shortcut.registerShortcut("properties:recent:apply:" + count,
885 tr("Apply recent tag {0}", count), KeyEvent.VK_0 + (count % 10), Shortcut.CTRL_SHIFT);
886 final JosmAction actionShift = new JosmAction(
887 tr("Apply recent tag {0}", count), null, tr("Use this tag again"), scShift, false) {
888 @Override
889 public void actionPerformed(ActionEvent e) {
890 action.actionPerformed(null);
891 performTagAdding();
892 refreshRecentTags();
893 selectKeysComboBox();
894 }
895 };
896 recentTagsActions.add(action);
897 recentTagsActions.add(actionShift);
898 if (keyExists && PROPERTY_RECENT_EXISTING.get() == RecentExisting.DISABLE) {
899 action.setEnabled(false);
900 }
901 // Find and display icon
902 ImageIcon icon = MapPaintStyles.getNodeIcon(t, false); // Filters deprecated icon
903 if (icon == null) {
904 // If no icon found in map style look at presets
905 Map<String, String> map = new HashMap<>();
906 map.put(t.getKey(), t.getValue());
907 for (TaggingPreset tp : TaggingPresets.getMatchingPresets(null, map, false)) {
908 icon = tp.getIcon();
909 if (icon != null) {
910 break;
911 }
912 }
913 // If still nothing display an empty icon
914 if (icon == null) {
915 icon = new ImageIcon(new BufferedImage(16, 16, BufferedImage.TYPE_INT_ARGB));
916 }
917 }
918 GridBagConstraints gbc = new GridBagConstraints();
919 gbc.ipadx = 5;
920 recentTagsPanel.add(new JLabel(action.isEnabled() ? icon : GuiHelper.getDisabledIcon(icon)), gbc);
921 // Create tag label
922 final String color = action.isEnabled() ? "" : "; color:gray";
923 final JLabel tagLabel = new JLabel("<html>"
924 + "<style>td{" + color + "}</style>"
925 + "<table><tr>"
926 + "<td>" + count + ".</td>"
927 + "<td style='border:1px solid gray'>" + XmlWriter.encode(t.toString(), true) + '<' +
928 "/td></tr></table></html>");
929 tagLabel.setFont(tagLabel.getFont().deriveFont(Font.PLAIN));
930 if (action.isEnabled() && sc != null && scShift != null) {
931 // Register action
932 recentTagsPanel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(sc.getKeyStroke(), "choose"+count);
933 recentTagsPanel.getActionMap().put("choose"+count, action);
934 recentTagsPanel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(scShift.getKeyStroke(), "apply"+count);
935 recentTagsPanel.getActionMap().put("apply"+count, actionShift);
936 }
937 if (action.isEnabled()) {
938 // Make the tag label clickable and set tooltip to the action description (this displays also the keyboard shortcut)
939 tagLabel.setToolTipText((String) action.getValue(Action.SHORT_DESCRIPTION));
940 tagLabel.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
941 tagLabel.addMouseListener(new MouseAdapter() {
942 @Override
943 public void mouseClicked(MouseEvent e) {
944 action.actionPerformed(null);
945 if (e.isShiftDown()) {
946 // add tags on Shift-Click
947 performTagAdding();
948 refreshRecentTags();
949 selectKeysComboBox();
950 } else if (e.getClickCount() > 1) {
951 // add tags and close window on double-click
952 buttonAction(0, null); // emulate OK click and close the dialog
953 }
954 }
955 });
956 } else {
957 // Disable tag label
958 tagLabel.setEnabled(false);
959 // Explain in the tooltip why
960 tagLabel.setToolTipText(tr("The key ''{0}'' is already used", t.getKey()));
961 }
962 // Finally add label to the resulting panel
963 JPanel tagPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0));
964 tagPanel.add(tagLabel);
965 recentTagsPanel.add(tagPanel, GBC.eol().fill(GBC.HORIZONTAL));
966 }
967 // Clear label if no tags were added
968 if (count == 0) {
969 recentTagsPanel.removeAll();
970 }
971 }
972
973 public void destroyActions() {
974 for (JosmAction action : recentTagsActions) {
975 action.destroy();
976 }
977 }
978
979 /**
980 * Read tags from comboboxes and add it to all selected objects
981 */
982 public final void performTagAdding() {
983 String key = Tag.removeWhiteSpaces(keys.getEditor().getItem().toString());
984 String value = Tag.removeWhiteSpaces(values.getEditor().getItem().toString());
985 if (key.isEmpty() || value.isEmpty())
986 return;
987 for (OsmPrimitive osm : sel) {
988 String val = osm.get(key);
989 if (val != null && !val.equals(value)) {
990 if (!warnOverwriteKey(tr("You changed the value of ''{0}'' from ''{1}'' to ''{2}''.", key, val, value),
991 "overwriteAddKey"))
992 return;
993 break;
994 }
995 }
996 lastAddKey = key;
997 lastAddValue = value;
998 recentTags.put(new Tag(key, value), null);
999 valueCount.put(key, new TreeMap<String, Integer>());
1000 AutoCompletionManager.rememberUserInput(key, value, false);
1001 commandCount++;
1002 Main.main.undoRedo.add(new ChangePropertyCommand(sel, key, value));
1003 changedKey = key;
1004 clearEntries();
1005 }
1006
1007 protected void clearEntries() {
1008 keys.getEditor().setItem("");
1009 values.getEditor().setItem("");
1010 }
1011
1012 public void undoAllTagsAdding() {
1013 Main.main.undoRedo.undo(commandCount);
1014 }
1015
1016 private boolean keyExists(final Tag t) {
1017 return valueCount.containsKey(t.getKey());
1018 }
1019
1020 private void refreshRecentTags() {
1021 switch (PROPERTY_REFRESH_RECENT.get()) {
1022 case REFRESH: cacheRecentTags(); // break missing intentionally
1023 case STATUS: suggestRecentlyAddedTags();
1024 }
1025 }
1026 }
1027}
Note: See TracBrowser for help on using the repository browser.