source: josm/trunk/src/org/openstreetmap/josm/gui/tagging/TaggingPresetItems.java @ 6340

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

refactor of some GUI/widgets classes (impacts some plugins):

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