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

Last change on this file since 9465 was 9465, checked in by simon04, 8 years ago

fix #12160 - Combobox List Entries with Empty Value – display_value and short_description not shown

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