Ticket #15217: 15217.patch

File 15217.patch, 69.5 KB (added by taylor.smock, 21 months ago)

Add autofill support based off of other presets

  • resources/data/defaultpresets.xml

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/resources/data/defaultpresets.xml b/resources/data/defaultpresets.xml
    a b  
    11<?xml version="1.0" encoding="UTF-8"?>
    2 <presets xmlns="http://josm.openstreetmap.de/tagging-preset-1.0">
     2<presets xmlns="http://josm.openstreetmap.de/tagging-preset-1.0" name="defaultpresets">
    33<!--
    44    Format description: https://josm.openstreetmap.de/wiki/TaggingPresets
    55-->
  • resources/data/tagging-preset.xsd

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/resources/data/tagging-preset.xsd b/resources/data/tagging-preset.xsd
    a b  
    7575                        </documentation>
    7676                    </annotation>
    7777                </attribute>
     78                <attribute name="object_keys" type="string">
     79                    <annotation>
     80                        <documentation>
     81                            The keys to indicate that the preset is a real object. Some keys may be an object by default, such as highway.
     82                        </documentation>
     83                    </annotation>
     84                </attribute>
    7885
    7986                <anyAttribute processContents="skip" />
    8087            </extension>
     
    134141        </sequence>
    135142        <attributeGroup ref="tns:attributes.name" />
    136143        <attributeGroup ref="tns:attributes.icon" />
     144        <attributeGroup ref="tns:attributes.deprecated" />
    137145        <attribute name="type" type="string">
    138146            <annotation>
    139147                <documentation><![CDATA[
     
    268276            </annotation>
    269277        </attribute>
    270278        <attribute name="match" type="tns:match" />
     279        <attribute name="object" type="boolean">
     280            <annotation>
     281                <documentation>
     282                    Specify that this preset is an object. Example: highways are objects, contact information is not.
     283                </documentation>
     284            </annotation>
     285        </attribute>
    271286    </complexType>
    272287
    273288    <complexType name="link">
     
    300315        </attribute>
    301316        <attributeGroup ref="tns:attributes.text" />
    302317        <attribute name="name" use="prohibited" />
     318        <attribute name="alternative" type="boolean">
     319            <annotation>
     320                <documentation>
     321                    If specified to be true, this indicates that the preset_link points to an alternative tagging of this object
     322                </documentation>
     323            </annotation>
     324        </attribute>
     325        <attribute name="parent" type="boolean">
     326            <annotation>
     327                <documentation>
     328                    Indicate that the linked preset should use values from this preset for autofill purposes
     329                </documentation>
     330            </annotation>
     331        </attribute>
    303332        <anyAttribute processContents="skip" />
    304333    </complexType>
    305334
     
    328357        <attributeGroup ref="tns:attributes.key" />
    329358        <attributeGroup ref="tns:attributes.text" />
    330359        <attributeGroup ref="tns:attributes.icon" />
     360        <attributeGroup ref="tns:attributes.deprecated" />
    331361        <attribute name="use_last_as_default" type="tns:last_default" />
    332362        <attribute name="auto_increment" type="string">
    333363            <annotation>
     
    353383                ]]></documentation>
    354384            </annotation>
    355385        </attribute>
     386
     387        <attribute name="i18n" type="boolean">
     388            <annotation>
     389                <documentation>
     390                    If true, this freeform key can have i18n variants. Examples would be name and name:en.
     391                </documentation>
     392            </annotation>
     393        </attribute>
    356394
    357395        <attribute name="type" use="prohibited" />
    358396        <attribute name="name" use="prohibited" />
     
    378416            </annotation>
    379417        </attribute>
    380418        <attributeGroup ref="tns:attributes.icon" />
     419        <attributeGroup ref="tns:attributes.deprecated" />
    381420        <anyAttribute processContents="skip" />
    382421    </complexType>
    383422
     
    403442        <attributeGroup ref="tns:attributes.text" />
    404443        <attributeGroup ref="tns:attributes.icon" />
    405444        <attributeGroup ref="tns:attributes.values" />
     445        <attributeGroup ref="tns:attributes.deprecated" />
    406446        <attribute name="use_last_as_default" type="tns:last_default" />
    407447        <attribute name="editable" type="boolean">
    408448            <annotation>
     
    431471        <attributeGroup ref="tns:attributes.text" />
    432472        <attributeGroup ref="tns:attributes.icon" />
    433473        <attributeGroup ref="tns:attributes.values" />
     474        <attributeGroup ref="tns:attributes.deprecated" />
    434475        <attribute name="use_last_as_default" type="tns:last_default" />
    435476        <attribute name="match" type="tns:match" />
    436477
     
    439480        <attribute name="name" use="prohibited" />
    440481        <attribute name="delete-if-empty" use="prohibited" />
    441482        <attribute name="display-values" use="prohibited" />
     483        <attribute name="value_count_key" type="string">
     484            <annotation>
     485                <documentation>
     486                    The reference to the key that will hold the number of values that this multiselect should contain. lanes and turn:lanes would use this.
     487                </documentation>
     488            </annotation>
     489        </attribute>
    442490        <anyAttribute processContents="skip" />
    443491    </complexType>
    444492
     
    591639
    592640    <!-- Types and documentation for attributes -->
    593641
     642    <simpleType name="type.value_type">
     643        <restriction base="string">
     644            <enumeration value="opening_hours">
     645                <annotation>
     646                    <documentation>
     647                        A standard opening hours tag
     648                    </documentation>
     649                </annotation>
     650            </enumeration>
     651            <enumeration value="opening_hours_mixed">
     652                <annotation>
     653                    <documentation>
     654                        A tag with both text values and opening hours
     655                    </documentation>
     656                </annotation>
     657            </enumeration>
     658            <enumeration value="conditional">
     659                <annotation>
     660                    <documentation>
     661                        A conditional tag such as maxspeed:conditional
     662                    </documentation>
     663                </annotation>
     664            </enumeration>
     665            <enumeration value="integer"/>
     666            <enumeration value="website"/>
     667            <enumeration value="phone"/>
     668            <enumeration value="wikipedia"/>
     669            <enumeration value="wikidata"/>
     670        </restriction>
     671    </simpleType>
     672
    594673    <attributeGroup name="attributes.name">
    595674        <attribute name="name" type="string" use="required">
    596675            <annotation>
     
    607686            </annotation>
    608687        </attribute>
    609688    </attributeGroup>
     689
     690    <attributeGroup name="attributes.deprecated">
     691        <attribute name="deprecated" type="boolean">
     692            <annotation>
     693                <documentation>
     694                    If indicated to be true, this preset is deprecated and will not normally show up.
     695                </documentation>
     696            </annotation>
     697        </attribute>
     698    </attributeGroup>
    610699
    611700    <attributeGroup name="attributes.key">
    612701        <attribute name="key" type="string" use="required">
     
    698787                <documentation><![CDATA[
    699788                    The character that separates values. In case of <combo /> the default is comma. In case of <multiselect /> the default is semicolon and this will also be used to separate selected values in the tag.
    700789                ]]></documentation>
     790            </annotation>
     791        </attribute>
     792        <attribute name="value_type" type="tns:type.value_type">
     793            <annotation>
     794                <documentation>
     795                    The type of value to avoid hardcoding the property in JOSM
     796                </documentation>
    701797            </annotation>
    702798        </attribute>
    703799    </attributeGroup>
  • src/org/openstreetmap/josm/data/preferences/sources/ExtendedSourceEntry.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/preferences/sources/ExtendedSourceEntry.java b/src/org/openstreetmap/josm/data/preferences/sources/ExtendedSourceEntry.java
    a b  
    2828    /** minimum JOSM version required to enable this source entry */
    2929    public Integer minJosmVersion;
    3030
     31    /**
     32     * A constructor that is only supposed to be used by {@link org.openstreetmap.josm.tools.XmlObjectParser}.
     33     * All values will be filled in using field references.
     34     * @since xxx
     35     */
     36    public ExtendedSourceEntry() {
     37        this(null, null, null);
     38    }
     39
    3140    /**
    3241     * Constructs a new {@code ExtendedSourceEntry}.
    3342     * @param type type of source entry
  • new file src/org/openstreetmap/josm/data/tagging/ac/AutoCompletionItemRunnable.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/tagging/ac/AutoCompletionItemRunnable.java b/src/org/openstreetmap/josm/data/tagging/ac/AutoCompletionItemRunnable.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.tagging.ac;
     3
     4import java.util.Objects;
     5
     6import org.openstreetmap.josm.gui.util.GuiHelper;
     7
     8/**
     9 * An autocompletion item that performs additional actions when it is selected
     10 * @since xxx
     11 */
     12public class AutoCompletionItemRunnable extends AutoCompletionItem implements Runnable {
     13    private final Runnable runnable;
     14
     15    /**
     16     * Create a new AutoCompletionItemRunnable
     17     * @param runnable The runnable to use. This will be run in the EDT (non-blocking).
     18     * @param value The value for the key
     19     * @param priority The {@link AutoCompletionPriority}, but it should usually be at least {@link AutoCompletionPriority#IS_IN_STANDARD}
     20     */
     21    public AutoCompletionItemRunnable(Runnable runnable, String value, AutoCompletionPriority priority) {
     22        super(value, priority);
     23        Objects.requireNonNull(runnable, "runnable");
     24        this.runnable = runnable;
     25    }
     26
     27    @Override
     28    public void run() {
     29        GuiHelper.runInEDT(this.runnable);
     30    }
     31
     32    @Override
     33    public int hashCode() {
     34        return super.hashCode() * 31 + this.runnable.hashCode();
     35    }
     36
     37    @Override
     38    public boolean equals(Object obj) {
     39        return super.equals(obj)
     40            && this.getClass().equals(obj.getClass())
     41            && this.runnable.equals(((AutoCompletionItemRunnable) obj).runnable);
     42    }
     43}
  • src/org/openstreetmap/josm/gui/tagging/ac/AutoCompletingTextField.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/gui/tagging/ac/AutoCompletingTextField.java b/src/org/openstreetmap/josm/gui/tagging/ac/AutoCompletingTextField.java
    a b  
    298298        rememberOriginalValue(getText());
    299299        return this;
    300300    }
     301
     302    @Override
     303    public void focusLost(FocusEvent e) {
     304        super.focusLost(e);
     305        if (this.getHighlighter().getHighlights().length != 0) {
     306            final String currentFilter = this.autoCompletionList.getFilter();
     307            try {
     308                this.autoCompletionList.applyFilter(this.getText());
     309                if (this.autoCompletionList.getFilteredSize() == 1 && this.autoCompletionList.getFilteredItemAt(0) instanceof Runnable) {
     310                    ((Runnable) this.autoCompletionList.getFilteredItemAt(0)).run();
     311                }
     312            } finally {
     313                this.autoCompletionList.applyFilter(currentFilter);
     314            }
     315        }
     316    }
    301317}
  • src/org/openstreetmap/josm/gui/tagging/ac/AutoCompletionList.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/gui/tagging/ac/AutoCompletionList.java b/src/org/openstreetmap/josm/gui/tagging/ac/AutoCompletionList.java
    a b  
    187187        // apply the pattern to list of possible values. If it matches, add the
    188188        // value to the list of filtered values
    189189        //
    190         list.stream().filter(e -> e.getValue().startsWith(filter)).forEach(filtered::add);
     190        // Prefer items that are autofill capable
     191        list.stream().filter(e -> e instanceof Runnable && e.getValue().startsWith(filter)).forEach(filtered::add);
     192        // But fall back to non-autofill items
     193        if (list.isEmpty()) {
     194            list.stream().filter(e -> e.getValue().startsWith(filter)).forEach(filtered::add);
     195        }
    191196        fireTableDataChanged();
    192197    }
    193198
  • src/org/openstreetmap/josm/gui/tagging/ac/AutoCompletionManager.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/gui/tagging/ac/AutoCompletionManager.java b/src/org/openstreetmap/josm/gui/tagging/ac/AutoCompletionManager.java
    a b  
    22package org.openstreetmap.josm.gui.tagging.ac;
    33
    44import java.util.ArrayList;
    5 import java.util.Arrays;
    65import java.util.Collection;
    76import java.util.Collections;
    87import java.util.Comparator;
     
    1312import java.util.Map;
    1413import java.util.Map.Entry;
    1514import java.util.Objects;
     15import java.util.Optional;
    1616import java.util.Set;
    17 import java.util.function.Function;
     17import java.util.function.Consumer;
    1818import java.util.stream.Collectors;
    1919
    2020import org.openstreetmap.josm.data.osm.DataSet;
     
    3131import org.openstreetmap.josm.data.osm.event.TagsChangedEvent;
    3232import org.openstreetmap.josm.data.osm.event.WayNodesChangedEvent;
    3333import org.openstreetmap.josm.data.tagging.ac.AutoCompletionItem;
     34import org.openstreetmap.josm.data.tagging.ac.AutoCompletionItemRunnable;
    3435import org.openstreetmap.josm.data.tagging.ac.AutoCompletionPriority;
    3536import org.openstreetmap.josm.data.tagging.ac.AutoCompletionSet;
    3637import org.openstreetmap.josm.gui.MainApplication;
     
    4142import org.openstreetmap.josm.gui.layer.OsmDataLayer;
    4243import org.openstreetmap.josm.gui.tagging.presets.TaggingPreset;
    4344import org.openstreetmap.josm.gui.tagging.presets.TaggingPresets;
    44 import org.openstreetmap.josm.gui.tagging.presets.items.Roles.Role;
     45import org.openstreetmap.josm.gui.tagging.presets.items.Key;
    4546import org.openstreetmap.josm.tools.CheckParameterUtil;
    4647import org.openstreetmap.josm.tools.MultiMap;
    4748import org.openstreetmap.josm.tools.Utils;
     
    4950/**
    5051 * AutoCompletionManager holds a cache of keys with a list of
    5152 * possible auto completion values for each key.
    52  *
     53 * <p>
    5354 * Each DataSet can be assigned one AutoCompletionManager instance such that
    5455 * <ol>
    5556 *   <li>any key used in a tag in the data set is part of the key list in the cache</li>
     
    6061 * slow down tabbing from input field to input field. Looping through the complete
    6162 * data set in order to build up the auto completion list for a specific input
    6263 * field is not efficient enough, hence this cache.
    63  *
     64 * <p>
    6465 * TODO: respect the relation type for member role autocompletion
    6566 */
    6667public class AutoCompletionManager implements DataSetListener {
    67 
    6868    /**
    6969     * Data class to remember tags that the user has entered.
    7070     */
     
    187187
    188188    /**
    189189     * make sure, the keys and values of all tags held by primitive are
    190      * in the auto completion cache
     190     * in the autocompletion cache
    191191     *
    192192     * @param primitive an OSM primitive
    193193     */
     
    239239    }
    240240
    241241    /**
    242      * replies the auto completion values allowed for a specific key. Replies
     242     * replies the autocompletion values allowed for a specific key. Replies
    243243     * an empty list if key is null or if key is not in {@link #getTagKeys()}.
    244244     *
    245245     * @param key OSM key
    246      * @return the list of auto completion values
     246     * @return the list of autocompletion values
    247247     */
    248248    protected List<String> getDataValues(String key) {
    249249        return new ArrayList<>(getTagCache().getValues(key));
     
    293293        if (r != null && !Utils.isEmpty(presets)) {
    294294            for (TaggingPreset tp : presets) {
    295295                if (tp.roles != null) {
    296                     list.add(Utils.transform(tp.roles.roles, (Function<Role, String>) x -> x.key), AutoCompletionPriority.IS_IN_STANDARD);
     296                    list.add(Utils.transform(tp.roles.roles, x -> x.key), AutoCompletionPriority.IS_IN_STANDARD);
    297297                }
    298298            }
    299299            list.add(r.getMemberRoles(), AutoCompletionPriority.IS_IN_DATASET);
     
    321321     * @param key the tag key
    322322     */
    323323    public void populateWithTagValues(AutoCompletionList list, String key) {
    324         populateWithTagValues(list, Arrays.asList(key));
     324        populateWithTagValues(list, Collections.singletonList(key));
    325325    }
    326326
    327327    /**
     
    352352     * @since 18221
    353353     */
    354354    public List<AutoCompletionItem> getAllForKeys(List<String> keys) {
    355         Map<String, AutoCompletionPriority> map = new HashMap<>();
     355        return getAllForKeys(null, null, keys);
     356    }
     357
     358    /**
     359     * Returns all cached {@link AutoCompletionItem}s for given keys.
     360     *
     361     * @param runnable The runnable to call when the AutoCompletionItem is selected
     362     * @param keys retrieve the items for these keys
     363     * @param currentPreset the preset to use to find child presets
     364     * @return the currently cached items, sorted by priority and alphabet
     365     * @since xxx
     366     */
     367    public List<AutoCompletionItem> getAllForKeys(Consumer<TaggingPreset> runnable, TaggingPreset currentPreset, List<String> keys) {
     368        Map<String, AutoCompletionPriority> map = new HashMap<>(keys.size());
    356369
    357370        for (String key : keys) {
    358371            for (String value : TaggingPresets.getPresetValues(key)) {
     
    365378                map.merge(value, AutoCompletionPriority.UNKNOWN, AutoCompletionPriority::mergeWith);
    366379            }
    367380        }
    368         return map.entrySet().stream().map(e -> new AutoCompletionItem(e.getKey(), e.getValue())).sorted().collect(Collectors.toList());
     381        final boolean containsPrimaryKey = keys.stream().anyMatch(TaggingPresets::isPrimaryKey);
     382        return map.entrySet().stream().map(entry -> createAutoCompletionItem(containsPrimaryKey, entry, runnable, currentPreset)).sorted()
     383                .collect(Collectors.toList());
     384    }
     385
     386    /**
     387     * Create an autocompletion item from an entry
     388     *
     389     * @param primaryKey    {@code true} if the key is something that can uniquely identify the object
     390     * @param entry         The entry to create the item from
     391     * @param runnable      The runnable to use if we end up creating a {@link AutoCompletionItemRunnable}
     392     * @param currentPreset The current preset
     393     * @return The created autocompletion item
     394     */
     395    private static AutoCompletionItem createAutoCompletionItem(boolean primaryKey, Entry<String, AutoCompletionPriority> entry,
     396                                                               Consumer<TaggingPreset> runnable,
     397                                                               TaggingPreset currentPreset) {
     398        if (runnable == null || AutoCompletionPriority.UNKNOWN.equals(entry.getValue())
     399                || AutoCompletionPriority.IS_IN_SELECTION.equals(entry.getValue())
     400                || AutoCompletionPriority.IS_IN_DATASET.equals(entry.getValue())
     401        || !primaryKey) {
     402            return new AutoCompletionItem(entry.getKey(), entry.getValue());
     403        } else if (AutoCompletionPriority.IS_IN_STANDARD.equals(entry.getValue())
     404                || AutoCompletionPriority.IS_IN_STANDARD_AND_IN_DATASET.equals(entry.getValue())) {
     405            final Map<String, String> tagMap = currentPreset.getMinimalTagMap();
     406            Optional<TaggingPreset> best = currentPreset.getChildrenPresets().stream().filter(p ->
     407                    // Allow any preset where the preset name matches the value
     408                    (p.name != null && p.name.equals(entry.getKey())) ||
     409                    // Filter out results that don't have any keys with the value
     410                    p.data.stream().filter(Key.class::isInstance).map(Key.class::cast).anyMatch(key -> key.value.equals(entry.getKey())))
     411                    .max(Comparator.comparingLong(p -> countCommonKeys(p, tagMap) + (p.name != null && p.name.equals(entry.getKey()) ? 1 : 0)));
     412            if (best.isPresent()) {
     413                return new AutoCompletionItemRunnable(() -> runnable.accept(best.get()), entry.getKey(), entry.getValue());
     414            }
     415            return new AutoCompletionItem(entry.getKey(), entry.getValue());
     416        }
     417        throw new IllegalStateException("Unexpected value: " + entry.getValue());
     418    }
     419
     420    private static int countCommonKeys(TaggingPreset preset, Map<String, String> keys) {
     421        return preset.getMinimalTagMap().entrySet().stream().mapToInt(presetEntry -> {
     422            if (keys.containsKey(presetEntry.getKey())) {
     423                if (Objects.equals(keys.get(presetEntry.getKey()), presetEntry.getValue())) {
     424                    return 2;
     425                }
     426                return 1;
     427            }
     428            return 0;
     429        }).sum();
    369430    }
    370431
    371432    /**
     
    396457     * @since 12859
    397458     */
    398459    public AutoCompletionSet getTagValues(String key) {
    399         return getTagValues(Arrays.asList(key));
     460        return getTagValues(Collections.singletonList(key));
    400461    }
    401462
    402463    /**
  • src/org/openstreetmap/josm/gui/tagging/ac/AutoCompTextField.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/gui/tagging/ac/AutoCompTextField.java b/src/org/openstreetmap/josm/gui/tagging/ac/AutoCompTextField.java
    a b  
    55import java.awt.datatransfer.Clipboard;
    66import java.awt.datatransfer.StringSelection;
    77import java.awt.datatransfer.Transferable;
     8import java.awt.event.FocusEvent;
    89import java.awt.event.KeyEvent;
    910import java.awt.event.KeyListener;
    1011import java.util.EventObject;
     
    338339        rememberOriginalValue(getText());
    339340        return this;
    340341    }
     342
     343    @Override
     344    public void focusLost(FocusEvent e) {
     345        super.focusLost(e);
     346        if (this.autocompleteEnabled) {
     347            E item = getModel().findBestCandidate(getText());
     348            if (item instanceof Runnable) {
     349                ((Runnable) item).run();
     350            }
     351        }
     352    }
    341353}
  • src/org/openstreetmap/josm/gui/tagging/presets/items/Check.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/gui/tagging/presets/items/Check.java b/src/org/openstreetmap/josm/gui/tagging/presets/items/Check.java
    a b  
    9292            p.add(check, GBC.eol()); // Do not fill, see #15104
    9393        }
    9494        check.addChangeListener(l -> support.fireItemValueModified(this, key, getValue()));
     95        support.addField(this.key, this.check);
    9596        return true;
    9697    }
    9798
  • src/org/openstreetmap/josm/gui/tagging/presets/items/Combo.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/gui/tagging/presets/items/Combo.java b/src/org/openstreetmap/josm/gui/tagging/presets/items/Combo.java
    a b  
    1010import java.awt.event.ActionListener;
    1111import java.awt.event.ComponentAdapter;
    1212import java.awt.event.ComponentEvent;
    13 import java.util.Arrays;
     13import java.util.Collections;
    1414import java.util.Comparator;
     15import java.util.List;
     16import java.util.Objects;
    1517
    1618import javax.swing.AbstractAction;
    1719import javax.swing.JButton;
     
    2729import org.openstreetmap.josm.gui.tagging.ac.AutoCompComboBoxEditor;
    2830import org.openstreetmap.josm.gui.tagging.ac.AutoCompComboBoxModel;
    2931import org.openstreetmap.josm.gui.tagging.ac.AutoCompTextField;
     32import org.openstreetmap.josm.gui.tagging.presets.TaggingPreset;
    3033import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItem;
    3134import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItemGuiSupport;
    3235import org.openstreetmap.josm.gui.widgets.JosmComboBox;
     
    8588
    8689    @Override
    8790    protected boolean addToPanel(JPanel p, TaggingPresetItemGuiSupport support) {
     91        return addToPanel(p, support, null);
     92    }
     93
     94    @Override
     95    protected boolean addToPanel(JPanel p, TaggingPresetItemGuiSupport support, TaggingPreset preset) {
    8896        initializeLocaleText(null);
    8997        usage = determineTextUsage(support.getSelected(), key);
    9098        seenValues.clear();
     
    124132        combobox.setEditable(editable);
    125133
    126134        autoCompModel = new AutoCompComboBoxModel<>(Comparator.<AutoCompletionItem>naturalOrder());
    127         getAllForKeys(Arrays.asList(key)).forEach(autoCompModel::addElement);
    128         getDisplayValues().forEach(s -> autoCompModel.addElement(new AutoCompletionItem(s, AutoCompletionPriority.IS_IN_STANDARD)));
     135        List<AutoCompletionItem> allForKeys = getAllForKeys(support::fillFromPreset, preset, Collections.singletonList(key));
     136        autoCompModel.addAllElements(allForKeys);
     137        getDisplayValues().stream().filter(value -> allForKeys.stream().noneMatch(item -> Objects.equals(item.getValue(), value)))
     138                        .map(s -> new AutoCompletionItem(s, AutoCompletionPriority.IS_IN_STANDARD))
     139                                .forEach(autoCompModel::addElement);
    129140
    130141        AutoCompTextField<AutoCompletionItem> tf = editor.getEditorComponent();
    131142        tf.setModel(autoCompModel);
     
    168179        combobox.setToolTipText(getKeyTooltipText());
    169180        combobox.applyComponentOrientation(OrientationAction.getValueOrientation(key));
    170181
     182        support.addField(this.key, this.combobox);
    171183        return true;
    172184    }
    173185
  • src/org/openstreetmap/josm/gui/tagging/presets/items/Key.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/gui/tagging/presets/items/Key.java b/src/org/openstreetmap/josm/gui/tagging/presets/items/Key.java
    a b  
    44import java.util.Collection;
    55import java.util.Collections;
    66import java.util.List;
     7import java.util.Map;
     8import java.util.Objects;
    79
    810import javax.swing.JPanel;
    911
     
    4648        return Collections.singleton(value);
    4749    }
    4850
     51    @Override
     52    public Boolean matches(Map<String, String> tags) {
     53        switch (MatchType.ofString(match)) {
     54            case KEY_VALUE:
     55                return tags.containsKey(key) && value.equals(tags.get(key)) ? Boolean.TRUE : null;
     56            case KEY_VALUE_REQUIRED:
     57                return tags.containsKey(key) && value.equals(tags.get(key));
     58            default:
     59                return super.matches(tags);
     60        }
     61    }
     62
    4963    @Override
    5064    public String toString() {
    5165        return "Key [key=" + key + ", value=" + value + ", text=" + text
    5266                + ", text_context=" + text_context + ", match=" + match
    5367                + ']';
    5468    }
     69
     70    @Override
     71    public int hashCode() {
     72        return Objects.hash(this.key, this.value, this.text, this.text_context, this.match);
     73    }
     74
     75    @Override
     76    public boolean equals(Object obj) {
     77        if (obj != null && obj.getClass() == this.getClass()) {
     78            Key other = (Key) obj;
     79            return Objects.equals(this.key, other.key)
     80                    && Objects.equals(this.value, other.value)
     81                    && Objects.equals(this.text, other.text)
     82                    && Objects.equals(this.text_context, other.text_context)
     83                    && Objects.equals(this.match, other.match);
     84        }
     85        return false;
     86    }
    5587}
  • src/org/openstreetmap/josm/gui/tagging/presets/items/KeyedItem.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/gui/tagging/presets/items/KeyedItem.java b/src/org/openstreetmap/josm/gui/tagging/presets/items/KeyedItem.java
    a b  
    44import static org.openstreetmap.josm.tools.I18n.tr;
    55
    66import java.util.Collection;
    7 import java.util.EnumSet;
    87import java.util.HashMap;
    98import java.util.Map;
    109import java.util.SortedMap;
     
    6867        /** Positive if key and value matches, negative otherwise. */
    6968        KEY_VALUE_REQUIRED("keyvalue!");
    7069
     70        // Avoid many array instantiations
     71        private static MatchType[] allValues = values();
     72
    7173        private final String value;
    7274
    7375        MatchType(String value) {
     
    8890         * @return the {@code MatchType} for the given textual value
    8991         */
    9092        public static MatchType ofString(String type) {
    91             for (MatchType i : EnumSet.allOf(MatchType.class)) {
     93            for (MatchType i : allValues) {
    9294                if (i.getValue().equals(type))
    9395                    return i;
    9496            }
  • src/org/openstreetmap/josm/gui/tagging/presets/items/MultiSelect.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/gui/tagging/presets/items/MultiSelect.java b/src/org/openstreetmap/josm/gui/tagging/presets/items/MultiSelect.java
    a b  
    9797        list.addListSelectionListener(l -> support.fireItemValueModified(this, key, getSelectedItem().value));
    9898        list.setToolTipText(getKeyTooltipText());
    9999        list.applyComponentOrientation(OrientationAction.getValueOrientation(key));
     100        support.addField(this.key, this.list);
    100101
    101102        return true;
    102103    }
  • src/org/openstreetmap/josm/gui/tagging/presets/items/PresetLink.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/gui/tagging/presets/items/PresetLink.java b/src/org/openstreetmap/josm/gui/tagging/presets/items/PresetLink.java
    a b  
    55
    66import java.awt.event.MouseAdapter;
    77import java.awt.event.MouseEvent;
     8import java.util.ArrayList;
    89import java.util.Collection;
     10import java.util.Collections;
    911import java.util.List;
     12import java.util.Objects;
    1013import java.util.Optional;
     14import java.util.regex.Matcher;
     15import java.util.regex.Pattern;
     16import java.util.stream.Stream;
    1117
    1218import javax.swing.JLabel;
    1319import javax.swing.JPanel;
    1420
     21
    1522import org.openstreetmap.josm.data.osm.OsmPrimitive;
    1623import org.openstreetmap.josm.data.osm.Tag;
    1724import org.openstreetmap.josm.gui.tagging.presets.TaggingPreset;
    1825import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItemGuiSupport;
    1926import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetLabel;
     27import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetMenu;
    2028import org.openstreetmap.josm.gui.tagging.presets.TaggingPresets;
    2129import org.openstreetmap.josm.tools.GBC;
    2230
     
    4149        }
    4250    }
    4351
    44     /** The exact name of the preset to link to. Required. */
     52    /**
     53     * The pattern for linking to specific presets. josm-preset:[//&lt;preset name&gt;][/]&lt;path&gt;, see #12716.
     54     * If just josm-preset:&lt;path&gt; is specified, the preset path is relative (so the linked preset must
     55     * exist in the file that created this preset).
     56     */
     57    private static final Pattern PRESET_LINK_PATTERN = Pattern.compile("^josm-preset:(//(.*?)/)?(.*)$");
     58
     59    /**
     60     * The exact name of the preset to link to. Required.
     61     * This may match {@link #PRESET_LINK_PATTERN}.
     62     */
    4563    public String preset_name = ""; // NOSONAR
     64    /**
     65     * If {@code true}, the {@link #preset_name} is a parent of this preset.
     66     * Used for autocomplete, but {@link #preset_name} <i>must</i> match the {@link #PRESET_LINK_PATTERN}.
     67     * @since xxx
     68     */
     69    public boolean parent; // NOSONAR
     70
     71    /**
     72     * Get the preset for a given preset name
     73     * @param taggingPreset The tagging preset this link is part of. May be {@code null}
     74     * @param presetName The preset name to find. May either be a straight name or it may match {@link #PRESET_LINK_PATTERN}.
     75     * @return The preset, if any
     76     * @since xxx
     77     */
     78    private static Optional<TaggingPreset> getPreset(TaggingPreset taggingPreset, String presetName) {
     79        final Matcher matcher = PRESET_LINK_PATTERN.matcher(presetName);
     80        Stream<TaggingPreset> presetStream = TaggingPresets.getTaggingPresets().stream()
     81                .filter(preset -> !(preset instanceof TaggingPresetMenu));
     82        if (matcher.matches()) {
     83            String path = matcher.group(3);
     84            if (path != null) {
     85                String presetList = matcher.group(2);
     86                if (presetList != null) {
     87                    presetStream = presetStream.filter(preset -> presetList.equals(preset.preset_list_name));
     88                } else if (taggingPreset != null) {
     89                    presetStream = presetStream.filter(preset -> Objects.equals(taggingPreset.preset_list_name, preset.preset_list_name));
     90                }
     91                final String[] expectedPath = path.split("/", -1);
     92                final List<String> groupList = new ArrayList<>();
     93                presetStream = presetStream.filter(preset -> checkPath(expectedPath, preset, groupList));
     94            }
     95        } else {
     96            presetStream = presetStream.filter(preset -> presetName.equals(preset.name));
     97        }
     98        return presetStream.findFirst();
     99    }
     100
     101    /**
     102     * Ensure that a path matches a preset path
     103     * @param expectedPath The expected path
     104     * @param preset The preset to build a path from
     105     * @return {@code true} if the paths are equal
     106     */
     107    private static synchronized boolean checkPath(String[] expectedPath, TaggingPreset preset, List<String> presetList) {
     108        TaggingPreset current = preset;
     109        presetList.clear();
     110        while (current != null && current.name != null) {
     111            presetList.add(current.name);
     112            current = current.group;
     113        }
     114        Collections.reverse(presetList);
     115        if (expectedPath.length == presetList.size()) {
     116            for (int i = 0; i < expectedPath.length; i++) {
     117                if (!presetList.get(i).equals(expectedPath[i])) {
     118                    return false;
     119                }
     120            }
     121            return true;
     122        }
     123        return false;
     124    }
     125
     126    /**
     127     * If this preset has a parent (see {@link #parent}), then this preset will be used
     128     * for autocomplete values for {@link #preset_name}. Please note that we currently
     129     * force {@link #preset_name} to match the {@link #PRESET_LINK_PATTERN}.
     130     * @param taggingPreset The tagging preset this link is part of. May be {@code null}.
     131     * @since xxx
     132     */
     133    public void reverseLinkPreset(TaggingPreset taggingPreset) {
     134        if (this.parent) {
     135            final String presetName = preset_name;
     136            final Matcher matcher = PRESET_LINK_PATTERN.matcher(presetName);
     137            if (!matcher.matches()) {
     138                throw new IllegalArgumentException(
     139                        "JOSM only supports matching presets to parents if the parent preset_name is declarative. "
     140                                + PRESET_LINK_PATTERN);
     141            }
     142            Optional<TaggingPreset> preset = getPreset(taggingPreset, presetName);
     143            preset.ifPresent(tPreset -> tPreset.addChildPreset(taggingPreset));
     144        }
     145    }
    46146
    47147    /**
    48148     * Creates a label to be inserted aboive this link
     
    55155
    56156    @Override
    57157    public boolean addToPanel(JPanel p, TaggingPresetItemGuiSupport support) {
    58         final String presetName = preset_name;
    59         Optional<TaggingPreset> found = TaggingPresets.getTaggingPresets().stream().filter(preset -> presetName.equals(preset.name)).findFirst();
     158        final Optional<TaggingPreset> found = getPreset(null, preset_name);
    60159        if (found.isPresent()) {
    61160            TaggingPreset t = found.get();
    62161            JLabel lbl = new TaggingPresetLabel(t);
  • src/org/openstreetmap/josm/gui/tagging/presets/items/Text.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/gui/tagging/presets/items/Text.java b/src/org/openstreetmap/josm/gui/tagging/presets/items/Text.java
    a b  
    1010import java.text.NumberFormat;
    1111import java.text.ParseException;
    1212import java.util.ArrayList;
     13import java.util.Arrays;
    1314import java.util.Collection;
    1415import java.util.Collections;
    1516import java.util.List;
     
    2930import org.openstreetmap.josm.gui.tagging.ac.AutoCompComboBoxModel;
    3031import org.openstreetmap.josm.gui.tagging.ac.AutoCompTextField;
    3132import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionManager;
     33import org.openstreetmap.josm.gui.tagging.presets.TaggingPreset;
    3234import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItem;
    3335import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItemGuiSupport;
    3436import org.openstreetmap.josm.gui.util.DocumentAdapter;
     
    7274    private transient TemplateEntry valueTemplate;
    7375
    7476    @Override
    75     public boolean addToPanel(JPanel p, TaggingPresetItemGuiSupport support) {
     77    protected boolean addToPanel(JPanel p, TaggingPresetItemGuiSupport support) {
     78        return this.addToPanel(p, support, null);
     79    }
     80
     81    @Override
     82    public boolean addToPanel(JPanel p, TaggingPresetItemGuiSupport support, TaggingPreset preset) {
    7683
    7784        AutoCompComboBoxModel<AutoCompletionItem> model = new AutoCompComboBoxModel<>();
    7885        List<String> keys = new ArrayList<>();
    7986        keys.add(key);
    8087        if (alternative_autocomplete_keys != null) {
    81             for (String k : alternative_autocomplete_keys.split(",", -1)) {
    82                 keys.add(k);
    83             }
     88            keys.addAll(Arrays.asList(alternative_autocomplete_keys.split(",", -1)));
    8489        }
    85         getAllForKeys(keys).forEach(model::addElement);
     90        model.addAllElements(getAllForKeys(support::fillFromPreset, preset, keys));
    8691
    8792        AutoCompTextField<AutoCompletionItem> textField;
    8893        AutoCompComboBoxEditor<AutoCompletionItem> editor = null;
     
    208213        label.applyComponentOrientation(support.getDefaultComponentOrientation());
    209214        value.setToolTipText(getKeyTooltipText());
    210215        value.applyComponentOrientation(OrientationAction.getNamelikeOrientation(key));
     216        support.addField(this.key, this.value);
    211217        return true;
    212218    }
    213219
  • src/org/openstreetmap/josm/gui/tagging/presets/TaggingPreset.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/gui/tagging/presets/TaggingPreset.java b/src/org/openstreetmap/josm/gui/tagging/presets/TaggingPreset.java
    a b  
    1414import java.io.File;
    1515import java.util.ArrayList;
    1616import java.util.Collection;
     17import java.util.Collections;
    1718import java.util.EnumSet;
     19import java.util.HashMap;
     20import java.util.HashSet;
    1821import java.util.LinkedHashSet;
    1922import java.util.List;
    2023import java.util.Map;
     
    6265import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener;
    6366import org.openstreetmap.josm.gui.preferences.ToolbarPreferences;
    6467import org.openstreetmap.josm.gui.tagging.presets.items.Key;
     68import org.openstreetmap.josm.gui.tagging.presets.items.KeyedItem;
    6569import org.openstreetmap.josm.gui.tagging.presets.items.Link;
    6670import org.openstreetmap.josm.gui.tagging.presets.items.Optional;
    6771import org.openstreetmap.josm.gui.tagging.presets.items.PresetLink;
     
    136140     * Show the preset name if true
    137141     */
    138142    public boolean preset_name_label;
     143    /**
     144     * The name of the preset list (e.g. the name suggestion index)
     145     * @since xxx
     146     */
     147    public String preset_list_name;
    139148
    140149    /**
    141150     * The types as preparsed collection.
     
    170179
    171180    /** Support functions */
    172181    protected TaggingPresetItemGuiSupport itemGuiSupport;
     182    private final Collection<TaggingPreset> children = new HashSet<>();
    173183
    174184    /**
    175185     * Create an empty tagging preset. This will not have any items and
     
    349359        }
    350360    }
    351361
     362    /**
     363     * Add a child preset to be used for autocomplete suggestions
     364     * @param child The child preset
     365     * @since xxx
     366     */
     367    public void addChildPreset(TaggingPreset child) {
     368        this.children.add(child);
     369    }
     370
     371    /**
     372     * Get the children presets to be used for autocompletion
     373     * @return The child presets
     374     */
     375    public Collection<TaggingPreset> getChildrenPresets() {
     376        return Collections.unmodifiableCollection(this.children);
     377    }
     378
    352379    private static class PresetPanel extends JPanel {
    353380        private boolean hasElements;
    354381
     
    422449        TaggingPresetItem previous = null;
    423450        for (TaggingPresetItem i : data) {
    424451            if (i instanceof Link) {
    425                 i.addToPanel(linkPanel, itemGuiSupport);
     452                i.addToPanel(linkPanel, itemGuiSupport, this);
    426453                p.hasElements = true;
    427454            } else {
    428455                if (i instanceof PresetLink) {
     
    431458                        itemPanel.add(link.createLabel(), GBC.eol().insets(0, 8, 0, 0));
    432459                    }
    433460                }
    434                 if (i.addToPanel(itemPanel, itemGuiSupport)) {
     461                if (i.addToPanel(itemPanel, itemGuiSupport, this)) {
    435462                    p.hasElements = true;
    436463                }
    437464            }
     
    735762        }
    736763    }
    737764
     765    /**
     766     * This builds a map of key->value objects, where the value may be {@code null}.
     767     * If the value is {@code null}, it means that it is not pre-set.
     768     * @return A map of tags
     769     * @since xxx
     770     */
     771    public Map<String, String> getMinimalTagMap() {
     772        Map<String, String> tagMap = new HashMap<>();
     773        for (TaggingPresetItem item : this.data) {
     774            if (item instanceof Key) {
     775                Key key = (Key) item;
     776                tagMap.put(key.key, key.value);
     777            } else if (item instanceof KeyedItem) {
     778                tagMap.put(((KeyedItem) item).key, null);
     779            }
     780        }
     781        return tagMap;
     782    }
     783
    738784    /**
    739785     * Action that adds or removes the button on main toolbar
    740786     */
  • src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetItem.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetItem.java b/src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetItem.java
    a b  
    1111import java.util.EnumSet;
    1212import java.util.List;
    1313import java.util.Map;
     14import java.util.RandomAccess;
    1415import java.util.Set;
     16import java.util.function.Consumer;
    1517
    1618import javax.swing.ImageIcon;
    1719import javax.swing.JPanel;
     
    6668     * @since 18221
    6769     */
    6870    protected List<AutoCompletionItem> getAllForKeys(List<String> keys) {
     71        return getAllForKeys(null, null, keys);
     72    }
     73
     74    /**
     75     * Returns all cached {@link AutoCompletionItem}s for given keys.
     76     *
     77     * @param consumer a consumer to be called when an AutoCompletionItem is selected. May be {@code null}.
     78     * @param currentPreset The preset we are getting keys for
     79     * @param keys retrieve the items for these keys
     80     * @return the currently cached items, sorted by priority and alphabet
     81     * @since xxx
     82     */
     83    protected List<AutoCompletionItem> getAllForKeys(Consumer<TaggingPreset> consumer, TaggingPreset currentPreset, List<String> keys) {
    6984        DataSet data = OsmDataManager.getInstance().getEditDataSet();
    7085        if (data == null) {
    7186            return Collections.emptyList();
    7287        }
    73         return AutoCompletionManager.of(data).getAllForKeys(keys);
     88        return AutoCompletionManager.of(data).getAllForKeys(consumer, currentPreset, keys);
    7489    }
    7590
    7691    /**
     
    8398     */
    8499    protected abstract boolean addToPanel(JPanel p, TaggingPresetItemGuiSupport support);
    85100
     101    /**
     102     * Called by {@link TaggingPreset#createPanel} during tagging preset panel creation.
     103     * All components defining this tagging preset item must be added to given panel.
     104     *
     105     * @param p The panel where components must be added
     106     * @param support supporting class for creating the GUI
     107     * @param preset the preset that this data item is being added to the panel for
     108     * @return {@code true} if this item adds semantic tagging elements, {@code false} otherwise.
     109     * @since xxx
     110     */
     111    protected boolean addToPanel(JPanel p, TaggingPresetItemGuiSupport support, TaggingPreset preset) {
     112        return addToPanel(p, support);
     113    }
     114
    86115    /**
    87116     * Adds the new tags to apply to selected OSM primitives when the preset holding this item is applied.
    88117     * @param changedTags The list of changed tags to modify if needed
     
    170199     */
    171200    public static boolean matches(Iterable<? extends TaggingPresetItem> data, Map<String, String> tags) {
    172201        boolean atLeastOnePositiveMatch = false;
    173         for (TaggingPresetItem item : data) {
    174             Boolean m = item.matches(tags);
    175             if (m != null && !m)
    176                 return false;
    177             else if (m != null) {
    178                 atLeastOnePositiveMatch = true;
     202        if (data instanceof List && data instanceof RandomAccess) {
     203            List<? extends TaggingPresetItem> dataList = (List<? extends TaggingPresetItem>) data;
     204            for (int i = 0; i < dataList.size(); i++) {
     205                Boolean m = dataList.get(i).matches(tags);
     206                if (m != null && !m)
     207                    return false;
     208                else if (m != null) {
     209                    atLeastOnePositiveMatch = true;
     210                }
     211            }
     212        } else {
     213            for (TaggingPresetItem item : data) {
     214                Boolean m = item.matches(tags);
     215                if (m != null && !m)
     216                    return false;
     217                else if (m != null) {
     218                    atLeastOnePositiveMatch = true;
     219                }
    179220            }
    180221        }
    181222        return atLeastOnePositiveMatch;
  • src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetItemGuiSupport.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetItemGuiSupport.java b/src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetItemGuiSupport.java
    a b  
    55import java.util.Arrays;
    66import java.util.Collection;
    77import java.util.Collections;
     8import java.util.HashMap;
     9import java.util.HashSet;
     10import java.util.List;
     11import java.util.Set;
    812import java.util.function.Supplier;
    913
     14import javax.swing.ComboBoxModel;
     15import javax.swing.JComboBox;
     16import javax.swing.JComponent;
     17import javax.swing.JList;
     18import javax.swing.JTextField;
     19import javax.swing.ListModel;
     20import javax.swing.ListSelectionModel;
     21
    1022import org.openstreetmap.josm.data.osm.OsmPrimitive;
    1123import org.openstreetmap.josm.data.osm.Tag;
    1224import org.openstreetmap.josm.data.osm.Tagged;
    1325import org.openstreetmap.josm.data.osm.search.SearchCompiler;
     26import org.openstreetmap.josm.data.tagging.ac.AutoCompletionItem;
     27import org.openstreetmap.josm.gui.tagging.presets.items.Key;
     28import org.openstreetmap.josm.gui.tagging.presets.items.PresetListEntry;
    1429import org.openstreetmap.josm.gui.widgets.OrientationAction;
     30import org.openstreetmap.josm.gui.widgets.QuadStateCheckBox;
     31import org.openstreetmap.josm.tools.JosmRuntimeException;
    1532import org.openstreetmap.josm.tools.ListenerList;
    1633import org.openstreetmap.josm.tools.Utils;
    1734import org.openstreetmap.josm.tools.template_engine.TemplateEngineDataProvider;
     
    2239 * @since 17609
    2340 */
    2441public final class TaggingPresetItemGuiSupport implements TemplateEngineDataProvider {
     42    private final HashMap<String, JComponent> keyComponentPrefillMap = new HashMap<>();
     43    private final HashMap<JComponent, Object> defaultMap = new HashMap<>();
    2544
    2645    private final Collection<OsmPrimitive> selected;
    2746    /** True if all selected primitives matched this preset at the moment it was openend. */
     
    181200        if (enabled)
    182201            listeners.fireEvent(e -> e.itemValueModified(source, key, newValue));
    183202    }
     203
     204    /**
     205     * Add a field to be used when updating/filling from a preset.
     206     * This should be called after the field has been set up with its default value (if any).
     207     * @param key The key that will be used to update the component
     208     * @param component The component to update
     209     */
     210    public void addField(String key, JComponent component) {
     211        this.keyComponentPrefillMap.put(key, component);
     212        if (component instanceof JTextField) {
     213            this.defaultMap.put(component, ((JTextField) component).getText());
     214        } else if (component instanceof QuadStateCheckBox) {
     215            this.defaultMap.put(component, ((QuadStateCheckBox) component).getState());
     216        } else if (component instanceof JList) {
     217            this.defaultMap.put(component, ((JList<?>) component).getSelectedValuesList());
     218        } else if (component instanceof JComboBox) {
     219            this.defaultMap.put(component, ((JComboBox<?>) component).getSelectedObjects());
     220        } else if (component != null) {
     221            throw new JosmRuntimeException(component.getClass() + " is not supported");
     222        }
     223    }
     224
     225    /**
     226     * Update the UI elements with those from another preset
     227     * @param presetItem The item to fill data in from
     228     * @since xxx
     229     */
     230    public void fillFromPreset(TaggingPreset presetItem) {
     231        // Clear current fields
     232        resetComponents();
     233        for (TaggingPresetItem item : presetItem.data) {
     234            if (item instanceof Key) {
     235                fillComponent((Key) item);
     236            }
     237        }
     238    }
     239
     240    private void resetComponents() {
     241        for (JComponent component : this.keyComponentPrefillMap.values()) {
     242            if (component instanceof JTextField) {
     243                ((JTextField) component).setText((String) this.defaultMap.get(component));
     244            } else if (component instanceof QuadStateCheckBox) {
     245                QuadStateCheckBox.State state = (QuadStateCheckBox.State) this.defaultMap.get(component);
     246                ((QuadStateCheckBox) component).setState(state != null ? state : QuadStateCheckBox.State.UNSET);
     247            } else if (component instanceof JComboBox) {
     248                ((JComboBox<?>) component).setSelectedItem(null); // TODO avoid losing original state
     249            } else if (component instanceof JList) {
     250                ((JList<?>) component).clearSelection(); // TODO avoid losing original state
     251            } else if (component != null) {
     252                throw new JosmRuntimeException(component.getClass() + " is not supported");
     253            }
     254        }
     255    }
     256
     257    /**
     258     * Update the data in a component
     259     * @param item The data to fill in
     260     */
     261    private void fillComponent(Key item) {
     262        JComponent component = this.keyComponentPrefillMap.get(item.key);
     263        if (component instanceof JTextField) {
     264            ((JTextField) component).setText(item.value);
     265        } else if (component instanceof JComboBox) {
     266            ComboBoxModel<?> comboBoxModel = ((JComboBox<?>) component).getModel();
     267            Object firstElement = comboBoxModel.getElementAt(0);
     268            if (firstElement instanceof String) {
     269                throw new JosmRuntimeException(comboBoxModel.getElementAt(0).getClass() + " is not supported");
     270            } else if (firstElement instanceof AutoCompletionItem) {
     271                throw new JosmRuntimeException(comboBoxModel.getElementAt(0).getClass() + " is not supported");
     272            } else if (firstElement instanceof PresetListEntry) {
     273                Set<String> values = new HashSet<>(splitOsmValue(item.value));
     274                for (int i = 0; i < comboBoxModel.getSize(); i++) {
     275                    PresetListEntry entry = (PresetListEntry) comboBoxModel.getElementAt(i);
     276                    if (values.contains(entry.value)) {
     277                        comboBoxModel.setSelectedItem(entry);
     278                        break;
     279                    }
     280                }
     281            } else {
     282                throw new JosmRuntimeException(comboBoxModel.getElementAt(0).getClass() + " is not supported");
     283            }
     284        } else if (component instanceof QuadStateCheckBox) {
     285            QuadStateCheckBox box = (QuadStateCheckBox) component;
     286            final boolean selected = Boolean.parseBoolean(item.value) || "yes".equals(item.value);
     287            box.setState(selected ? QuadStateCheckBox.State.SELECTED : QuadStateCheckBox.State.NOT_SELECTED);
     288        } else if (component instanceof JList) {
     289            ListModel<?> model = ((JList<?>) component).getModel();
     290            ListSelectionModel selectionModel = ((JList<?>) component).getSelectionModel();
     291            Set<String> values = new HashSet<>(splitOsmValue(item.value));
     292            for (int i = 0; i < model.getSize(); i++) {
     293                PresetListEntry entry = (PresetListEntry) model.getElementAt(i);
     294                if (values.contains(entry.value)) {
     295                    selectionModel.addSelectionInterval(i, i);
     296                }
     297            }
     298        } else if (component != null) {
     299            throw new JosmRuntimeException(component.getClass() + " is not supported");
     300        }
     301    }
     302
     303    /**
     304     * Split osm values using `;`
     305     * @param value the value to split
     306     * @return The list of values
     307     */
     308    private static List<String> splitOsmValue(String value) {
     309        return Arrays.asList(value.split(";", -1));
     310    }
    184311}
  • src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetReader.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetReader.java b/src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetReader.java
    a b  
    2323
    2424import javax.swing.JOptionPane;
    2525
     26import org.openstreetmap.josm.data.preferences.sources.ExtendedSourceEntry;
    2627import org.openstreetmap.josm.data.preferences.sources.PresetPrefHelper;
    2728import org.openstreetmap.josm.gui.MainApplication;
    2829import org.openstreetmap.josm.gui.tagging.presets.items.Check;
     
    135136
    136137    private static XmlObjectParser buildParser() {
    137138        XmlObjectParser parser = new XmlObjectParser();
     139        parser.mapOnStart("presets", ExtendedSourceEntry.class);
    138140        parser.mapOnStart("item", TaggingPreset.class);
    139141        parser.mapOnStart("separator", TaggingPresetSeparator.class);
    140142        parser.mapBoth("group", TaggingPresetMenu.class);
     
    180182    static Collection<TaggingPreset> readAll(Reader in, boolean validate, HashSetWithLast<TaggingPreset> all) throws SAXException {
    181183        XmlObjectParser parser = buildParser();
    182184
     185        /** to be used to help link presets */
     186        ExtendedSourceEntry entry = null;
    183187        /** to detect end of {@code <checkgroup>} */
    184188        CheckGroup lastcheckgroup = null;
    185189        /** to detect end of {@code <group>} */
     
    247251                }
    248252                continue;
    249253            }
     254            if (o instanceof ExtendedSourceEntry) {
     255                if (entry != null) {
     256                    throw new SAXException(tr("Preset has multiple <preset> elements"));
     257                }
     258                entry = (ExtendedSourceEntry) o;
     259                continue;
     260            }
    250261            if (!(o instanceof TaggingPresetItem) && !checks.isEmpty()) {
    251262                all.getLast().data.addAll(checks);
    252263                checks.clear();
     
    338349            all.getLast().data.addAll(checks);
    339350            checks.clear();
    340351        }
     352        if (entry != null) {
     353            for (TaggingPreset preset : all) {
     354                preset.preset_list_name = entry.name;
     355            }
     356        }
    341357        return all;
    342358    }
    343359
     
    364380     */
    365381    static Collection<TaggingPreset> readAll(String source, boolean validate, HashSetWithLast<TaggingPreset> all)
    366382            throws SAXException, IOException {
    367         Collection<TaggingPreset> tp;
     383        Collection<TaggingPreset> tp = all;
    368384        Logging.debug("Reading presets from {0}", source);
    369385        Stopwatch stopwatch = Stopwatch.createStarted();
    370386        try (
     
    377393                I18n.addTexts(zipIcons);
    378394            }
    379395            try (InputStreamReader r = UTFInputStreamReader.create(zip == null ? cf.getInputStream() : zip)) {
    380                 tp = readAll(new BufferedReader(r), validate, all);
     396                tp.addAll(readAll(new BufferedReader(r), validate, new HashSetWithLast<>()));
    381397            }
    382398        }
    383399        Logging.debug(stopwatch.toString("Reading presets"));
  • src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresets.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresets.java b/src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresets.java
    a b  
    44import static org.openstreetmap.josm.tools.I18n.tr;
    55
    66import java.util.ArrayList;
     7import java.util.Arrays;
    78import java.util.Collection;
    89import java.util.Collections;
    910import java.util.HashMap;
    1011import java.util.HashSet;
     12import java.util.List;
    1113import java.util.Map;
     14import java.util.Objects;
    1215import java.util.Set;
     16import java.util.concurrent.ExecutionException;
     17import java.util.regex.Pattern;
     18import java.util.stream.Collectors;
    1319
    1420import javax.swing.JMenu;
    1521import javax.swing.JMenuItem;
     22import javax.swing.JOptionPane;
    1623import javax.swing.JSeparator;
     24import javax.swing.SwingWorker;
    1725
    1826import org.openstreetmap.josm.actions.PreferencesAction;
    1927import org.openstreetmap.josm.data.osm.IPrimitive;
     
    2331import org.openstreetmap.josm.gui.MainApplication;
    2432import org.openstreetmap.josm.gui.MainMenu;
    2533import org.openstreetmap.josm.gui.MenuScroller;
     34import org.openstreetmap.josm.gui.Notification;
    2635import org.openstreetmap.josm.gui.preferences.ToolbarPreferences;
    2736import org.openstreetmap.josm.gui.preferences.map.TaggingPresetPreference;
    2837import org.openstreetmap.josm.gui.tagging.presets.items.CheckGroup;
     38import org.openstreetmap.josm.gui.tagging.presets.items.Key;
    2939import org.openstreetmap.josm.gui.tagging.presets.items.KeyedItem;
     40import org.openstreetmap.josm.gui.tagging.presets.items.PresetLink;
    3041import org.openstreetmap.josm.gui.tagging.presets.items.Roles;
    3142import org.openstreetmap.josm.gui.tagging.presets.items.Roles.Role;
     43import org.openstreetmap.josm.spi.preferences.Config;
     44import org.openstreetmap.josm.tools.JosmRuntimeException;
    3245import org.openstreetmap.josm.tools.Logging;
    3346import org.openstreetmap.josm.tools.MultiMap;
    3447import org.openstreetmap.josm.tools.SubclassFilteredCollection;
     
    3952 */
    4053public final class TaggingPresets {
    4154
     55    /** Patterns for primary keys */
     56    private static final List<Pattern> PRIMARY_KEY_PATTERNS = Config.getPref().getList("tagging.presets.primary.key.patterns",
     57            Arrays.asList("^name(:.*)?$", "^brand$")).stream().map(Pattern::compile).collect(Collectors.toList());
     58
    4259    /** The collection of tagging presets */
    4360    private static final Collection<TaggingPreset> taggingPresets = new ArrayList<>();
    4461
     
    5875     */
    5976    public static final ListProperty ICON_SOURCES = new ListProperty("taggingpreset.icon.sources", null);
    6077    private static final IntegerProperty MIN_ELEMENTS_FOR_SCROLLER = new IntegerProperty("taggingpreset.min-elements-for-scroller", 15);
     78    /**
     79     * A runnable that builds links for autocomplete usage. This can take a few minutes, so run in a background thread.
     80     * It doesn't matter if it is done sequentially, so we can use the {@link SwingWorker} executor.
     81     */
     82    private static PresetLinkBuilder presetLinkBuilder;
     83
     84    private static class PresetLinkBuilder extends SwingWorker<Void, Void> {
     85
     86        @Override
     87        protected Void doInBackground() {
     88            // This defaults to false since it significantly slows down startup.
     89            if (Config.getPref().getBoolean("expert.tagging.preset.children.from.heuristics", true)) {
     90                // If we want to try and guess what preset something goes to. Only accounts for presets that ask no questions.
     91                getPresetChildrenFromTags();
     92            }
     93            if (isCancelled()) {
     94                return null;
     95            }
     96            getPresetChildrenFromLinks();
     97            return null;
     98        }
     99
     100        @Override
     101        protected void done() {
     102            super.done();
     103            resetPresetLinkBuilder(this);
     104            if (MainApplication.isDisplayingMapView() && !isCancelled()) {
     105                new Notification(tr("Preset linking done. Autofill is now available")).setIcon(JOptionPane.INFORMATION_MESSAGE).show();
     106            }
     107            try {
     108                get();
     109            } catch (ExecutionException e) {
     110                throw new JosmRuntimeException(e);
     111            } catch (InterruptedException e) {
     112                Logging.trace(e);
     113                Thread.currentThread().interrupt();
     114            }
     115        }
     116
     117        private static void resetPresetLinkBuilder(PresetLinkBuilder current) {
     118            if (presetLinkBuilder == current) {
     119                presetLinkBuilder = null;
     120            }
     121        }
     122    }
    61123
    62124    private TaggingPresets() {
    63125        // Hide constructor for utility classes
     
    67129     * Initializes tagging presets from preferences.
    68130     */
    69131    public static void readFromPreferences() {
     132        if (presetLinkBuilder != null) {
     133            presetLinkBuilder.cancel(true);
     134        }
    70135        taggingPresets.clear();
    71136        taggingPresets.addAll(TaggingPresetReader.readFromPreferences(false, false));
    72137        cachePresets(taggingPresets);
     138        presetLinkBuilder = new PresetLinkBuilder();
     139        presetLinkBuilder.execute();
     140    }
     141
     142    /**
     143     * Generate preset children from the tags. Example: `amenity=fast_food` + `name=McDonald's` will have a parent with `amenity=fast_food`.
     144     * This adds a {@link PresetLink} that has {@link PresetLink#parent} set to {@code true}.
     145     * This only generates where all of the {@link TaggingPreset#data} objects are {@link Key} objects.
     146     */
     147    private static void getPresetChildrenFromTags() {
     148        // Used to avoid multiple instantiations of an array list
     149        List<String> parentList = new ArrayList<>();
     150        for (TaggingPreset preset : taggingPresets) {
     151            if (preset.data.stream().allMatch(Key.class::isInstance)) {
     152                // OK. Let us find the parent.
     153                Collection<TaggingPreset> matching = TaggingPresets.getMatchingPresets(null, preset.getMinimalTagMap(), false);
     154                List<TaggingPreset> other = matching.stream().filter(p -> !p.equals(preset) && !p.data.equals(preset.data))
     155                        .collect(Collectors.toList());
     156                if (other.size() == 1) {
     157                    TaggingPreset parent = other.iterator().next();
     158                    parentList.clear();
     159                    String presetListName = parent.preset_list_name;
     160                    while (parent != null && parent.name != null) {
     161                        parentList.add(parent.name);
     162                        parent = parent.group;
     163                    }
     164                    // Add this last in order to have it in the "right" position when the list is reversed
     165                    parentList.add(presetListName);
     166                    Collections.reverse(parentList);
     167                    PresetLink presetLink = new PresetLink();
     168                    presetLink.preset_name = parentList.stream()
     169                            .filter(Objects::nonNull).collect(Collectors.joining("/", "josm-preset://", ""));
     170                    presetLink.parent = presetLink.preset_name.length() > "josm-preset://".length();
     171                    if (presetLink.parent) {
     172                        preset.data.add(presetLink);
     173                    }
     174                }
     175            }
     176        }
     177    }
     178
     179    /**
     180     * Tell the parent preset about their children for autocomplete
     181     */
     182    private static void getPresetChildrenFromLinks() {
     183        for (TaggingPreset preset : taggingPresets) {
     184            for (TaggingPresetItem item : preset.data) {
     185                if (item instanceof PresetLink) {
     186                    ((PresetLink) item).reverseLinkPreset(preset);
     187                }
     188            }
     189        }
    73190    }
    74191
    75192    /**
     
    228345        return PRESET_TAG_CACHE.get(key) != null;
    229346    }
    230347
     348    /**
     349     * Check if a key is a primary (important) key
     350     * @param key The key to check
     351     * @return {@code true} if it can be used to prefill other data
     352     * @since xxx
     353     */
     354    public static boolean isPrimaryKey(String key) {
     355        for (Pattern pattern : PRIMARY_KEY_PATTERNS) {
     356            if (pattern.matcher(key).matches()) {
     357                return true;
     358            }
     359        }
     360        return false;
     361    }
     362
    231363    /**
    232364     * Replies a new collection of all presets matching the parameters.
    233365     *
    234      * @param t the preset types to include
     366     * @param t the preset types to include. {@code null} means any preset type is ok.
    235367     * @param tags the tags to perform matching on, see {@link TaggingPresetItem#matches(Map)}
    236368     * @param onlyShowable whether only {@link TaggingPreset#isShowable() showable} presets should be returned
    237369     * @return a new collection of all presets matching the parameters.