Changeset 18254 in josm for trunk/src/org/openstreetmap


Ignore:
Timestamp:
2021-10-06T13:24:19+02:00 (3 years ago)
Author:
Don-vip
Message:

fix #21408 - fix #19013, fix #21385, fix #21404 - Fix MultiSelect issues (patch by marcello)

Location:
trunk/src/org/openstreetmap/josm/gui/tagging/presets
Files:
8 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetItemGuiSupport.java

    r18221 r18254  
    2525
    2626    private final Collection<OsmPrimitive> selected;
     27    /** True if all selected primitives matched this preset at the moment it was openend. */
    2728    private final boolean presetInitiallyMatches;
    2829    private final Supplier<Collection<Tag>> changedTagsSupplier;
     
    8485
    8586    /**
    86      * Returns whether the preset initially matched (before opening the dialog)
     87     * Returns true if all selected primitives matched this preset (before opening the dialog)
    8788     *
    88      * @return whether the preset initially matched
     89     * @return true if the preset initially matched
    8990     */
    9091    public boolean isPresetInitiallyMatches() {
  • trunk/src/org/openstreetmap/josm/gui/tagging/presets/items/Check.java

    r18221 r18254  
    4040        // find out if our key is already used in the selection.
    4141        final Usage usage = determineBooleanUsage(support.getSelected(), key);
    42         final String oneValue = usage.values.isEmpty() ? null : usage.values.last();
     42        final String oneValue = usage.map.isEmpty() ? null : usage.map.lastKey();
    4343        def = "on".equals(default_) ? Boolean.TRUE : "off".equals(default_) ? Boolean.FALSE : null;
    4444
    4545        initializeLocaleText(null);
    4646
    47         if (usage.values.size() < 2 && (oneValue == null || value_on.equals(oneValue) || value_off.equals(oneValue))) {
     47        if (usage.map.size() < 2 && (oneValue == null || value_on.equals(oneValue) || value_off.equals(oneValue))) {
    4848            if (def != null && !PROP_FILL_DEFAULT.get()) {
    4949                // default is set and filling default values feature is disabled - check if all primitives are untagged
  • trunk/src/org/openstreetmap/josm/gui/tagging/presets/items/Combo.java

    r18221 r18254  
    1313import java.util.Arrays;
    1414import java.util.Comparator;
     15import java.util.TreeMap;
    1516
    1617import javax.swing.AbstractAction;
     
    1819import javax.swing.JColorChooser;
    1920import javax.swing.JComponent;
     21import javax.swing.JLabel;
    2022import javax.swing.JPanel;
    2123
     
    3032import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItemGuiSupport;
    3133import org.openstreetmap.josm.gui.widgets.JosmComboBox;
     34import org.openstreetmap.josm.gui.widgets.OrientationAction;
    3235import org.openstreetmap.josm.tools.ColorHelper;
    3336import org.openstreetmap.josm.tools.GBC;
     
    7578    }
    7679
     80    private void addEntry(PresetListEntry entry) {
     81        if (!seenValues.containsKey(entry.value)) {
     82            dropDownModel.addElement(entry);
     83            seenValues.put(entry.value, entry);
     84        }
     85    }
     86
    7787    @Override
    78     protected JComponent addToPanelAnchor(JPanel p, String def, TaggingPresetItemGuiSupport support) {
    79         if (!usage.unused()) {
    80             for (String s : usage.values) {
    81                 presetListEntries.add(new PresetListEntry(s));
    82             }
    83         }
    84         if (def != null) {
    85             presetListEntries.add(new PresetListEntry(def));
    86         }
    87         presetListEntries.add(new PresetListEntry(""));
    88 
     88    protected boolean addToPanel(JPanel p, TaggingPresetItemGuiSupport support) {
     89        initializeLocaleText(null);
     90        usage = determineTextUsage(support.getSelected(), key);
     91        seenValues = new TreeMap<>();
     92        // get the standard values from the preset definition
     93        initListEntries();
     94
     95        // init the model
    8996        dropDownModel = new AutoCompComboBoxModel<PresetListEntry>(Comparator.naturalOrder());
    90         autoCompModel = new AutoCompComboBoxModel<AutoCompletionItem>(Comparator.naturalOrder());
    91         presetListEntries.forEach(dropDownModel::addElement);
     97
     98        if (!usage.hasUniqueValue() && !usage.unused()) {
     99            addEntry(PresetListEntry.ENTRY_DIFFERENT);
     100        }
     101        presetListEntries.forEach(this::addEntry);
     102        if (default_ != null) {
     103            addEntry(new PresetListEntry(default_, this));
     104        }
     105        addEntry(PresetListEntry.ENTRY_EMPTY);
     106
     107        usage.map.forEach((value, count) -> {
     108            addEntry(new PresetListEntry(value, this));
     109        });
    92110
    93111        combobox = new JosmComboBox<>(dropDownModel);
     
    98116        // the dropdown list.  We don't want that to happen because we want to show taller items in
    99117        // the list than in the editor.  We can't use
    100         // {@code combobox.setPrototypeDisplayValue(new PresetListEntry(" "));} because that would
     118        // {@code combobox.setPrototypeDisplayValue(PresetListEntry.ENTRY_EMPTY);} because that would
    101119        // set a fixed cell height in JList.
    102120        combobox.setPreferredHeight(combobox.getPreferredSize().height);
     
    107125        combobox.setEditable(editable);
    108126
     127        autoCompModel = new AutoCompComboBoxModel<AutoCompletionItem>(Comparator.naturalOrder());
    109128        getAllForKeys(Arrays.asList(key)).forEach(autoCompModel::addElement);
    110129        getDisplayValues().forEach(s -> autoCompModel.addElement(new AutoCompletionItem(s, AutoCompletionPriority.IS_IN_STANDARD)));
     
    120139        }
    121140
     141        JLabel label = addLabel(p);
     142
    122143        if (key != null && ("colour".equals(key) || key.startsWith("colour:") || key.endsWith(":colour"))) {
    123             p.add(combobox, GBC.std().fill(GBC.HORIZONTAL));
     144            p.add(combobox, GBC.std().fill(GBC.HORIZONTAL)); // NOSONAR
    124145            JButton button = new JButton(new ChooseColorAction());
    125146            button.setOpaque(true);
    126147            button.setBorderPainted(false);
    127148            button.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
    128             p.add(button, GBC.eol().fill(GBC.VERTICAL));
     149            p.add(button, GBC.eol().fill(GBC.VERTICAL)); // NOSONAR
    129150            ActionListener updateColor = ignore -> button.setBackground(getColor());
    130151            updateColor.actionPerformed(null);
    131152            combobox.addActionListener(updateColor);
    132153        } else {
    133             p.add(combobox, GBC.eol().fill(GBC.HORIZONTAL));
    134         }
    135 
    136         Object itemToSelect = getItemToSelect(default_, support, false);
    137         combobox.setSelectedItemText(itemToSelect == null ? null : itemToSelect.toString());
    138         combobox.addActionListener(l -> support.fireItemValueModified(this, key, getSelectedValue()));
     154            p.add(combobox, GBC.eol().fill(GBC.HORIZONTAL)); // NOSONAR
     155        }
     156
     157        String valueToSelect = getInitialValue(default_);
     158        if (valueToSelect != null) {
     159            PresetListEntry selItem = find(valueToSelect);
     160            if (selItem != null) {
     161                combobox.setSelectedItem(selItem);
     162            } else {
     163                combobox.setText(valueToSelect);
     164            }
     165        }
     166        combobox.addActionListener(l -> support.fireItemValueModified(this, key, getSelectedItem().value));
    139167        combobox.addComponentListener(new ComponentListener());
    140         return combobox;
     168
     169        label.setLabelFor(combobox);
     170        combobox.setToolTipText(getKeyTooltipText());
     171        combobox.applyComponentOrientation(OrientationAction.getValueOrientation(key));
     172
     173        return true;
     174    }
     175
     176    /**
     177     * Finds the PresetListEntry that matches value.
     178     * <p>
     179     * Looks in the model for an element whose {@code value} matches {@code value}.
     180     *
     181     * @param value The value to match.
     182     * @return The entry or null
     183     */
     184    private PresetListEntry find(String value) {
     185        return dropDownModel.asCollection().stream().filter(o -> o.value.equals(value)).findAny().orElse(null);
    141186    }
    142187
     
    161206
    162207    protected Color getColor() {
    163         String colorString = String.valueOf(getSelectedValue());
     208        String colorString = getSelectedItem().value;
    164209        return colorString.startsWith("#")
    165210                ? ColorHelper.html2color(colorString)
     
    168213
    169214    @Override
    170     protected Object getSelectedItem() {
    171         return combobox.getSelectedItem();
    172     }
    173 
    174     @Override
    175     protected String getDisplayIfNull() {
    176         if (combobox.isEditable())
    177             return combobox.getEditor().getItem().toString();
    178         else
    179             return null;
     215    protected PresetListEntry getSelectedItem() {
     216        Object sel = combobox.getSelectedItem();
     217        if (sel instanceof PresetListEntry)
     218            // selected from the dropdown
     219            return (PresetListEntry) sel;
     220        if (sel instanceof String) {
     221            // free edit.  If the free edit corresponds to a known entry, use that entry.  This is
     222            // to avoid that we write a display_value to the tag's value, eg. if the user did an
     223            // undo.
     224            PresetListEntry selItem = dropDownModel.find((String) sel);
     225            if (selItem != null)
     226                return selItem;
     227            return new PresetListEntry((String) sel, this);
     228        }
     229        return PresetListEntry.ENTRY_EMPTY;
    180230    }
    181231}
  • trunk/src/org/openstreetmap/josm/gui/tagging/presets/items/ComboMultiSelect.java

    r18251 r18254  
    33
    44import static org.openstreetmap.josm.tools.I18n.tr;
    5 import static org.openstreetmap.josm.tools.I18n.trc;
    65
    76import java.awt.Component;
     
    1312import java.util.Collections;
    1413import java.util.List;
    15 import java.util.Objects;
    16 import java.util.Set;
    17 import java.util.concurrent.CopyOnWriteArraySet;
     14import java.util.Map;
     15import java.util.TreeMap;
    1816import java.util.stream.Collectors;
    1917
    20 import javax.swing.JComponent;
    2118import javax.swing.JLabel;
    2219import javax.swing.JList;
     
    2522
    2623import org.openstreetmap.josm.data.osm.Tag;
    27 import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItemGuiSupport;
    2824import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetSelector;
    2925import org.openstreetmap.josm.gui.tagging.presets.TaggingPresets;
    3026import org.openstreetmap.josm.gui.widgets.JosmListCellRenderer;
    3127import org.openstreetmap.josm.gui.widgets.OrientationAction;
     28import org.openstreetmap.josm.tools.AlphanumComparator;
    3229import org.openstreetmap.josm.tools.GBC;
    3330import org.openstreetmap.josm.tools.Logging;
    34 import org.openstreetmap.josm.tools.Utils;
    3531
    3632/**
     
    8985    public boolean values_searchable; // NOSONAR
    9086
    91     protected final Set<PresetListEntry> presetListEntries = new CopyOnWriteArraySet<>();
    92     private boolean initialized;
     87    /**
     88     * The standard entries in the combobox dropdown or multiselect list. These entries are defined
     89     * in {@code defaultpresets.xml} (or in other custom preset files).
     90     */
     91    protected final List<PresetListEntry> presetListEntries = new ArrayList<>();
     92    /** Helps avoid duplicate list entries */
     93    protected Map<String, PresetListEntry> seenValues = new TreeMap<>();
    9394    protected Usage usage;
    94     protected Object originalValue;
     95    /** Used to see if the user edited the value. May be null. */
     96    protected String originalValue;
    9597
    9698    /**
     
    134136                l.setText(value.getListDisplay(width));
    135137            }
    136             String tt = value.value;
    137             if (tt != null && !tt.isEmpty()) {
    138                 l.setToolTipText(tr("Sets the key ''{0}'' to the value ''{1}''.", key, tt));
    139             } else {
    140                 l.setToolTipText(tr("Clears the key ''{0}''.", key));
    141             }
     138            l.setToolTipText(value.getToolTipText(key));
    142139            l.setIcon(value.getIcon());
    143140            return l;
     
    153150     * @return splitted items
    154151     */
    155     public static String[] splitEscaped(char delimiter, String s) {
     152    public static List<String> splitEscaped(char delimiter, String s) {
    156153        if (s == null)
    157             return new String[0];
     154            return null; // NOSONAR
     155
    158156        List<String> result = new ArrayList<>();
    159157        boolean backslash = false;
     
    176174            result.add(item.toString());
    177175        }
    178         return result.toArray(new String[0]);
    179     }
    180 
    181     protected abstract Object getSelectedItem();
    182 
    183     protected abstract JComponent addToPanelAnchor(JPanel p, String def, TaggingPresetItemGuiSupport support);
     176        return result;
     177    }
     178
     179    /**
     180     * Returns the value selected in the combobox or a synthetic value if a multiselect.
     181     *
     182     * @return the value
     183     */
     184    protected abstract PresetListEntry getSelectedItem();
    184185
    185186    @Override
    186187    public Collection<String> getValues() {
    187         initListEntries(false);
     188        initListEntries();
    188189        return presetListEntries.stream().map(x -> x.value).collect(Collectors.toSet());
    189190    }
     
    194195     */
    195196    public Collection<String> getDisplayValues() {
    196         initListEntries(false);
     197        initListEntries();
    197198        return presetListEntries.stream().map(PresetListEntry::getDisplayValue).collect(Collectors.toList());
    198199    }
    199200
    200     @Override
    201     public boolean addToPanel(JPanel p, TaggingPresetItemGuiSupport support) {
    202         initListEntries(true);
    203 
    204         // find out if our key is already used in the selection.
    205         usage = determineTextUsage(support.getSelected(), key);
    206         if (!usage.hasUniqueValue() && !usage.unused()) {
    207             presetListEntries.add(new PresetListEntry(DIFFERENT));
    208         }
    209 
     201    /**
     202     * Adds the label to the panel
     203     *
     204     * @param p the panel
     205     * @return the label
     206     */
     207    protected JLabel addLabel(JPanel p) {
    210208        final JLabel label = new JLabel(tr("{0}:", locale_text));
    211209        addIcon(label);
     
    214212        label.applyComponentOrientation(OrientationAction.getDefaultComponentOrientation());
    215213        p.add(label, GBC.std().insets(0, 0, 10, 0));
    216         JComponent component = addToPanelAnchor(p, default_, support);
    217         label.setLabelFor(component);
    218         component.setToolTipText(getKeyTooltipText());
    219         component.applyComponentOrientation(OrientationAction.getValueOrientation(key));
    220 
    221         return true;
    222     }
    223 
    224     private void initListEntries(boolean cleanup) {
    225         if (initialized) {
    226             if (cleanup) { // do not cleanup for #getDisplayValues used in Combo#addToPanelAnchor
    227                 presetListEntries.remove(new PresetListEntry(DIFFERENT)); // possibly added in #addToPanel
     214        return label;
     215    }
     216
     217    protected void initListEntries() {
     218        if (presetListEntries.isEmpty()) {
     219            initListEntriesFromAttributes();
     220        }
     221    }
     222
     223    private List<String> getValuesFromCode(String values_from) {
     224        // get the values from a Java function
     225        String[] classMethod = values_from.split("#", -1);
     226        if (classMethod.length == 2) {
     227            try {
     228                Method method = Class.forName(classMethod[0]).getMethod(classMethod[1]);
     229                // Check method is public static String[] methodName()
     230                int mod = method.getModifiers();
     231                if (Modifier.isPublic(mod) && Modifier.isStatic(mod)
     232                        && method.getReturnType().equals(String[].class) && method.getParameterTypes().length == 0) {
     233                    return Arrays.asList((String[]) method.invoke(null));
     234                } else {
     235                    Logging.error(tr("Broken tagging preset \"{0}-{1}\" - Java method given in ''values_from'' is not \"{2}\"", key, text,
     236                            "public static String[] methodName()"));
     237                }
     238            } catch (ReflectiveOperationException e) {
     239                Logging.error(tr("Broken tagging preset \"{0}-{1}\" - Java method given in ''values_from'' threw {2} ({3})", key, text,
     240                        e.getClass().getName(), e.getMessage()));
     241                Logging.debug(e);
    228242            }
     243        }
     244        return null; // NOSONAR
     245    }
     246
     247    /**
     248     * Checks if list {@code a} is either null or the same length as list {@code b}.
     249     *
     250     * @param a The list to check
     251     * @param b The other list
     252     * @param name The name of the list for error reporting
     253     * @return {@code a} if both lists have the same length or {@code null}
     254     */
     255    private List<String> checkListsSameLength(List<String> a, List<String> b, String name) {
     256        if (a != null && a.size() != b.size()) {
     257            Logging.error(tr("Broken tagging preset \"{0}-{1}\" - number of items in ''{2}List'' must be the same as in ''values''",
     258                            key, text, name));
     259            Logging.error(tr("Detailed information: {0} <> {1}", a, b));
     260            return null; // NOSONAR
     261        }
     262        return a;
     263    }
     264
     265    protected void initListEntriesFromAttributes() {
     266        List<String> valueList = null;
     267        List<String> displayList = null;
     268        List<String> localeDisplayList = null;
     269
     270        if (values_from != null) {
     271            valueList = getValuesFromCode(values_from);
     272        }
     273
     274        if (valueList == null) {
     275            // get from {@code values} attribute
     276            valueList = splitEscaped(delimiter, values);
     277        }
     278        if (valueList == null) {
    229279            return;
    230         } else if (presetListEntries.isEmpty()) {
    231             initListEntriesFromAttributes();
    232         } else {
    233             if (values != null) {
    234                 Logging.warn(tr("Warning in tagging preset \"{0}-{1}\": "
    235                         + "Ignoring ''{2}'' attribute as ''{3}'' elements are given.",
    236                         key, text, "values", "list_entry"));
    237             }
    238             if (display_values != null || locale_display_values != null) {
    239                 Logging.warn(tr("Warning in tagging preset \"{0}-{1}\": "
    240                         + "Ignoring ''{2}'' attribute as ''{3}'' elements are given.",
    241                         key, text, "display_values", "list_entry"));
    242             }
    243             if (short_descriptions != null || locale_short_descriptions != null) {
    244                 Logging.warn(tr("Warning in tagging preset \"{0}-{1}\": "
    245                         + "Ignoring ''{2}'' attribute as ''{3}'' elements are given.",
    246                         key, text, "short_descriptions", "list_entry"));
    247             }
    248             for (PresetListEntry e : presetListEntries) {
    249                 if (e.value_context == null) {
    250                     e.value_context = values_context;
    251                 }
    252             }
    253         }
    254         initializeLocaleText(null);
    255         initialized = true;
    256     }
    257 
    258     private void initListEntriesFromAttributes() {
    259 
    260         String[] valueArray = null;
    261 
    262         if (values_from != null) {
    263             String[] classMethod = values_from.split("#", -1);
    264             if (classMethod.length == 2) {
    265                 try {
    266                     Method method = Class.forName(classMethod[0]).getMethod(classMethod[1]);
    267                     // Check method is public static String[] methodName()
    268                     int mod = method.getModifiers();
    269                     if (Modifier.isPublic(mod) && Modifier.isStatic(mod)
    270                             && method.getReturnType().equals(String[].class) && method.getParameterTypes().length == 0) {
    271                         valueArray = (String[]) method.invoke(null);
    272                     } else {
    273                         Logging.error(tr("Broken tagging preset \"{0}-{1}\" - Java method given in ''values_from'' is not \"{2}\"", key, text,
    274                                 "public static String[] methodName()"));
    275                     }
    276                 } catch (ReflectiveOperationException e) {
    277                     Logging.error(tr("Broken tagging preset \"{0}-{1}\" - Java method given in ''values_from'' threw {2} ({3})", key, text,
    278                             e.getClass().getName(), e.getMessage()));
    279                     Logging.debug(e);
    280                 }
    281             }
    282         }
    283 
    284         if (valueArray == null) {
    285             valueArray = splitEscaped(delimiter, values);
    286             values = null;
    287         }
    288 
    289         String[] displayArray = valueArray;
     280        }
     281
    290282        if (!values_no_i18n) {
    291             final String displ = Utils.firstNonNull(locale_display_values, display_values);
    292             displayArray = displ == null ? valueArray : splitEscaped(delimiter, displ);
    293         }
    294 
    295         final String descr = Utils.firstNonNull(locale_short_descriptions, short_descriptions);
    296         String[] shortDescriptionsArray = descr == null ? null : splitEscaped(delimiter, descr);
    297 
    298         if (displayArray.length != valueArray.length) {
    299             Logging.error(tr("Broken tagging preset \"{0}-{1}\" - number of items in ''display_values'' must be the same as in ''values''",
    300                             key, text));
    301             Logging.error(tr("Detailed information: {0} <> {1}", Arrays.toString(displayArray), Arrays.toString(valueArray)));
    302             displayArray = valueArray;
    303         }
    304 
    305         if (shortDescriptionsArray != null && shortDescriptionsArray.length != valueArray.length) {
    306             Logging.error(tr("Broken tagging preset \"{0}-{1}\" - number of items in ''short_descriptions'' must be the same as in ''values''",
    307                             key, text));
    308             Logging.error(tr("Detailed information: {0} <> {1}", Arrays.toString(shortDescriptionsArray), Arrays.toString(valueArray)));
    309             shortDescriptionsArray = null;
    310         }
    311 
    312         final List<PresetListEntry> entries = new ArrayList<>(valueArray.length);
    313         for (int i = 0; i < valueArray.length; i++) {
    314             final PresetListEntry e = new PresetListEntry(valueArray[i]);
    315             final String value = locale_display_values != null || values_no_i18n
    316                     ? displayArray[i]
    317                     : trc(values_context, fixPresetString(displayArray[i]));
    318             e.locale_display_value = value == null ? null : value.intern();
    319             if (shortDescriptionsArray != null) {
    320                 final String description = locale_short_descriptions != null
    321                         ? shortDescriptionsArray[i]
    322                         : tr(fixPresetString(shortDescriptionsArray[i]));
    323                 e.locale_short_description = description == null ? null : description.intern();
    324             }
    325 
    326             entries.add(e);
     283            localeDisplayList = splitEscaped(delimiter, locale_display_values);
     284            displayList = splitEscaped(delimiter, display_values);
     285        }
     286        List<String> localeShortDescriptionsList = splitEscaped(delimiter, locale_short_descriptions);
     287        List<String> shortDescriptionsList = splitEscaped(delimiter, short_descriptions);
     288
     289        displayList = checkListsSameLength(displayList, valueList, "display");
     290        localeDisplayList = checkListsSameLength(localeDisplayList, valueList, "localeDisplay");
     291        shortDescriptionsList = checkListsSameLength(shortDescriptionsList, valueList, "shortDescriptions");
     292        localeShortDescriptionsList = checkListsSameLength(localeShortDescriptionsList, valueList, "localeShortDescriptions");
     293
     294        for (int i = 0; i < valueList.size(); i++) {
     295            final PresetListEntry e = new PresetListEntry(valueList.get(i), this);
     296            if (displayList != null)
     297                e.display_value = displayList.get(i);
     298            if (localeDisplayList != null)
     299                e.locale_display_value = localeDisplayList.get(i);
     300            if (shortDescriptionsList != null)
     301                e.short_description = shortDescriptionsList.get(i);
     302            if (localeShortDescriptionsList != null)
     303                e.locale_short_description = localeShortDescriptionsList.get(i);
     304            addListEntry(e);
    327305        }
    328306
    329307        if (values_sort && TaggingPresets.SORT_MENU.get()) {
    330             Collections.sort(entries);
    331         }
    332 
    333         addListEntries(entries);
    334     }
    335 
    336     protected String getDisplayIfNull() {
    337         return null;
    338     }
    339 
    340     protected Object getItemToSelect(String def, TaggingPresetItemGuiSupport support, boolean multi) {
    341         final Object itemToSelect;
     308            Collections.sort(presetListEntries, (a, b) -> AlphanumComparator.getInstance().compare(a.getDisplayValue(), b.getDisplayValue()));
     309        }
     310    }
     311
     312    /**
     313     * Returns the initial value to use for this preset.
     314     * <p>
     315     * The initial value is the value shown in the control when the preset dialogs opens.
     316     *
     317     * @param def The default value
     318     * @return The initial value to use.
     319     */
     320    protected String getInitialValue(String def) {
     321        String initialValue = null;
     322
    342323        if (usage.hasUniqueValue()) {
    343             // all items have the same value (and there were no unset items)
    344             originalValue = multi ? usage.getFirst() : getListEntry(usage.getFirst());
    345             itemToSelect = originalValue;
    346         } else if (def != null && usage.unused()) {
    347             // default is set and all items were unset
    348             if (!usage.hadKeys() || PROP_FILL_DEFAULT.get() || isForceUseLastAsDefault()) {
    349                 // selected osm primitives are untagged or filling default feature is enabled
    350                 if (multi) {
    351                     itemToSelect = def;
    352                 } else {
    353                     PresetListEntry entry = getListEntry(def);
    354                     itemToSelect = entry == null ? "" : entry.getDisplayValue();
    355                 }
    356             } else {
    357                 // selected osm primitives are tagged and filling default feature is disabled
    358                 itemToSelect = "";
    359             }
    360             originalValue = multi ? DIFFERENT : getListEntry(DIFFERENT);
    361         } else if (usage.unused()) {
    362             // all items were unset (and so is default)
    363             originalValue = multi ? null : getListEntry("");
    364             if (LAST_VALUES.containsKey(key) && isUseLastAsDefault() && (!support.isPresetInitiallyMatches() || isForceUseLastAsDefault())) {
    365                 itemToSelect = getListEntry(LAST_VALUES.get(key));
    366             } else {
    367                 itemToSelect = originalValue;
    368             }
    369         } else {
    370             originalValue = multi ? DIFFERENT : getListEntry(DIFFERENT);
    371             itemToSelect = originalValue;
    372         }
    373         return itemToSelect;
    374     }
    375 
    376     protected String getSelectedValue() {
    377         Object obj = getSelectedItem();
    378         String display = obj == null ? getDisplayIfNull() : obj.toString();
    379 
    380         if (display == null) {
    381             return "";
    382         }
    383         return presetListEntries.stream()
    384                 .filter(entry -> Objects.equals(entry.toString(), display))
    385                 .findFirst()
    386                 .map(entry -> entry.value)
    387                 .map(Utils::removeWhiteSpaces)
    388                 .orElse(display);
     324            // all selected primitives have the same not empty value for this key
     325            initialValue = usage.getFirst();
     326        } else if (!usage.unused()) {
     327            // at least one primitive has a value for this key (but not all have the same one)
     328            initialValue = DIFFERENT;
     329        } else if (PROP_FILL_DEFAULT.get() || isForceUseLastAsDefault()) {
     330            // at this point no primitive had any value for this key
     331            // use the last value no matter what
     332            initialValue = LAST_VALUES.get(key);
     333        } else if (!usage.hadKeys() && isUseLastAsDefault()) {
     334            // use the last value only on objects with no keys at all
     335            initialValue = LAST_VALUES.get(key);
     336        } else if (!usage.hadKeys()) {
     337            // use the default only on objects with no keys at all
     338            initialValue = def;
     339        }
     340        originalValue = initialValue;
     341        return initialValue;
    389342    }
    390343
    391344    @Override
    392345    public void addCommands(List<Tag> changedTags) {
    393         String value = getSelectedValue();
     346        String value = getSelectedItem().value;
    394347
    395348        // no change if same as before
    396         if (originalValue == null) {
    397             if (value.isEmpty())
    398                 return;
    399         } else if (value.equals(originalValue.toString()))
     349        if (value.isEmpty() && originalValue == null)
    400350            return;
     351        if (value.equals(originalValue))
     352            return;
     353        changedTags.add(new Tag(key, value));
    401354
    402355        if (isUseLastAsDefault()) {
    403356            LAST_VALUES.put(key, value);
    404357        }
    405         changedTags.add(new Tag(key, value));
    406358    }
    407359
     
    420372    }
    421373
     374    /**
     375     * Returns true if the last entered value should be used as default.
     376     * <p>
     377     * Note: never used in {@code defaultpresets.xml}.
     378     *
     379     * @return true if the last entered value should be used as default.
     380     */
    422381    protected boolean isUseLastAsDefault() {
    423382        return use_last_as_default > 0;
    424383    }
    425384
     385    /**
     386     * Returns true if the last entered value should be used as default also on primitives that
     387     * already have tags.
     388     * <p>
     389     * Note: used for {@code addr:*} tags in {@code defaultpresets.xml}.
     390     *
     391     * @return true if see above
     392     */
    426393    protected boolean isForceUseLastAsDefault() {
    427394        return use_last_as_default == 2;
     
    434401    public void addListEntry(PresetListEntry e) {
    435402        presetListEntries.add(e);
     403        // we need to fix the entries because the XML Parser
     404        // {@link org.openstreetmap.josm.tools.XmlObjectParser.Parser#startElement} has used the
     405        // default standard constructor for {@link PresetListEntry} if the list entry was defined
     406        // using XML {@code <list_entry>}.
     407        e.cms = this;
    436408    }
    437409
     
    446418    }
    447419
    448     protected PresetListEntry getListEntry(String value) {
    449         return presetListEntries.stream().filter(e -> Objects.equals(e.value, value)).findFirst().orElse(null);
    450     }
    451 
    452420    @Override
    453421    public MatchType getDefaultMatch() {
  • trunk/src/org/openstreetmap/josm/gui/tagging/presets/items/KeyedItem.java

    r17610 r18254  
    88import java.util.HashMap;
    99import java.util.Map;
     10import java.util.SortedMap;
    1011import java.util.NoSuchElementException;
    11 import java.util.SortedSet;
    12 import java.util.TreeSet;
     12import java.util.TreeMap;
    1313
    1414import javax.swing.JPopupMenu;
     
    2727public abstract class KeyedItem extends TextItem {
    2828
    29     /** Translation of "&lt;different&gt;". Use in combo boxes to display an entry matching several different values. */
    30     protected static final String DIFFERENT = tr("<different>");
    31 
     29    /** The constant value {@code "<different>"}. */
     30    protected static final String DIFFERENT = "<different>";
     31    /** Translation of {@code "<different>"}. */
     32    protected static final String DIFFERENT_I18N = tr(DIFFERENT);
     33
     34    /** True if the default value should also be set on primitives that already have tags.  */
    3235    protected static final BooleanProperty PROP_FILL_DEFAULT = new BooleanProperty("taggingpreset.fill-default-for-tagged-primitives", false);
    3336
     
    100103     */
    101104    public static class Usage {
    102         /**
    103          * A set of values that were used for this key.
    104          */
    105         public final SortedSet<String> values = new TreeSet<>(); // NOSONAR
     105        /** Usage count for all values used for this key */
     106        public final SortedMap<String, Integer> map = new TreeMap<>();
    106107        private boolean hadKeys;
    107108        private boolean hadEmpty;
     
    112113         */
    113114        public boolean hasUniqueValue() {
    114             return values.size() == 1 && !hadEmpty;
     115            return map.size() == 1 && !hadEmpty;
    115116        }
    116117
     
    120121         */
    121122        public boolean unused() {
    122             return values.isEmpty();
     123            return map.isEmpty();
    123124        }
    124125
     
    129130         */
    130131        public String getFirst() {
    131             return values.first();
     132            return map.firstKey();
    132133        }
    133134
     
    152153            String v = s.get(key);
    153154            if (v != null) {
    154                 returnValue.values.add(v);
     155                returnValue.map.merge(v, 1, Integer::sum);
    155156            } else {
    156157                returnValue.hadEmpty = true;
     
    168169            String booleanValue = OsmUtils.getNamedOsmBoolean(s.get(key));
    169170            if (booleanValue != null) {
    170                 returnValue.values.add(booleanValue);
     171                returnValue.map.merge(booleanValue, 1, Integer::sum);
    171172            }
    172173        }
     
    203204        switch (MatchType.ofString(match)) {
    204205        case NONE:
    205             return null;
     206            return null; // NOSONAR
    206207        case KEY:
    207208            return tags.containsKey(key) ? Boolean.TRUE : null;
  • trunk/src/org/openstreetmap/josm/gui/tagging/presets/items/MultiSelect.java

    r18221 r18254  
    22package org.openstreetmap.josm.gui.tagging.presets.items;
    33
    4 import java.awt.Dimension;
    5 import java.util.Arrays;
    6 import java.util.List;
    7 import java.util.Set;
    8 import java.util.TreeSet;
     4import java.util.TreeMap;
     5import java.util.stream.Collectors;
    96
    10 import javax.swing.JComponent;
     7import javax.swing.DefaultListModel;
     8import javax.swing.JLabel;
    119import javax.swing.JList;
    1210import javax.swing.JPanel;
    1311import javax.swing.JScrollPane;
    14 import javax.swing.ListModel;
    1512
    16 import org.openstreetmap.josm.data.osm.Tag;
    1713import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItemGuiSupport;
     14import org.openstreetmap.josm.gui.widgets.OrientationAction;
    1815import org.openstreetmap.josm.tools.GBC;
    1916
     
    2825    public short rows; // NOSONAR
    2926
    30     protected ConcatenatingJList list;
     27    /** The model for the JList */
     28    protected final DefaultListModel<PresetListEntry> model = new DefaultListModel<>();
     29    /** The swing component */
     30    protected final JList<PresetListEntry> list = new JList<>(model);
    3131
    32     @Override
    33     protected JComponent addToPanelAnchor(JPanel p, String def, TaggingPresetItemGuiSupport support) {
    34         list = new ConcatenatingJList(delimiter, presetListEntries.toArray(new PresetListEntry[0]));
    35         ComboMultiSelectListCellRenderer renderer = new ComboMultiSelectListCellRenderer(list, list.getCellRenderer(), 200, key);
    36         list.setCellRenderer(renderer);
    37         Object itemToSelect = getItemToSelect(def, support, true);
    38         list.setSelectedItem(itemToSelect == null ? null : new PresetListEntry(itemToSelect.toString()));
    39         JScrollPane sp = new JScrollPane(list);
    40         // if a number of rows has been specified in the preset,
    41         // modify preferred height of scroll pane to match that row count.
    42         if (rows > 0) {
    43             double height = renderer.getListCellRendererComponent(list,
    44                     new PresetListEntry("x"), 0, false, false).getPreferredSize().getHeight() * rows;
    45             sp.setPreferredSize(new Dimension((int) sp.getPreferredSize().getWidth(), (int) height));
     32    private void addEntry(PresetListEntry entry) {
     33        if (!seenValues.containsKey(entry.value)) {
     34            model.addElement(entry);
     35            seenValues.put(entry.value, entry);
    4636        }
    47         list.addListSelectionListener(l -> support.fireItemValueModified(this, key, getSelectedValue()));
    48         p.add(sp, GBC.eol().fill(GBC.HORIZONTAL));
    49         return list;
    5037    }
    5138
    5239    @Override
    53     protected Object getSelectedItem() {
    54         return list.getSelectedItem();
     40    protected boolean addToPanel(JPanel p, TaggingPresetItemGuiSupport support) {
     41        initializeLocaleText(null);
     42        usage = determineTextUsage(support.getSelected(), key);
     43        seenValues = new TreeMap<>();
     44        initListEntries();
     45
     46        model.clear();
     47        if (!usage.hasUniqueValue() && !usage.unused()) {
     48            addEntry(PresetListEntry.ENTRY_DIFFERENT);
     49        }
     50
     51        String initialValue = getInitialValue(default_);
     52        if (initialValue != null) {
     53            // add all values already present to the list, otherwise we would remove all
     54            // custom entries unknown to the preset
     55            for (String value : initialValue.split(String.valueOf(delimiter), -1)) {
     56                PresetListEntry e = new PresetListEntry(value, this);
     57                addEntry(e);
     58                int i = model.indexOf(e);
     59                list.addSelectionInterval(i, i);
     60            }
     61        }
     62
     63        presetListEntries.forEach(this::addEntry);
     64
     65        ComboMultiSelectListCellRenderer renderer = new ComboMultiSelectListCellRenderer(list, list.getCellRenderer(), 200, key);
     66        list.setCellRenderer(renderer);
     67
     68        if (rows > 0) {
     69            list.setVisibleRowCount(rows);
     70        }
     71        JLabel label = addLabel(p);
     72        p.add(new JScrollPane(list), GBC.eol().fill(GBC.HORIZONTAL)); // NOSONAR
     73        label.setLabelFor(list);
     74
     75        list.addListSelectionListener(l -> support.fireItemValueModified(this, key, getSelectedItem().value));
     76        list.setToolTipText(getKeyTooltipText());
     77        list.applyComponentOrientation(OrientationAction.getValueOrientation(key));
     78
     79        return true;
    5580    }
    5681
    5782    @Override
    58     public void addCommands(List<Tag> changedTags) {
    59         // Do not create any commands if list has been disabled because of an unknown value (fix #8605)
    60         if (list.isEnabled()) {
    61             super.addCommands(changedTags);
    62         }
    63     }
    64 
    65     /**
    66      * Class that allows list values to be assigned and retrieved as a comma-delimited
    67      * string (extracted from TaggingPreset)
    68      */
    69     private static class ConcatenatingJList extends JList<PresetListEntry> {
    70         private final char delimiter;
    71 
    72         protected ConcatenatingJList(char del, PresetListEntry... o) {
    73             super(o);
    74             delimiter = del;
    75         }
    76 
    77         public void setSelectedItem(Object o) {
    78             if (o == null) {
    79                 clearSelection();
    80             } else {
    81                 String s = o.toString();
    82                 Set<String> parts = new TreeSet<>(Arrays.asList(s.split(String.valueOf(delimiter), -1)));
    83                 ListModel<PresetListEntry> lm = getModel();
    84                 int[] intParts = new int[lm.getSize()];
    85                 int j = 0;
    86                 for (int i = 0; i < lm.getSize(); i++) {
    87                     final String value = lm.getElementAt(i).value;
    88                     if (parts.contains(value)) {
    89                         intParts[j++] = i;
    90                         parts.remove(value);
    91                     }
    92                 }
    93                 setSelectedIndices(Arrays.copyOf(intParts, j));
    94                 // check if we have actually managed to represent the full
    95                 // value with our presets. if not, cop out; we will not offer
    96                 // a selection list that threatens to ruin the value.
    97                 setEnabled(parts.isEmpty());
    98             }
    99         }
    100 
    101         public String getSelectedItem() {
    102             ListModel<PresetListEntry> lm = getModel();
    103             int[] si = getSelectedIndices();
    104             StringBuilder builder = new StringBuilder();
    105             for (int i = 0; i < si.length; i++) {
    106                 if (i > 0) {
    107                     builder.append(delimiter);
    108                 }
    109                 builder.append(lm.getElementAt(si[i]).value);
    110             }
    111             return builder.toString();
    112         }
     83    protected PresetListEntry getSelectedItem() {
     84        return new PresetListEntry(list.getSelectedValuesList()
     85            .stream().map(e -> e.value).collect(Collectors.joining(String.valueOf(delimiter))), this);
    11386    }
    11487}
  • trunk/src/org/openstreetmap/josm/gui/tagging/presets/items/PresetListEntry.java

    r18221 r18254  
    1717/**
    1818 * Preset list entry.
     19 * <p>
     20 * Used for controls that offer a list of items to choose from like {@link Combo} and
     21 * {@link MultiSelect}.
    1922 */
    2023public class PresetListEntry implements Comparable<PresetListEntry> {
    21     /** Entry value */
     24    /** Used to display an entry matching several different values. */
     25    protected static final PresetListEntry ENTRY_DIFFERENT = new PresetListEntry(KeyedItem.DIFFERENT, null);
     26    /** Used to display an empty entry used to clear values. */
     27    protected static final PresetListEntry ENTRY_EMPTY = new PresetListEntry("", null);
     28
     29    /**
     30     * This is the value that is going to be written to the tag on the selected primitive(s). Except
     31     * when the value is {@code "<different>"}, which is never written, or the value is empty, which
     32     * deletes the tag.  {@code value} is never translated.
     33     */
    2234    public String value; // NOSONAR
    23     /** The context used for translating {@link #value} */
    24     public String value_context; // NOSONAR
    25     /** Value displayed to the user */
     35    /** The ComboMultiSelect that displays the list */
     36    public ComboMultiSelect cms; // NOSONAR
     37    /** Text displayed to the user instead of {@link #value}. */
    2638    public String display_value; // NOSONAR
    27     /** Text to be displayed below {@code display_value}. */
     39    /** Text to be displayed below {@link #display_value} in the combobox list. */
    2840    public String short_description; // NOSONAR
    2941    /** The location of icon file to display */
     
    3648    public String locale_short_description; // NOSONAR
    3749
     50    private String cachedDisplayValue = null;
     51    private String cachedShortDescription = null;
     52    private ImageIcon cachedIcon = null;
     53
    3854    /**
    3955     * Constructs a new {@code PresetListEntry}, uninitialized.
     56     *
     57     * Public default constructor is needed by {@link org.openstreetmap.josm.tools.XmlObjectParser.Parser#startElement}
    4058     */
    4159    public PresetListEntry() {
    42         // Public default constructor is needed
    4360    }
    4461
    4562    /**
    46      * Constructs a new {@code PresetListEntry}, initialized with a value.
     63     * Constructs a new {@code PresetListEntry}, initialized with a value and
     64     * {@link ComboMultiSelect} context.
     65     *
    4766     * @param value value
     67     * @param cms the ComboMultiSelect
    4868     */
    49     public PresetListEntry(String value) {
     69    public PresetListEntry(String value, ComboMultiSelect cms) {
    5070        this.value = value;
     71        this.cms = cms;
    5172    }
    5273
     
    6182     */
    6283    public String getListDisplay(int width) {
    63         if (value.equals(KeyedItem.DIFFERENT)) {
    64             return "<b>" + KeyedItem.DIFFERENT + "</b>";
     84        String displayValue = getDisplayValue();
     85        Integer count = cms == null ? null : cms.usage.map.get(value);
     86
     87        if (count != null) {
     88            displayValue = String.format("%s (%d)", displayValue, count);
    6589        }
    6690
    67         String shortDescription = getShortDescription(true);
    68         String displayValue = getDisplayValue();
     91        if (this.equals(ENTRY_DIFFERENT)) {
     92            return "<html><b>" + Utils.escapeReservedCharactersHTML(displayValue) + "</b></html>";
     93        }
     94
     95        String shortDescription = getShortDescription();
    6996
    7097        if (shortDescription.isEmpty()) {
     98            // avoids a collapsed list entry if value == ""
    7199            if (displayValue.isEmpty()) {
    72100                return " ";
     
    87115     */
    88116    public ImageIcon getIcon() {
    89         return icon == null ? null : TaggingPresetItem.loadImageIcon(icon, TaggingPresetReader.getZipIcons(), (int) icon_size);
     117        if (icon != null && cachedIcon == null) {
     118            cachedIcon = TaggingPresetItem.loadImageIcon(icon, TaggingPresetReader.getZipIcons(), (int) icon_size);
     119        }
     120        return cachedIcon;
    90121    }
    91122
    92123    /**
    93      * Returns the contents of the current item view.
     124     * Returns the contents displayed in the current item view.
    94125     * @return the value to display
    95126     */
    96127    public String getDisplayValue() {
    97         return Utils.firstNonNull(locale_display_value, tr(display_value), trc(value_context, value));
     128        if (cachedDisplayValue == null) {
     129            if (cms != null && cms.values_no_i18n) {
     130                cachedDisplayValue = Utils.firstNonNull(value, " ");
     131            } else {
     132                cachedDisplayValue = Utils.firstNonNull(
     133                    locale_display_value, tr(display_value), trc(cms == null ? null : cms.values_context, value), " ");
     134            }
     135        }
     136        return cachedDisplayValue;
    98137    }
    99138
    100139    /**
    101140     * Returns the short description to display.
    102      * @param translated whether the text must be translated
    103141     * @return the short description to display
    104142     */
    105     public String getShortDescription(boolean translated) {
    106         String shortDesc = translated
    107                 ? Utils.firstNonNull(locale_short_description, tr(short_description))
    108                         : short_description;
    109         return shortDesc == null ? "" : shortDesc;
     143    public String getShortDescription() {
     144        if (cachedShortDescription == null) {
     145            cachedShortDescription = Utils.firstNonNull(locale_short_description, tr(short_description), "");
     146        }
     147        return cachedShortDescription;
     148    }
     149
     150    /**
     151     * Returns the tooltip for this entry.
     152     * @param key the tag key
     153     * @return the tooltip
     154     */
     155    public String getToolTipText(String key) {
     156        if (this.equals(ENTRY_DIFFERENT)) {
     157            return tr("Keeps the original values of the selected objects unchanged.");
     158        }
     159        if (value != null && !value.isEmpty()) {
     160            return tr("Sets the key ''{0}'' to the value ''{1}''.", key, value);
     161        }
     162        return tr("Clears the key ''{0}''.", key);
    110163    }
    111164
     
    113166    @Override
    114167    public String toString() {
    115         if (KeyedItem.DIFFERENT.equals(value))
    116             return KeyedItem.DIFFERENT;
    117         String displayValue = getDisplayValue();
    118         return displayValue != null ? displayValue.replaceAll("\\s*<.*>\\s*", " ") : ""; // remove additional markup, e.g. <br>
     168        if (this.equals(ENTRY_DIFFERENT))
     169            return getDisplayValue();
     170        return getDisplayValue().replaceAll("\\s*<.*>\\s*", " "); // remove additional markup, e.g. <br>
    119171    }
    120172
     
    134186    @Override
    135187    public int compareTo(PresetListEntry o) {
    136         return AlphanumComparator.getInstance().compare(this.getDisplayValue(), o.getDisplayValue());
     188        return AlphanumComparator.getInstance().compare(this.value, o.value);
    137189    }
    138190}
  • trunk/src/org/openstreetmap/josm/gui/tagging/presets/items/Text.java

    r18221 r18254  
    138138            // cannot be an AutoCompComboBox because the values in the dropdown are different from
    139139            // those we autocomplete on.
    140             JosmComboBox<String> comboBox = new JosmComboBox<>(usage.values.toArray(new String[0]));
     140            JosmComboBox<String> comboBox = new JosmComboBox<>();
     141            comboBox.getModel().addAllElements(usage.map.keySet());
    141142            comboBox.setEditable(true);
    142143            comboBox.setEditor(editor);
    143             comboBox.getEditor().setItem(DIFFERENT);
     144            comboBox.getEditor().setItem(DIFFERENT_I18N);
    144145            value = comboBox;
    145             originalValue = DIFFERENT;
     146            originalValue = DIFFERENT_I18N;
    146147        }
    147148        initializeLocaleText(null);
Note: See TracChangeset for help on using the changeset viewer.