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

Last change on this file since 12846 was 12846, checked in by bastiK, 7 years ago

see #15229 - use Config.getPref() wherever possible

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