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

Last change on this file since 16063 was 16063, checked in by simon04, 4 years ago

PMD

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