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

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

refactor of some GUI/widgets classes (impacts some plugins):

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