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

Last change on this file since 6074 was 6074, checked in by Don-vip, 11 years ago

code cleanup in TaggingPreset* classes

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