source: josm/trunk/src/org/openstreetmap/josm/gui/tagging/presets/items/ComboMultiSelect.java@ 11553

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

refactor handling of null values - use Java 8 Optional where possible

File size: 22.0 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.tagging.presets.items;
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.io.File;
11import java.lang.reflect.Method;
12import java.lang.reflect.Modifier;
13import java.util.ArrayList;
14import java.util.Arrays;
15import java.util.Collection;
16import java.util.Collections;
17import java.util.LinkedHashMap;
18import java.util.List;
19import java.util.Map;
20import java.util.Map.Entry;
21import java.util.Set;
22import java.util.TreeSet;
23import java.util.stream.Collectors;
24
25import javax.swing.ImageIcon;
26import javax.swing.JComponent;
27import javax.swing.JLabel;
28import javax.swing.JList;
29import javax.swing.JPanel;
30import javax.swing.ListCellRenderer;
31import javax.swing.ListModel;
32
33import org.openstreetmap.josm.Main;
34import org.openstreetmap.josm.data.osm.OsmPrimitive;
35import org.openstreetmap.josm.data.osm.Tag;
36import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetReader;
37import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetSelector;
38import org.openstreetmap.josm.tools.AlphanumComparator;
39import org.openstreetmap.josm.tools.GBC;
40import org.openstreetmap.josm.tools.Utils;
41
42/**
43 * Abstract superclass for combo box and multi-select list types.
44 */
45public abstract class ComboMultiSelect extends KeyedItem {
46
47 private static final Renderer RENDERER = new Renderer();
48
49 /** The localized version of {@link #text}. */
50 public String locale_text; // NOSONAR
51 /**
52 * A list of entries.
53 * The list has to be separated by commas (for the {@link Combo} box) or by the specified delimiter (for the {@link MultiSelect}).
54 * If a value contains the delimiter, the delimiter may be escaped with a backslash.
55 * If a value contains a backslash, it must also be escaped with a backslash. */
56 public String values; // NOSONAR
57 /**
58 * To use instead of {@link #values} if the list of values has to be obtained with a Java method of this form:
59 * <p>{@code public static String[] getValues();}<p>
60 * The value must be: {@code full.package.name.ClassName#methodName}.
61 */
62 public String values_from; // NOSONAR
63 /** The context used for translating {@link #values} */
64 public String values_context; // NOSONAR
65 /** Disabled internationalisation for value to avoid mistakes, see #11696 */
66 public boolean values_no_i18n; // NOSONAR
67 /** Whether to sort the values, defaults to true. */
68 public boolean values_sort = true; // NOSONAR
69 /**
70 * A list of entries that is displayed to the user.
71 * Must be the same number and order of entries as {@link #values} and editable must be false or not specified.
72 * For the delimiter character and escaping, see the remarks at {@link #values}.
73 */
74 public String display_values; // NOSONAR
75 /** The localized version of {@link #display_values}. */
76 public String locale_display_values; // NOSONAR
77 /**
78 * A delimiter-separated list of texts to be displayed below each {@code display_value}.
79 * (Only if it is not possible to describe the entry in 2-3 words.)
80 * Instead of comma separated list instead using {@link #values}, {@link #display_values} and {@link #short_descriptions},
81 * the following form is also supported:<p>
82 * {@code <list_entry value="" display_value="" short_description="" icon="" icon_size="" />}
83 */
84 public String short_descriptions; // NOSONAR
85 /** The localized version of {@link #short_descriptions}. */
86 public String locale_short_descriptions; // NOSONAR
87 /** The default value for the item. If not specified, the current value of the key is chosen as default (if applicable).*/
88 public String default_; // NOSONAR
89 /**
90 * The character that separates values.
91 * In case of {@link Combo} the default is comma.
92 * In case of {@link MultiSelect} the default is semicolon and this will also be used to separate selected values in the tag.
93 */
94 public String delimiter = ";"; // NOSONAR
95 /** whether the last value is used as default. Using "force" enforces this behaviour also for already tagged objects. Default is "false".*/
96 public String use_last_as_default = "false"; // NOSONAR
97 /** whether to use values for search via {@link TaggingPresetSelector} */
98 public String values_searchable = "false"; // NOSONAR
99
100 protected JComponent component;
101 protected final Map<String, PresetListEntry> lhm = new LinkedHashMap<>();
102 private boolean initialized;
103 protected Usage usage;
104 protected Object originalValue;
105
106 private static final class Renderer implements ListCellRenderer<PresetListEntry> {
107
108 private final JLabel lbl = new JLabel();
109
110 @Override
111 public Component getListCellRendererComponent(JList<? extends PresetListEntry> list, PresetListEntry item, int index,
112 boolean isSelected, boolean cellHasFocus) {
113
114 // Only return cached size, item is not shown
115 if (!list.isShowing() && item.prefferedWidth != -1 && item.prefferedHeight != -1) {
116 if (index == -1) {
117 lbl.setPreferredSize(new Dimension(item.prefferedWidth, 10));
118 } else {
119 lbl.setPreferredSize(new Dimension(item.prefferedWidth, item.prefferedHeight));
120 }
121 return lbl;
122 }
123
124 lbl.setPreferredSize(null);
125
126 if (isSelected) {
127 lbl.setBackground(list.getSelectionBackground());
128 lbl.setForeground(list.getSelectionForeground());
129 } else {
130 lbl.setBackground(list.getBackground());
131 lbl.setForeground(list.getForeground());
132 }
133
134 lbl.setOpaque(true);
135 lbl.setFont(lbl.getFont().deriveFont(Font.PLAIN));
136 lbl.setText("<html>" + item.getListDisplay() + "</html>");
137 lbl.setIcon(item.getIcon());
138 lbl.setEnabled(list.isEnabled());
139
140 // Cache size
141 item.prefferedWidth = lbl.getPreferredSize().width;
142 item.prefferedHeight = lbl.getPreferredSize().height;
143
144 // We do not want the editor to have the maximum height of all
145 // entries. Return a dummy with bogus height.
146 if (index == -1) {
147 lbl.setPreferredSize(new Dimension(lbl.getPreferredSize().width, 10));
148 }
149 return lbl;
150 }
151 }
152
153 /**
154 * Class that allows list values to be assigned and retrieved as a comma-delimited
155 * string (extracted from TaggingPreset)
156 */
157 protected static class ConcatenatingJList extends JList<PresetListEntry> {
158 private final String delimiter;
159
160 protected ConcatenatingJList(String del, PresetListEntry ... o) {
161 super(o);
162 delimiter = del;
163 }
164
165 public void setSelectedItem(Object o) {
166 if (o == null) {
167 clearSelection();
168 } else {
169 String s = o.toString();
170 Set<String> parts = new TreeSet<>(Arrays.asList(s.split(delimiter)));
171 ListModel<PresetListEntry> lm = getModel();
172 int[] intParts = new int[lm.getSize()];
173 int j = 0;
174 for (int i = 0; i < lm.getSize(); i++) {
175 final String value = lm.getElementAt(i).value;
176 if (parts.contains(value)) {
177 intParts[j++] = i;
178 parts.remove(value);
179 }
180 }
181 setSelectedIndices(Arrays.copyOf(intParts, j));
182 // check if we have actually managed to represent the full
183 // value with our presets. if not, cop out; we will not offer
184 // a selection list that threatens to ruin the value.
185 setEnabled(parts.isEmpty());
186 }
187 }
188
189 public String getSelectedItem() {
190 ListModel<PresetListEntry> lm = getModel();
191 int[] si = getSelectedIndices();
192 StringBuilder builder = new StringBuilder();
193 for (int i = 0; i < si.length; i++) {
194 if (i > 0) {
195 builder.append(delimiter);
196 }
197 builder.append(lm.getElementAt(si[i]).value);
198 }
199 return builder.toString();
200 }
201 }
202
203 /**
204 * Preset list entry.
205 */
206 public static class PresetListEntry implements Comparable<PresetListEntry> {
207 /** Entry value */
208 public String value; // NOSONAR
209 /** The context used for translating {@link #value} */
210 public String value_context; // NOSONAR
211 /** Value displayed to the user */
212 public String display_value; // NOSONAR
213 /** Text to be displayed below {@code display_value}. */
214 public String short_description; // NOSONAR
215 /** The location of icon file to display */
216 public String icon; // NOSONAR
217 /** The size of displayed icon. If not set, default is size from icon file */
218 public String icon_size; // NOSONAR
219 /** The localized version of {@link #display_value}. */
220 public String locale_display_value; // NOSONAR
221 /** The localized version of {@link #short_description}. */
222 public String locale_short_description; // NOSONAR
223 private final File zipIcons = TaggingPresetReader.getZipIcons();
224
225 /** Cached width (currently only for Combo) to speed up preset dialog initialization */
226 public int prefferedWidth = -1; // NOSONAR
227 /** Cached height (currently only for Combo) to speed up preset dialog initialization */
228 public int prefferedHeight = -1; // NOSONAR
229
230 /**
231 * Constructs a new {@code PresetListEntry}, uninitialized.
232 */
233 public PresetListEntry() {
234 // Public default constructor is needed
235 }
236
237 /**
238 * Constructs a new {@code PresetListEntry}, initialized with a value.
239 * @param value value
240 */
241 public PresetListEntry(String value) {
242 this.value = value;
243 }
244
245 /**
246 * Returns HTML formatted contents.
247 * @return HTML formatted contents
248 */
249 public String getListDisplay() {
250 if (value.equals(DIFFERENT))
251 return "<b>" + Utils.escapeReservedCharactersHTML(DIFFERENT) + "</b>";
252
253 String displayValue = Utils.escapeReservedCharactersHTML(getDisplayValue(true));
254 String shortDescription = getShortDescription(true);
255
256 if (displayValue.isEmpty() && (shortDescription == null || shortDescription.isEmpty()))
257 return "&nbsp;";
258
259 final StringBuilder res = new StringBuilder("<b>").append(displayValue).append("</b>");
260 if (shortDescription != null) {
261 // wrap in table to restrict the text width
262 res.append("<div style=\"width:300px; padding:0 0 5px 5px\">")
263 .append(shortDescription)
264 .append("</div>");
265 }
266 return res.toString();
267 }
268
269 /**
270 * Returns the entry icon, if any.
271 * @return the entry icon, or {@code null}
272 */
273 public ImageIcon getIcon() {
274 return icon == null ? null : loadImageIcon(icon, zipIcons, parseInteger(icon_size));
275 }
276
277 /**
278 * Returns the value to display.
279 * @param translated whether the text must be translated
280 * @return the value to display
281 */
282 public String getDisplayValue(boolean translated) {
283 return translated
284 ? Utils.firstNonNull(locale_display_value, tr(display_value), trc(value_context, value))
285 : Utils.firstNonNull(display_value, value);
286 }
287
288 /**
289 * Returns the short description to display.
290 * @param translated whether the text must be translated
291 * @return the short description to display
292 */
293 public String getShortDescription(boolean translated) {
294 return translated
295 ? Utils.firstNonNull(locale_short_description, tr(short_description))
296 : short_description;
297 }
298
299 // toString is mainly used to initialize the Editor
300 @Override
301 public String toString() {
302 if (DIFFERENT.equals(value))
303 return DIFFERENT;
304 String displayValue = getDisplayValue(true);
305 return displayValue != null ? displayValue.replaceAll("<.*>", "") : ""; // remove additional markup, e.g. <br>
306 }
307
308 @Override
309 public int compareTo(PresetListEntry o) {
310 return AlphanumComparator.getInstance().compare(this.getDisplayValue(true), o.getDisplayValue(true));
311 }
312 }
313
314 /**
315 * allow escaped comma in comma separated list:
316 * "A\, B\, C,one\, two" --&gt; ["A, B, C", "one, two"]
317 * @param delimiter the delimiter, e.g. a comma. separates the entries and
318 * must be escaped within one entry
319 * @param s the string
320 * @return splitted items
321 */
322 public static String[] splitEscaped(char delimiter, String s) {
323 if (s == null)
324 return new String[0];
325 List<String> result = new ArrayList<>();
326 boolean backslash = false;
327 StringBuilder item = new StringBuilder();
328 for (int i = 0; i < s.length(); i++) {
329 char ch = s.charAt(i);
330 if (backslash) {
331 item.append(ch);
332 backslash = false;
333 } else if (ch == '\\') {
334 backslash = true;
335 } else if (ch == delimiter) {
336 result.add(item.toString());
337 item.setLength(0);
338 } else {
339 item.append(ch);
340 }
341 }
342 if (item.length() > 0) {
343 result.add(item.toString());
344 }
345 return result.toArray(new String[result.size()]);
346 }
347
348 protected abstract Object getSelectedItem();
349
350 protected abstract void addToPanelAnchor(JPanel p, String def, boolean presetInitiallyMatches);
351
352 protected char getDelChar() {
353 return delimiter.isEmpty() ? ';' : delimiter.charAt(0);
354 }
355
356 @Override
357 public Collection<String> getValues() {
358 initListEntries();
359 return lhm.keySet();
360 }
361
362 /**
363 * Returns the values to display.
364 * @return the values to display
365 */
366 public Collection<String> getDisplayValues() {
367 initListEntries();
368 return lhm.values().stream().map(x -> x.getDisplayValue(true)).collect(Collectors.toList());
369 }
370
371 @Override
372 public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel, boolean presetInitiallyMatches) {
373 initListEntries();
374
375 // find out if our key is already used in the selection.
376 usage = determineTextUsage(sel, key);
377 if (!usage.hasUniqueValue() && !usage.unused()) {
378 lhm.put(DIFFERENT, new PresetListEntry(DIFFERENT));
379 }
380
381 final JLabel label = new JLabel(tr("{0}:", locale_text));
382 label.setToolTipText(getKeyTooltipText());
383 p.add(label, GBC.std().insets(0, 0, 10, 0));
384 addToPanelAnchor(p, default_, presetInitiallyMatches);
385 label.setLabelFor(component);
386 component.setToolTipText(getKeyTooltipText());
387
388 return true;
389 }
390
391 private void initListEntries() {
392 if (initialized) {
393 lhm.remove(DIFFERENT); // possibly added in #addToPanel
394 return;
395 } else if (lhm.isEmpty()) {
396 initListEntriesFromAttributes();
397 } else {
398 if (values != null) {
399 Main.warn(tr("Warning in tagging preset \"{0}-{1}\": "
400 + "Ignoring ''{2}'' attribute as ''{3}'' elements are given.",
401 key, text, "values", "list_entry"));
402 }
403 if (display_values != null || locale_display_values != null) {
404 Main.warn(tr("Warning in tagging preset \"{0}-{1}\": "
405 + "Ignoring ''{2}'' attribute as ''{3}'' elements are given.",
406 key, text, "display_values", "list_entry"));
407 }
408 if (short_descriptions != null || locale_short_descriptions != null) {
409 Main.warn(tr("Warning in tagging preset \"{0}-{1}\": "
410 + "Ignoring ''{2}'' attribute as ''{3}'' elements are given.",
411 key, text, "short_descriptions", "list_entry"));
412 }
413 for (PresetListEntry e : lhm.values()) {
414 if (e.value_context == null) {
415 e.value_context = values_context;
416 }
417 }
418 }
419 if (locale_text == null) {
420 locale_text = getLocaleText(text, text_context, null);
421 }
422 initialized = true;
423 }
424
425 private void initListEntriesFromAttributes() {
426 char delChar = getDelChar();
427
428 String[] valueArray = null;
429
430 if (values_from != null) {
431 String[] classMethod = values_from.split("#");
432 if (classMethod.length == 2) {
433 try {
434 Method method = Class.forName(classMethod[0]).getMethod(classMethod[1]);
435 // Check method is public static String[] methodName()
436 int mod = method.getModifiers();
437 if (Modifier.isPublic(mod) && Modifier.isStatic(mod)
438 && method.getReturnType().equals(String[].class) && method.getParameterTypes().length == 0) {
439 valueArray = (String[]) method.invoke(null);
440 } else {
441 Main.error(tr("Broken tagging preset \"{0}-{1}\" - Java method given in ''values_from'' is not \"{2}\"", key, text,
442 "public static String[] methodName()"));
443 }
444 } catch (ReflectiveOperationException e) {
445 Main.error(tr("Broken tagging preset \"{0}-{1}\" - Java method given in ''values_from'' threw {2} ({3})", key, text,
446 e.getClass().getName(), e.getMessage()));
447 Main.debug(e);
448 }
449 }
450 }
451
452 if (valueArray == null) {
453 valueArray = splitEscaped(delChar, values);
454 }
455
456 String[] displayArray = valueArray;
457 if (!values_no_i18n) {
458 final String displ = Utils.firstNonNull(locale_display_values, display_values);
459 displayArray = displ == null ? valueArray : splitEscaped(delChar, displ);
460 }
461
462 final String descr = Utils.firstNonNull(locale_short_descriptions, short_descriptions);
463 String[] shortDescriptionsArray = descr == null ? null : splitEscaped(delChar, descr);
464
465 if (displayArray.length != valueArray.length) {
466 Main.error(tr("Broken tagging preset \"{0}-{1}\" - number of items in ''display_values'' must be the same as in ''values''",
467 key, text));
468 Main.error(tr("Detailed information: {0} <> {1}", Arrays.toString(displayArray), Arrays.toString(valueArray)));
469 displayArray = valueArray;
470 }
471
472 if (shortDescriptionsArray != null && shortDescriptionsArray.length != valueArray.length) {
473 Main.error(tr("Broken tagging preset \"{0}-{1}\" - number of items in ''short_descriptions'' must be the same as in ''values''",
474 key, text));
475 Main.error(tr("Detailed information: {0} <> {1}", Arrays.toString(shortDescriptionsArray), Arrays.toString(valueArray)));
476 shortDescriptionsArray = null;
477 }
478
479 final List<PresetListEntry> entries = new ArrayList<>(valueArray.length);
480 for (int i = 0; i < valueArray.length; i++) {
481 final PresetListEntry e = new PresetListEntry(valueArray[i]);
482 e.locale_display_value = locale_display_values != null || values_no_i18n
483 ? displayArray[i]
484 : trc(values_context, fixPresetString(displayArray[i]));
485 if (shortDescriptionsArray != null) {
486 e.locale_short_description = locale_short_descriptions != null
487 ? shortDescriptionsArray[i]
488 : tr(fixPresetString(shortDescriptionsArray[i]));
489 }
490
491 entries.add(e);
492 }
493
494 if (values_sort && Main.pref.getBoolean("taggingpreset.sortvalues", true)) {
495 Collections.sort(entries);
496 }
497
498 for (PresetListEntry i : entries) {
499 lhm.put(i.value, i);
500 }
501 }
502
503 protected String getDisplayIfNull() {
504 return null;
505 }
506
507 @Override
508 public void addCommands(List<Tag> changedTags) {
509 Object obj = getSelectedItem();
510 String display = obj == null ? getDisplayIfNull() : obj.toString();
511 String value = null;
512
513 if (display != null) {
514 for (Entry<String, PresetListEntry> entry : lhm.entrySet()) {
515 String k = entry.getValue().toString();
516 if (k.equals(display)) {
517 value = entry.getKey();
518 break;
519 }
520 }
521 if (value == null) {
522 value = display;
523 }
524 } else {
525 value = "";
526 }
527 value = Tag.removeWhiteSpaces(value);
528
529 // no change if same as before
530 if (originalValue == null) {
531 if (value.isEmpty())
532 return;
533 } else if (value.equals(originalValue.toString()))
534 return;
535
536 if (!"false".equals(use_last_as_default)) {
537 LAST_VALUES.put(key, value);
538 }
539 changedTags.add(new Tag(key, value));
540 }
541
542 /**
543 * Adds a preset list entry.
544 * @param e list entry to add
545 */
546 public void addListEntry(PresetListEntry e) {
547 lhm.put(e.value, e);
548 }
549
550 /**
551 * Adds a collection of preset list entries.
552 * @param e list entries to add
553 */
554 public void addListEntries(Collection<PresetListEntry> e) {
555 for (PresetListEntry i : e) {
556 addListEntry(i);
557 }
558 }
559
560 protected ListCellRenderer<PresetListEntry> getListCellRenderer() {
561 return RENDERER;
562 }
563
564 @Override
565 public MatchType getDefaultMatch() {
566 return MatchType.NONE;
567 }
568}
Note: See TracBrowser for help on using the repository browser.