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