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

Last change on this file since 9877 was 9877, checked in by simon04, 8 years ago

fix #12488 - Fix ArrayIndexOutOfBoundsException when using tag filter in properties dialog

  • Property svn:eol-style set to native
File size: 43.4 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 tag table
172 * @param propertyData table model
173 * @param valueCount tag value count
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
524 AbstractTagsDialog(Component parent, String title, String[] buttonTexts) {
525 super(parent, title, buttonTexts);
526 addMouseListener(new PopupMenuLauncher(popupMenu));
527 }
528
529 @Override
530 public void setupDialog() {
531 super.setupDialog();
532 final Dimension size = getSize();
533 // Set resizable only in width
534 setMinimumSize(size);
535 setPreferredSize(size);
536 // setMaximumSize does not work, and never worked, but still it seems not to bother Oracle to fix this 10-year-old bug
537 // https://bugs.openjdk.java.net/browse/JDK-6200438
538 // https://bugs.openjdk.java.net/browse/JDK-6464548
539
540 setRememberWindowGeometry(getClass().getName() + ".geometry",
541 WindowGeometry.centerInWindow(Main.parent, size));
542 }
543
544 @Override
545 public void setVisible(boolean visible) {
546 // Do not want dialog to be resizable in height, as its size may increase each time because of the recently added tags
547 // So need to modify the stored geometry (size part only) in order to use the automatic positioning mechanism
548 if (visible) {
549 WindowGeometry geometry = initWindowGeometry();
550 Dimension storedSize = geometry.getSize();
551 Dimension size = getSize();
552 if (!storedSize.equals(size)) {
553 if (storedSize.width < size.width) {
554 storedSize.width = size.width;
555 }
556 if (storedSize.height != size.height) {
557 storedSize.height = size.height;
558 }
559 rememberWindowGeometry(geometry);
560 }
561 keys.setFixedLocale(PROPERTY_FIX_TAG_LOCALE.get());
562 }
563 super.setVisible(visible);
564 }
565
566 private void selectACComboBoxSavingUnixBuffer(AutoCompletingComboBox cb) {
567 // select combobox with saving unix system selection (middle mouse paste)
568 Clipboard sysSel = GuiHelper.getSystemSelection();
569 if (sysSel != null) {
570 Transferable old = Utils.getTransferableContent(sysSel);
571 cb.requestFocusInWindow();
572 cb.getEditor().selectAll();
573 sysSel.setContents(old, null);
574 } else {
575 cb.requestFocusInWindow();
576 cb.getEditor().selectAll();
577 }
578 }
579
580 public void selectKeysComboBox() {
581 selectACComboBoxSavingUnixBuffer(keys);
582 }
583
584 public void selectValuesCombobox() {
585 selectACComboBoxSavingUnixBuffer(values);
586 }
587
588 /**
589 * Create a focus handling adapter and apply in to the editor component of value
590 * autocompletion box.
591 * @param autocomplete Manager handling the autocompletion
592 * @param comparator Class to decide what values are offered on autocompletion
593 * @return The created adapter
594 */
595 protected FocusAdapter addFocusAdapter(final AutoCompletionManager autocomplete, final Comparator<AutoCompletionListItem> comparator) {
596 // get the combo box' editor component
597 final JTextComponent editor = values.getEditorComponent();
598 // Refresh the values model when focus is gained
599 FocusAdapter focus = new FocusAdapter() {
600 @Override
601 public void focusGained(FocusEvent e) {
602 String key = keys.getEditor().getItem().toString();
603
604 List<AutoCompletionListItem> valueList = autocomplete.getValues(getAutocompletionKeys(key));
605 Collections.sort(valueList, comparator);
606 if (Main.isTraceEnabled()) {
607 Main.trace("Focus gained by {0}, e={1}", values, e);
608 }
609 values.setPossibleACItems(valueList);
610 values.getEditor().selectAll();
611 objKey = key;
612 }
613 };
614 editor.addFocusListener(focus);
615 return focus;
616 }
617
618 protected JPopupMenu popupMenu = new JPopupMenu() {
619 private final JCheckBoxMenuItem fixTagLanguageCb = new JCheckBoxMenuItem(
620 new AbstractAction(tr("Use English language for tag by default")) {
621 @Override
622 public void actionPerformed(ActionEvent e) {
623 boolean use = ((JCheckBoxMenuItem) e.getSource()).getState();
624 PROPERTY_FIX_TAG_LOCALE.put(use);
625 keys.setFixedLocale(use);
626 }
627 });
628 {
629 add(fixTagLanguageCb);
630 fixTagLanguageCb.setState(PROPERTY_FIX_TAG_LOCALE.get());
631 }
632 };
633 }
634
635 protected class AddTagsDialog extends AbstractTagsDialog {
636 private final List<JosmAction> recentTagsActions = new ArrayList<>();
637 protected final transient FocusAdapter focus;
638 private JPanel mainPanel;
639 private JPanel recentTagsPanel;
640
641 // Counter of added commands for possible undo
642 private int commandCount;
643
644 protected AddTagsDialog() {
645 super(Main.parent, tr("Add value?"), new String[] {tr("OK"), tr("Cancel")});
646 setButtonIcons(new String[] {"ok", "cancel"});
647 setCancelButton(2);
648 configureContextsensitiveHelp("/Dialog/AddValue", true /* show help button */);
649
650 mainPanel = new JPanel(new GridBagLayout());
651 keys = new AutoCompletingComboBox();
652 values = new AutoCompletingComboBox();
653
654 mainPanel.add(new JLabel("<html>"+trn("This will change up to {0} object.",
655 "This will change up to {0} objects.", sel.size(), sel.size())
656 +"<br><br>"+tr("Please select a key")), GBC.eol().fill(GBC.HORIZONTAL));
657
658 AutoCompletionManager autocomplete = Main.main.getEditLayer().data.getAutoCompletionManager();
659 List<AutoCompletionListItem> keyList = autocomplete.getKeys();
660
661 AutoCompletionListItem itemToSelect = null;
662 // remove the object's tag keys from the list
663 Iterator<AutoCompletionListItem> iter = keyList.iterator();
664 while (iter.hasNext()) {
665 AutoCompletionListItem item = iter.next();
666 if (item.getValue().equals(lastAddKey)) {
667 itemToSelect = item;
668 }
669 for (int i = 0; i < tagData.getRowCount(); ++i) {
670 if (item.getValue().equals(tagData.getValueAt(i, 0) /* sic! do not use getDataKey*/)) {
671 if (itemToSelect == item) {
672 itemToSelect = null;
673 }
674 iter.remove();
675 break;
676 }
677 }
678 }
679
680 Collections.sort(keyList, defaultACItemComparator);
681 keys.setPossibleACItems(keyList);
682 keys.setEditable(true);
683
684 mainPanel.add(keys, GBC.eop().fill(GBC.HORIZONTAL));
685
686 mainPanel.add(new JLabel(tr("Please select a value")), GBC.eol());
687 values.setEditable(true);
688 mainPanel.add(values, GBC.eop().fill(GBC.HORIZONTAL));
689 if (itemToSelect != null) {
690 keys.setSelectedItem(itemToSelect);
691 if (lastAddValue != null) {
692 values.setSelectedItem(lastAddValue);
693 }
694 }
695
696 focus = addFocusAdapter(autocomplete, defaultACItemComparator);
697 // fire focus event in advance or otherwise the popup list will be too small at first
698 focus.focusGained(null);
699
700 // Add tag on Shift-Enter
701 mainPanel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
702 KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, InputEvent.SHIFT_MASK), "addAndContinue");
703 mainPanel.getActionMap().put("addAndContinue", new AbstractAction() {
704 @Override
705 public void actionPerformed(ActionEvent e) {
706 performTagAdding();
707 refreshRecentTags();
708 selectKeysComboBox();
709 }
710 });
711
712 cacheRecentTags();
713 suggestRecentlyAddedTags();
714
715 mainPanel.add(Box.createVerticalGlue(), GBC.eop().fill());
716 setContent(mainPanel, false);
717
718 selectKeysComboBox();
719
720 popupMenu.add(new AbstractAction(tr("Set number of recently added tags")) {
721 @Override
722 public void actionPerformed(ActionEvent e) {
723 selectNumberOfTags();
724 suggestRecentlyAddedTags();
725 }
726 });
727
728 popupMenu.add(buildMenuRecentExisting());
729 popupMenu.add(buildMenuRefreshRecent());
730
731 JCheckBoxMenuItem rememberLastTags = new JCheckBoxMenuItem(
732 new AbstractAction(tr("Remember last used tags after a restart")) {
733 @Override
734 public void actionPerformed(ActionEvent e) {
735 boolean state = ((JCheckBoxMenuItem) e.getSource()).getState();
736 PROPERTY_REMEMBER_TAGS.put(state);
737 if (state)
738 saveTagsIfNeeded();
739 }
740 });
741 rememberLastTags.setState(PROPERTY_REMEMBER_TAGS.get());
742 popupMenu.add(rememberLastTags);
743 }
744
745 private JMenu buildMenuRecentExisting() {
746 JMenu menu = new JMenu(tr("Recent tags with existing key"));
747 TreeMap<RecentExisting, String> radios = new TreeMap<>();
748 radios.put(RecentExisting.ENABLE, tr("Enable"));
749 radios.put(RecentExisting.DISABLE, tr("Disable"));
750 radios.put(RecentExisting.HIDE, tr("Hide"));
751 ButtonGroup buttonGroup = new ButtonGroup();
752 for (final Map.Entry<RecentExisting, String> entry : radios.entrySet()) {
753 JRadioButtonMenuItem radio = new JRadioButtonMenuItem(new AbstractAction(entry.getValue()) {
754 @Override
755 public void actionPerformed(ActionEvent e) {
756 PROPERTY_RECENT_EXISTING.put(entry.getKey());
757 suggestRecentlyAddedTags();
758 }
759 });
760 buttonGroup.add(radio);
761 radio.setSelected(PROPERTY_RECENT_EXISTING.get() == entry.getKey());
762 menu.add(radio);
763 }
764 return menu;
765 }
766
767 private JMenu buildMenuRefreshRecent() {
768 JMenu menu = new JMenu(tr("Refresh recent tags list after applying tag"));
769 TreeMap<RefreshRecent, String> radios = new TreeMap<>();
770 radios.put(RefreshRecent.NO, tr("No refresh"));
771 radios.put(RefreshRecent.STATUS, tr("Refresh tag status only (enabled / disabled)"));
772 radios.put(RefreshRecent.REFRESH, tr("Refresh tag status and list of recently added tags"));
773 ButtonGroup buttonGroup = new ButtonGroup();
774 for (final Map.Entry<RefreshRecent, String> entry : radios.entrySet()) {
775 JRadioButtonMenuItem radio = new JRadioButtonMenuItem(new AbstractAction(entry.getValue()) {
776 @Override
777 public void actionPerformed(ActionEvent e) {
778 PROPERTY_REFRESH_RECENT.put(entry.getKey());
779 }
780 });
781 buttonGroup.add(radio);
782 radio.setSelected(PROPERTY_REFRESH_RECENT.get() == entry.getKey());
783 menu.add(radio);
784 }
785 return menu;
786 }
787
788 @Override
789 public void setContentPane(Container contentPane) {
790 final int commandDownMask = GuiHelper.getMenuShortcutKeyMaskEx();
791 List<String> lines = new ArrayList<>();
792 Shortcut sc = Shortcut.findShortcut(KeyEvent.VK_1, commandDownMask);
793 if (sc != null) {
794 lines.add(sc.getKeyText() + " " + tr("to apply first suggestion"));
795 }
796 lines.add(KeyEvent.getKeyModifiersText(KeyEvent.SHIFT_MASK)+'+'+KeyEvent.getKeyText(KeyEvent.VK_ENTER) + " "
797 +tr("to add without closing the dialog"));
798 sc = Shortcut.findShortcut(KeyEvent.VK_1, commandDownMask | KeyEvent.SHIFT_DOWN_MASK);
799 if (sc != null) {
800 lines.add(sc.getKeyText() + " " + tr("to add first suggestion without closing the dialog"));
801 }
802 final JLabel helpLabel = new JLabel("<html>" + Utils.join("<br>", lines) + "</html>");
803 helpLabel.setFont(helpLabel.getFont().deriveFont(Font.PLAIN));
804 contentPane.add(helpLabel, GBC.eol().fill(GridBagConstraints.HORIZONTAL).insets(5, 5, 5, 5));
805 super.setContentPane(contentPane);
806 }
807
808 protected void selectNumberOfTags() {
809 String s = String.format("%d", PROPERTY_RECENT_TAGS_NUMBER.get());
810 while (true) {
811 s = JOptionPane.showInputDialog(this, tr("Please enter the number of recently added tags to display"), s);
812 if (s == null) {
813 return;
814 }
815 try {
816 int v = Integer.parseInt(s);
817 if (v >= 0 && v <= MAX_LRU_TAGS_NUMBER) {
818 PROPERTY_RECENT_TAGS_NUMBER.put(v);
819 return;
820 }
821 } catch (NumberFormatException ex) {
822 Main.warn(ex);
823 }
824 JOptionPane.showMessageDialog(this, tr("Please enter integer number between 0 and {0}", MAX_LRU_TAGS_NUMBER));
825 }
826 }
827
828 protected void suggestRecentlyAddedTags() {
829 if (recentTagsPanel == null) {
830 recentTagsPanel = new JPanel(new GridBagLayout());
831 buildRecentTagsPanel();
832 mainPanel.add(recentTagsPanel, GBC.eol().fill(GBC.HORIZONTAL));
833 } else {
834 Dimension panelOldSize = recentTagsPanel.getPreferredSize();
835 recentTagsPanel.removeAll();
836 buildRecentTagsPanel();
837 Dimension panelNewSize = recentTagsPanel.getPreferredSize();
838 Dimension dialogOldSize = getMinimumSize();
839 Dimension dialogNewSize = new Dimension(dialogOldSize.width, dialogOldSize.height-panelOldSize.height+panelNewSize.height);
840 setMinimumSize(dialogNewSize);
841 setPreferredSize(dialogNewSize);
842 setSize(dialogNewSize);
843 revalidate();
844 repaint();
845 }
846 }
847
848 protected void buildRecentTagsPanel() {
849 final int tagsToShow = Math.min(PROPERTY_RECENT_TAGS_NUMBER.get(), MAX_LRU_TAGS_NUMBER);
850 if (!(tagsToShow > 0 && !recentTags.isEmpty()))
851 return;
852 recentTagsPanel.add(new JLabel(tr("Recently added tags")), GBC.eol());
853
854 int count = 0;
855 destroyActions();
856 // We store the maximum number of recent tags to allow dynamic change of number of tags shown in the preferences.
857 // This implies to iterate in descending order, as the oldest elements will only be removed after we reach the maximum
858 // number and not the number of tags to show.
859 // However, as Set does not allow to iterate in descending order, we need to copy its elements into a List we can access
860 // in reverse order.
861 for (int i = tags.size()-1; i >= 0 && count < tagsToShow; i--) {
862 final Tag t = tags.get(i);
863 boolean keyExists = keyExists(t);
864 if (keyExists && PROPERTY_RECENT_EXISTING.get() == RecentExisting.HIDE)
865 continue;
866 count++;
867 // Create action for reusing the tag, with keyboard shortcut
868 /* POSSIBLE SHORTCUTS: 1,2,3,4,5,6,7,8,9,0=10 */
869 final Shortcut sc = count > 10 ? null : Shortcut.registerShortcut("properties:recent:" + count,
870 tr("Choose recent tag {0}", count), KeyEvent.VK_0 + (count % 10), Shortcut.CTRL);
871 final JosmAction action = new JosmAction(
872 tr("Choose recent tag {0}", count), null, tr("Use this tag again"), sc, false) {
873 @Override
874 public void actionPerformed(ActionEvent e) {
875 keys.setSelectedItem(t.getKey());
876 // fix #7951, #8298 - update list of values before setting value (?)
877 focus.focusGained(null);
878 values.setSelectedItem(t.getValue());
879 selectValuesCombobox();
880 }
881 };
882 /* POSSIBLE SHORTCUTS: 1,2,3,4,5,6,7,8,9,0=10 */
883 final Shortcut scShift = count > 10 ? null : Shortcut.registerShortcut("properties:recent:apply:" + count,
884 tr("Apply recent tag {0}", count), KeyEvent.VK_0 + (count % 10), Shortcut.CTRL_SHIFT);
885 final JosmAction actionShift = new JosmAction(
886 tr("Apply recent tag {0}", count), null, tr("Use this tag again"), scShift, false) {
887 @Override
888 public void actionPerformed(ActionEvent e) {
889 action.actionPerformed(null);
890 performTagAdding();
891 refreshRecentTags();
892 selectKeysComboBox();
893 }
894 };
895 recentTagsActions.add(action);
896 recentTagsActions.add(actionShift);
897 if (keyExists && PROPERTY_RECENT_EXISTING.get() == RecentExisting.DISABLE) {
898 action.setEnabled(false);
899 }
900 // Find and display icon
901 ImageIcon icon = MapPaintStyles.getNodeIcon(t, false); // Filters deprecated icon
902 if (icon == null) {
903 // If no icon found in map style look at presets
904 Map<String, String> map = new HashMap<>();
905 map.put(t.getKey(), t.getValue());
906 for (TaggingPreset tp : TaggingPresets.getMatchingPresets(null, map, false)) {
907 icon = tp.getIcon();
908 if (icon != null) {
909 break;
910 }
911 }
912 // If still nothing display an empty icon
913 if (icon == null) {
914 icon = new ImageIcon(new BufferedImage(16, 16, BufferedImage.TYPE_INT_ARGB));
915 }
916 }
917 GridBagConstraints gbc = new GridBagConstraints();
918 gbc.ipadx = 5;
919 recentTagsPanel.add(new JLabel(action.isEnabled() ? icon : GuiHelper.getDisabledIcon(icon)), gbc);
920 // Create tag label
921 final String color = action.isEnabled() ? "" : "; color:gray";
922 final JLabel tagLabel = new JLabel("<html>"
923 + "<style>td{" + color + "}</style>"
924 + "<table><tr>"
925 + "<td>" + count + ".</td>"
926 + "<td style='border:1px solid gray'>" + XmlWriter.encode(t.toString(), true) + '<' +
927 "/td></tr></table></html>");
928 tagLabel.setFont(tagLabel.getFont().deriveFont(Font.PLAIN));
929 if (action.isEnabled() && sc != null && scShift != null) {
930 // Register action
931 recentTagsPanel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(sc.getKeyStroke(), "choose"+count);
932 recentTagsPanel.getActionMap().put("choose"+count, action);
933 recentTagsPanel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(scShift.getKeyStroke(), "apply"+count);
934 recentTagsPanel.getActionMap().put("apply"+count, actionShift);
935 }
936 if (action.isEnabled()) {
937 // Make the tag label clickable and set tooltip to the action description (this displays also the keyboard shortcut)
938 tagLabel.setToolTipText((String) action.getValue(Action.SHORT_DESCRIPTION));
939 tagLabel.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
940 tagLabel.addMouseListener(new MouseAdapter() {
941 @Override
942 public void mouseClicked(MouseEvent e) {
943 action.actionPerformed(null);
944 if (e.isShiftDown()) {
945 // add tags on Shift-Click
946 performTagAdding();
947 refreshRecentTags();
948 selectKeysComboBox();
949 } else if (e.getClickCount() > 1) {
950 // add tags and close window on double-click
951 buttonAction(0, null); // emulate OK click and close the dialog
952 }
953 }
954 });
955 } else {
956 // Disable tag label
957 tagLabel.setEnabled(false);
958 // Explain in the tooltip why
959 tagLabel.setToolTipText(tr("The key ''{0}'' is already used", t.getKey()));
960 }
961 // Finally add label to the resulting panel
962 JPanel tagPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0));
963 tagPanel.add(tagLabel);
964 recentTagsPanel.add(tagPanel, GBC.eol().fill(GBC.HORIZONTAL));
965 }
966 // Clear label if no tags were added
967 if (count == 0) {
968 recentTagsPanel.removeAll();
969 }
970 }
971
972 public void destroyActions() {
973 for (JosmAction action : recentTagsActions) {
974 action.destroy();
975 }
976 }
977
978 /**
979 * Read tags from comboboxes and add it to all selected objects
980 */
981 public final void performTagAdding() {
982 String key = Tag.removeWhiteSpaces(keys.getEditor().getItem().toString());
983 String value = Tag.removeWhiteSpaces(values.getEditor().getItem().toString());
984 if (key.isEmpty() || value.isEmpty())
985 return;
986 for (OsmPrimitive osm : sel) {
987 String val = osm.get(key);
988 if (val != null && !val.equals(value)) {
989 if (!warnOverwriteKey(tr("You changed the value of ''{0}'' from ''{1}'' to ''{2}''.", key, val, value),
990 "overwriteAddKey"))
991 return;
992 break;
993 }
994 }
995 lastAddKey = key;
996 lastAddValue = value;
997 recentTags.put(new Tag(key, value), null);
998 valueCount.put(key, new TreeMap<String, Integer>());
999 AutoCompletionManager.rememberUserInput(key, value, false);
1000 commandCount++;
1001 Main.main.undoRedo.add(new ChangePropertyCommand(sel, key, value));
1002 changedKey = key;
1003 clearEntries();
1004 }
1005
1006 protected void clearEntries() {
1007 keys.getEditor().setItem("");
1008 values.getEditor().setItem("");
1009 }
1010
1011 public void undoAllTagsAdding() {
1012 Main.main.undoRedo.undo(commandCount);
1013 }
1014
1015 private boolean keyExists(final Tag t) {
1016 return valueCount.containsKey(t.getKey());
1017 }
1018
1019 private void refreshRecentTags() {
1020 switch (PROPERTY_REFRESH_RECENT.get()) {
1021 case REFRESH: cacheRecentTags(); // break missing intentionally
1022 case STATUS: suggestRecentlyAddedTags();
1023 }
1024 }
1025 }
1026}
Note: See TracBrowser for help on using the repository browser.