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

Last change on this file since 6795 was 6795, checked in by simon04, 10 years ago

Refactor tagging presets in order to get rid of static variable presetInitiallyMatches - see #8413

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