source: josm/trunk/src/org/openstreetmap/josm/gui/tagging/TaggingPresetItems.java@ 8811

Last change on this file since 8811 was 8811, checked in by simon04, 9 years ago

see #11916 - Refactoring of SearchAction/SearchCompiler

  • Property svn:eol-style set to native
File size: 59.6 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.tagging;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5import static org.openstreetmap.josm.tools.I18n.trc;
6
7import java.awt.Component;
8import java.awt.Dimension;
9import java.awt.Font;
10import java.awt.GridBagLayout;
11import java.awt.GridLayout;
12import java.awt.Insets;
13import java.awt.event.ActionEvent;
14import java.awt.event.ActionListener;
15import java.awt.event.MouseAdapter;
16import java.awt.event.MouseEvent;
17import java.io.File;
18import java.lang.reflect.Method;
19import java.lang.reflect.Modifier;
20import java.text.NumberFormat;
21import java.text.ParseException;
22import java.util.ArrayList;
23import java.util.Arrays;
24import java.util.Collection;
25import java.util.Collections;
26import java.util.EnumSet;
27import java.util.HashMap;
28import java.util.LinkedHashMap;
29import java.util.LinkedList;
30import java.util.List;
31import java.util.Map;
32import java.util.Map.Entry;
33import java.util.Set;
34import java.util.SortedSet;
35import java.util.TreeSet;
36
37import javax.swing.AbstractButton;
38import javax.swing.BorderFactory;
39import javax.swing.ButtonGroup;
40import javax.swing.Icon;
41import javax.swing.ImageIcon;
42import javax.swing.JButton;
43import javax.swing.JComponent;
44import javax.swing.JLabel;
45import javax.swing.JList;
46import javax.swing.JPanel;
47import javax.swing.JScrollPane;
48import javax.swing.JSeparator;
49import javax.swing.JToggleButton;
50import javax.swing.ListCellRenderer;
51import javax.swing.ListModel;
52
53import org.openstreetmap.josm.Main;
54import org.openstreetmap.josm.actions.search.SearchAction;
55import org.openstreetmap.josm.actions.search.SearchCompiler;
56import org.openstreetmap.josm.data.osm.OsmPrimitive;
57import org.openstreetmap.josm.data.osm.OsmUtils;
58import org.openstreetmap.josm.data.osm.Tag;
59import org.openstreetmap.josm.data.preferences.BooleanProperty;
60import org.openstreetmap.josm.gui.tagging.ac.AutoCompletingTextField;
61import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionItemPriority;
62import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionList;
63import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionManager;
64import org.openstreetmap.josm.gui.widgets.JosmComboBox;
65import org.openstreetmap.josm.gui.widgets.JosmTextField;
66import org.openstreetmap.josm.gui.widgets.QuadStateCheckBox;
67import org.openstreetmap.josm.gui.widgets.UrlLabel;
68import org.openstreetmap.josm.tools.AlphanumComparator;
69import org.openstreetmap.josm.tools.GBC;
70import org.openstreetmap.josm.tools.ImageProvider;
71import org.openstreetmap.josm.tools.Predicate;
72import org.openstreetmap.josm.tools.Utils;
73import org.xml.sax.SAXException;
74
75/**
76 * Class that contains all subtypes of TaggingPresetItem, static supplementary data, types and methods
77 * @since 6068
78 */
79public final class TaggingPresetItems {
80 private TaggingPresetItems() {
81 }
82
83 private static int auto_increment_selected = 0;
84 /** Translatation of "<different>". Use in combo boxes to display en entry matching several different values. */
85 public static final String DIFFERENT = tr("<different>");
86
87 private static final BooleanProperty PROP_FILL_DEFAULT = new BooleanProperty("taggingpreset.fill-default-for-tagged-primitives", false);
88
89 // cache the parsing of types using a LRU cache (http://java-planet.blogspot.com/2005/08/how-to-set-up-simple-lru-cache-using.html)
90 private static final Map<String, Set<TaggingPresetType>> TYPE_CACHE = new LinkedHashMap<>(16, 1.1f, true);
91
92 /**
93 * Last value of each key used in presets, used for prefilling corresponding fields
94 */
95 private static final Map<String, String> LAST_VALUES = new HashMap<>();
96
97 public static class PresetListEntry implements Comparable<PresetListEntry> {
98 public String value;
99 /** The context used for translating {@link #value} */
100 public String value_context;
101 public String display_value;
102 public String short_description;
103 /** The location of icon file to display */
104 public String icon;
105 /** The size of displayed icon. If not set, default is size from icon file */
106 public String icon_size;
107 /** The localized version of {@link #display_value}. */
108 public String locale_display_value;
109 /** The localized version of {@link #short_description}. */
110 public String locale_short_description;
111 private final File zipIcons = TaggingPresetReader.getZipIcons();
112
113 // Cached size (currently only for Combo) to speed up preset dialog initialization
114 private int prefferedWidth = -1;
115 private int prefferedHeight = -1;
116
117 public String getListDisplay() {
118 if (value.equals(DIFFERENT))
119 return "<b>"+DIFFERENT.replaceAll("<", "&lt;").replaceAll(">", "&gt;")+"</b>";
120
121 if (value.isEmpty())
122 return "&nbsp;";
123
124 final StringBuilder res = new StringBuilder("<b>");
125 res.append(getDisplayValue(true).replaceAll("<", "&lt;").replaceAll(">", "&gt;"))
126 .append("</b>");
127 if (getShortDescription(true) != null) {
128 // wrap in table to restrict the text width
129 res.append("<div style=\"width:300px; padding:0 0 5px 5px\">")
130 .append(getShortDescription(true))
131 .append("</div>");
132 }
133 return res.toString();
134 }
135
136 /**
137 * Returns the entry icon, if any.
138 * @return the entry icon, or {@code null}
139 */
140 public ImageIcon getIcon() {
141 return icon == null ? null : loadImageIcon(icon, zipIcons, parseInteger(icon_size));
142 }
143
144 /**
145 * Constructs a new {@code PresetListEntry}, uninitialized.
146 */
147 public PresetListEntry() {
148 }
149
150 public PresetListEntry(String value) {
151 this.value = value;
152 }
153
154 public String getDisplayValue(boolean translated) {
155 return translated
156 ? Utils.firstNonNull(locale_display_value, tr(display_value), trc(value_context, value))
157 : Utils.firstNonNull(display_value, value);
158 }
159
160 public String getShortDescription(boolean translated) {
161 return translated
162 ? Utils.firstNonNull(locale_short_description, tr(short_description))
163 : short_description;
164 }
165
166 // toString is mainly used to initialize the Editor
167 @Override
168 public String toString() {
169 if (value.equals(DIFFERENT))
170 return DIFFERENT;
171 return getDisplayValue(true).replaceAll("<.*>", ""); // remove additional markup, e.g. <br>
172 }
173
174 @Override
175 public int compareTo(PresetListEntry o) {
176 return AlphanumComparator.getInstance().compare(this.getDisplayValue(true), o.getDisplayValue(true));
177 }
178 }
179
180 public static class Role {
181 public Set<TaggingPresetType> types;
182 public String key;
183 /** The text to display */
184 public String text;
185 /** The context used for translating {@link #text} */
186 public String text_context;
187 /** The localized version of {@link #text}. */
188 public String locale_text;
189 public SearchCompiler.Match memberExpression;
190
191 public boolean required = false;
192 private long count = 0;
193
194 public void setType(String types) throws SAXException {
195 this.types = getType(types);
196 }
197
198 public void setRequisite(String str) throws SAXException {
199 if ("required".equals(str)) {
200 required = true;
201 } else if (!"optional".equals(str))
202 throw new SAXException(tr("Unknown requisite: {0}", str));
203 }
204
205 public void setMember_expression(String member_expression) throws SAXException {
206 try {
207 final SearchAction.SearchSetting searchSetting = new SearchAction.SearchSetting();
208 searchSetting.text = member_expression;
209 searchSetting.caseSensitive = true;
210 searchSetting.regexSearch = true;
211 this.memberExpression = SearchCompiler.compile(searchSetting);
212 } catch (SearchCompiler.ParseError ex) {
213 throw new SAXException(tr("Illegal member expression: {0}", ex.getMessage()), ex);
214 }
215 }
216
217 public void setCount(String count) {
218 this.count = Long.parseLong(count);
219 }
220
221 /**
222 * Return either argument, the highest possible value or the lowest allowed value
223 */
224 public long getValidCount(long c) {
225 if (count > 0 && !required)
226 return c != 0 ? count : 0;
227 else if (count > 0)
228 return count;
229 else if (!required)
230 return c != 0 ? c : 0;
231 else
232 return c != 0 ? c : 1;
233 }
234
235 public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel) {
236 String cstring;
237 if (count > 0 && !required) {
238 cstring = "0,"+count;
239 } else if (count > 0) {
240 cstring = String.valueOf(count);
241 } else if (!required) {
242 cstring = "0-...";
243 } else {
244 cstring = "1-...";
245 }
246 if (locale_text == null) {
247 locale_text = getLocaleText(text, text_context, null);
248 }
249 p.add(new JLabel(locale_text+":"), GBC.std().insets(0, 0, 10, 0));
250 p.add(new JLabel(key), GBC.std().insets(0, 0, 10, 0));
251 p.add(new JLabel(cstring), types == null ? GBC.eol() : GBC.std().insets(0, 0, 10, 0));
252 if (types != null) {
253 JPanel pp = new JPanel();
254 for (TaggingPresetType t : types) {
255 pp.add(new JLabel(ImageProvider.get(t.getIconName())));
256 }
257 p.add(pp, GBC.eol());
258 }
259 return true;
260 }
261 }
262
263 /**
264 * Enum denoting how a match (see {@link TaggingPresetItem#matches}) is performed.
265 */
266 public static enum MatchType {
267
268 /** Neutral, i.e., do not consider this item for matching. */
269 NONE("none"),
270 /** Positive if key matches, neutral otherwise. */
271 KEY("key"),
272 /** Positive if key matches, negative otherwise. */
273 KEY_REQUIRED("key!"),
274 /** Positive if key and value matches, neutral otherwise. */
275 KEY_VALUE("keyvalue"),
276 /** Positive if key and value matches, negative otherwise. */
277 KEY_VALUE_REQUIRED("keyvalue!");
278
279 private final String value;
280
281 MatchType(String value) {
282 this.value = value;
283 }
284
285 /**
286 * Replies the associated textual value.
287 * @return the associated textual value
288 */
289 public String getValue() {
290 return value;
291 }
292
293 /**
294 * Determines the {@code MatchType} for the given textual value.
295 * @param type the textual value
296 * @return the {@code MatchType} for the given textual value
297 */
298 public static MatchType ofString(String type) {
299 for (MatchType i : EnumSet.allOf(MatchType.class)) {
300 if (i.getValue().equals(type))
301 return i;
302 }
303 throw new IllegalArgumentException(type + " is not allowed");
304 }
305 }
306
307 public static class Usage {
308 private SortedSet<String> values;
309 private boolean hadKeys = false;
310 private boolean hadEmpty = false;
311
312 public boolean hasUniqueValue() {
313 return values.size() == 1 && !hadEmpty;
314 }
315
316 public boolean unused() {
317 return values.isEmpty();
318 }
319
320 public String getFirst() {
321 return values.first();
322 }
323
324 public boolean hadKeys() {
325 return hadKeys;
326 }
327 }
328
329 /**
330 * A tagging preset item displaying a localizable text.
331 * @since 6190
332 */
333 public abstract static class TaggingPresetTextItem extends TaggingPresetItem {
334
335 /** The text to display */
336 public String text;
337
338 /** The context used for translating {@link #text} */
339 public String text_context;
340
341 /** The localized version of {@link #text} */
342 public String locale_text;
343
344 protected final void initializeLocaleText(String defaultText) {
345 if (locale_text == null) {
346 locale_text = getLocaleText(text, text_context, defaultText);
347 }
348 }
349
350 @Override
351 void addCommands(List<Tag> changedTags) {
352 }
353
354 protected String fieldsToString() {
355 return (text != null ? "text=" + text + ", " : "")
356 + (text_context != null ? "text_context=" + text_context + ", " : "")
357 + (locale_text != null ? "locale_text=" + locale_text : "");
358 }
359
360 @Override
361 public String toString() {
362 return getClass().getSimpleName() + " [" + fieldsToString() + "]";
363 }
364 }
365
366 /**
367 * Label type.
368 */
369 public static class Label extends TaggingPresetTextItem {
370
371 /** The location of icon file to display (optional) */
372 public String icon;
373 /** The size of displayed icon. If not set, default is 16px */
374 public String icon_size;
375
376 @Override
377 public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel, boolean presetInitiallyMatches) {
378 initializeLocaleText(null);
379 addLabel(p, getIcon(), locale_text);
380 return true;
381 }
382
383 /**
384 * Adds a new {@code JLabel} to the given panel.
385 * @param p The panel
386 * @param icon the icon (optional, can be null)
387 * @param label The text label
388 */
389 public static void addLabel(JPanel p, Icon icon, String label) {
390 p.add(new JLabel(label, icon, JLabel.LEADING), GBC.eol().fill(GBC.HORIZONTAL));
391 }
392
393 /**
394 * Returns the label icon, if any.
395 * @return the label icon, or {@code null}
396 */
397 public ImageIcon getIcon() {
398 Integer size = parseInteger(icon_size);
399 return icon == null ? null : loadImageIcon(icon, TaggingPresetReader.getZipIcons(), size != null ? size : 16);
400 }
401 }
402
403 /**
404 * Hyperlink type.
405 */
406 public static class Link extends TaggingPresetTextItem {
407
408 /** The link to display. */
409 public String href;
410
411 /** The localized version of {@link #href}. */
412 public String locale_href;
413
414 @Override
415 public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel, boolean presetInitiallyMatches) {
416 initializeLocaleText(tr("More information about this feature"));
417 String url = locale_href;
418 if (url == null) {
419 url = href;
420 }
421 if (url != null) {
422 p.add(new UrlLabel(url, locale_text, 2), GBC.eol().insets(0, 10, 0, 0).fill(GBC.HORIZONTAL));
423 }
424 return false;
425 }
426
427 @Override
428 protected String fieldsToString() {
429 return super.fieldsToString()
430 + (href != null ? "href=" + href + ", " : "")
431 + (locale_href != null ? "locale_href=" + locale_href + ", " : "");
432 }
433 }
434
435 public static class PresetLink extends TaggingPresetItem {
436
437 public String preset_name = "";
438
439 @Override
440 boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel, boolean presetInitiallyMatches) {
441 final String presetName = preset_name;
442 final TaggingPreset t = Utils.filter(TaggingPresets.getTaggingPresets(), new Predicate<TaggingPreset>() {
443 @Override
444 public boolean evaluate(TaggingPreset object) {
445 return presetName.equals(object.name);
446 }
447 }).iterator().next();
448 if (t == null) return false;
449 JLabel lbl = new PresetLabel(t);
450 lbl.addMouseListener(new MouseAdapter() {
451 @Override
452 public void mouseClicked(MouseEvent arg0) {
453 t.actionPerformed(null);
454 }
455 });
456 p.add(lbl, GBC.eol().fill(GBC.HORIZONTAL));
457 return false;
458 }
459
460 @Override
461 void addCommands(List<Tag> changedTags) {
462 }
463 }
464
465 public static class Roles extends TaggingPresetItem {
466
467 public final List<Role> roles = new LinkedList<>();
468
469 @Override
470 public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel, boolean presetInitiallyMatches) {
471 p.add(new JLabel(" "), GBC.eol()); // space
472 if (!roles.isEmpty()) {
473 JPanel proles = new JPanel(new GridBagLayout());
474 proles.add(new JLabel(tr("Available roles")), GBC.std().insets(0, 0, 10, 0));
475 proles.add(new JLabel(tr("role")), GBC.std().insets(0, 0, 10, 0));
476 proles.add(new JLabel(tr("count")), GBC.std().insets(0, 0, 10, 0));
477 proles.add(new JLabel(tr("elements")), GBC.eol());
478 for (Role i : roles) {
479 i.addToPanel(proles, sel);
480 }
481 p.add(proles, GBC.eol());
482 }
483 return false;
484 }
485
486 @Override
487 public void addCommands(List<Tag> changedTags) {
488 }
489 }
490
491 public static class Optional extends TaggingPresetTextItem {
492
493 // TODO: Draw a box around optional stuff
494 @Override
495 public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel, boolean presetInitiallyMatches) {
496 initializeLocaleText(tr("Optional Attributes:"));
497 p.add(new JLabel(" "), GBC.eol()); // space
498 p.add(new JLabel(locale_text), GBC.eol());
499 p.add(new JLabel(" "), GBC.eol()); // space
500 return false;
501 }
502 }
503
504 /**
505 * Horizontal separator type.
506 */
507 public static class Space extends TaggingPresetItem {
508
509 @Override
510 public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel, boolean presetInitiallyMatches) {
511 p.add(new JLabel(" "), GBC.eol()); // space
512 return false;
513 }
514
515 @Override
516 public void addCommands(List<Tag> changedTags) {
517 }
518
519 @Override
520 public String toString() {
521 return "Space";
522 }
523 }
524
525 /**
526 * Class used to represent a {@link JSeparator} inside tagging preset window.
527 * @since 6198
528 */
529 public static class ItemSeparator extends TaggingPresetItem {
530
531 @Override
532 public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel, boolean presetInitiallyMatches) {
533 p.add(new JSeparator(), GBC.eol().fill(GBC.HORIZONTAL).insets(0, 5, 0, 5));
534 return false;
535 }
536
537 @Override
538 public void addCommands(List<Tag> changedTags) {
539 }
540
541 @Override
542 public String toString() {
543 return "ItemSeparator";
544 }
545 }
546
547 /**
548 * Preset item associated to an OSM key.
549 */
550 public abstract static class KeyedItem extends TaggingPresetItem {
551
552 public String key;
553 /** The text to display */
554 public String text;
555 /** The context used for translating {@link #text} */
556 public String text_context;
557 public String match = getDefaultMatch().getValue();
558
559 public abstract MatchType getDefaultMatch();
560
561 public abstract Collection<String> getValues();
562
563 @Override
564 Boolean matches(Map<String, String> tags) {
565 switch (MatchType.ofString(match)) {
566 case NONE:
567 return null;
568 case KEY:
569 return tags.containsKey(key) ? Boolean.TRUE : null;
570 case KEY_REQUIRED:
571 return tags.containsKey(key);
572 case KEY_VALUE:
573 return tags.containsKey(key) && getValues().contains(tags.get(key)) ? Boolean.TRUE : null;
574 case KEY_VALUE_REQUIRED:
575 return tags.containsKey(key) && getValues().contains(tags.get(key));
576 default:
577 throw new IllegalStateException();
578 }
579 }
580
581 @Override
582 public String toString() {
583 return "KeyedItem [key=" + key + ", text=" + text
584 + ", text_context=" + text_context + ", match=" + match
585 + "]";
586 }
587 }
588
589 /**
590 * Invisible type allowing to hardcode an OSM key/value from the preset definition.
591 */
592 public static class Key extends KeyedItem {
593
594 /** The hardcoded value for key */
595 public String value;
596
597 @Override
598 public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel, boolean presetInitiallyMatches) {
599 return false;
600 }
601
602 @Override
603 public void addCommands(List<Tag> changedTags) {
604 changedTags.add(new Tag(key, value));
605 }
606
607 @Override
608 public MatchType getDefaultMatch() {
609 return MatchType.KEY_VALUE_REQUIRED;
610 }
611
612 @Override
613 public Collection<String> getValues() {
614 return Collections.singleton(value);
615 }
616
617 @Override
618 public String toString() {
619 return "Key [key=" + key + ", value=" + value + ", text=" + text
620 + ", text_context=" + text_context + ", match=" + match
621 + "]";
622 }
623 }
624
625 /**
626 * Text field type.
627 */
628 public static class Text extends KeyedItem {
629
630 /** The localized version of {@link #text}. */
631 public String locale_text;
632 public String default_;
633 public String originalValue;
634 public String use_last_as_default = "false";
635 public String auto_increment;
636 public String length;
637 public String alternative_autocomplete_keys;
638
639 private JComponent value;
640
641 @Override
642 public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel, boolean presetInitiallyMatches) {
643
644 // find out if our key is already used in the selection.
645 Usage usage = determineTextUsage(sel, key);
646 AutoCompletingTextField textField = new AutoCompletingTextField();
647 if (alternative_autocomplete_keys != null) {
648 initAutoCompletionField(textField, (key + "," + alternative_autocomplete_keys).split(","));
649 } else {
650 initAutoCompletionField(textField, key);
651 }
652 if (Main.pref.getBoolean("taggingpreset.display-keys-as-hint", true)) {
653 textField.setHint(key);
654 }
655 if (length != null && !length.isEmpty()) {
656 textField.setMaxChars(Integer.valueOf(length));
657 }
658 if (usage.unused()) {
659 if (auto_increment_selected != 0 && auto_increment != null) {
660 try {
661 textField.setText(Integer.toString(Integer.parseInt(LAST_VALUES.get(key)) + auto_increment_selected));
662 } catch (NumberFormatException ex) {
663 // Ignore - cannot auto-increment if last was non-numeric
664 if (Main.isTraceEnabled()) {
665 Main.trace(ex.getMessage());
666 }
667 }
668 } else if (!usage.hadKeys() || PROP_FILL_DEFAULT.get() || "force".equals(use_last_as_default)) {
669 // selected osm primitives are untagged or filling default values feature is enabled
670 if (!"false".equals(use_last_as_default) && LAST_VALUES.containsKey(key) && !presetInitiallyMatches) {
671 textField.setText(LAST_VALUES.get(key));
672 } else {
673 textField.setText(default_);
674 }
675 } else {
676 // selected osm primitives are tagged and filling default values feature is disabled
677 textField.setText("");
678 }
679 value = textField;
680 originalValue = null;
681 } else if (usage.hasUniqueValue()) {
682 // all objects use the same value
683 textField.setText(usage.getFirst());
684 value = textField;
685 originalValue = usage.getFirst();
686 } else {
687 // the objects have different values
688 JosmComboBox<String> comboBox = new JosmComboBox<>(usage.values.toArray(new String[0]));
689 comboBox.setEditable(true);
690 comboBox.setEditor(textField);
691 comboBox.getEditor().setItem(DIFFERENT);
692 value = comboBox;
693 originalValue = DIFFERENT;
694 }
695 if (locale_text == null) {
696 locale_text = getLocaleText(text, text_context, null);
697 }
698
699 // if there's an auto_increment setting, then wrap the text field
700 // into a panel, appending a number of buttons.
701 // auto_increment has a format like -2,-1,1,2
702 // the text box being the first component in the panel is relied
703 // on in a rather ugly fashion further down.
704 if (auto_increment != null) {
705 ButtonGroup bg = new ButtonGroup();
706 JPanel pnl = new JPanel(new GridBagLayout());
707 pnl.add(value, GBC.std().fill(GBC.HORIZONTAL));
708
709 // first, one button for each auto_increment value
710 for (final String ai : auto_increment.split(",")) {
711 JToggleButton aibutton = new JToggleButton(ai);
712 aibutton.setToolTipText(tr("Select auto-increment of {0} for this field", ai));
713 aibutton.setMargin(new java.awt.Insets(0, 0, 0, 0));
714 aibutton.setFocusable(false);
715 saveHorizontalSpace(aibutton);
716 bg.add(aibutton);
717 try {
718 // TODO there must be a better way to parse a number like "+3" than this.
719 final int buttonvalue = (NumberFormat.getIntegerInstance().parse(ai.replace("+", ""))).intValue();
720 if (auto_increment_selected == buttonvalue) aibutton.setSelected(true);
721 aibutton.addActionListener(new ActionListener() {
722 @Override
723 public void actionPerformed(ActionEvent e) {
724 auto_increment_selected = buttonvalue;
725 }
726 });
727 pnl.add(aibutton, GBC.std());
728 } catch (ParseException x) {
729 Main.error("Cannot parse auto-increment value of '" + ai + "' into an integer");
730 }
731 }
732
733 // an invisible toggle button for "release" of the button group
734 final JToggleButton clearbutton = new JToggleButton("X");
735 clearbutton.setVisible(false);
736 clearbutton.setFocusable(false);
737 bg.add(clearbutton);
738 // and its visible counterpart. - this mechanism allows us to
739 // have *no* button selected after the X is clicked, instead
740 // of the X remaining selected
741 JButton releasebutton = new JButton("X");
742 releasebutton.setToolTipText(tr("Cancel auto-increment for this field"));
743 releasebutton.setMargin(new java.awt.Insets(0, 0, 0, 0));
744 releasebutton.setFocusable(false);
745 releasebutton.addActionListener(new ActionListener() {
746 @Override
747 public void actionPerformed(ActionEvent e) {
748 auto_increment_selected = 0;
749 clearbutton.setSelected(true);
750 }
751 });
752 saveHorizontalSpace(releasebutton);
753 pnl.add(releasebutton, GBC.eol());
754 value = pnl;
755 }
756 p.add(new JLabel(locale_text+":"), GBC.std().insets(0, 0, 10, 0));
757 p.add(value, GBC.eol().fill(GBC.HORIZONTAL));
758 return true;
759 }
760
761 private static void saveHorizontalSpace(AbstractButton button) {
762 Insets insets = button.getBorder().getBorderInsets(button);
763 // Ensure the current look&feel does not waste horizontal space (as seen in Nimbus & Aqua)
764 if (insets != null && insets.left+insets.right > insets.top+insets.bottom) {
765 int min = Math.min(insets.top, insets.bottom);
766 button.setBorder(BorderFactory.createEmptyBorder(insets.top, min, insets.bottom, min));
767 }
768 }
769
770 private static String getValue(Component comp) {
771 if (comp instanceof JosmComboBox) {
772 return ((JosmComboBox<?>) comp).getEditor().getItem().toString();
773 } else if (comp instanceof JosmTextField) {
774 return ((JosmTextField) comp).getText();
775 } else if (comp instanceof JPanel) {
776 return getValue(((JPanel) comp).getComponent(0));
777 } else {
778 return null;
779 }
780 }
781
782 @Override
783 public void addCommands(List<Tag> changedTags) {
784
785 // return if unchanged
786 String v = getValue(value);
787 if (v == null) {
788 Main.error("No 'last value' support for component " + value);
789 return;
790 }
791
792 v = Tag.removeWhiteSpaces(v);
793
794 if (!"false".equals(use_last_as_default) || auto_increment != null) {
795 LAST_VALUES.put(key, v);
796 }
797 if (v.equals(originalValue) || (originalValue == null && v.isEmpty()))
798 return;
799
800 changedTags.add(new Tag(key, v));
801 AutoCompletionManager.rememberUserInput(key, v, true);
802 }
803
804 @Override
805 boolean requestFocusInWindow() {
806 return value.requestFocusInWindow();
807 }
808
809 @Override
810 public MatchType getDefaultMatch() {
811 return MatchType.NONE;
812 }
813
814 @Override
815 public Collection<String> getValues() {
816 if (default_ == null || default_.isEmpty())
817 return Collections.emptyList();
818 return Collections.singleton(default_);
819 }
820 }
821
822 /**
823 * A group of {@link Check}s.
824 * @since 6114
825 */
826 public static class CheckGroup extends TaggingPresetItem {
827
828 /**
829 * Number of columns (positive integer)
830 */
831 public String columns;
832
833 /**
834 * List of checkboxes
835 */
836 public final List<Check> checks = new LinkedList<>();
837
838 @Override
839 boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel, boolean presetInitiallyMatches) {
840 Integer cols = Integer.valueOf(columns);
841 int rows = (int) Math.ceil(checks.size()/cols.doubleValue());
842 JPanel panel = new JPanel(new GridLayout(rows, cols));
843
844 for (Check check : checks) {
845 check.addToPanel(panel, sel, presetInitiallyMatches);
846 }
847
848 p.add(panel, GBC.eol());
849 return false;
850 }
851
852 @Override
853 void addCommands(List<Tag> changedTags) {
854 for (Check check : checks) {
855 check.addCommands(changedTags);
856 }
857 }
858
859 @Override
860 Boolean matches(Map<String, String> tags) {
861 for (Check check : checks) {
862 if (Boolean.TRUE.equals(check.matches(tags))) {
863 return true;
864 }
865 }
866 return null;
867 }
868
869 @Override
870 public String toString() {
871 return "CheckGroup [columns=" + columns + "]";
872 }
873 }
874
875 /**
876 * Checkbox type.
877 */
878 public static class Check extends KeyedItem {
879
880 /** The localized version of {@link #text}. */
881 public String locale_text;
882 /** the value to set when checked (default is "yes") */
883 public String value_on = OsmUtils.trueval;
884 /** the value to set when unchecked (default is "no") */
885 public String value_off = OsmUtils.falseval;
886 /** whether the off value is disabled in the dialog, i.e., only unset or yes are provided */
887 public boolean disable_off = false;
888 /** ticked on/off (default is "off") */
889 public boolean default_ = false; // only used for tagless objects
890
891 private QuadStateCheckBox check;
892 private QuadStateCheckBox.State initialState;
893 private boolean def;
894
895 @Override
896 public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel, boolean presetInitiallyMatches) {
897
898 // find out if our key is already used in the selection.
899 final Usage usage = determineBooleanUsage(sel, key);
900 final String oneValue = usage.values.isEmpty() ? null : usage.values.last();
901 def = default_;
902
903 if (locale_text == null) {
904 locale_text = getLocaleText(text, text_context, null);
905 }
906
907 if (usage.values.size() < 2 && (oneValue == null || value_on.equals(oneValue) || value_off.equals(oneValue))) {
908 if (def && !PROP_FILL_DEFAULT.get()) {
909 // default is set and filling default values feature is disabled - check if all primitives are untagged
910 for (OsmPrimitive s : sel) {
911 if (s.hasKeys()) {
912 def = false;
913 }
914 }
915 }
916
917 // all selected objects share the same value which is either true or false or unset,
918 // we can display a standard check box.
919 initialState = value_on.equals(oneValue)
920 ? QuadStateCheckBox.State.SELECTED
921 : value_off.equals(oneValue)
922 ? QuadStateCheckBox.State.NOT_SELECTED
923 : def
924 ? QuadStateCheckBox.State.SELECTED
925 : QuadStateCheckBox.State.UNSET;
926 } else {
927 def = false;
928 // the objects have different values, or one or more objects have something
929 // else than true/false. we display a quad-state check box
930 // in "partial" state.
931 initialState = QuadStateCheckBox.State.PARTIAL;
932 }
933
934 final List<QuadStateCheckBox.State> allowedStates = new ArrayList<>(4);
935 if (QuadStateCheckBox.State.PARTIAL.equals(initialState))
936 allowedStates.add(QuadStateCheckBox.State.PARTIAL);
937 allowedStates.add(QuadStateCheckBox.State.SELECTED);
938 if (!disable_off || value_off.equals(oneValue))
939 allowedStates.add(QuadStateCheckBox.State.NOT_SELECTED);
940 allowedStates.add(QuadStateCheckBox.State.UNSET);
941 check = new QuadStateCheckBox(locale_text, initialState,
942 allowedStates.toArray(new QuadStateCheckBox.State[allowedStates.size()]));
943
944 p.add(check, GBC.eol().fill(GBC.HORIZONTAL));
945 return true;
946 }
947
948 @Override
949 public void addCommands(List<Tag> changedTags) {
950 // if the user hasn't changed anything, don't create a command.
951 if (check.getState() == initialState && !def) return;
952
953 // otherwise change things according to the selected value.
954 changedTags.add(new Tag(key,
955 check.getState() == QuadStateCheckBox.State.SELECTED ? value_on :
956 check.getState() == QuadStateCheckBox.State.NOT_SELECTED ? value_off :
957 null));
958 }
959
960 @Override
961 boolean requestFocusInWindow() {
962 return check.requestFocusInWindow();
963 }
964
965 @Override
966 public MatchType getDefaultMatch() {
967 return MatchType.NONE;
968 }
969
970 @Override
971 public Collection<String> getValues() {
972 return disable_off ? Arrays.asList(value_on) : Arrays.asList(value_on, value_off);
973 }
974
975 @Override
976 public String toString() {
977 return "Check ["
978 + (locale_text != null ? "locale_text=" + locale_text + ", " : "")
979 + (value_on != null ? "value_on=" + value_on + ", " : "")
980 + (value_off != null ? "value_off=" + value_off + ", " : "")
981 + "default_=" + default_ + ", "
982 + (check != null ? "check=" + check + ", " : "")
983 + (initialState != null ? "initialState=" + initialState
984 + ", " : "") + "def=" + def + "]";
985 }
986 }
987
988 /**
989 * Abstract superclass for combo box and multi-select list types.
990 */
991 public abstract static class ComboMultiSelect extends KeyedItem {
992
993 /** The localized version of {@link #text}. */
994 public String locale_text;
995 public String values;
996 public String values_from;
997 /** The context used for translating {@link #values} */
998 public String values_context;
999 public String display_values;
1000 /** The localized version of {@link #display_values}. */
1001 public String locale_display_values;
1002 public String short_descriptions;
1003 /** The localized version of {@link #short_descriptions}. */
1004 public String locale_short_descriptions;
1005 public String default_;
1006 public String delimiter = ";";
1007 public String use_last_as_default = "false";
1008 /** whether to use values for search via {@link TaggingPresetSelector} */
1009 public String values_searchable = "false";
1010
1011 protected JComponent component;
1012 protected final Map<String, PresetListEntry> lhm = new LinkedHashMap<>();
1013 private boolean initialized = false;
1014 protected Usage usage;
1015 protected Object originalValue;
1016
1017 protected abstract Object getSelectedItem();
1018
1019 protected abstract void addToPanelAnchor(JPanel p, String def, boolean presetInitiallyMatches);
1020
1021 protected char getDelChar() {
1022 return delimiter.isEmpty() ? ';' : delimiter.charAt(0);
1023 }
1024
1025 @Override
1026 public Collection<String> getValues() {
1027 initListEntries();
1028 return lhm.keySet();
1029 }
1030
1031 public Collection<String> getDisplayValues() {
1032 initListEntries();
1033 return Utils.transform(lhm.values(), new Utils.Function<PresetListEntry, String>() {
1034 @Override
1035 public String apply(PresetListEntry x) {
1036 return x.getDisplayValue(true);
1037 }
1038 });
1039 }
1040
1041 @Override
1042 public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel, boolean presetInitiallyMatches) {
1043
1044 initListEntries();
1045
1046 // find out if our key is already used in the selection.
1047 usage = determineTextUsage(sel, key);
1048 if (!usage.hasUniqueValue() && !usage.unused()) {
1049 lhm.put(DIFFERENT, new PresetListEntry(DIFFERENT));
1050 }
1051
1052 p.add(new JLabel(tr("{0}:", locale_text)), GBC.std().insets(0, 0, 10, 0));
1053 addToPanelAnchor(p, default_, presetInitiallyMatches);
1054
1055 return true;
1056
1057 }
1058
1059 private void initListEntries() {
1060 if (initialized) {
1061 lhm.remove(DIFFERENT); // possibly added in #addToPanel
1062 return;
1063 } else if (lhm.isEmpty()) {
1064 initListEntriesFromAttributes();
1065 } else {
1066 if (values != null) {
1067 Main.warn(tr("Warning in tagging preset \"{0}-{1}\": "
1068 + "Ignoring ''{2}'' attribute as ''{3}'' elements are given.",
1069 key, text, "values", "list_entry"));
1070 }
1071 if (display_values != null || locale_display_values != null) {
1072 Main.warn(tr("Warning in tagging preset \"{0}-{1}\": "
1073 + "Ignoring ''{2}'' attribute as ''{3}'' elements are given.",
1074 key, text, "display_values", "list_entry"));
1075 }
1076 if (short_descriptions != null || locale_short_descriptions != null) {
1077 Main.warn(tr("Warning in tagging preset \"{0}-{1}\": "
1078 + "Ignoring ''{2}'' attribute as ''{3}'' elements are given.",
1079 key, text, "short_descriptions", "list_entry"));
1080 }
1081 for (PresetListEntry e : lhm.values()) {
1082 if (e.value_context == null) {
1083 e.value_context = values_context;
1084 }
1085 }
1086 }
1087 if (locale_text == null) {
1088 locale_text = getLocaleText(text, text_context, null);
1089 }
1090 initialized = true;
1091 }
1092
1093 private void initListEntriesFromAttributes() {
1094 char delChar = getDelChar();
1095
1096 String[] value_array = null;
1097
1098 if (values_from != null) {
1099 String[] class_method = values_from.split("#");
1100 if (class_method != null && class_method.length == 2) {
1101 try {
1102 Method method = Class.forName(class_method[0]).getMethod(class_method[1]);
1103 // Check method is public static String[] methodName()
1104 int mod = method.getModifiers();
1105 if (Modifier.isPublic(mod) && Modifier.isStatic(mod)
1106 && method.getReturnType().equals(String[].class) && method.getParameterTypes().length == 0) {
1107 value_array = (String[]) method.invoke(null);
1108 } else {
1109 Main.error(tr("Broken tagging preset \"{0}-{1}\" - Java method given in ''values_from'' is not \"{2}\"", key, text,
1110 "public static String[] methodName()"));
1111 }
1112 } catch (Exception e) {
1113 Main.error(tr("Broken tagging preset \"{0}-{1}\" - Java method given in ''values_from'' threw {2} ({3})", key, text,
1114 e.getClass().getName(), e.getMessage()));
1115 }
1116 }
1117 }
1118
1119 if (value_array == null) {
1120 value_array = splitEscaped(delChar, values);
1121 }
1122
1123 final String displ = Utils.firstNonNull(locale_display_values, display_values);
1124 String[] display_array = displ == null ? value_array : splitEscaped(delChar, displ);
1125
1126 final String descr = Utils.firstNonNull(locale_short_descriptions, short_descriptions);
1127 String[] short_descriptions_array = descr == null ? null : splitEscaped(delChar, descr);
1128
1129 if (display_array.length != value_array.length) {
1130 Main.error(tr("Broken tagging preset \"{0}-{1}\" - number of items in ''display_values'' must be the same as in ''values''",
1131 key, text));
1132 display_array = value_array;
1133 }
1134
1135 if (short_descriptions_array != null && short_descriptions_array.length != value_array.length) {
1136 Main.error(tr("Broken tagging preset \"{0}-{1}\" - number of items in ''short_descriptions'' must be the same as in ''values''",
1137 key, text));
1138 short_descriptions_array = null;
1139 }
1140
1141 final List<PresetListEntry> entries = new ArrayList<>(value_array.length);
1142 for (int i = 0; i < value_array.length; i++) {
1143 final PresetListEntry e = new PresetListEntry(value_array[i]);
1144 e.locale_display_value = locale_display_values != null
1145 ? display_array[i]
1146 : trc(values_context, fixPresetString(display_array[i]));
1147 if (short_descriptions_array != null) {
1148 e.locale_short_description = locale_short_descriptions != null
1149 ? short_descriptions_array[i]
1150 : tr(fixPresetString(short_descriptions_array[i]));
1151 }
1152
1153 entries.add(e);
1154 }
1155
1156 if (Main.pref.getBoolean("taggingpreset.sortvalues", true)) {
1157 Collections.sort(entries);
1158 }
1159
1160 for (PresetListEntry i : entries) {
1161 lhm.put(i.value, i);
1162 }
1163
1164 }
1165
1166 protected String getDisplayIfNull() {
1167 return null;
1168 }
1169
1170 @Override
1171 public void addCommands(List<Tag> changedTags) {
1172 Object obj = getSelectedItem();
1173 String display = (obj == null) ? null : obj.toString();
1174 String value = null;
1175 if (display == null) {
1176 display = getDisplayIfNull();
1177 }
1178
1179 if (display != null) {
1180 for (Entry<String, PresetListEntry> entry : lhm.entrySet()) {
1181 String k = entry.getValue().toString();
1182 if (k != null && k.equals(display)) {
1183 value = entry.getKey();
1184 break;
1185 }
1186 }
1187 if (value == null) {
1188 value = display;
1189 }
1190 } else {
1191 value = "";
1192 }
1193 value = Tag.removeWhiteSpaces(value);
1194
1195 // no change if same as before
1196 if (originalValue == null) {
1197 if (value.isEmpty())
1198 return;
1199 } else if (value.equals(originalValue.toString()))
1200 return;
1201
1202 if (!"false".equals(use_last_as_default)) {
1203 LAST_VALUES.put(key, value);
1204 }
1205 changedTags.add(new Tag(key, value));
1206 }
1207
1208 public void addListEntry(PresetListEntry e) {
1209 lhm.put(e.value, e);
1210 }
1211
1212 public void addListEntries(Collection<PresetListEntry> e) {
1213 for (PresetListEntry i : e) {
1214 addListEntry(i);
1215 }
1216 }
1217
1218 @Override
1219 boolean requestFocusInWindow() {
1220 return component.requestFocusInWindow();
1221 }
1222
1223 private static final ListCellRenderer<PresetListEntry> RENDERER = new ListCellRenderer<PresetListEntry>() {
1224
1225 private final JLabel lbl = new JLabel();
1226
1227 @Override
1228 public Component getListCellRendererComponent(JList<? extends PresetListEntry> list, PresetListEntry item, int index,
1229 boolean isSelected, boolean cellHasFocus) {
1230
1231 // Only return cached size, item is not shown
1232 if (!list.isShowing() && item.prefferedWidth != -1 && item.prefferedHeight != -1) {
1233 if (index == -1) {
1234 lbl.setPreferredSize(new Dimension(item.prefferedWidth, 10));
1235 } else {
1236 lbl.setPreferredSize(new Dimension(item.prefferedWidth, item.prefferedHeight));
1237 }
1238 return lbl;
1239 }
1240
1241 lbl.setPreferredSize(null);
1242
1243 if (isSelected) {
1244 lbl.setBackground(list.getSelectionBackground());
1245 lbl.setForeground(list.getSelectionForeground());
1246 } else {
1247 lbl.setBackground(list.getBackground());
1248 lbl.setForeground(list.getForeground());
1249 }
1250
1251 lbl.setOpaque(true);
1252 lbl.setFont(lbl.getFont().deriveFont(Font.PLAIN));
1253 lbl.setText("<html>" + item.getListDisplay() + "</html>");
1254 lbl.setIcon(item.getIcon());
1255 lbl.setEnabled(list.isEnabled());
1256
1257 // Cache size
1258 item.prefferedWidth = lbl.getPreferredSize().width;
1259 item.prefferedHeight = lbl.getPreferredSize().height;
1260
1261 // We do not want the editor to have the maximum height of all
1262 // entries. Return a dummy with bogus height.
1263 if (index == -1) {
1264 lbl.setPreferredSize(new Dimension(lbl.getPreferredSize().width, 10));
1265 }
1266 return lbl;
1267 }
1268 };
1269
1270 protected ListCellRenderer<PresetListEntry> getListCellRenderer() {
1271 return RENDERER;
1272 }
1273
1274 @Override
1275 public MatchType getDefaultMatch() {
1276 return MatchType.NONE;
1277 }
1278 }
1279
1280 /**
1281 * Combobox type.
1282 */
1283 public static class Combo extends ComboMultiSelect {
1284
1285 public boolean editable = true;
1286 protected JosmComboBox<PresetListEntry> combo;
1287 public String length;
1288
1289 /**
1290 * Constructs a new {@code Combo}.
1291 */
1292 public Combo() {
1293 delimiter = ",";
1294 }
1295
1296 @Override
1297 protected void addToPanelAnchor(JPanel p, String def, boolean presetInitiallyMatches) {
1298 if (!usage.unused()) {
1299 for (String s : usage.values) {
1300 if (!lhm.containsKey(s)) {
1301 lhm.put(s, new PresetListEntry(s));
1302 }
1303 }
1304 }
1305 if (def != null && !lhm.containsKey(def)) {
1306 lhm.put(def, new PresetListEntry(def));
1307 }
1308 lhm.put("", new PresetListEntry(""));
1309
1310 combo = new JosmComboBox<>(lhm.values().toArray(new PresetListEntry[0]));
1311 component = combo;
1312 combo.setRenderer(getListCellRenderer());
1313 combo.setEditable(editable);
1314 combo.reinitialize(lhm.values());
1315 AutoCompletingTextField tf = new AutoCompletingTextField();
1316 initAutoCompletionField(tf, key);
1317 if (Main.pref.getBoolean("taggingpreset.display-keys-as-hint", true)) {
1318 tf.setHint(key);
1319 }
1320 if (length != null && !length.isEmpty()) {
1321 tf.setMaxChars(Integer.valueOf(length));
1322 }
1323 AutoCompletionList acList = tf.getAutoCompletionList();
1324 if (acList != null) {
1325 acList.add(getDisplayValues(), AutoCompletionItemPriority.IS_IN_STANDARD);
1326 }
1327 combo.setEditor(tf);
1328
1329 if (usage.hasUniqueValue()) {
1330 // all items have the same value (and there were no unset items)
1331 originalValue = lhm.get(usage.getFirst());
1332 combo.setSelectedItem(originalValue);
1333 } else if (def != null && usage.unused()) {
1334 // default is set and all items were unset
1335 if (!usage.hadKeys() || PROP_FILL_DEFAULT.get() || "force".equals(use_last_as_default)) {
1336 // selected osm primitives are untagged or filling default feature is enabled
1337 combo.setSelectedItem(lhm.get(def).getDisplayValue(true));
1338 } else {
1339 // selected osm primitives are tagged and filling default feature is disabled
1340 combo.setSelectedItem("");
1341 }
1342 originalValue = lhm.get(DIFFERENT);
1343 } else if (usage.unused()) {
1344 // all items were unset (and so is default)
1345 originalValue = lhm.get("");
1346 if ("force".equals(use_last_as_default) && LAST_VALUES.containsKey(key) && !presetInitiallyMatches) {
1347 combo.setSelectedItem(lhm.get(LAST_VALUES.get(key)));
1348 } else {
1349 combo.setSelectedItem(originalValue);
1350 }
1351 } else {
1352 originalValue = lhm.get(DIFFERENT);
1353 combo.setSelectedItem(originalValue);
1354 }
1355 p.add(combo, GBC.eol().fill(GBC.HORIZONTAL));
1356
1357 }
1358
1359 @Override
1360 protected Object getSelectedItem() {
1361 return combo.getSelectedItem();
1362
1363 }
1364
1365 @Override
1366 protected String getDisplayIfNull() {
1367 if (combo.isEditable())
1368 return combo.getEditor().getItem().toString();
1369 else
1370 return null;
1371 }
1372 }
1373
1374 /**
1375 * Multi-select list type.
1376 */
1377 public static class MultiSelect extends ComboMultiSelect {
1378
1379 /**
1380 * Number of rows to display (positive integer, optional).
1381 */
1382 public String rows;
1383 protected ConcatenatingJList list;
1384
1385 @Override
1386 protected void addToPanelAnchor(JPanel p, String def, boolean presetInitiallyMatches) {
1387 list = new ConcatenatingJList(delimiter, lhm.values().toArray(new PresetListEntry[0]));
1388 component = list;
1389 ListCellRenderer<PresetListEntry> renderer = getListCellRenderer();
1390 list.setCellRenderer(renderer);
1391
1392 if (usage.hasUniqueValue() && !usage.unused()) {
1393 originalValue = usage.getFirst();
1394 list.setSelectedItem(originalValue);
1395 } else if (def != null && !usage.hadKeys() || PROP_FILL_DEFAULT.get() || "force".equals(use_last_as_default)) {
1396 originalValue = DIFFERENT;
1397 list.setSelectedItem(def);
1398 } else if (usage.unused()) {
1399 originalValue = null;
1400 list.setSelectedItem(originalValue);
1401 } else {
1402 originalValue = DIFFERENT;
1403 list.setSelectedItem(originalValue);
1404 }
1405
1406 JScrollPane sp = new JScrollPane(list);
1407 // if a number of rows has been specified in the preset,
1408 // modify preferred height of scroll pane to match that row count.
1409 if (rows != null) {
1410 double height = renderer.getListCellRendererComponent(list,
1411 new PresetListEntry("x"), 0, false, false).getPreferredSize().getHeight() * Integer.parseInt(rows);
1412 sp.setPreferredSize(new Dimension((int) sp.getPreferredSize().getWidth(), (int) height));
1413 }
1414 p.add(sp, GBC.eol().fill(GBC.HORIZONTAL));
1415 }
1416
1417 @Override
1418 protected Object getSelectedItem() {
1419 return list.getSelectedItem();
1420 }
1421
1422 @Override
1423 public void addCommands(List<Tag> changedTags) {
1424 // Do not create any commands if list has been disabled because of an unknown value (fix #8605)
1425 if (list.isEnabled()) {
1426 super.addCommands(changedTags);
1427 }
1428 }
1429 }
1430
1431 /**
1432 * Class that allows list values to be assigned and retrieved as a comma-delimited
1433 * string (extracted from TaggingPreset)
1434 */
1435 private static class ConcatenatingJList extends JList<PresetListEntry> {
1436 private String delimiter;
1437
1438 public ConcatenatingJList(String del, PresetListEntry[] o) {
1439 super(o);
1440 delimiter = del;
1441 }
1442
1443 public void setSelectedItem(Object o) {
1444 if (o == null) {
1445 clearSelection();
1446 } else {
1447 String s = o.toString();
1448 Set<String> parts = new TreeSet<>(Arrays.asList(s.split(delimiter)));
1449 ListModel<PresetListEntry> lm = getModel();
1450 int[] intParts = new int[lm.getSize()];
1451 int j = 0;
1452 for (int i = 0; i < lm.getSize(); i++) {
1453 final String value = lm.getElementAt(i).value;
1454 if (parts.contains(value)) {
1455 intParts[j++] = i;
1456 parts.remove(value);
1457 }
1458 }
1459 setSelectedIndices(Arrays.copyOf(intParts, j));
1460 // check if we have actually managed to represent the full
1461 // value with our presets. if not, cop out; we will not offer
1462 // a selection list that threatens to ruin the value.
1463 setEnabled(parts.isEmpty());
1464 }
1465 }
1466
1467 public String getSelectedItem() {
1468 ListModel<PresetListEntry> lm = getModel();
1469 int[] si = getSelectedIndices();
1470 StringBuilder builder = new StringBuilder();
1471 for (int i = 0; i < si.length; i++) {
1472 if (i > 0) {
1473 builder.append(delimiter);
1474 }
1475 builder.append(lm.getElementAt(si[i]).value);
1476 }
1477 return builder.toString();
1478 }
1479 }
1480
1481 public static Set<TaggingPresetType> getType(String types) throws SAXException {
1482 if (types == null || types.isEmpty()) {
1483 throw new SAXException(tr("Unknown type: {0}", types));
1484 }
1485 if (TYPE_CACHE.containsKey(types))
1486 return TYPE_CACHE.get(types);
1487 Set<TaggingPresetType> result = EnumSet.noneOf(TaggingPresetType.class);
1488 for (String type : Arrays.asList(types.split(","))) {
1489 try {
1490 TaggingPresetType presetType = TaggingPresetType.fromString(type);
1491 result.add(presetType);
1492 } catch (IllegalArgumentException e) {
1493 throw new SAXException(tr("Unknown type: {0}", type), e);
1494 }
1495 }
1496 TYPE_CACHE.put(types, result);
1497 return result;
1498 }
1499
1500 static String fixPresetString(String s) {
1501 return s == null ? s : s.replaceAll("'", "''");
1502 }
1503
1504 private static String getLocaleText(String text, String text_context, String defaultText) {
1505 if (text == null) {
1506 return defaultText;
1507 } else if (text_context != null) {
1508 return trc(text_context, fixPresetString(text));
1509 } else {
1510 return tr(fixPresetString(text));
1511 }
1512 }
1513
1514 /**
1515 * allow escaped comma in comma separated list:
1516 * "A\, B\, C,one\, two" --&gt; ["A, B, C", "one, two"]
1517 * @param delimiter the delimiter, e.g. a comma. separates the entries and
1518 * must be escaped within one entry
1519 * @param s the string
1520 */
1521 private static String[] splitEscaped(char delimiter, String s) {
1522 if (s == null)
1523 return new String[0];
1524 List<String> result = new ArrayList<>();
1525 boolean backslash = false;
1526 StringBuilder item = new StringBuilder();
1527 for (int i = 0; i < s.length(); i++) {
1528 char ch = s.charAt(i);
1529 if (backslash) {
1530 item.append(ch);
1531 backslash = false;
1532 } else if (ch == '\\') {
1533 backslash = true;
1534 } else if (ch == delimiter) {
1535 result.add(item.toString());
1536 item.setLength(0);
1537 } else {
1538 item.append(ch);
1539 }
1540 }
1541 if (item.length() > 0) {
1542 result.add(item.toString());
1543 }
1544 return result.toArray(new String[result.size()]);
1545 }
1546
1547 static Usage determineTextUsage(Collection<OsmPrimitive> sel, String key) {
1548 Usage returnValue = new Usage();
1549 returnValue.values = new TreeSet<>();
1550 for (OsmPrimitive s : sel) {
1551 String v = s.get(key);
1552 if (v != null) {
1553 returnValue.values.add(v);
1554 } else {
1555 returnValue.hadEmpty = true;
1556 }
1557 if (s.hasKeys()) {
1558 returnValue.hadKeys = true;
1559 }
1560 }
1561 return returnValue;
1562 }
1563
1564 static Usage determineBooleanUsage(Collection<OsmPrimitive> sel, String key) {
1565
1566 Usage returnValue = new Usage();
1567 returnValue.values = new TreeSet<>();
1568 for (OsmPrimitive s : sel) {
1569 String booleanValue = OsmUtils.getNamedOsmBoolean(s.get(key));
1570 if (booleanValue != null) {
1571 returnValue.values.add(booleanValue);
1572 }
1573 }
1574 return returnValue;
1575 }
1576
1577 protected static ImageIcon loadImageIcon(String iconName, File zipIcons, Integer maxSize) {
1578 final Collection<String> s = Main.pref.getCollection("taggingpreset.icon.sources", null);
1579 ImageProvider imgProv = new ImageProvider(iconName).setDirs(s).setId("presets").setArchive(zipIcons).setOptional(true);
1580 if (maxSize != null) {
1581 imgProv.setMaxSize(maxSize);
1582 }
1583 return imgProv.get();
1584 }
1585
1586 protected static Integer parseInteger(String str) {
1587 if (str == null || str.isEmpty())
1588 return null;
1589 try {
1590 return Integer.valueOf(str);
1591 } catch (Exception e) {
1592 if (Main.isTraceEnabled()) {
1593 Main.trace(e.getMessage());
1594 }
1595 }
1596 return null;
1597 }
1598}
Note: See TracBrowser for help on using the repository browser.