Index: resources/images/in_dataset.svg
===================================================================
--- resources/images/in_dataset.svg	(nonexistent)
+++ resources/images/in_dataset.svg	(working copy)
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   version="1.1"
+   width="16px"
+   height="16px"
+   id="svg79288"
+   sodipodi:docname="in_dataset.svg"
+   inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <defs
+     id="defs79292" />
+  <sodipodi:namedview
+     id="namedview79290"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     showgrid="true"
+     inkscape:zoom="89.875"
+     inkscape:cx="3.3769124"
+     inkscape:cy="8.0055633"
+     inkscape:window-width="3840"
+     inkscape:window-height="2085"
+     inkscape:window-x="0"
+     inkscape:window-y="0"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="svg79288">
+    <inkscape:grid
+       type="xygrid"
+       id="grid79357"
+       empspacing="8"
+       dotted="false"
+       originx="8"
+       originy="8"
+       spacingx="0.5"
+       spacingy="0.5" />
+  </sodipodi:namedview>
+  <path
+     style="fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+     d="M 3,3 13,6 5,13 Z"
+     id="path80027"
+     sodipodi:nodetypes="cccc" />
+  <path
+     style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#df421e;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+     d="m 11.5,4.5 h 3 v 3 h -3 z"
+     id="path79392" />
+  <path
+     style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#df421e;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+     d="m 1.5,1.5 h 3 v 3 h -3 z"
+     id="path79392-7" />
+  <path
+     style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#df421e;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+     d="m 3.5,11.5 h 3 v 3 h -3 z"
+     id="path79392-4" />
+</svg>
Index: resources/images/in_standard.svg
===================================================================
--- resources/images/in_standard.svg	(nonexistent)
+++ resources/images/in_standard.svg	(working copy)
@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
+   sodipodi:docname="in_standard.svg"
+   id="svg12"
+   version="1.1"
+   width="16"
+   viewBox="0 0 16 16"
+   height="16"
+   enable-background="new 0 0 96 96"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:dc="http://purl.org/dc/elements/1.1/">
+  <metadata
+     id="metadata18">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <defs
+     id="defs16" />
+  <sodipodi:namedview
+     inkscape:current-layer="svg12"
+     inkscape:window-maximized="1"
+     inkscape:window-y="0"
+     inkscape:window-x="0"
+     inkscape:cy="9.5865506"
+     inkscape:cx="7.8530512"
+     inkscape:zoom="66.916666"
+     showgrid="true"
+     id="namedview14"
+     inkscape:window-height="2085"
+     inkscape:window-width="3840"
+     inkscape:pageshadow="2"
+     inkscape:pageopacity="0"
+     guidetolerance="10"
+     gridtolerance="10"
+     objecttolerance="10"
+     borderopacity="1"
+     bordercolor="#666666"
+     pagecolor="#ffffff"
+     inkscape:pagecheckerboard="0"
+     inkscape:lockguides="false">
+    <inkscape:grid
+       id="grid843"
+       type="xygrid"
+       originx="0"
+       originy="0"
+       empspacing="4" />
+  </sodipodi:namedview>
+  <text
+     xml:space="preserve"
+     style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:16px;line-height:125%;font-family:FreeSans;-inkscape-font-specification:FreeSans;text-align:end;letter-spacing:0px;word-spacing:0px;text-anchor:end;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+     x="11.570769"
+     y="11.741786"
+     id="text12471"><tspan
+       sodipodi:role="line"
+       id="tspan12469"
+       x="11.570769"
+       y="11.741786"></tspan></text>
+  <text
+     xml:space="preserve"
+     style="font-size:13.3333px;line-height:125%;font-family:FreeSans;-inkscape-font-specification:FreeSans;text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#df421e;fill-opacity:1;stroke:#df421e;stroke-width:0.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+     x="8.0417976"
+     y="11.405445"
+     id="text59954"><tspan
+       sodipodi:role="line"
+       id="tspan59952"
+       x="8.0417976"
+       y="11.405445">§</tspan></text>
+</svg>
Index: scripts/TagInfoExtract.java
===================================================================
--- scripts/TagInfoExtract.java	(revision 18366)
+++ scripts/TagInfoExtract.java	(working copy)
@@ -28,7 +28,6 @@
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
-import java.util.stream.Stream;
 
 import javax.imageio.ImageIO;
 import javax.json.Json;
@@ -68,11 +67,11 @@
 import org.openstreetmap.josm.gui.mappaint.styleelement.LineElement;
 import org.openstreetmap.josm.gui.mappaint.styleelement.StyleElement;
 import org.openstreetmap.josm.gui.preferences.map.TaggingPresetPreference;
+import org.openstreetmap.josm.gui.tagging.presets.KeyedItem;
 import org.openstreetmap.josm.gui.tagging.presets.TaggingPreset;
 import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetReader;
 import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetType;
-import org.openstreetmap.josm.gui.tagging.presets.items.CheckGroup;
-import org.openstreetmap.josm.gui.tagging.presets.items.KeyedItem;
+import org.openstreetmap.josm.gui.tagging.presets.TaggingPresets;
 import org.openstreetmap.josm.io.CachedFile;
 import org.openstreetmap.josm.io.OsmTransferException;
 import org.openstreetmap.josm.spi.preferences.Config;
@@ -166,7 +165,7 @@
         Path baseDir = Paths.get("");
         Path imageDir = Paths.get("taginfo-img");
         String imageUrlPrefix;
-        CachedFile inputFile;
+        String inputUrl;
         Path outputFile;
         boolean noexit;
 
@@ -174,18 +173,18 @@
             mode = Mode.valueOf(value.toUpperCase(Locale.ENGLISH));
             switch (mode) {
                 case MAPPAINT:
-                    inputFile = new CachedFile("resource://styles/standard/elemstyles.mapcss");
+                    inputUrl = "resource://styles/standard/elemstyles.mapcss";
                     break;
                 case PRESETS:
-                    inputFile = new CachedFile("resource://data/defaultpresets.xml");
+                    inputUrl = "resource://data/defaultpresets.xml";
                     break;
                 default:
-                    inputFile = null;
+                    inputUrl = null;
             }
         }
 
         void setInputFile(String value) {
-            inputFile = new CachedFile(value);
+            inputUrl = value;
         }
 
         void setOutputFile(String value) {
@@ -254,12 +253,11 @@
 
         @Override
         void run() throws IOException, OsmTransferException, SAXException {
-            try (BufferedReader reader = options.inputFile.getContentReader()) {
-                Collection<TaggingPreset> presets = TaggingPresetReader.readAll(reader, true);
-                List<TagInfoTag> tags = convertPresets(presets, "", true);
-                Logging.info("Converting {0} internal presets", tags.size());
-                writeJson("JOSM main presets", "Tags supported by the default presets in the OSM editor JOSM", tags);
-            }
+            TaggingPresets.testInitialize(options.inputUrl);
+            Collection<TaggingPreset> presets = TaggingPresets.getTaggingPresets();
+            List<TagInfoTag> tags = convertPresets(presets, "", true);
+            Logging.info("Converting {0} internal presets", tags.size());
+            writeJson("JOSM main presets", "Tags supported by the default presets in the OSM editor JOSM", tags);
         }
 
         List<TagInfoTag> convertPresets(Iterable<TaggingPreset> presets, String descriptionPrefix, boolean addImages) {
@@ -267,25 +265,23 @@
             final Map<Tag, TagInfoTag> requiredTags = new LinkedHashMap<>();
             final Map<Tag, TagInfoTag> optionalTags = new LinkedHashMap<>();
             for (TaggingPreset preset : presets) {
-                preset.data.stream()
-                        .flatMap(item -> item instanceof KeyedItem
-                                ? Stream.of(((KeyedItem) item))
-                                : item instanceof CheckGroup
-                                ? ((CheckGroup) item).checks.stream()
-                                : Stream.empty())
-                        .forEach(item -> {
+                preset.getAllItems()
+                    .forEach(i -> {
+                        if (i instanceof KeyedItem) {
+                            KeyedItem item = (KeyedItem) i;
                             for (String value : values(item)) {
-                                Set<TagInfoTag.Type> types = TagInfoTag.Type.forPresetTypes(preset.types);
+                                Set<TagInfoTag.Type> types = TagInfoTag.Type.forPresetTypes(preset.getTypes());
                                 if (item.isKeyRequired()) {
                                     fillTagsMap(requiredTags, item, value, preset.getName(), types,
                                             descriptionPrefix + TagInfoTag.REQUIRED_FOR_COUNT + ": ",
-                                            addImages && preset.iconName != null ? options.findImageUrl(preset.iconName) : null);
+                                            addImages && preset.getIconName() != null ? options.findImageUrl(preset.getIconName()) : null);
                                 } else {
                                     fillTagsMap(optionalTags, item, value, preset.getName(), types,
                                             descriptionPrefix + TagInfoTag.OPTIONAL_FOR_COUNT + ": ", null);
                                 }
                             }
-                        });
+                        }
+                    });
             }
             tags.addAll(requiredTags.values());
             tags.addAll(optionalTags.values());
@@ -294,9 +290,9 @@
 
         private void fillTagsMap(Map<Tag, TagInfoTag> optionalTags, KeyedItem item, String value,
                 String presetName, Set<TagInfoTag.Type> types, String descriptionPrefix, String iconUrl) {
-            optionalTags.compute(new Tag(item.key, value), (osmTag, tagInfoTag) -> {
+            optionalTags.compute(new Tag(item.getKey(), value), (osmTag, tagInfoTag) -> {
                 if (tagInfoTag == null) {
-                    return new TagInfoTag(descriptionPrefix + presetName, item.key, value, types, iconUrl);
+                    return new TagInfoTag(descriptionPrefix + presetName, item.getKey(), value, types, iconUrl);
                 } else {
                     tagInfoTag.descriptions.add(presetName);
                     tagInfoTag.objectTypes.addAll(types);
@@ -325,7 +321,8 @@
                 }
                 try {
                     Logging.info("Loading {0}", source.url);
-                    Collection<TaggingPreset> presets = TaggingPresetReader.readAll(source.url, false);
+                    TaggingPresets.testInitialize(source.url);
+                    Collection<TaggingPreset> presets = TaggingPresets.getTaggingPresets();
                     final List<TagInfoTag> t = convertPresets(presets, source.title + " ", false);
                     Logging.info("Converted {0} presets of {1} to {2} tags", presets.size(), source.title, t.size());
                     tags.addAll(t);
@@ -355,7 +352,7 @@
          * @throws ParseException in case of parsing error
          */
         private void parseStyleSheet() throws IOException, ParseException {
-            try (BufferedReader reader = options.inputFile.getContentReader()) {
+            try (BufferedReader reader = new CachedFile(options.inputUrl).getContentReader()) {
                 MapCSSParser parser = new MapCSSParser(reader, MapCSSParser.LexicalState.DEFAULT);
                 styleSource = new MapCSSStyleSource("");
                 styleSource.url = "";
Index: src/org/openstreetmap/josm/actions/UploadAction.java
===================================================================
--- src/org/openstreetmap/josm/actions/UploadAction.java	(revision 18366)
+++ src/org/openstreetmap/josm/actions/UploadAction.java	(working copy)
@@ -240,8 +240,7 @@
         ChangesetUpdater.check();
 
         final UploadDialog dialog = UploadDialog.getUploadDialog();
-        dialog.setUploadedPrimitives(apiData);
-        dialog.initLifeCycle(layer.getDataSet());
+        dialog.initLifeCycle(layer.getDataSet(), apiData);
         dialog.setVisible(true);
         dialog.rememberUserInput();
         if (dialog.isCanceled()) {
Index: src/org/openstreetmap/josm/data/osm/DefaultNameFormatter.java
===================================================================
--- src/org/openstreetmap/josm/data/osm/DefaultNameFormatter.java	(revision 18366)
+++ src/org/openstreetmap/josm/data/osm/DefaultNameFormatter.java	(working copy)
@@ -184,7 +184,7 @@
                 }
                 name.append(n);
             } else {
-                preset.nameTemplate.appendText(name, (TemplateEngineDataProvider) node);
+                preset.getNameTemplate().appendText(name, (TemplateEngineDataProvider) node);
             }
             if (node.isLatLonKnown() && Config.getPref().getBoolean("osm-primitives.showcoor")) {
                 name.append(" \u200E(");
@@ -252,7 +252,7 @@
 
                 name.append(n);
             } else {
-                preset.nameTemplate.appendText(name, (TemplateEngineDataProvider) way);
+                preset.getNameTemplate().appendText(name, (TemplateEngineDataProvider) way);
             }
 
             int nodesNo = way.getRealNodesCount();
@@ -355,7 +355,7 @@
             }
             result.append(" (").append(relationName).append(", ");
         } else {
-            preset.nameTemplate.appendText(result, (TemplateEngineDataProvider) relation);
+            preset.getNameTemplate().appendText(result, (TemplateEngineDataProvider) relation);
             result.append('(');
         }
         return result;
Index: src/org/openstreetmap/josm/data/osm/search/SearchCompiler.java
===================================================================
--- src/org/openstreetmap/josm/data/osm/search/SearchCompiler.java	(revision 18366)
+++ src/org/openstreetmap/josm/data/osm/search/SearchCompiler.java	(working copy)
@@ -45,8 +45,6 @@
 import org.openstreetmap.josm.gui.mappaint.mapcss.parsergen.MapCSSParser;
 import org.openstreetmap.josm.gui.mappaint.mapcss.parsergen.ParseException;
 import org.openstreetmap.josm.gui.tagging.presets.TaggingPreset;
-import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetMenu;
-import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetSeparator;
 import org.openstreetmap.josm.gui.tagging.presets.TaggingPresets;
 import org.openstreetmap.josm.tools.AlphanumComparator;
 import org.openstreetmap.josm.tools.CheckParameterUtil;
@@ -1911,7 +1909,7 @@
 
             this.presets = TaggingPresets.getTaggingPresets()
                     .stream()
-                    .filter(preset -> !(preset instanceof TaggingPresetMenu || preset instanceof TaggingPresetSeparator))
+                    .filter(preset -> preset.getClass() == TaggingPreset.class)
                     .filter(preset -> presetNameMatch(presetName, preset, matchStrictly))
                     .collect(Collectors.toList());
 
@@ -1929,16 +1927,7 @@
             if (matchStrictly) {
                 return name.equalsIgnoreCase(preset.getRawName());
             }
-
-            try {
-                String groupSuffix = name.substring(0, name.length() - 2); // try to remove '/*'
-                TaggingPresetMenu group = preset.group;
-
-                return group != null && groupSuffix.equalsIgnoreCase(group.getRawName());
-            } catch (StringIndexOutOfBoundsException ex) {
-                Logging.trace(ex);
-                return false;
-            }
+            return preset.nameMatchesGlob(name);
         }
 
         @Override
Index: src/org/openstreetmap/josm/data/tagging/ac/AutoCompItemCellRenderer.java
===================================================================
--- src/org/openstreetmap/josm/data/tagging/ac/AutoCompItemCellRenderer.java	(nonexistent)
+++ src/org/openstreetmap/josm/data/tagging/ac/AutoCompItemCellRenderer.java	(working copy)
@@ -0,0 +1,79 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.tagging.ac;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.Font;
+import java.util.Map;
+
+import javax.swing.ImageIcon;
+import javax.swing.JLabel;
+import javax.swing.JList;
+import javax.swing.ListCellRenderer;
+
+import org.openstreetmap.josm.gui.widgets.JosmListCellRenderer;
+import org.openstreetmap.josm.tools.ImageProvider;
+import org.openstreetmap.josm.tools.ImageProvider.ImageSizes;
+
+/**
+ * A custom list cell renderer for autocompletion items that colorizes and adds the value count to
+ * some items.
+ * <p>
+ * See also: {@link AutoCompletionPriority#compareTo}
+ */
+public class AutoCompItemCellRenderer extends JosmListCellRenderer<AutoCompletionItem> {
+    /** The color used to render items found in the dataset. */
+    public static final Color BGCOLOR_1 = new Color(254, 226, 214);
+    /** The color used to render items found in the standard */
+    public static final Color BGCOLOR_2 = new Color(235, 255, 177);
+
+    protected Map<String, Integer> map;
+    private static final ImageIcon iconEmpty = ImageProvider.getEmpty(ImageSizes.POPUPMENU);
+    private static final ImageIcon iconDataSet = ImageProvider.get("in_dataset", ImageSizes.POPUPMENU);
+    private static final ImageIcon iconStandard = ImageProvider.get("in_standard", ImageSizes.POPUPMENU);
+
+    /**
+     * Constructs the cell renderer.
+     *
+     * @param component The component the renderer is attached to. JComboBox or JList.
+     * @param renderer The L&amp;F renderer. Usually obtained by calling {@code getRenderer()} on {@code component}.
+     * @param map A map from key to count.
+     */
+    public AutoCompItemCellRenderer(Component component, ListCellRenderer<? super AutoCompletionItem> renderer, Map<String, Integer> map) {
+        super(component, renderer);
+        this.map = map;
+    }
+
+    @Override
+    public Component getListCellRendererComponent(JList<? extends AutoCompletionItem> list, AutoCompletionItem value,
+                                                int index, boolean isSelected, boolean cellHasFocus) {
+        Integer count = null;
+        if (value == null)
+            value = new AutoCompletionItem("", AutoCompletionPriority.IS_IN_STANDARD);
+
+        // if there is a value count add it to the text
+        if (map != null) {
+            String text = value.toString();
+            count = map.get(text);
+            if (count != null) {
+                value = new AutoCompletionItem(tr("{0} ({1})", text, count), value.getPriority());
+            }
+        }
+
+        JLabel l = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
+        l.setIcon(iconEmpty);
+        if (value.getPriority().isInDataSet()) {
+            l.setIcon(iconDataSet);
+        }
+        if (value.getPriority().isInStandard()) {
+            l.setIcon(iconStandard);
+        }
+        l.setComponentOrientation(component.getComponentOrientation());
+        if (count != null) {
+            l.setFont(l.getFont().deriveFont(Font.ITALIC + Font.BOLD));
+        }
+        return l;
+    }
+}
Index: src/org/openstreetmap/josm/data/validation/tests/MapCSSTagChecker.java
===================================================================
--- src/org/openstreetmap/josm/data/validation/tests/MapCSSTagChecker.java	(revision 18366)
+++ src/org/openstreetmap/josm/data/validation/tests/MapCSSTagChecker.java	(working copy)
@@ -41,6 +41,7 @@
 import org.openstreetmap.josm.gui.mappaint.mapcss.parsergen.ParseException;
 import org.openstreetmap.josm.gui.mappaint.mapcss.parsergen.TokenMgrError;
 import org.openstreetmap.josm.gui.progress.ProgressMonitor;
+import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetHandler;
 import org.openstreetmap.josm.io.CachedFile;
 import org.openstreetmap.josm.io.FileWatcher;
 import org.openstreetmap.josm.io.UTFInputStreamReader;
@@ -150,6 +151,20 @@
     }
 
     /**
+     * Obtains all {@link TestError}s for all primitives in the data handler.
+     * @param handler the handler that holds the primitives to check
+     * @param includeOtherSeverity if {@code true}, errors of severity {@link Severity#OTHER} (info) will also be returned
+     * @return all errors, with or without those of "info" severity
+     */
+    public synchronized Collection<TestError> checkTaggingPresetHandler(TaggingPresetHandler handler, boolean includeOtherSeverity) {
+        Collection<TestError> errors = new ArrayList<>();
+        for (OsmPrimitive primitive : handler.getPrimitives()) {
+            errors.addAll(getErrorsForPrimitive(primitive, includeOtherSeverity));
+        }
+        return errors;
+    }
+
+    /**
      * Obtains all {@link TestError}s for the {@link OsmPrimitive} {@code p}.
      * @param p The OSM primitive
      * @param includeOtherSeverity if {@code true}, errors of severity {@link Severity#OTHER} (info) will also be returned
Index: src/org/openstreetmap/josm/data/validation/tests/OpeningHourTest.java
===================================================================
--- src/org/openstreetmap/josm/data/validation/tests/OpeningHourTest.java	(revision 18366)
+++ src/org/openstreetmap/josm/data/validation/tests/OpeningHourTest.java	(working copy)
@@ -21,6 +21,7 @@
 import org.openstreetmap.josm.data.preferences.sources.ValidatorPrefHelper;
 import org.openstreetmap.josm.data.validation.Severity;
 import org.openstreetmap.josm.data.validation.Test.TagTest;
+import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetHandler;
 import org.openstreetmap.josm.data.validation.TestError;
 import org.openstreetmap.josm.tools.GBC;
 import org.openstreetmap.josm.tools.Utils;
@@ -148,6 +149,25 @@
         }
     }
 
+    /**
+     * Checks the tags of the given primitive and adds validation errors to the given list.
+     * @param handler the handler that holds the data to check
+     * @param errors The list to add validation errors to
+     * @since 17643
+     */
+    public void checkTaggingPresetHandler(TaggingPresetHandler handler, Collection<TestError> errors) {
+        for (OsmPrimitive primitive : handler.getPrimitives()) {
+            for (String key : KEYS_TO_CHECK) {
+                errors.addAll(checkOpeningHourSyntax(key, primitive.get(key), primitive, Locale.getDefault()));
+            }
+            // COVID-19, a few additional values are permitted, see #19048, see https://wiki.openstreetmap.org/wiki/Key:opening_hours:covid19
+            final String keyCovid19 = "opening_hours:covid19";
+            if (primitive.hasTag(keyCovid19) && !primitive.hasTag(keyCovid19, "same", "restricted", "open", "off")) {
+                errors.addAll(checkOpeningHourSyntax(keyCovid19, primitive.get(keyCovid19), primitive, Locale.getDefault()));
+            }
+        }
+    }
+
     @Override
     public void addGui(JPanel testPanel) {
         super.addGui(testPanel);
Index: src/org/openstreetmap/josm/data/validation/tests/RelationChecker.java
===================================================================
--- src/org/openstreetmap/josm/data/validation/tests/RelationChecker.java	(revision 18366)
+++ src/org/openstreetmap/josm/data/validation/tests/RelationChecker.java	(working copy)
@@ -30,15 +30,14 @@
 import org.openstreetmap.josm.data.validation.Test;
 import org.openstreetmap.josm.data.validation.TestError;
 import org.openstreetmap.josm.gui.progress.ProgressMonitor;
+import org.openstreetmap.josm.gui.tagging.presets.Item;
+import org.openstreetmap.josm.gui.tagging.presets.KeyedItem;
+import org.openstreetmap.josm.gui.tagging.presets.Role;
 import org.openstreetmap.josm.gui.tagging.presets.TaggingPreset;
-import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItem;
 import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetListener;
 import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetType;
+import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetUtils;
 import org.openstreetmap.josm.gui.tagging.presets.TaggingPresets;
-import org.openstreetmap.josm.gui.tagging.presets.items.KeyedItem;
-import org.openstreetmap.josm.gui.tagging.presets.items.Roles;
-import org.openstreetmap.josm.gui.tagging.presets.items.Roles.Role;
-import org.openstreetmap.josm.tools.SubclassFilteredCollection;
 import org.openstreetmap.josm.tools.Utils;
 
 /**
@@ -106,7 +105,7 @@
             return;
         }
         for (TaggingPreset p : TaggingPresets.getTaggingPresets()) {
-            if (p.data.stream().anyMatch(i -> i instanceof Roles)) {
+            if (p.getAllItems().stream().anyMatch(i -> i instanceof Role)) {
                 relationpresets.add(p);
             }
         }
@@ -175,16 +174,21 @@
         return map;
     }
 
-    // return Roles grouped by key
-    private static Map<Role, String> buildAllRoles(Relation n) {
+    /**
+     * Returns all roles with preset name
+     *
+     * @param rel the relation
+     * @return all roles in all matching presets
+     */
+    private static Map<Role, String> buildAllRoles(Relation rel) {
         Map<Role, String> allroles = new LinkedHashMap<>();
 
         for (TaggingPreset p : relationpresets) {
-            final boolean matches = TaggingPresetItem.matches(Utils.filteredCollection(p.data, KeyedItem.class), n.getKeys());
-            final SubclassFilteredCollection<TaggingPresetItem, Roles> roles = Utils.filteredCollection(p.data, Roles.class);
-            if (matches && !roles.isEmpty()) {
-                for (Role role: roles.iterator().next().roles) {
-                    allroles.put(role, p.name);
+            List<Item> items = p.getAllItems();
+            final boolean matches = TaggingPresetUtils.matches(Utils.filteredCollection(items, KeyedItem.class), rel.getKeys());
+            if (matches) {
+                for (Role role: p.getAllRoles()) {
+                    allroles.put(role, p.getBaseName());
                 }
             }
         }
@@ -191,28 +195,6 @@
         return allroles;
     }
 
-    private static boolean checkMemberType(Role r, RelationMember member) {
-        if (r.types != null) {
-            switch (member.getDisplayType()) {
-            case NODE:
-                return r.types.contains(TaggingPresetType.NODE);
-            case CLOSEDWAY:
-                return r.types.contains(TaggingPresetType.CLOSEDWAY);
-            case WAY:
-                return r.types.contains(TaggingPresetType.WAY);
-            case MULTIPOLYGON:
-                return r.types.contains(TaggingPresetType.MULTIPOLYGON);
-            case RELATION:
-                return r.types.contains(TaggingPresetType.RELATION);
-            default: // not matching type
-                return false;
-            }
-        } else {
-            // if no types specified, then test is passed
-            return true;
-        }
-    }
-
     /**
      * get all role definition for specified key and check, if some definition matches
      *
@@ -226,7 +208,7 @@
         String role = member.getRole();
         String name = null;
         // Set of all accepted types in preset
-        Collection<TaggingPresetType> types = EnumSet.noneOf(TaggingPresetType.class);
+        EnumSet<TaggingPresetType> types = EnumSet.noneOf(TaggingPresetType.class);
         TestError possibleMatchError = null;
         // iterate through all of the role definition within preset
         // and look for any matching definition
@@ -236,10 +218,10 @@
                 continue;
             }
             name = e.getValue();
-            types.addAll(r.types);
-            if (checkMemberType(r, member)) {
+            types.addAll(r.getTypes());
+            if (r.appliesTo(member.getDisplayType())) {
                 // member type accepted by role definition
-                if (r.memberExpression == null) {
+                if (r.getMemberExpression() == null) {
                     // no member expression - so all requirements met
                     return true;
                 } else {
@@ -251,7 +233,7 @@
                         return true;
                     } else {
                         // verify expression
-                        if (r.memberExpression.match(primitive)) {
+                        if (r.getMemberExpression().match(primitive)) {
                             return true;
                         } else {
                             // possible match error
@@ -261,7 +243,7 @@
                             possibleMatchError = TestError.builder(this, Severity.WARNING, WRONG_ROLE)
                                     .message(ROLE_VERIF_PROBLEM_MSG,
                                             marktr("Role of relation member does not match template expression ''{0}'' in preset {1}"),
-                                            r.memberExpression, name)
+                                            r.getMemberExpression(), name)
                                     .primitives(member.getMember().isUsable() ? member.getMember() : n)
                                     .build();
                         }
@@ -268,7 +250,7 @@
                     }
                 }
             } else if (OsmPrimitiveType.RELATION == member.getType() && !member.getMember().isUsable()
-                    && r.types.contains(TaggingPresetType.MULTIPOLYGON)) {
+                    && r.appliesTo(TaggingPresetType.MULTIPOLYGON)) {
                 // if relation is incomplete we cannot verify if it's a multipolygon - so we just skip it
                 return true;
             }
@@ -275,7 +257,7 @@
         }
 
         if (name == null) {
-           return true;
+            return true;
         } else if (possibleMatchError != null) {
             // if any error found, then assume that member type was correct
             // and complain about not matching the memberExpression
@@ -318,11 +300,11 @@
 
         // verify role counts based on whole role sets
         for (Role r: allroles.keySet()) {
-            String keyname = r.key;
+            String keyname = r.getKey();
             if (keyname.isEmpty()) {
                 keyname = tr("<empty>");
             }
-            checkRoleCounts(n, r, keyname, map.get(r.key));
+            checkRoleCounts(n, r, keyname, map.get(r.getKey()));
         }
         if ("network".equals(n.get("type")) && !"bicycle".equals(n.get("route"))) {
             return;
@@ -331,7 +313,7 @@
         for (String key : map.keySet()) {
             if (allroles.keySet().stream().noneMatch(role -> role.isRole(key))) {
                 String templates = allroles.keySet().stream()
-                        .map(r -> r.key)
+                        .map(r -> r.getKey())
                         .map(r -> Utils.isEmpty(r) ? tr("<empty>") : r)
                         .distinct()
                         .collect(Collectors.joining("/"));
Index: src/org/openstreetmap/josm/data/validation/tests/TagChecker.java
===================================================================
--- src/org/openstreetmap/josm/data/validation/tests/TagChecker.java	(revision 18366)
+++ src/org/openstreetmap/josm/data/validation/tests/TagChecker.java	(working copy)
@@ -55,14 +55,13 @@
 import org.openstreetmap.josm.data.validation.TestError;
 import org.openstreetmap.josm.data.validation.util.Entities;
 import org.openstreetmap.josm.gui.progress.ProgressMonitor;
+import org.openstreetmap.josm.gui.tagging.presets.Item;
+import org.openstreetmap.josm.gui.tagging.presets.KeyedItem;
 import org.openstreetmap.josm.gui.tagging.presets.TaggingPreset;
-import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItem;
 import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetListener;
 import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetType;
+import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetUtils;
 import org.openstreetmap.josm.gui.tagging.presets.TaggingPresets;
-import org.openstreetmap.josm.gui.tagging.presets.items.Check;
-import org.openstreetmap.josm.gui.tagging.presets.items.CheckGroup;
-import org.openstreetmap.josm.gui.tagging.presets.items.KeyedItem;
 import org.openstreetmap.josm.gui.widgets.EditableList;
 import org.openstreetmap.josm.io.CachedFile;
 import org.openstreetmap.josm.spi.preferences.Config;
@@ -90,7 +89,7 @@
     private static volatile HashSet<String> additionalPresetsValueData;
     /** often used tags which are not in presets */
     private static final MultiMap<String, String> oftenUsedTags = new MultiMap<>();
-    private static final Map<TaggingPreset, List<TaggingPresetItem>> presetIndex = new LinkedHashMap<>();
+    private static final Map<TaggingPreset, List<Item>> presetIndex = new LinkedHashMap<>();
 
     private static final Pattern UNWANTED_NON_PRINTING_CONTROL_CHARACTERS = Pattern.compile(
             "[\\x00-\\x09\\x0B\\x0C\\x0E-\\x1F\\x7F\\u200e-\\u200f\\u202a-\\u202e]");
@@ -388,16 +387,12 @@
         if (!presets.isEmpty()) {
             initAdditionalPresetsValueData();
             for (TaggingPreset p : presets) {
-                List<TaggingPresetItem> minData = new ArrayList<>();
-                for (TaggingPresetItem i : p.data) {
+                List<Item> minData = new ArrayList<>();
+                for (Item i : p.getAllItems()) {
                     if (i instanceof KeyedItem) {
-                        if (!"none".equals(((KeyedItem) i).match))
+                        if (!"none".equals(((KeyedItem) i).getMatchType()))
                             minData.add(i);
                         addPresetValue((KeyedItem) i);
-                    } else if (i instanceof CheckGroup) {
-                        for (Check c : ((CheckGroup) i).checks) {
-                            addPresetValue(c);
-                        }
                     }
                 }
                 if (!minData.isEmpty()) {
@@ -416,8 +411,8 @@
     }
 
     private static void addPresetValue(KeyedItem ky) {
-        if (ky.key != null && ky.getValues() != null) {
-            addToKeyDictionary(ky.key);
+        if (ky.getKey() != null && ky.getValues() != null) {
+            addToKeyDictionary(ky.getKey());
         }
     }
 
@@ -660,7 +655,7 @@
             EnumSet<TaggingPresetType> presetTypes = EnumSet.of(presetType);
 
             Collection<TaggingPreset> matchingPresets = presetIndex.entrySet().stream()
-                    .filter(e -> TaggingPresetItem.matches(e.getValue(), tags))
+                    .filter(e -> TaggingPresetUtils.matches(e.getValue(), tags))
                     .map(Entry::getKey)
                     .collect(Collectors.toCollection(LinkedHashSet::new));
             Collection<TaggingPreset> matchingPresetsOK = matchingPresets.stream().filter(
@@ -670,14 +665,14 @@
 
             for (TaggingPreset tp : matchingPresetsKO) {
                 // Potential error, unless matching tags are all known by a supported preset
-                Map<String, String> matchingTags = tp.data.stream()
+                Map<String, String> matchingTags = tp.getAllItems().stream()
                     .filter(i -> Boolean.TRUE.equals(i.matches(tags)))
-                    .filter(i -> i instanceof KeyedItem).map(i -> ((KeyedItem) i).key)
+                    .filter(i -> i instanceof KeyedItem).map(i -> ((KeyedItem) i).getKey())
                     .collect(Collectors.toMap(k -> k, tags::get));
                 if (matchingPresetsOK.stream().noneMatch(
                         tp2 -> matchingTags.entrySet().stream().allMatch(
-                                e -> tp2.data.stream().anyMatch(
-                                        i -> i instanceof KeyedItem && ((KeyedItem) i).key.equals(e.getKey()))))) {
+                                e -> tp2.getAllItems().stream().anyMatch(
+                                        i -> i instanceof KeyedItem && ((KeyedItem) i).getKey().equals(e.getKey()))))) {
                     errors.add(TestError.builder(this, Severity.OTHER, INVALID_PRESETS_TYPE)
                             .message(tr("Object type not in preset"),
                                     marktr("Object type {0} is not supported by tagging preset: {1}"),
Index: src/org/openstreetmap/josm/gui/dialogs/properties/PresetListPanel.java
===================================================================
--- src/org/openstreetmap/josm/gui/dialogs/properties/PresetListPanel.java	(revision 18366)
+++ src/org/openstreetmap/josm/gui/dialogs/properties/PresetListPanel.java	(working copy)
@@ -11,9 +11,8 @@
 import javax.swing.JLabel;
 import javax.swing.JPanel;
 
-import org.openstreetmap.josm.data.osm.DataSet;
-import org.openstreetmap.josm.data.osm.OsmPrimitive;
 import org.openstreetmap.josm.gui.tagging.presets.TaggingPreset;
+import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetDialog;
 import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetHandler;
 import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetLabel;
 import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetType;
@@ -26,26 +25,17 @@
 public class PresetListPanel extends JPanel {
 
     static final class LabelMouseAdapter extends MouseAdapter {
-        private final TaggingPreset t;
-        private final TaggingPresetHandler presetHandler;
+        private final TaggingPreset preset;
+        private final TaggingPresetHandler handler;
 
-        LabelMouseAdapter(TaggingPreset t, TaggingPresetHandler presetHandler) {
-            this.t = t;
-            this.presetHandler = presetHandler;
+        LabelMouseAdapter(TaggingPreset preset, TaggingPresetHandler handler) {
+            this.preset = preset;
+            this.handler = handler;
         }
 
         @Override
         public void mouseClicked(MouseEvent e) {
-            Collection<OsmPrimitive> selection = t.createSelection(presetHandler.getSelection());
-            if (selection.isEmpty())
-                return;
-            int answer = t.showDialog(selection, false);
-            DataSet ds = selection.iterator().next().getDataSet();
-            boolean locked = ds != null && ds.isLocked();
-
-            if (answer == TaggingPreset.DIALOG_ANSWER_APPLY && !locked) {
-                presetHandler.updateTags(t.getChangedTags());
-            }
+            TaggingPresetDialog.showAndApply(preset, handler, false);
         }
     }
 
Index: src/org/openstreetmap/josm/gui/dialogs/properties/PropertiesDialog.java
===================================================================
--- src/org/openstreetmap/josm/gui/dialogs/properties/PropertiesDialog.java	(revision 18366)
+++ src/org/openstreetmap/josm/gui/dialogs/properties/PropertiesDialog.java	(working copy)
@@ -97,7 +97,7 @@
 import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent;
 import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener;
 import org.openstreetmap.josm.gui.layer.OsmDataLayer;
-import org.openstreetmap.josm.gui.tagging.presets.TaggingPreset;
+import org.openstreetmap.josm.gui.tagging.presets.DataSetTaggingPresetHandler;
 import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetHandler;
 import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetListener;
 import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetType;
@@ -980,7 +980,7 @@
     static final class TaggingPresetCommandHandler implements TaggingPresetHandler {
         @Override
         public void updateTags(List<Tag> tags) {
-            Command command = TaggingPreset.createCommand(getSelection(), tags);
+            Command command = DataSetTaggingPresetHandler.createCommand(getPrimitives(), tags);
             if (command != null) {
                 UndoRedoHandler.getInstance().add(command);
             }
@@ -987,7 +987,7 @@
         }
 
         @Override
-        public Collection<OsmPrimitive> getSelection() {
+        public Collection<OsmPrimitive> getPrimitives() {
             return OsmDataManager.getInstance().getInProgressSelection();
         }
     }
Index: src/org/openstreetmap/josm/gui/dialogs/relation/GenericRelationEditor.java
===================================================================
--- src/org/openstreetmap/josm/gui/dialogs/relation/GenericRelationEditor.java	(revision 18366)
+++ src/org/openstreetmap/josm/gui/dialogs/relation/GenericRelationEditor.java	(working copy)
@@ -10,12 +10,11 @@
 import java.awt.FlowLayout;
 import java.awt.GridBagConstraints;
 import java.awt.GridBagLayout;
+import java.awt.Insets;
 import java.awt.Window;
 import java.awt.datatransfer.Clipboard;
 import java.awt.datatransfer.FlavorListener;
 import java.awt.event.ActionEvent;
-import java.awt.event.FocusAdapter;
-import java.awt.event.FocusEvent;
 import java.awt.event.InputEvent;
 import java.awt.event.KeyEvent;
 import java.awt.event.MouseAdapter;
@@ -27,7 +26,9 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.EnumSet;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 import java.util.stream.Collectors;
 
@@ -35,6 +36,7 @@
 import javax.swing.BorderFactory;
 import javax.swing.InputMap;
 import javax.swing.JButton;
+import javax.swing.JCheckBox;
 import javax.swing.JComponent;
 import javax.swing.JLabel;
 import javax.swing.JMenuItem;
@@ -45,6 +47,7 @@
 import javax.swing.JSplitPane;
 import javax.swing.JTabbedPane;
 import javax.swing.JTable;
+import javax.swing.JTextField;
 import javax.swing.JToolBar;
 import javax.swing.KeyStroke;
 
@@ -53,11 +56,15 @@
 import org.openstreetmap.josm.command.Command;
 import org.openstreetmap.josm.data.UndoRedoHandler;
 import org.openstreetmap.josm.data.UndoRedoHandler.CommandQueueListener;
+import org.openstreetmap.josm.data.osm.DataSet;
 import org.openstreetmap.josm.data.osm.DefaultNameFormatter;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
 import org.openstreetmap.josm.data.osm.Relation;
 import org.openstreetmap.josm.data.osm.RelationMember;
 import org.openstreetmap.josm.data.osm.Tag;
+import org.openstreetmap.josm.data.tagging.ac.AutoCompItemCellRenderer;
+import org.openstreetmap.josm.data.tagging.ac.AutoCompletionItem;
+import org.openstreetmap.josm.data.tagging.ac.AutoCompletionPriority;
 import org.openstreetmap.josm.data.validation.tests.RelationChecker;
 import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil;
 import org.openstreetmap.josm.gui.MainApplication;
@@ -98,9 +105,11 @@
 import org.openstreetmap.josm.gui.layer.OsmDataLayer;
 import org.openstreetmap.josm.gui.tagging.TagEditorModel;
 import org.openstreetmap.josm.gui.tagging.TagEditorPanel;
-import org.openstreetmap.josm.gui.tagging.ac.AutoCompletingTextField;
-import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionList;
+import org.openstreetmap.josm.gui.tagging.TagTable;
+import org.openstreetmap.josm.gui.tagging.ac.AutoCompComboBox;
+import org.openstreetmap.josm.gui.tagging.ac.AutoCompComboBoxModel;
 import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionManager;
+import org.openstreetmap.josm.gui.tagging.ac.DefaultAutoCompListener;
 import org.openstreetmap.josm.gui.tagging.presets.TaggingPreset;
 import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetHandler;
 import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetType;
@@ -108,6 +117,7 @@
 import org.openstreetmap.josm.gui.util.WindowGeometry;
 import org.openstreetmap.josm.spi.preferences.Config;
 import org.openstreetmap.josm.tools.CheckParameterUtil;
+import org.openstreetmap.josm.tools.GBC;
 import org.openstreetmap.josm.tools.InputMapUtils;
 import org.openstreetmap.josm.tools.Logging;
 import org.openstreetmap.josm.tools.Shortcut;
@@ -118,6 +128,9 @@
  * @since 343
  */
 public class GenericRelationEditor extends RelationEditor implements CommandQueueListener {
+    private static final String PREF_LASTROLE = "relation.editor.generic.lastrole";
+    private static final String PREF_USE_ROLE_FILTER = "relation.editor.use_role_filter";
+
     /** the tag table and its model */
     private final TagEditorPanel tagEditorPanel;
     private final ReferringRelationsBrowser referrerBrowser;
@@ -131,7 +144,8 @@
     private final SelectionTable selectionTable;
     private final SelectionTableModel selectionTableModel;
 
-    private final AutoCompletingTextField tfRole;
+    private final AutoCompletionManager manager;
+    private final AutoCompComboBox<AutoCompletionItem> cbRole;
 
     /**
      * the menu item in the windows menu. Required to properly hide on dialog close.
@@ -172,6 +186,7 @@
 
     private Component selectedTabPane;
     private JTabbedPane tabbedPane;
+    private JCheckBox btnFilter = new JCheckBox(tr("Filter"));
 
     /**
      * Creates a new relation editor for the given relation. The relation will be saved if the user
@@ -189,8 +204,21 @@
         setRememberWindowGeometry(getClass().getName() + ".geometry",
                 WindowGeometry.centerInWindow(MainApplication.getMainFrame(), new Dimension(700, 650)));
 
+        /**
+         * The data handler we pass to any preset dialog opened from here.
+         * <p>
+         * This handler creates a new relation and a new dataset because the validator assumes that
+         * the primitive to validate has a dataset. On read it copies the current state of the tag
+         * editor to the relation. On write the data goes to the tag table.
+         */
         final TaggingPresetHandler presetHandler = new TaggingPresetHandler() {
+            Relation relation = new Relation();
+            DataSet ds = new DataSet();
 
+            /* anon constructor */ {
+                ds.addPrimitive(relation);
+            }
+
             @Override
             public void updateTags(List<Tag> tags) {
                 tagEditorPanel.getModel().updateTags(tags);
@@ -197,8 +225,9 @@
             }
 
             @Override
-            public Collection<OsmPrimitive> getSelection() {
-                Relation relation = new Relation();
+            public Collection<OsmPrimitive> getPrimitives() {
+                relation.setKeys(null);
+                // copy the current state of the tag table
                 tagEditorPanel.getModel().applyToPrimitive(relation);
                 return Collections.<OsmPrimitive>singletonList(relation);
             }
@@ -212,25 +241,69 @@
         selectionTableModel.register();
         referrerModel = new ReferringRelationsBrowserModel(relation);
 
+        manager = AutoCompletionManager.of(this.getLayer().data);
+
         tagEditorPanel = new TagEditorPanel(relation, presetHandler);
+        TagTable tagTable = tagEditorPanel.getTable();
         populateModels(relation);
         tagEditorPanel.getModel().ensureOneTag();
 
+        // setting up the tag table
+        AutoCompComboBox<AutoCompletionItem> keyEditor = new AutoCompComboBox<>();
+        AutoCompComboBox<AutoCompletionItem> valueEditor = new AutoCompComboBox<>();
+        KeyAutoCompManager keyAutoCompManager = new KeyAutoCompManager();
+        ValueAutoCompManager valueAutoCompManager = new ValueAutoCompManager();
+        keyEditor.getEditorComponent().setMaxTextLength(256);
+        keyEditor.getEditorComponent().setBorder(BorderFactory.createEmptyBorder(1, 1, 1, 1));
+        keyEditor.getEditorComponent().enableUndoRedo(false);
+        keyEditor.getEditorComponent().addAutoCompListener(keyAutoCompManager);
+        keyEditor.addPopupMenuListener(keyAutoCompManager);
+        keyEditor.putClientProperty("JComboBox.isTableCellEditor", Boolean.TRUE);
+        keyEditor.setRenderer(new AutoCompItemCellRenderer(keyEditor, keyEditor.getRenderer(), null));
+
+        valueEditor.getEditorComponent().setMaxTextLength(-1);
+        valueEditor.getEditorComponent().setBorder(BorderFactory.createEmptyBorder(1, 1, 1, 1));
+        valueEditor.getEditorComponent().enableUndoRedo(false);
+        valueEditor.getEditorComponent().addAutoCompListener(valueAutoCompManager);
+        valueEditor.addPopupMenuListener(valueAutoCompManager);
+        valueEditor.putClientProperty("JComboBox.isTableCellEditor", Boolean.TRUE);
+        valueEditor.setRenderer(new AutoCompItemCellRenderer(valueEditor, valueEditor.getRenderer(), null));
+
+        tagTable.setRowHeight(keyEditor.getEditorComponent().getPreferredSize().height);
+        tagTable.setKeyEditor(keyEditor);
+        tagTable.setValueEditor(valueEditor);
+
         // setting up the member table
-        memberTable = new MemberTable(getLayer(), getRelation(), memberTableModel);
+        AutoCompComboBox<AutoCompletionItem> cbRoleEditor = new AutoCompComboBox<>();
+        RoleAutoCompManager roleAutoCompManager = new RoleAutoCompManager();
+        cbRoleEditor.getEditorComponent().addAutoCompListener(roleAutoCompManager);
+        cbRoleEditor.addPopupMenuListener(roleAutoCompManager);
+        cbRoleEditor.getEditorComponent().enableUndoRedo(false);
+        Insets insets = cbRoleEditor.getEditorComponent().getInsets();
+        cbRoleEditor.getEditorComponent().setBorder(BorderFactory.createEmptyBorder(0, insets.left, 0, insets.right));
+        cbRoleEditor.setToolTipText(tr("Select a role for this relation member"));
+        cbRoleEditor.setRenderer(new AutoCompItemCellRenderer(cbRoleEditor, cbRoleEditor.getRenderer(), null));
+
+        int height = cbRoleEditor.getEditorComponent().getPreferredSize().height;
+        memberTable = new MemberTable(getLayer(), cbRoleEditor, memberTableModel);
         memberTable.addMouseListener(new MemberTableDblClickAdapter());
+        memberTable.setRowHeight(height);
         memberTableModel.addMemberModelListener(memberTable);
 
-        MemberRoleCellEditor ce = (MemberRoleCellEditor) memberTable.getColumnModel().getColumn(0).getCellEditor();
         selectionTable = new SelectionTable(selectionTableModel, memberTableModel);
-        selectionTable.setRowHeight(ce.getEditor().getPreferredSize().height);
+        selectionTable.setRowHeight(height);
 
         LeftButtonToolbar leftButtonToolbar = new LeftButtonToolbar(new RelationEditorActionAccess());
-        tfRole = buildRoleTextField(this);
+        cbRole = new AutoCompComboBox<>();
+        cbRole.getEditorComponent().addAutoCompListener(roleAutoCompManager);
+        cbRole.addPopupMenuListener(roleAutoCompManager);
+        cbRole.setText(Config.getPref().get(PREF_LASTROLE, ""));
+        cbRole.setToolTipText(tr("Select a role"));
+        cbRole.setRenderer(new AutoCompItemCellRenderer(cbRole, cbRole.getRenderer(), null));
 
         JSplitPane pane = buildSplitPane(
                 buildTagEditorPanel(tagEditorPanel),
-                buildMemberEditorPanel(leftButtonToolbar, new RelationEditorActionAccess()),
+                buildMemberEditorPanel(leftButtonToolbar),
                 this);
         pane.setPreferredSize(new Dimension(100, 100));
 
@@ -310,7 +383,7 @@
                 @Override
                 public void actionPerformed(ActionEvent e) {
                     super.actionPerformed(e);
-                    tfRole.requestFocusInWindow();
+                    cbRole.requestFocusInWindow();
                 }
             }, "PASTE_MEMBERS", key, getRootPane(), memberTable, selectionTable);
         }
@@ -446,47 +519,14 @@
     }
 
     /**
-     * builds the role text field
-     * @param re relation editor
-     * @return the role text field
-     */
-    protected static AutoCompletingTextField buildRoleTextField(final IRelationEditor re) {
-        final AutoCompletingTextField tfRole = new AutoCompletingTextField(10);
-        tfRole.setToolTipText(tr("Enter a role and apply it to the selected relation members"));
-        tfRole.addFocusListener(new FocusAdapter() {
-            @Override
-            public void focusGained(FocusEvent e) {
-                tfRole.selectAll();
-            }
-        });
-        tfRole.setAutoCompletionList(new AutoCompletionList());
-        tfRole.addFocusListener(
-                new FocusAdapter() {
-                    @Override
-                    public void focusGained(FocusEvent e) {
-                        AutoCompletionList list = tfRole.getAutoCompletionList();
-                        if (list != null) {
-                            list.clear();
-                            AutoCompletionManager.of(re.getLayer().data).populateWithMemberRoles(list, re.getRelation());
-                        }
-                    }
-                }
-        );
-        tfRole.setText(Config.getPref().get("relation.editor.generic.lastrole", ""));
-        return tfRole;
-    }
-
-    /**
      * builds the panel for the relation member editor
      * @param leftButtonToolbar left button toolbar
-     * @param editorAccess The relation editor
      *
      * @return the panel for the relation member editor
      */
-    protected static JPanel buildMemberEditorPanel(
-            LeftButtonToolbar leftButtonToolbar, IRelationEditorActionAccess editorAccess) {
+    protected JPanel buildMemberEditorPanel(LeftButtonToolbar leftButtonToolbar) {
         final JPanel pnl = new JPanel(new GridBagLayout());
-        final JScrollPane scrollPane = new JScrollPane(editorAccess.getMemberTable());
+        final JScrollPane scrollPane = new JScrollPane(memberTable);
 
         GridBagConstraints gc = new GridBagConstraints();
         gc.gridx = 0;
@@ -518,22 +558,34 @@
         pnl.add(scrollPane, gc);
 
         // --- role editing
-        JPanel p3 = new JPanel(new FlowLayout(FlowLayout.LEFT));
-        p3.add(new JLabel(tr("Apply Role:")));
-        p3.add(editorAccess.getTextFieldRole());
-        SetRoleAction setRoleAction = new SetRoleAction(editorAccess);
-        editorAccess.getMemberTableModel().getSelectionModel().addListSelectionListener(setRoleAction);
-        editorAccess.getTextFieldRole().getDocument().addDocumentListener(setRoleAction);
-        editorAccess.getTextFieldRole().addActionListener(setRoleAction);
-        editorAccess.getMemberTableModel().getSelectionModel().addListSelectionListener(
-                e -> editorAccess.getTextFieldRole().setEnabled(editorAccess.getMemberTable().getSelectedRowCount() > 0)
+        JPanel p3 = new JPanel(new GridBagLayout());
+        GBC gbc = GBC.std().fill(GridBagConstraints.NONE);
+        JLabel lbl = new JLabel(tr("Role:"));
+        p3.add(lbl, gbc);
+
+        p3.add(cbRole, gbc.insets(3, 3, 0, 3).fill(GridBagConstraints.HORIZONTAL));
+
+        SetRoleAction setRoleAction = new SetRoleAction(new RelationEditorActionAccess());
+        memberTableModel.getSelectionModel().addListSelectionListener(setRoleAction);
+        cbRole.getEditorComponent().getDocument().addDocumentListener(setRoleAction);
+        cbRole.getEditorComponent().addActionListener(setRoleAction);
+        memberTableModel.getSelectionModel().addListSelectionListener(
+                e -> cbRole.setEnabled(memberTable.getSelectedRowCount() > 0)
         );
-        editorAccess.getTextFieldRole().setEnabled(editorAccess.getMemberTable().getSelectedRowCount() > 0);
+        cbRole.setEnabled(memberTable.getSelectedRowCount() > 0);
+
         JButton btnApply = new JButton(setRoleAction);
-        btnApply.setPreferredSize(new Dimension(20, 20));
+        int height = cbRole.getPreferredSize().height;
+        btnApply.setPreferredSize(new Dimension(height, height));
         btnApply.setText("");
-        p3.add(btnApply);
+        p3.add(btnApply, gbc.weight(0, 0).fill(GridBagConstraints.NONE));
 
+        btnFilter.setToolTipText(tr("Filter suggestions based on context"));
+        btnFilter.setSelected(Config.getPref().getBoolean(PREF_USE_ROLE_FILTER, false));
+        p3.add(btnFilter, gbc.span(GridBagConstraints.REMAINDER));
+
+        //
+
         gc.gridx = 1;
         gc.gridy = 2;
         gc.fill = GridBagConstraints.HORIZONTAL;
@@ -562,7 +614,7 @@
         gc.anchor = GridBagConstraints.NORTHWEST;
         gc.weightx = 0.0;
         gc.weighty = 1.0;
-        pnl2.add(new ScrollViewport(buildSelectionControlButtonToolbar(editorAccess),
+        pnl2.add(new ScrollViewport(buildSelectionControlButtonToolbar(new RelationEditorActionAccess()),
                 ScrollViewport.VERTICAL_DIRECTION), gc);
 
         gc.gridx = 1;
@@ -570,21 +622,19 @@
         gc.weightx = 1.0;
         gc.weighty = 1.0;
         gc.fill = GridBagConstraints.BOTH;
-        pnl2.add(buildSelectionTablePanel(editorAccess.getSelectionTable()), gc);
+        pnl2.add(buildSelectionTablePanel(selectionTable), gc);
 
         final JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT);
         splitPane.setLeftComponent(pnl);
         splitPane.setRightComponent(pnl2);
         splitPane.setOneTouchExpandable(false);
-        if (editorAccess.getEditor() instanceof Window) {
-            ((Window) editorAccess.getEditor()).addWindowListener(new WindowAdapter() {
-                @Override
-                public void windowOpened(WindowEvent e) {
-                    // has to be called when the window is visible, otherwise no effect
-                    splitPane.setDividerLocation(0.6);
-                }
-            });
-        }
+        addWindowListener(new WindowAdapter() {
+            @Override
+            public void windowOpened(WindowEvent e) {
+                // has to be called when the window is visible, otherwise no effect
+                splitPane.setDividerLocation(0.6);
+            }
+        });
 
         JPanel pnl3 = new JPanel(new BorderLayout());
         pnl3.add(splitPane, BorderLayout.CENTER);
@@ -739,9 +789,6 @@
         if (isVisible() == visible) {
             return;
         }
-        if (visible) {
-            tagEditorPanel.initAutoCompletion(getLayer());
-        }
         super.setVisible(visible);
         Clipboard clipboard = ClipboardUtils.getClipboard();
         if (visible) {
@@ -754,6 +801,9 @@
                 clipboard.addFlavorListener(listener);
             }
         } else {
+            Config.getPref().put(PREF_LASTROLE, cbRole.getText());
+            Config.getPref().putBoolean(PREF_USE_ROLE_FILTER, btnFilter.isSelected());
+
             // make sure all registered listeners are unregistered
             //
             memberTable.stopHighlighting();
@@ -1039,10 +1089,9 @@
         }
 
         @Override
-        public AutoCompletingTextField getTextFieldRole() {
-            return tfRole;
+        public JTextField getTextFieldRole() {
+            return cbRole.getEditorComponent();
         }
-
     }
 
     @Override
@@ -1054,4 +1103,80 @@
             applyAction.updateEnabledState();
         }
     }
+
+    private class KeyAutoCompManager extends DefaultAutoCompListener<AutoCompletionItem> {
+        @Override
+        protected void updateAutoCompModel(AutoCompComboBoxModel<AutoCompletionItem> model) {
+            Map<String, AutoCompletionPriority> map;
+            Map<String, String> keys = tagEditorPanel.getModel().getTags();
+            Map<String, String> matchKeys = btnFilter.isSelected() ? keys : null;
+
+            map = AutoCompletionManager.merge(
+                manager.getKeysForRelation(matchKeys),
+                manager.getPresetKeys(EnumSet.of(TaggingPresetType.RELATION), matchKeys)
+            );
+
+            model.replaceAllElements(map.entrySet().stream().filter(e -> !keys.containsKey(e.getKey()))
+                .map(e -> new AutoCompletionItem(e.getKey(), e.getValue()))
+                .sorted(AutoCompletionManager.ALPHABETIC_COMPARATOR).collect(Collectors.toList()));
+        }
+    }
+
+    private class ValueAutoCompManager extends DefaultAutoCompListener<AutoCompletionItem> {
+        @Override
+        protected void updateAutoCompModel(AutoCompComboBoxModel<AutoCompletionItem> model) {
+            Map<String, AutoCompletionPriority> map;
+            Map<String, String> keys = btnFilter.isSelected() ? tagEditorPanel.getModel().getTags() : null;
+            String key = (String) tagEditorPanel.getModel().getValueAt(tagEditorPanel.getTable().getEditingRow(), 0);
+
+            map = AutoCompletionManager.merge(
+                manager.getValuesForRelation(keys, key),
+                manager.getPresetValues(EnumSet.of(TaggingPresetType.RELATION), keys, key)
+            );
+
+            model.replaceAllElements(map.entrySet().stream()
+                .map(e -> new AutoCompletionItem(e.getKey(), e.getValue()))
+                .sorted(AutoCompletionManager.ALPHABETIC_COMPARATOR).collect(Collectors.toList()));
+        }
+    }
+
+    /**
+     * Returns the roles currently edited in the members table.
+     * @param types the preset types to include, (node / way / relation ...) or null to include all types
+     * @return the roles currently edited in the members table.
+     */
+    private Map<String, AutoCompletionPriority> getCurrentRoles(Collection<TaggingPresetType> types) {
+        Map<String, AutoCompletionPriority> map = new HashMap<>();
+        for (int i = 0; i < memberTableModel.getRowCount(); ++i) {
+            RelationMember member = memberTableModel.getValue(i);
+            if (types == null || types.contains(TaggingPresetType.forPrimitiveType(member.getDisplayType()))) {
+                map.merge(member.getRole(), AutoCompletionPriority.IS_IN_SELECTION, AutoCompletionPriority::mergeWith);
+            }
+        }
+        return map;
+    }
+
+    private class RoleAutoCompManager extends DefaultAutoCompListener<AutoCompletionItem> {
+        @Override
+        protected void updateAutoCompModel(AutoCompComboBoxModel<AutoCompletionItem> model) {
+            Map<String, AutoCompletionPriority> map;
+            Map<String, String> keys = btnFilter.isSelected() ? tagEditorPanel.getModel().getTags() : null;
+
+            EnumSet<TaggingPresetType> selectedTypes = EnumSet.noneOf(TaggingPresetType.class);
+            for (RelationMember member : memberTableModel.getSelectedMembers()) {
+                selectedTypes.add(TaggingPresetType.forPrimitiveType(member.getDisplayType()));
+            }
+
+            map = AutoCompletionManager.merge(
+                manager.getRolesForRelation(keys, selectedTypes),
+                manager.getPresetRoles(keys, selectedTypes),
+                getCurrentRoles(selectedTypes)
+            );
+
+            // turn into AutoCompletionItems
+            model.replaceAllElements(map.entrySet().stream()
+                .map(e -> new AutoCompletionItem(e.getKey(), e.getValue()))
+                .sorted(AutoCompletionManager.ALPHABETIC_COMPARATOR).collect(Collectors.toList()));
+        }
+    }
 }
Index: src/org/openstreetmap/josm/gui/dialogs/relation/MemberRoleCellEditor.java
===================================================================
--- src/org/openstreetmap/josm/gui/dialogs/relation/MemberRoleCellEditor.java	(revision 18366)
+++ src/org/openstreetmap/josm/gui/dialogs/relation/MemberRoleCellEditor.java	(nonexistent)
@@ -1,66 +0,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.gui.dialogs.relation;
-
-import java.awt.Component;
-
-import javax.swing.AbstractCellEditor;
-import javax.swing.BorderFactory;
-import javax.swing.CellEditor;
-import javax.swing.JTable;
-import javax.swing.table.TableCellEditor;
-
-import org.openstreetmap.josm.data.osm.Relation;
-import org.openstreetmap.josm.gui.tagging.ac.AutoCompletingTextField;
-import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionList;
-import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionManager;
-
-/**
- * The {@link CellEditor} for the role cell in the table. Supports autocompletion.
- */
-public class MemberRoleCellEditor extends AbstractCellEditor implements TableCellEditor {
-    private final AutoCompletingTextField editor;
-    private final AutoCompletionManager autoCompletionManager;
-    private final transient Relation relation;
-
-    /** user input is matched against this list of auto completion items */
-    private final AutoCompletionList autoCompletionList;
-
-    /**
-     * Constructs a new {@code MemberRoleCellEditor}.
-     * @param autoCompletionManager the auto completion manager. Must not be null
-     * @param relation the relation. Can be null
-     * @since 13675
-     */
-    public MemberRoleCellEditor(AutoCompletionManager autoCompletionManager, Relation relation) {
-        this.autoCompletionManager = autoCompletionManager;
-        this.relation = relation;
-        editor = new AutoCompletingTextField(0, false);
-        editor.setBorder(BorderFactory.createEmptyBorder(1, 1, 1, 1));
-        autoCompletionList = new AutoCompletionList();
-        editor.setAutoCompletionList(autoCompletionList);
-    }
-
-    @Override
-    public Component getTableCellEditorComponent(JTable table,
-            Object value, boolean isSelected, int row, int column) {
-
-        String role = (String) value;
-        editor.setText(role);
-        autoCompletionList.clear();
-        autoCompletionManager.populateWithMemberRoles(autoCompletionList, relation);
-        return editor;
-    }
-
-    @Override
-    public Object getCellEditorValue() {
-        return editor.getText();
-    }
-
-    /**
-     * Returns the edit field for this cell editor.
-     * @return the edit field for this cell editor
-     */
-    public AutoCompletingTextField getEditor() {
-        return editor;
-    }
-}

Property changes on: src/org/openstreetmap/josm/gui/dialogs/relation/MemberRoleCellEditor.java
___________________________________________________________________
Deleted: svn:eol-style
## -1 +0,0 ##
-native
\ No newline at end of property
Index: src/org/openstreetmap/josm/gui/dialogs/relation/MemberTable.java
===================================================================
--- src/org/openstreetmap/josm/gui/dialogs/relation/MemberTable.java	(revision 18366)
+++ src/org/openstreetmap/josm/gui/dialogs/relation/MemberTable.java	(working copy)
@@ -20,6 +20,7 @@
 import javax.swing.SwingUtilities;
 import javax.swing.event.ListSelectionEvent;
 import javax.swing.event.ListSelectionListener;
+import javax.swing.table.TableCellEditor;
 
 import org.openstreetmap.josm.actions.AbstractShowHistoryAction;
 import org.openstreetmap.josm.actions.AutoScaleAction;
@@ -27,7 +28,6 @@
 import org.openstreetmap.josm.actions.HistoryInfoAction;
 import org.openstreetmap.josm.actions.ZoomToAction;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
-import org.openstreetmap.josm.data.osm.Relation;
 import org.openstreetmap.josm.data.osm.RelationMember;
 import org.openstreetmap.josm.data.osm.Way;
 import org.openstreetmap.josm.gui.MainApplication;
@@ -41,7 +41,6 @@
 import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent;
 import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener;
 import org.openstreetmap.josm.gui.layer.OsmDataLayer;
-import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionManager;
 import org.openstreetmap.josm.gui.util.HighlightHelper;
 import org.openstreetmap.josm.gui.widgets.OsmPrimitivesTable;
 import org.openstreetmap.josm.spi.preferences.Config;
@@ -60,16 +59,15 @@
      * constructor for relation member table
      *
      * @param layer the data layer of the relation. Must not be null
-     * @param relation the relation. Can be null
+     * @param roleCellEditor the role editor combobox
      * @param model the table model
      */
-    public MemberTable(OsmDataLayer layer, Relation relation, MemberTableModel model) {
-        super(model, new MemberTableColumnModel(AutoCompletionManager.of(layer.data), relation), model.getSelectionModel());
+    public MemberTable(OsmDataLayer layer, TableCellEditor roleCellEditor, MemberTableModel model) {
+        super(model, new MemberTableColumnModel(roleCellEditor), model.getSelectionModel());
         setLayer(layer);
         model.addMemberModelListener(this);
 
-        MemberRoleCellEditor ce = (MemberRoleCellEditor) getColumnModel().getColumn(0).getCellEditor();
-        setRowHeight(ce.getEditor().getPreferredSize().height);
+        setRowHeight(roleCellEditor.getTableCellEditorComponent(this, "", false, 0, 0).getPreferredSize().height);
         setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);
         setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
         putClientProperty("terminateEditOnFocusLost", Boolean.TRUE);
@@ -83,9 +81,7 @@
         if (!GraphicsEnvironment.isHeadless()) {
             setTransferHandler(new MemberTransferHandler());
             setFillsViewportHeight(true); // allow drop on empty table
-            if (!GraphicsEnvironment.isHeadless()) {
-                setDragEnabled(true);
-            }
+            setDragEnabled(true);
             setDropMode(DropMode.INSERT_ROWS);
         }
     }
Index: src/org/openstreetmap/josm/gui/dialogs/relation/MemberTableColumnModel.java
===================================================================
--- src/org/openstreetmap/josm/gui/dialogs/relation/MemberTableColumnModel.java	(revision 18366)
+++ src/org/openstreetmap/josm/gui/dialogs/relation/MemberTableColumnModel.java	(working copy)
@@ -4,11 +4,9 @@
 import static org.openstreetmap.josm.tools.I18n.tr;
 
 import javax.swing.table.DefaultTableColumnModel;
+import javax.swing.table.TableCellEditor;
 import javax.swing.table.TableColumn;
 
-import org.openstreetmap.josm.data.osm.Relation;
-import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionManager;
-
 /**
  * This is the column model for the {@link MemberTable}
  */
@@ -16,11 +14,10 @@
 
     /**
      * Constructs a new {@code MemberTableColumnModel}.
-     * @param autoCompletionManager the auto completion manager. Must not be null
-     * @param relation the relation. Can be null
+     * @param roleCellEditor the role editor combobox
      * @since 13675
      */
-    public MemberTableColumnModel(AutoCompletionManager autoCompletionManager, Relation relation) {
+    public MemberTableColumnModel(TableCellEditor roleCellEditor) {
         TableColumn col;
 
         // column 0 - the member role
@@ -29,7 +26,7 @@
         col.setResizable(true);
         col.setPreferredWidth(100);
         col.setCellRenderer(new MemberTableRoleCellRenderer());
-        col.setCellEditor(new MemberRoleCellEditor(autoCompletionManager, relation));
+        col.setCellEditor(roleCellEditor);
         addColumn(col);
 
         // column 1 - the member
Index: src/org/openstreetmap/josm/gui/dialogs/relation/MemberTableModel.java
===================================================================
--- src/org/openstreetmap/josm/gui/dialogs/relation/MemberTableModel.java	(revision 18366)
+++ src/org/openstreetmap/josm/gui/dialogs/relation/MemberTableModel.java	(working copy)
@@ -433,7 +433,7 @@
     RelationMember getRelationMemberForPrimitive(final OsmPrimitive primitive) {
         final Collection<TaggingPreset> presets = TaggingPresets.getMatchingPresets(
                 EnumSet.of(relation != null ? TaggingPresetType.forPrimitive(relation) : TaggingPresetType.RELATION),
-                presetHandler.getSelection().iterator().next().getKeys(), false);
+                presetHandler.getPrimitives().iterator().next().getKeys(), false);
         Collection<String> potentialRoles = presets.stream()
                 .map(tp -> tp.suggestRoleForOsmPrimitive(primitive))
                 .filter(Objects::nonNull)
Index: src/org/openstreetmap/josm/gui/dialogs/relation/actions/IRelationEditorActionAccess.java
===================================================================
--- src/org/openstreetmap/josm/gui/dialogs/relation/actions/IRelationEditorActionAccess.java	(revision 18366)
+++ src/org/openstreetmap/josm/gui/dialogs/relation/actions/IRelationEditorActionAccess.java	(working copy)
@@ -2,6 +2,7 @@
 package org.openstreetmap.josm.gui.dialogs.relation.actions;
 
 import javax.swing.Action;
+import javax.swing.JTextField;
 
 import org.openstreetmap.josm.gui.dialogs.relation.IRelationEditor;
 import org.openstreetmap.josm.gui.dialogs.relation.MemberTable;
@@ -9,7 +10,6 @@
 import org.openstreetmap.josm.gui.dialogs.relation.SelectionTable;
 import org.openstreetmap.josm.gui.dialogs.relation.SelectionTableModel;
 import org.openstreetmap.josm.gui.tagging.TagEditorModel;
-import org.openstreetmap.josm.gui.tagging.ac.AutoCompletingTextField;
 
 /**
  * This interface provides access to the relation editor for actions.
@@ -69,7 +69,7 @@
      * Get the text field that is used to edit the role.
      * @return The role text field.
      */
-    AutoCompletingTextField getTextFieldRole();
+    JTextField getTextFieldRole();
 
     /**
      * Tells the member table editor to stop editing and accept any partially edited value as the value of the editor.
Index: src/org/openstreetmap/josm/gui/dialogs/relation/actions/SavingAction.java
===================================================================
--- src/org/openstreetmap/josm/gui/dialogs/relation/actions/SavingAction.java	(revision 18366)
+++ src/org/openstreetmap/josm/gui/dialogs/relation/actions/SavingAction.java	(working copy)
@@ -8,6 +8,7 @@
 import java.util.List;
 
 import javax.swing.JOptionPane;
+import javax.swing.JTextField;
 import javax.swing.SwingUtilities;
 
 import org.openstreetmap.josm.command.AddCommand;
@@ -27,7 +28,6 @@
 import org.openstreetmap.josm.gui.dialogs.relation.RelationDialogManager;
 import org.openstreetmap.josm.gui.dialogs.relation.RelationEditor;
 import org.openstreetmap.josm.gui.tagging.TagEditorModel;
-import org.openstreetmap.josm.gui.tagging.ac.AutoCompletingTextField;
 import org.openstreetmap.josm.tools.ImageProvider;
 import org.openstreetmap.josm.tools.Utils;
 
@@ -38,7 +38,7 @@
 abstract class SavingAction extends AbstractRelationEditorAction {
     private static final long serialVersionUID = 1L;
 
-    protected final AutoCompletingTextField tfRole;
+    protected final JTextField tfRole;
 
     protected SavingAction(IRelationEditorActionAccess editorAccess, IRelationEditorUpdateOn... updateOn) {
         super(editorAccess, updateOn);
Index: src/org/openstreetmap/josm/gui/dialogs/relation/actions/SetRoleAction.java
===================================================================
--- src/org/openstreetmap/josm/gui/dialogs/relation/actions/SetRoleAction.java	(revision 18366)
+++ src/org/openstreetmap/josm/gui/dialogs/relation/actions/SetRoleAction.java	(working copy)
@@ -7,12 +7,12 @@
 import java.awt.event.ActionEvent;
 
 import javax.swing.JOptionPane;
+import javax.swing.JTextField;
 import javax.swing.event.DocumentEvent;
 import javax.swing.event.DocumentListener;
 
 import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil;
 import org.openstreetmap.josm.gui.MainApplication;
-import org.openstreetmap.josm.gui.tagging.ac.AutoCompletingTextField;
 import org.openstreetmap.josm.tools.ImageProvider;
 import org.openstreetmap.josm.tools.Utils;
 
@@ -23,7 +23,7 @@
 public class SetRoleAction extends AbstractRelationEditorAction implements DocumentListener {
     private static final long serialVersionUID = 1L;
 
-    private final transient AutoCompletingTextField tfRole;
+    private final transient JTextField tfRole;
 
     /**
      * Constructs a new {@code SetRoleAction}.
@@ -32,7 +32,7 @@
     public SetRoleAction(IRelationEditorActionAccess editorAccess) {
         super(editorAccess);
         this.tfRole = editorAccess.getTextFieldRole();
-        putValue(SHORT_DESCRIPTION, tr("Sets a role for the selected members"));
+        putValue(SHORT_DESCRIPTION, tr("Apply the role to the selected members"));
         new ImageProvider("apply").getResource().attachImageIcon(this);
         putValue(NAME, tr("Apply Role"));
         updateEnabledState();
Index: src/org/openstreetmap/josm/gui/io/UploadDialog.java
===================================================================
--- src/org/openstreetmap/josm/gui/io/UploadDialog.java	(revision 18366)
+++ src/org/openstreetmap/josm/gui/io/UploadDialog.java	(working copy)
@@ -18,7 +18,7 @@
 import java.beans.PropertyChangeListener;
 import java.lang.Character.UnicodeBlock;
 import java.util.ArrayList;
-import java.util.Collections;
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Locale;
@@ -43,6 +43,10 @@
 import org.openstreetmap.josm.gui.help.ContextSensitiveHelpAction;
 import org.openstreetmap.josm.gui.help.HelpUtil;
 import org.openstreetmap.josm.gui.tagging.TagEditorPanel;
+import org.openstreetmap.josm.gui.tagging.TagTable;
+import org.openstreetmap.josm.gui.tagging.ac.AutoCompComboBox;
+import org.openstreetmap.josm.gui.tagging.ac.AutoCompComboBoxModel;
+import org.openstreetmap.josm.gui.tagging.ac.DefaultAutoCompListener;
 import org.openstreetmap.josm.gui.util.GuiHelper;
 import org.openstreetmap.josm.gui.util.MultiLineFlowLayout;
 import org.openstreetmap.josm.gui.util.WindowGeometry;
@@ -88,8 +92,6 @@
     /** the model keeping the state of the changeset tags */
     private final transient UploadDialogModel model = new UploadDialogModel();
 
-    private transient DataSet dataSet;
-
     /**
      * Constructs a new {@code UploadDialog}.
      */
@@ -141,6 +143,30 @@
         pnlTagEditor = new TagEditorPanel(model, null, Changeset.MAX_CHANGESET_TAG_LENGTH);
         pnlTagEditorBorder.add(pnlTagEditor, BorderLayout.CENTER);
 
+        // setting up the tag table
+        TagTable tagTable = pnlTagEditor.getTable();
+        AutoCompComboBox<String> keyEditor = new AutoCompComboBox<>();
+        AutoCompComboBox<String> valueEditor = new AutoCompComboBox<>();
+        KeyAutoCompManager keyAutoCompManager = new KeyAutoCompManager();
+        ValueAutoCompManager valueAutoCompManager = new ValueAutoCompManager();
+        keyEditor.getEditorComponent().setMaxTextLength(Changeset.MAX_CHANGESET_TAG_LENGTH);
+        keyEditor.getEditorComponent().setBorder(BorderFactory.createEmptyBorder(1, 1, 1, 1));
+        keyEditor.getEditorComponent().enableUndoRedo(false);
+        keyEditor.getEditorComponent().addAutoCompListener(keyAutoCompManager);
+        keyEditor.addPopupMenuListener(keyAutoCompManager);
+        keyEditor.putClientProperty("JComboBox.isTableCellEditor", Boolean.TRUE);
+
+        valueEditor.getEditorComponent().setMaxTextLength(Changeset.MAX_CHANGESET_TAG_LENGTH);
+        valueEditor.getEditorComponent().setBorder(BorderFactory.createEmptyBorder(1, 1, 1, 1));
+        valueEditor.getEditorComponent().enableUndoRedo(false);
+        valueEditor.getEditorComponent().addAutoCompListener(valueAutoCompManager);
+        valueEditor.addPopupMenuListener(valueAutoCompManager);
+        valueEditor.putClientProperty("JComboBox.isTableCellEditor", Boolean.TRUE);
+
+        tagTable.setRowHeight(keyEditor.getEditorComponent().getPreferredSize().height);
+        tagTable.setKeyEditor(keyEditor);
+        tagTable.setValueEditor(valueEditor);
+
         pnlChangesetManagement = new ChangesetManagementPanel();
         pnlUploadStrategySelectionPanel = new UploadStrategySelectionPanel();
         pnlSettings.add(pnlChangesetManagement, GBC.eop().fill(GridBagConstraints.HORIZONTAL));
@@ -228,19 +254,18 @@
      * this in the constructor because the dialog is a singleton.
      *
      * @param dataSet The Dataset we want to upload
+     * @param toUpload The primitves to upload
      * @since 18173
      */
-    public void initLifeCycle(DataSet dataSet) {
+    public void initLifeCycle(DataSet dataSet, APIDataSet toUpload) {
         Map<String, String> map = new HashMap<>();
-        this.dataSet = dataSet;
         pnlBasicUploadSettings.initLifeCycle(map);
         pnlChangesetManagement.initLifeCycle();
         model.clear();
-        model.putAll(map);          // init with tags from history
-        model.putAll(this.dataSet); // overwrite with tags from the dataset
+        model.putAll(map);     // init with tags from history
+        model.putAll(dataSet); // overwrite with tags from the dataset
 
         tpConfigPanels.setSelectedIndex(0);
-        pnlTagEditor.initAutoCompletion(MainApplication.getLayerManager().getEditLayer());
         pnlUploadStrategySelectionPanel.initFromPreferences();
 
         // update the summary
@@ -247,32 +272,11 @@
         UploadParameterSummaryPanel sumPnl = pnlBasicUploadSettings.getUploadParameterSummaryPanel();
         sumPnl.setUploadStrategySpecification(pnlUploadStrategySelectionPanel.getUploadStrategySpecification());
         sumPnl.setCloseChangesetAfterNextUpload(pnlChangesetManagement.isCloseChangesetAfterUpload());
-    }
 
-    /**
-     * Sets the collection of primitives to upload
-     *
-     * @param toUpload the dataset with the objects to upload. If null, assumes the empty
-     * set of objects to upload
-     *
-     */
-    public void setUploadedPrimitives(APIDataSet toUpload) {
-        UploadParameterSummaryPanel sumPnl = pnlBasicUploadSettings.getUploadParameterSummaryPanel();
-        if (toUpload == null) {
-            if (pnlUploadedObjects != null) {
-                List<OsmPrimitive> emptyList = Collections.emptyList();
-                pnlUploadedObjects.setUploadedPrimitives(emptyList, emptyList, emptyList);
-                sumPnl.setNumObjects(0);
-            }
-            return;
-        }
         List<OsmPrimitive> l = toUpload.getPrimitives();
         pnlBasicUploadSettings.setUploadedPrimitives(l);
-        pnlUploadedObjects.setUploadedPrimitives(
-                toUpload.getPrimitivesToAdd(),
-                toUpload.getPrimitivesToUpdate(),
-                toUpload.getPrimitivesToDelete()
-        );
+        pnlUploadedObjects.removeAll();
+        pnlUploadedObjects.build(toUpload);
         sumPnl.setNumObjects(l.size());
         pnlUploadStrategySelectionPanel.setNumUploadedObjects(l.size());
     }
@@ -512,6 +516,34 @@
         }
     }
 
+    private static class KeyAutoCompManager extends DefaultAutoCompListener<String> {
+        @Override
+        protected void updateAutoCompModel(AutoCompComboBoxModel<String> model) {
+            model.replaceAllElements(Arrays.asList("comment", "source", "review_requested", "created_by", "imagery_used", "locale"));
+            // FIXME add more tags from user upload history?
+        }
+    }
+
+    private class ValueAutoCompManager extends DefaultAutoCompListener<String> {
+        @Override
+        protected void updateAutoCompModel(AutoCompComboBoxModel<String> model) {
+            String key = (String) pnlTagEditor.getModel().getValueAt(pnlTagEditor.getTable().getEditingRow(), 0);
+            if ("comment".equals(key)) {
+                model.prefs(x ->x, x -> x).load(BasicUploadSettingsPanel.COMMENT_HISTORY_KEY);
+                return;
+            }
+            if ("source".equals(key)) {
+                model.prefs(x -> x, x -> x).load(BasicUploadSettingsPanel.SOURCE_HISTORY_KEY, BasicUploadSettingsPanel.getDefaultSources());
+                return;
+            }
+            if ("review_requested".equals(key)) {
+                model.replaceAllElements(Arrays.asList("yes", ""));
+                return;
+            }
+            model.replaceAllElements(Arrays.asList(""));
+        }
+    }
+
     /* -------------------------------------------------------------------------- */
     /* Interface PropertyChangeListener                                           */
     /* -------------------------------------------------------------------------- */
@@ -604,7 +636,6 @@
      * @since 14251
      */
     public void clean() {
-        setUploadedPrimitives(null);
-        dataSet = null;
+        pnlUploadedObjects.removeAll();
     }
 }
Index: src/org/openstreetmap/josm/gui/io/UploadDialogModel.java
===================================================================
--- src/org/openstreetmap/josm/gui/io/UploadDialogModel.java	(revision 18366)
+++ src/org/openstreetmap/josm/gui/io/UploadDialogModel.java	(working copy)
@@ -75,12 +75,12 @@
      * @return the hashtags separated by ";" or null
      */
     String findHashTags(String comment) {
-        String hashtags = String.join(";",
+        String hashTags = String.join(";",
             Arrays.stream(comment.split("\\s", -1))
                 .map(s -> Utils.strip(s, ",;"))
                 .filter(s -> s.matches("#[a-zA-Z0-9][-_a-zA-Z0-9]+"))
                 .collect(Collectors.toList()));
-        return hashtags.isEmpty() ? null : hashtags;
+        return hashTags.isEmpty() ? null : hashTags;
     }
 
     /**
Index: src/org/openstreetmap/josm/gui/io/UploadPrimitivesTask.java
===================================================================
--- src/org/openstreetmap/josm/gui/io/UploadPrimitivesTask.java	(revision 18366)
+++ src/org/openstreetmap/josm/gui/io/UploadPrimitivesTask.java	(working copy)
@@ -401,7 +401,7 @@
                         // return to the upload dialog
                         //
                         toUpload.removeProcessed(processedPrimitives);
-                        UploadDialog.getUploadDialog().setUploadedPrimitives(toUpload);
+                        UploadDialog.getUploadDialog().initLifeCycle(null, toUpload);
                         UploadDialog.getUploadDialog().setVisible(true);
                         break;
                     }
Index: src/org/openstreetmap/josm/gui/io/UploadStrategySelectionPanel.java
===================================================================
--- src/org/openstreetmap/josm/gui/io/UploadStrategySelectionPanel.java	(revision 18366)
+++ src/org/openstreetmap/josm/gui/io/UploadStrategySelectionPanel.java	(working copy)
@@ -69,7 +69,7 @@
 
     protected JPanel buildUploadStrategyPanel() {
         JPanel pnl = new JPanel(new GridBagLayout());
-        pnl.setBorder(BorderFactory.createTitledBorder(tr("Please select the upload strategy:")));
+        pnl.setBorder(BorderFactory.createTitledBorder(tr("Please select an upload strategy:")));
         ButtonGroup bgStrategies = new ButtonGroup();
         rbStrategy = new EnumMap<>(UploadStrategy.class);
         lblNumRequests = new EnumMap<>(UploadStrategy.class);
Index: src/org/openstreetmap/josm/gui/io/UploadedObjectsSummaryPanel.java
===================================================================
--- src/org/openstreetmap/josm/gui/io/UploadedObjectsSummaryPanel.java	(revision 18366)
+++ src/org/openstreetmap/josm/gui/io/UploadedObjectsSummaryPanel.java	(working copy)
@@ -1,7 +1,6 @@
 // License: GPL. For details, see LICENSE file.
 package org.openstreetmap.josm.gui.io;
 
-import static org.openstreetmap.josm.tools.I18n.tr;
 import static org.openstreetmap.josm.tools.I18n.trn;
 
 import java.awt.GridBagConstraints;
@@ -8,12 +7,11 @@
 import java.awt.GridBagLayout;
 import java.awt.event.MouseAdapter;
 import java.awt.event.MouseEvent;
-import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
-import java.util.Optional;
 
-import javax.swing.AbstractListModel;
+import javax.swing.BoxLayout;
+import javax.swing.DefaultListModel;
 import javax.swing.JLabel;
 import javax.swing.JList;
 import javax.swing.JPanel;
@@ -20,6 +18,7 @@
 import javax.swing.JScrollPane;
 
 import org.openstreetmap.josm.actions.AutoScaleAction;
+import org.openstreetmap.josm.data.APIDataSet;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
 import org.openstreetmap.josm.gui.PrimitiveRenderer;
 
@@ -28,180 +27,83 @@
  * @since 2599
  */
 public class UploadedObjectsSummaryPanel extends JPanel {
-    /** the list with the added primitives */
-    private PrimitiveList lstAdd;
-    private JLabel lblAdd;
-    private JScrollPane spAdd;
-    /** the list with the updated primitives */
-    private PrimitiveList lstUpdate;
-    private JLabel lblUpdate;
-    private JScrollPane spUpdate;
-    /** the list with the deleted primitives */
-    private PrimitiveList lstDelete;
-    private JLabel lblDelete;
-    private JScrollPane spDelete;
+    /**
+     * Zooms to the primitive on double-click
+     */
+    private static MouseAdapter mouseListener = new MouseAdapter() {
+        @Override
+        public void mouseClicked(MouseEvent evt) {
+            if (evt.getButton() == MouseEvent.BUTTON1 && evt.getClickCount() == 2) {
+                @SuppressWarnings("unchecked")
+                JList<OsmPrimitive> list = (JList<OsmPrimitive>) evt.getSource();
+                int index = list.locationToIndex(evt.getPoint());
+                AutoScaleAction.zoomTo(Collections.singleton(list.getModel().getElementAt(index)));
+            }
+        }
+    };
 
     /**
+     * A JLabel and a JList
+     */
+    private static class ListPanel extends JPanel {
+        /**
+         * Constructor
+         *
+         * @param primitives the list of primitives
+         * @param singular the singular form of the label
+         * @param plural the plural form of the label
+         */
+        ListPanel(List<OsmPrimitive> primitives, String singular, String plural) {
+            DefaultListModel<OsmPrimitive> model = new DefaultListModel<>();
+            JList<OsmPrimitive> jList = new JList<>(model);
+            primitives.forEach(model::addElement);
+            jList.setCellRenderer(new PrimitiveRenderer());
+            jList.addMouseListener(mouseListener);
+            JScrollPane scrollPane = new JScrollPane(jList);
+            JLabel label = new JLabel(trn(singular, plural, model.size(), model.size()));
+            label.setLabelFor(jList);
+            this.setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS));
+            this.add(label);
+            this.add(scrollPane);
+        }
+    }
+
+    /**
      * Constructs a new {@code UploadedObjectsSummaryPanel}.
      */
     public UploadedObjectsSummaryPanel() {
-        build();
+        super(new GridBagLayout());
     }
 
-    protected void build() {
-        setLayout(new GridBagLayout());
-        PrimitiveRenderer renderer = new PrimitiveRenderer();
-        MouseAdapter mouseListener = new MouseAdapter() {
-            @Override
-            public void mouseClicked(MouseEvent evt) {
-                if (evt.getButton() == MouseEvent.BUTTON1 && evt.getClickCount() == 2) {
-                    PrimitiveList list = (PrimitiveList) evt.getSource();
-                    int index = list.locationToIndex(evt.getPoint());
-                    AutoScaleAction.zoomTo(Collections.singleton(list.getModel().getElementAt(index)));
-                }
-            }
-        };
-        // initialize the three lists for uploaded primitives, but don't add them to the dialog yet, see setUploadedPrimitives()
-        //
-        lstAdd = new PrimitiveList();
-        lstAdd.setCellRenderer(renderer);
-        lstAdd.addMouseListener(mouseListener);
-        lstAdd.setVisibleRowCount(Math.min(lstAdd.getModel().getSize(), 10));
-        spAdd = new JScrollPane(lstAdd);
-        lblAdd = new JLabel(tr("Objects to add:"));
-        lblAdd.setLabelFor(lstAdd);
-
-        lstUpdate = new PrimitiveList();
-        lstUpdate.setCellRenderer(renderer);
-        lstUpdate.addMouseListener(mouseListener);
-        lstUpdate.setVisibleRowCount(Math.min(lstUpdate.getModel().getSize(), 10));
-        spUpdate = new JScrollPane(lstUpdate);
-        lblUpdate = new JLabel(tr("Objects to modify:"));
-        lblUpdate.setLabelFor(lstUpdate);
-
-        lstDelete = new PrimitiveList();
-        lstDelete.setCellRenderer(renderer);
-        lstDelete.addMouseListener(mouseListener);
-        lstDelete.setVisibleRowCount(Math.min(lstDelete.getModel().getSize(), 10));
-        spDelete = new JScrollPane(lstDelete);
-        lblDelete = new JLabel(tr("Objects to delete:"));
-        lblDelete.setLabelFor(lstDelete);
-    }
-
     /**
-     * Sets the collections of primitives which will be uploaded
+     * Builds the panel
      *
-     * @param add  the collection of primitives to add
-     * @param update the collection of primitives to update
-     * @param delete the collection of primitives to delete
+     * @param toUpload the primitives to upload
      */
-    public void setUploadedPrimitives(List<OsmPrimitive> add, List<OsmPrimitive> update, List<OsmPrimitive> delete) {
-        lstAdd.getPrimitiveListModel().setPrimitives(add);
-        lstUpdate.getPrimitiveListModel().setPrimitives(update);
-        lstDelete.getPrimitiveListModel().setPrimitives(delete);
-
-        GridBagConstraints gcLabel = new GridBagConstraints();
-        gcLabel.fill = GridBagConstraints.HORIZONTAL;
-        gcLabel.weightx = 1.0;
-        gcLabel.weighty = 0.0;
-        gcLabel.anchor = GridBagConstraints.FIRST_LINE_START;
-
+    public void build(APIDataSet toUpload) {
         GridBagConstraints gcList = new GridBagConstraints();
         gcList.fill = GridBagConstraints.BOTH;
         gcList.weightx = 1.0;
         gcList.weighty = 1.0;
         gcList.anchor = GridBagConstraints.CENTER;
+
         removeAll();
-        int y = -1;
-        if (!add.isEmpty()) {
-            y++;
-            gcLabel.gridy = y;
-            lblAdd.setText(trn("{0} object to add:", "{0} objects to add:", add.size(), add.size()));
-            add(lblAdd, gcLabel);
-            y++;
-            gcList.gridy = y;
-            add(spAdd, gcList);
+        List<OsmPrimitive> list = toUpload.getPrimitivesToAdd();
+        if (!list.isEmpty()) {
+            gcList.gridy++;
+            add(new ListPanel(list, "{0} object to add:", "{0} objects to add:"), gcList);
         }
-        if (!update.isEmpty()) {
-            y++;
-            gcLabel.gridy = y;
-            lblUpdate.setText(trn("{0} object to modify:", "{0} objects to modify:", update.size(), update.size()));
-            add(lblUpdate, gcLabel);
-            y++;
-            gcList.gridy = y;
-            add(spUpdate, gcList);
+        list = toUpload.getPrimitivesToUpdate();
+        if (!list.isEmpty()) {
+            gcList.gridy++;
+            add(new ListPanel(list, "{0} object to modify:", "{0} objects to modify:"), gcList);
         }
-        if (!delete.isEmpty()) {
-            y++;
-            gcLabel.gridy = y;
-            lblDelete.setText(trn("{0} object to delete:", "{0} objects to delete:", delete.size(), delete.size()));
-            add(lblDelete, gcLabel);
-            y++;
-            gcList.gridy = y;
-            add(spDelete, gcList);
+        list = toUpload.getPrimitivesToDelete();
+        if (!list.isEmpty()) {
+            gcList.gridy++;
+            add(new ListPanel(list, "{0} object to delete:", "{0} objects to delete:"), gcList);
         }
         revalidate();
+        repaint();
     }
-
-    /**
-     * Replies the number of objects to upload
-     *
-     * @return the number of objects to upload
-     */
-    public int getNumObjectsToUpload() {
-        return lstAdd.getModel().getSize()
-        + lstUpdate.getModel().getSize()
-        + lstDelete.getModel().getSize();
-    }
-
-    /**
-     * A simple list of OSM primitives.
-     */
-    static class PrimitiveList extends JList<OsmPrimitive> {
-        /**
-         * Constructs a new {@code PrimitiveList}.
-         */
-        PrimitiveList() {
-            super(new PrimitiveListModel());
-        }
-
-        public PrimitiveListModel getPrimitiveListModel() {
-            return (PrimitiveListModel) getModel();
-        }
-    }
-
-    /**
-     * A list model for a list of OSM primitives.
-     */
-    static class PrimitiveListModel extends AbstractListModel<OsmPrimitive> {
-        private transient List<OsmPrimitive> primitives;
-
-        /**
-         * Constructs a new {@code PrimitiveListModel}.
-         */
-        PrimitiveListModel() {
-            primitives = new ArrayList<>();
-        }
-
-        PrimitiveListModel(List<OsmPrimitive> primitives) {
-            setPrimitives(primitives);
-        }
-
-        public void setPrimitives(List<OsmPrimitive> primitives) {
-            this.primitives = Optional.ofNullable(primitives).orElseGet(ArrayList::new);
-            fireContentsChanged(this, 0, getSize());
-        }
-
-        @Override
-        public OsmPrimitive getElementAt(int index) {
-            if (primitives == null) return null;
-            return primitives.get(index);
-        }
-
-        @Override
-        public int getSize() {
-            if (primitives == null) return 0;
-            return primitives.size();
-        }
-    }
 }
Index: src/org/openstreetmap/josm/gui/layer/OsmDataLayer.java
===================================================================
--- src/org/openstreetmap/josm/gui/layer/OsmDataLayer.java	(revision 18366)
+++ src/org/openstreetmap/josm/gui/layer/OsmDataLayer.java	(working copy)
@@ -1280,7 +1280,7 @@
     @Override
     public AbstractUploadDialog getUploadDialog() {
         UploadDialog dialog = UploadDialog.getUploadDialog();
-        dialog.setUploadedPrimitives(new APIDataSet(data));
+        dialog.initLifeCycle(data, new APIDataSet(data));
         return dialog;
     }
 
Index: src/org/openstreetmap/josm/gui/preferences/ToolbarPreferences.java
===================================================================
--- src/org/openstreetmap/josm/gui/preferences/ToolbarPreferences.java	(revision 18366)
+++ src/org/openstreetmap/josm/gui/preferences/ToolbarPreferences.java	(working copy)
@@ -23,11 +23,13 @@
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
+import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 
 import javax.swing.AbstractAction;
@@ -238,6 +240,9 @@
         }
     }
 
+    /**
+     * Parses strings into action definitions and back.
+     */
     public static class ActionParser {
         private final Map<String, Action> actions;
         private final StringBuilder result = new StringBuilder();
@@ -274,7 +279,8 @@
         }
 
         /**
-         * Loads the action definition from its toolbar name.
+         * Parses an action definition from a string.
+         *
          * @param actionName action toolbar name
          * @return action definition or null
          */
@@ -353,6 +359,12 @@
             }
         }
 
+        /**
+         * Unparses an action definition
+         *
+         * @param action the given action
+         * @return the action as string
+         */
         @SuppressWarnings("unchecked")
         public String saveAction(ActionDefinition action) {
             result.setLength(0);
@@ -397,8 +409,8 @@
                 }
                 if (!first) {
                     result.append('}');
+                }
             }
-            }
 
             return result.toString();
         }
@@ -570,6 +582,7 @@
     }
 
     private final ToolbarPopupMenu popupMenu = new ToolbarPopupMenu();
+    private boolean showInfoAboutMissingActions;
 
     /**
      * Key: Registered name (property "toolbar" of action).
@@ -577,15 +590,13 @@
      */
     private final Map<String, Action> regactions = new ConcurrentHashMap<>();
 
-    private final DefaultMutableTreeNode rootActionsNode = new DefaultMutableTreeNode(tr("Actions"));
-
+    /** the swing component for the toolbar */
     public final JToolBar control = new JToolBar();
     private final Map<Object, ActionDefinition> buttonActions = new ConcurrentHashMap<>(30);
-    private boolean showInfoAboutMissingActions;
 
     @Override
     public PreferenceSetting createPreferenceSetting() {
-        return new Settings(rootActionsNode);
+        return new Settings(loadActions(MainApplication.getMenu(), regactions));
     }
 
     /**
@@ -1025,8 +1036,14 @@
         TaggingPresets.addListener(this);
     }
 
-    private static void loadAction(DefaultMutableTreeNode node, MenuElement menu, Map<String, Action> actionsInMenu) {
-        Object userObject = null;
+    /**
+     * Recursive part of {@link #loadActions}.
+     *
+     * @param node the parent node
+     * @param seen accumulator for all seen actions
+     * @param menu the menu to harvest
+     */
+    private void loadAction(DefaultMutableTreeNode node, Set<Action> seen, MenuElement menu) {
         MenuElement menuElement = menu;
         if (menu.getSubElements().length > 0 &&
                 menu.getSubElements()[0] instanceof JPopupMenu) {
@@ -1034,10 +1051,12 @@
         }
         for (MenuElement item : menuElement.getSubElements()) {
             if (item instanceof JMenuItem) {
+                DefaultMutableTreeNode newNode = null;
                 JMenuItem menuItem = (JMenuItem) item;
                 if (menuItem.getAction() != null) {
                     Action action = menuItem.getAction();
-                    userObject = action;
+                    newNode = new DefaultMutableTreeNode(action);
+                    seen.add(action);
                     Object tb = action.getValue("toolbar");
                     if (tb == null) {
                         Logging.info(tr("Toolbar action without name: {0}",
@@ -1049,34 +1068,38 @@
                             action.getClass().getName()));
                         }
                         continue;
-                    } else {
-                        String toolbar = (String) tb;
-                        Action r = actionsInMenu.get(toolbar);
-                        if (r != null && r != action && !toolbar.startsWith(IMAGERY_PREFIX)) {
-                            Logging.info(tr("Toolbar action {0} overwritten: {1} gets {2}",
-                            toolbar, r.getClass().getName(), action.getClass().getName()));
-                        }
-                        actionsInMenu.put(toolbar, action);
                     }
                 } else {
-                    userObject = menuItem.getText();
+                    newNode = new DefaultMutableTreeNode(menuItem.getText());
                 }
+                node.add(newNode);
+                loadAction(newNode, seen, item);
             }
-            DefaultMutableTreeNode newNode = new DefaultMutableTreeNode(userObject);
-            node.add(newNode);
-            loadAction(newNode, item, actionsInMenu);
         }
     }
 
-    private void loadActions(Map<String, Action> actionsInMenu) {
-        rootActionsNode.removeAllChildren();
-        loadAction(rootActionsNode, MainApplication.getMenu(), actionsInMenu);
-        for (Map.Entry<String, Action> a : regactions.entrySet()) {
-            if (actionsInMenu.get(a.getKey()) == null) {
-                rootActionsNode.add(new DefaultMutableTreeNode(a.getValue()));
+    /**
+     * Builds a JTree root node of known actions.
+     * <p>
+     * Builds a {@link JTree} with the same structure as the given menu, then adds all registered actions
+     * that are not already in the tree.
+     *
+     * @param menu the menu to scan for actions
+     * @param registeredActions the registered actions
+     * @return the root tree node of a JTree
+     */
+    private DefaultMutableTreeNode loadActions(MenuElement menu, Map<String, Action> registeredActions) {
+        final DefaultMutableTreeNode rootActionsNode = new DefaultMutableTreeNode(tr("Actions"));
+        final HashSet<Action> seen = new HashSet<>();
+
+        loadAction(rootActionsNode, seen, menu);
+        registeredActions.forEach((key, action) -> {
+            if (!seen.contains(action)) {
+                rootActionsNode.add(new DefaultMutableTreeNode(action));
             }
-        }
+        });
         rootActionsNode.add(new DefaultMutableTreeNode(null));
+        return rootActionsNode;
     }
 
     private static final String[] deftoolbar = {"open", "save", "download", "upload", "|",
@@ -1088,6 +1111,10 @@
     "tagginggroup_Facilities/Food+Drinks", "|", "tagginggroup_Man Made/Historic Places", "|",
     "tagginggroup_Man Made/Man Made"};
 
+    /**
+     * Returns the configured toolbar strings or {@link #deftoolbar default ones}.
+     * @return the toolstring
+     */
     public static Collection<String> getToolString() {
         Collection<String> toolStr = Config.getPref().getList("toolbar", Arrays.asList(deftoolbar));
         if (Utils.isEmpty(toolStr)) {
@@ -1097,14 +1124,7 @@
     }
 
     private Collection<ActionDefinition> getDefinedActions() {
-        Map<String, Action> actionsInMenu = new ConcurrentHashMap<>();
-
-        loadActions(actionsInMenu);
-
-        Map<String, Action> allActions = new ConcurrentHashMap<>(regactions);
-        allActions.putAll(actionsInMenu);
-        ActionParser actionParser = new ActionParser(allActions);
-
+        ActionParser actionParser = new ActionParser(regactions);
         Collection<ActionDefinition> result = new ArrayList<>();
 
         for (String s : getToolString()) {
@@ -1139,8 +1159,6 @@
                 Logging.info(tr("Registered toolbar action {0} overwritten: {1} gets {2}",
                     toolbar, r.getClass().getName(), action.getClass().getName()));
             }
-        }
-        if (toolbar != null) {
             regactions.put(toolbar, action);
         }
         return action;
@@ -1250,8 +1268,8 @@
             sc = ((JosmAction) action.getAction()).getShortcut();
             if (sc.getAssignedKey() == KeyEvent.CHAR_UNDEFINED) {
                 sc = null;
+            }
         }
-        }
 
         long paramCode = 0;
         if (action.hasParameters()) {
Index: src/org/openstreetmap/josm/gui/preferences/map/TaggingPresetPreference.java
===================================================================
--- src/org/openstreetmap/josm/gui/preferences/map/TaggingPresetPreference.java	(revision 18366)
+++ src/org/openstreetmap/josm/gui/preferences/map/TaggingPresetPreference.java	(working copy)
@@ -31,7 +31,6 @@
 import org.openstreetmap.josm.gui.preferences.PreferenceTabbedPane.PreferencePanel;
 import org.openstreetmap.josm.gui.preferences.PreferenceTabbedPane.ValidationListener;
 import org.openstreetmap.josm.gui.preferences.SourceEditor;
-import org.openstreetmap.josm.gui.tagging.presets.TaggingPreset;
 import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetReader;
 import org.openstreetmap.josm.gui.tagging.presets.TaggingPresets;
 import org.openstreetmap.josm.spi.preferences.Config;
@@ -58,7 +57,7 @@
                         i++;
                         boolean canLoad = false;
                         try {
-                            TaggingPresetReader.readAll(source.url, false);
+                            TaggingPresetReader.read(source.url, false);
                             canLoad = true;
                         } catch (IOException e) {
                             Logging.log(Logging.LEVEL_WARN, tr("Could not read tagging preset source: {0}", source), e);
@@ -82,7 +81,7 @@
                         String errorMessage = null;
 
                         try {
-                            TaggingPresetReader.readAll(source.url, true);
+                            TaggingPresetReader.read(source.url, true);
                         } catch (IOException e) {
                             // Should not happen, but at least show message
                             String msg = tr("Could not read tagging preset source: {0}", source);
@@ -170,8 +169,8 @@
 
     @Override
     public void addGui(PreferenceTabbedPane gui) {
-        useValidator = new JCheckBox(tr("Run data validator on user input"), TaggingPreset.USE_VALIDATOR.get());
-        sortMenu = new JCheckBox(tr("Sort presets menu alphabetically"), TaggingPresets.SORT_MENU.get());
+        useValidator = new JCheckBox(tr("Run data validator on user input"), TaggingPresets.USE_VALIDATOR.get());
+        sortMenu = new JCheckBox(tr("Sort presets menu alphabetically"), TaggingPresets.SORT_VALUES.get());
 
         final JPanel panel = new JPanel(new GridBagLayout());
         panel.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0));
@@ -251,8 +250,8 @@
 
     @Override
     public boolean ok() {
-        TaggingPreset.USE_VALIDATOR.put(useValidator.isSelected());
-        if (sources.finish() || TaggingPresets.SORT_MENU.put(sortMenu.isSelected())) {
+        TaggingPresets.USE_VALIDATOR.put(useValidator.isSelected());
+        if (sources.finish() || TaggingPresets.SORT_VALUES.put(sortMenu.isSelected())) {
             TaggingPresets.destroy();
             TaggingPresets.initialize();
         }
Index: src/org/openstreetmap/josm/gui/tagging/TagEditorModel.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/TagEditorModel.java	(revision 18366)
+++ src/org/openstreetmap/josm/gui/tagging/TagEditorModel.java	(working copy)
@@ -145,11 +145,20 @@
     }
 
     @Override
-    public Object getValueAt(int rowIndex, int columnIndex) {
-        if (rowIndex >= getRowCount())
-            throw new IndexOutOfBoundsException("unexpected rowIndex: rowIndex=" + rowIndex);
-
-        return tags.get(rowIndex);
+    public Object getValueAt(int row, int col) {
+        if (row >= getRowCount())
+            throw new IndexOutOfBoundsException("unexpected row: row=" + row);
+        if (col >= getColumnCount())
+            throw new IndexOutOfBoundsException("unexpected col: col=" + col);
+        TagModel tag = get(row);
+        switch(col) {
+            case 0:
+                return tag.getName();
+            case 1:
+                return tag.getValue();
+            default: // Do nothing
+        }
+        return null;
     }
 
     @Override
Index: src/org/openstreetmap/josm/gui/tagging/TagEditorPanel.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/TagEditorPanel.java	(revision 18366)
+++ src/org/openstreetmap/josm/gui/tagging/TagEditorPanel.java	(working copy)
@@ -21,12 +21,8 @@
 import org.openstreetmap.josm.gui.dialogs.properties.HelpAction;
 import org.openstreetmap.josm.gui.dialogs.properties.HelpTagAction;
 import org.openstreetmap.josm.gui.dialogs.properties.PresetListPanel;
-import org.openstreetmap.josm.gui.layer.OsmDataLayer;
-import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionList;
-import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionManager;
 import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetHandler;
 import org.openstreetmap.josm.spi.preferences.Config;
-import org.openstreetmap.josm.tools.CheckParameterUtil;
 
 /**
  * TagEditorPanel is a {@link JPanel} which can be embedded as UI component in
@@ -90,7 +86,6 @@
         JButton btn = new JButton(action);
         pnl.add(btn);
         btn.setMargin(new Insets(0, 0, 0, 0));
-        tagTable.addComponentNotStoppingCellEditing(btn);
     }
 
     /**
@@ -147,7 +142,7 @@
      * @param presetHandler tagging preset handler
      */
     public TagEditorPanel(OsmPrimitive primitive, TaggingPresetHandler presetHandler) {
-        this(new TagEditorModel().forPrimitive(primitive), presetHandler, 0);
+        this(new TagEditorModel().forPrimitive(primitive), presetHandler, -1);
     }
 
     /**
@@ -188,25 +183,11 @@
     }
 
     /**
-     * Initializes the auto completion infrastructure used in this
-     * tag editor panel. {@code layer} is the data layer from whose data set
-     * tag values are proposed as auto completion items.
-     *
-     * @param layer the data layer. Must not be null.
-     * @throws IllegalArgumentException if {@code layer} is null
+     * Returns the JTable
+     * @return the JTable
      */
-    public void initAutoCompletion(OsmDataLayer layer) {
-        CheckParameterUtil.ensureParameterNotNull(layer, "layer");
-
-        AutoCompletionManager autocomplete = AutoCompletionManager.of(layer.data);
-        AutoCompletionList acList = new AutoCompletionList();
-
-        TagCellEditor editor = (TagCellEditor) tagTable.getColumnModel().getColumn(0).getCellEditor();
-        editor.setAutoCompletionManager(autocomplete);
-        editor.setAutoCompletionList(acList);
-        editor = (TagCellEditor) tagTable.getColumnModel().getColumn(1).getCellEditor();
-        editor.setAutoCompletionManager(autocomplete);
-        editor.setAutoCompletionList(acList);
+    public TagTable getTable() {
+        return tagTable;
     }
 
     @Override
Index: src/org/openstreetmap/josm/gui/tagging/TagTable.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/TagTable.java	(revision 18366)
+++ src/org/openstreetmap/josm/gui/tagging/TagTable.java	(working copy)
@@ -5,18 +5,16 @@
 
 import java.awt.Component;
 import java.awt.Dimension;
-import java.awt.KeyboardFocusManager;
-import java.awt.Window;
 import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
 import java.awt.event.KeyEvent;
 import java.beans.PropertyChangeEvent;
 import java.beans.PropertyChangeListener;
 import java.util.Collections;
-import java.util.EventObject;
-import java.util.concurrent.CopyOnWriteArrayList;
 
 import javax.swing.AbstractAction;
 import javax.swing.CellEditor;
+import javax.swing.InputMap;
 import javax.swing.JComponent;
 import javax.swing.JTable;
 import javax.swing.KeyStroke;
@@ -24,17 +22,16 @@
 import javax.swing.SwingUtilities;
 import javax.swing.event.ListSelectionEvent;
 import javax.swing.event.ListSelectionListener;
-import javax.swing.text.JTextComponent;
+import javax.swing.table.DefaultTableCellRenderer;
+import javax.swing.table.TableCellEditor;
 
 import org.openstreetmap.josm.data.osm.Relation;
 import org.openstreetmap.josm.data.osm.TagMap;
 import org.openstreetmap.josm.gui.datatransfer.OsmTransferHandler;
 import org.openstreetmap.josm.gui.tagging.TagEditorModel.EndEditListener;
-import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionList;
-import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionManager;
+import org.openstreetmap.josm.gui.tagging.ac.AutoCompComboBox;
 import org.openstreetmap.josm.gui.widgets.JosmTable;
 import org.openstreetmap.josm.tools.ImageProvider;
-import org.openstreetmap.josm.tools.Logging;
 import org.openstreetmap.josm.tools.Utils;
 
 /**
@@ -41,59 +38,54 @@
  * This is the tabular editor component for OSM tags.
  * @since 1762
  */
-public class TagTable extends JosmTable implements EndEditListener {
-    /** the table cell editor used by this table */
-    private TagCellEditor editor;
+public class TagTable extends JosmTable implements ActionListener, EndEditListener {
     private final TagEditorModel model;
     private Component nextFocusComponent;
+    private final int LAST_COL = 1;
 
-    /** a list of components to which focus can be transferred without stopping
-     * cell editing this table.
-     */
-    private final CopyOnWriteArrayList<Component> doNotStopCellEditingWhenFocused = new CopyOnWriteArrayList<>();
-    private transient CellEditorRemover editorRemover;
+    /** the go to next cell action */
+    private final SelectNextColumnCellAction nextAction = new SelectNextColumnCellAction();
+    /** the go to previous cell action */
+    private final SelectPreviousColumnCellAction previousAction = new SelectPreviousColumnCellAction();
+    /** the delete action */
+    private final DeleteAction deleteAction = new DeleteAction();
+    /** the add action */
+    private final AddAction addAction = new AddAction();
+    /** the tag paste action */
+    private final PasteAction pasteAction = new PasteAction();
 
     /**
-     * Action to be run when the user navigates to the next cell in the table,
-     * for instance by pressing TAB or ENTER. The action alters the standard
-     * navigation path from cell to cell:
-     * <ul>
-     *   <li>it jumps over cells in the first column</li>
-     *   <li>it automatically add a new empty row when the user leaves the
-     *   last cell in the table</li>
-     * </ul>
+     * Action to be run when the user navigates to the next cell in the table, for instance by
+     * pressing TAB or ENTER. The action automatically adds a new empty row when the user leaves the
+     * last cell in the table.
      */
     class SelectNextColumnCellAction extends AbstractAction {
         @Override
         public void actionPerformed(ActionEvent e) {
-            run();
-        }
-
-        public void run() {
             int col = getSelectedColumn();
             int row = getSelectedRow();
-            if (getCellEditor() != null) {
-                getCellEditor().stopCellEditing();
-            }
 
             if (row == -1 && col == -1) {
                 requestFocusInCell(0, 0);
                 return;
             }
+            endCellEditing();
 
-            if (col == 0) {
+            if (col < LAST_COL) {
                 col++;
-            } else if (col == 1 && row < getRowCount()-1) {
+            } else if (row < getRowCount() - 1) {
                 col = 0;
                 row++;
-            } else if (col == 1 && row == getRowCount()-1) {
-                // we are at the end. Append an empty row and move the focus to its second column
-                String key = ((TagModel) model.getValueAt(row, 0)).getName();
+            } else {
+                // we are in the last cell.
+                String key = (String) model.getValueAt(row, 0);
                 if (!Utils.isStripEmpty(key)) {
+                    // append an empty row
                     model.appendNewTag();
                     col = 0;
                     row++;
                 } else {
+                    // exit the table
                     clearSelection();
                     if (nextFocusComponent != null)
                         nextFocusComponent.requestFocusInWindow();
@@ -114,16 +106,15 @@
         public void actionPerformed(ActionEvent e) {
             int col = getSelectedColumn();
             int row = getSelectedRow();
-            if (getCellEditor() != null) {
-                getCellEditor().stopCellEditing();
-            }
 
+            endCellEditing();
+
             if (col <= 0 && row <= 0) {
                 // change nothing
-            } else if (col == 1) {
+            } else if (col > 0) {
                 col--;
             } else {
-                col = 1;
+                col = LAST_COL;
                 row--;
             }
             requestFocusInCell(row, col);
@@ -131,25 +122,23 @@
     }
 
     /**
-     * Action to be run when the user invokes a delete action on the table, for
-     * instance by pressing DEL.
+     * Action to be run when the user invokes a delete action on the table, for instance by pressing
+     * DEL or hitting the "delete" button in the {@link TagEditorPanel}.
      *
-     * Depending on the shape on the current selection the action deletes individual
-     * values or entire tags from the model.
+     * Depending on the shape on the current selection the action deletes individual values or
+     * entire tags from the model.
      *
-     * If the current selection consists of cells in the second column only, the keys of
-     * the selected tags are set to the empty string.
+     * If the current selection consists of cells in the key column only, the keys of the selected
+     * tags are set to the empty string.
      *
-     * If the current selection consists of cell in the third column only, the values of the
+     * If the current selection consists of cell in the values column only, the values of the
      * selected tags are set to the empty string.
      *
-     *  If the current selection consists of cells in the second and the third column,
-     *  the selected tags are removed from the model.
+     * If the current selection consists of entire rows, the selected tags are removed from the
+     * model.
      *
-     *  This action listens to the table selection. It becomes enabled when the selection
-     *  is non-empty, otherwise it is disabled.
-     *
-     *
+     * This action listens to the table selection. It becomes enabled when the selection is
+     * non-empty, otherwise it is disabled.
      */
     class DeleteAction extends AbstractAction implements ListSelectionListener {
 
@@ -161,50 +150,22 @@
             updateEnabledState();
         }
 
-        /**
-         * delete a selection of tag names
-         */
-        protected void deleteTagNames() {
-            int[] rows = getSelectedRows();
-            model.deleteTagNames(rows);
-        }
-
-        /**
-         * delete a selection of tag values
-         */
-        protected void deleteTagValues() {
-            int[] rows = getSelectedRows();
-            model.deleteTagValues(rows);
-        }
-
-        /**
-         * delete a selection of tags
-         */
-        protected void deleteTags() {
-            int[] rows = getSelectedRows();
-            model.deleteTags(rows);
-        }
-
         @Override
         public void actionPerformed(ActionEvent e) {
-            if (!isEnabled())
-                return;
-            switch(getSelectedColumnCount()) {
+            switch (getSelectedColumnCount()) {
             case 1:
                 if (getSelectedColumn() == 0) {
-                    deleteTagNames();
+                    model.deleteTagNames(getSelectedRows());
                 } else if (getSelectedColumn() == 1) {
-                    deleteTagValues();
+                    model.deleteTagValues(getSelectedRows());
                 }
                 break;
             case 2:
-                deleteTags();
+                model.deleteTags(getSelectedRows());
                 break;
             default: // Do nothing
             }
 
-            endCellEditing();
-
             if (model.getRowCount() == 0) {
                 model.ensureOneTag();
                 requestFocusInCell(0, 0);
@@ -216,7 +177,10 @@
          */
         @Override
         public void valueChanged(ListSelectionEvent e) {
-            updateEnabledState();
+            // when the user clicks on the "delete" button the table loses focus and unselects all
+            // cells which in turn would disable the action. the delay allows the action to execute
+            // before it gets disabled
+            SwingUtilities.invokeLater(this::updateEnabledState);
         }
 
         protected final void updateEnabledState() {
@@ -243,7 +207,7 @@
                 cEditor.stopCellEditing();
             }
             final int rowIdx = model.getRowCount()-1;
-            if (rowIdx < 0 || !Utils.isStripEmpty(((TagModel) model.getValueAt(rowIdx, 0)).getName())) {
+            if (rowIdx < 0 || !Utils.isStripEmpty((String) model.getValueAt(rowIdx, 0))) {
                 model.appendNewTag();
             }
             requestFocusInCell(model.getRowCount()-1, 0);
@@ -288,15 +252,6 @@
         }
     }
 
-    /** the delete action */
-    private DeleteAction deleteAction;
-
-    /** the add action */
-    private AddAction addAction;
-
-    /** the tag paste action */
-    private PasteAction pasteAction;
-
     /**
      * Returns the delete action.
      * @return the delete action used by this table
@@ -322,121 +277,71 @@
     }
 
     /**
-     * initialize the table
-     * @param maxCharacters maximum number of characters allowed for keys and values, 0 for unlimited
-     */
-    protected final void init(final int maxCharacters) {
-        setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
-        setRowSelectionAllowed(true);
-        setColumnSelectionAllowed(true);
-        setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION);
-
-        // make ENTER behave like TAB
-        //
-        getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
-        .put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0, false), "selectNextColumnCell");
-
-        // install custom navigation actions
-        //
-        getActionMap().put("selectNextColumnCell", new SelectNextColumnCellAction());
-        getActionMap().put("selectPreviousColumnCell", new SelectPreviousColumnCellAction());
-
-        // create a delete action. Installing this action in the input and action map
-        // didn't work. We therefore handle delete requests in processKeyBindings(...)
-        //
-        deleteAction = new DeleteAction();
-
-        // create the add action
-        //
-        addAction = new AddAction();
-        getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
-        .put(KeyStroke.getKeyStroke(KeyEvent.VK_ADD, KeyEvent.CTRL_DOWN_MASK), "addTag");
-        getActionMap().put("addTag", addAction);
-
-        pasteAction = new PasteAction();
-
-        // create the table cell editor and set it to key and value columns
-        //
-        TagCellEditor tmpEditor = new TagCellEditor(maxCharacters);
-        setRowHeight(tmpEditor.getEditor().getPreferredSize().height);
-        setTagCellEditor(tmpEditor);
-    }
-
-    /**
      * Creates a new tag table
      *
      * @param model the tag editor model
-     * @param maxCharacters maximum number of characters allowed for keys and values, 0 for unlimited
+     * @param maxCharacters maximum number of characters allowed for keys and values, -1 for unlimited
      */
     public TagTable(TagEditorModel model, final int maxCharacters) {
-        super(model, new TagTableColumnModelBuilder(new TagCellRenderer(), tr("Key"), tr("Value"))
+        super(model, new TagTableColumnModelBuilder(new DefaultTableCellRenderer(), tr("Key"), tr("Value"))
                   .setSelectionModel(model.getColumnSelectionModel()).build(),
               model.getRowSelectionModel());
+
         this.model = model;
         model.setEndEditListener(this);
-        init(maxCharacters);
-    }
 
-    @Override
-    public Dimension getPreferredSize() {
-        return getPreferredFullWidthSize();
-    }
+        setAutoResizeMode(JTable.AUTO_RESIZE_NEXT_COLUMN);
+        setRowSelectionAllowed(true);
+        setColumnSelectionAllowed(true);
+        setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION);
+        putClientProperty("terminateEditOnFocusLost", Boolean.TRUE);
 
-    @Override
-    protected boolean processKeyBinding(KeyStroke ks, KeyEvent e, int condition, boolean pressed) {
+        InputMap im = getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
 
-        // handle delete key
-        //
-        if (e.getKeyCode() == KeyEvent.VK_DELETE) {
-            if (isEditing() && getSelectedColumnCount() == 1 && getSelectedRowCount() == 1)
-                // if DEL was pressed and only the currently edited cell is selected,
-                // don't run the delete action. DEL is handled by the CellEditor as normal
-                // DEL in the text input.
-                //
-                return super.processKeyBinding(ks, e, condition, pressed);
-            getDeleteAction().actionPerformed(null);
-        }
-        return super.processKeyBinding(ks, e, condition, pressed);
+        // make ENTER behave like TAB (does not work for ComboBoxes)
+        im.put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0, false), "selectNextColumnCell");
+        im.put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), "delete");
+        im.put(KeyStroke.getKeyStroke(KeyEvent.VK_ADD, KeyEvent.CTRL_DOWN_MASK), "addTag");
+
+        getActionMap().put("selectNextColumnCell", nextAction);
+        getActionMap().put("selectPreviousColumnCell", previousAction);
+        getActionMap().put("addTag", addAction);
+        getActionMap().put("delete", deleteAction);
     }
 
     /**
-     * Sets the editor autocompletion list
-     * @param autoCompletionList autocompletion list
+     * Sets a TableCellEditor for the keys column.
+     * @param editor the editor to set
      */
-    public void setAutoCompletionList(AutoCompletionList autoCompletionList) {
-        if (autoCompletionList == null)
-            return;
-        if (editor != null) {
-            editor.setAutoCompletionList(autoCompletionList);
-        }
+    public void setKeyEditor(TableCellEditor editor) {
+        if (editor instanceof AutoCompComboBox)
+            ((AutoCompComboBox<?>) editor).getActionMap().put("enterPressed", nextAction);
+        getColumnModel().getColumn(0).setCellEditor(editor);
     }
 
     /**
-     * Sets the autocompletion manager that should be used for editing the cells
-     * @param autocomplete The {@link AutoCompletionManager}
+     * Sets a TableCellEditor for the values column.
+     * @param editor the editor to set
      */
-    public void setAutoCompletionManager(AutoCompletionManager autocomplete) {
-        if (autocomplete == null) {
-            Logging.warn("argument autocomplete should not be null. Aborting.");
-            Logging.error(new Exception());
-            return;
-        }
-        if (editor != null) {
-            editor.setAutoCompletionManager(autocomplete);
-        }
+    public void setValueEditor(TableCellEditor editor) {
+        if (editor instanceof AutoCompComboBox)
+            ((AutoCompComboBox<?>) editor).getActionMap().put("enterPressed", nextAction);
+        getColumnModel().getColumn(1).setCellEditor(editor);
     }
 
-    /**
-     * Gets the {@link AutoCompletionList} the cell editor is synchronized with
-     * @return The list
-     */
-    public AutoCompletionList getAutoCompletionList() {
-        if (editor != null)
-            return editor.getAutoCompletionList();
-        else
-            return null;
+    @Override
+    public boolean getDragEnabled() {
+        // fix for comboboxes flashing when clicking the cell where the arrow button will be
+        // maybe a late focus request wants to focus the cb when the popup is already open?
+        // see: BasicTableUI#adjustSelection and mouseReleasedDND
+        return true;
     }
 
+    @Override
+    public Dimension getPreferredSize() {
+        return getPreferredFullWidthSize();
+    }
+
     /**
      * Sets the next component to request focus after navigation (with tab or enter).
      * @param nextFocusComponent next component to request focus after navigation (with tab or enter)
@@ -446,26 +351,6 @@
     }
 
     /**
-     * Gets the editor that is used for the table cells
-     * @return The editor that is used when the user wants to enter text into a cell
-     */
-    public TagCellEditor getTableCellEditor() {
-        return editor;
-    }
-
-    /**
-     * Inject a tag cell editor in the tag table
-     *
-     * @param editor tag cell editor
-     */
-    public void setTagCellEditor(TagCellEditor editor) {
-        endCellEditing();
-        this.editor = editor;
-        getColumnModel().getColumn(0).setCellEditor(editor);
-        getColumnModel().getColumn(1).setCellEditor(editor);
-    }
-
-    /**
      * Request the focus in a specific cell
      * @param row The row index
      * @param col The column index
@@ -475,70 +360,16 @@
         editCellAt(row, col);
         Component c = getEditorComponent();
         if (c != null) {
-            if (!c.requestFocusInWindow()) {
-                Logging.warn("Unable to request focus for " + c);
-            }
-            if (c instanceof JTextComponent) {
-                 ((JTextComponent) c).selectAll();
-            }
+            c.requestFocusInWindow();
         }
-        // there was a bug here - on older 1.6 Java versions Tab was not working
-        // after such activation. In 1.7 it works OK,
-        // previous solution of using awt.Robot was resetting mouse speed on Windows
     }
 
-    /**
-     * Marks a component that may be focused without stopping the cell editing
-     * @param component The component
-     */
-    public void addComponentNotStoppingCellEditing(Component component) {
-        if (component == null) return;
-        doNotStopCellEditingWhenFocused.addIfAbsent(component);
-    }
-
-    /**
-     * Removes a component added with {@link #addComponentNotStoppingCellEditing(Component)}
-     * @param component The component
-     */
-    public void removeComponentNotStoppingCellEditing(Component component) {
-        if (component == null) return;
-        doNotStopCellEditingWhenFocused.remove(component);
-    }
-
     @Override
-    public boolean editCellAt(int row, int column, EventObject e) {
-
-        // a snipped copied from the Java 1.5 implementation of JTable
-        //
-        if (cellEditor != null && !cellEditor.stopCellEditing())
-            return false;
-
-        if (row < 0 || row >= getRowCount() ||
-                column < 0 || column >= getColumnCount())
-            return false;
-
-        if (!isCellEditable(row, column))
-            return false;
-
-        // make sure our custom implementation of CellEditorRemover is created
-        if (editorRemover == null) {
-            KeyboardFocusManager fm =
-                KeyboardFocusManager.getCurrentKeyboardFocusManager();
-            editorRemover = new CellEditorRemover(fm);
-            fm.addPropertyChangeListener("permanentFocusOwner", editorRemover);
-        }
-
-        // delegate to the default implementation
-        return super.editCellAt(row, column, e);
-    }
-
-    @Override
     public void endCellEditing() {
-        if (isEditing()) {
-            CellEditor cEditor = getCellEditor();
-            if (cEditor != null) {
-                // First attempt to commit. If this does not work, cancel.
-                cEditor.stopCellEditing();
+        TableCellEditor cEditor = getCellEditor();
+        if (cEditor != null) {
+            // First attempt to commit. If this does not work, cancel.
+            if (!cEditor.stopCellEditing()) {
                 cEditor.cancelCellEditing();
             }
         }
@@ -545,62 +376,10 @@
     }
 
     @Override
-    public void removeEditor() {
-        // make sure we unregister our custom implementation of CellEditorRemover
-        KeyboardFocusManager.getCurrentKeyboardFocusManager().
-        removePropertyChangeListener("permanentFocusOwner", editorRemover);
-        editorRemover = null;
-        super.removeEditor();
-    }
-
-    @Override
-    public void removeNotify() {
-        // make sure we unregister our custom implementation of CellEditorRemover
-        KeyboardFocusManager.getCurrentKeyboardFocusManager().
-        removePropertyChangeListener("permanentFocusOwner", editorRemover);
-        editorRemover = null;
-        super.removeNotify();
-    }
-
-    /**
-     * This is a custom implementation of the CellEditorRemover used in JTable
-     * to handle the client property <code>terminateEditOnFocusLost</code>.
-     *
-     * This implementation also checks whether focus is transferred to one of a list
-     * of dedicated components, see {@link TagTable#doNotStopCellEditingWhenFocused}.
-     * A typical example for such a component is a button in {@link TagEditorPanel}
-     * which isn't a child component of {@link TagTable} but which should respond to
-     * to focus transfer in a similar way to a child of TagTable.
-     *
-     */
-    class CellEditorRemover implements PropertyChangeListener {
-        private final KeyboardFocusManager focusManager;
-
-        CellEditorRemover(KeyboardFocusManager fm) {
-            this.focusManager = fm;
+    public void actionPerformed(ActionEvent e) {
+        if ("enterPressed".equals(e.getActionCommand())) {
+            // make ENTER in combobox behave like TAB
+            nextAction.actionPerformed(e);
         }
-
-        @Override
-        public void propertyChange(PropertyChangeEvent ev) {
-            if (!isEditing())
-                return;
-
-            Component c = focusManager.getPermanentFocusOwner();
-            while (c != null) {
-                if (c == TagTable.this)
-                    // focus remains inside the table
-                    return;
-                if (doNotStopCellEditingWhenFocused.contains(c))
-                    // focus remains on one of the associated components
-                    return;
-                else if (c instanceof Window) {
-                    if (c == SwingUtilities.getRoot(TagTable.this) && !getCellEditor().stopCellEditing()) {
-                        getCellEditor().cancelCellEditing();
-                    }
-                    break;
-                }
-                c = c.getParent();
-            }
-        }
     }
 }
Index: src/org/openstreetmap/josm/gui/tagging/ac/AutoCompComboBox.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/ac/AutoCompComboBox.java	(revision 18366)
+++ src/org/openstreetmap/josm/gui/tagging/ac/AutoCompComboBox.java	(working copy)
@@ -1,11 +1,18 @@
 // License: GPL. For details, see LICENSE file.
 package org.openstreetmap.josm.gui.tagging.ac;
 
+import java.awt.Component;
+import java.awt.event.MouseEvent;
 import java.awt.im.InputContext;
+import java.util.EventObject;
 import java.util.Locale;
 
 import javax.swing.ComboBoxEditor;
+import javax.swing.JTable;
+import javax.swing.event.CellEditorListener;
+import javax.swing.table.TableCellEditor;
 
+import org.openstreetmap.josm.gui.util.CellEditorSupport;
 import org.openstreetmap.josm.gui.widgets.JosmComboBox;
 import org.openstreetmap.josm.tools.Logging;
 
@@ -21,7 +28,7 @@
  * @param <E> the type of the combobox entries
  * @since 18173
  */
-public class AutoCompComboBox<E> extends JosmComboBox<E> implements AutoCompListener {
+public class AutoCompComboBox<E> extends JosmComboBox<E> implements TableCellEditor, AutoCompListener {
 
     /** force a different keyboard input locale for the editor */
     private boolean useFixedLocale;
@@ -45,6 +52,7 @@
         setEditable(true);
         getEditorComponent().setModel(model);
         getEditorComponent().addAutoCompListener(this);
+        tableCellEditorSupport = new CellEditorSupport(this);
     }
 
     /**
@@ -91,7 +99,8 @@
         // Save the text in case item is null, because setSelectedItem will erase it.
         String savedText = getText();
         setSelectedItem(item);
-        setText(savedText);
+        if (item == null)
+            setText(savedText);
     }
 
     /**
@@ -140,7 +149,9 @@
         return super.getInputContext();
     }
 
-    /** AutoCompListener Interface */
+    /* ------------------------------------------------------------------------------------ */
+    /* AutoCompListener interface                                                           */
+    /* ------------------------------------------------------------------------------------ */
 
     @Override
     public void autoCompBefore(AutoCompEvent e) {
@@ -150,4 +161,78 @@
     public void autoCompPerformed(AutoCompEvent e) {
         autocomplete(e.getItem());
     }
+
+    /* ------------------------------------------------------------------------------------ */
+    /* TableCellEditor interface                                                            */
+    /* ------------------------------------------------------------------------------------ */
+
+    private transient CellEditorSupport tableCellEditorSupport;
+    private String originalValue;
+
+    @Override
+    public void addCellEditorListener(CellEditorListener l) {
+        tableCellEditorSupport.addCellEditorListener(l);
+    }
+
+    protected void rememberOriginalValue(String value) {
+        this.originalValue = value;
+    }
+
+    protected void restoreOriginalValue() {
+        setText(originalValue);
+    }
+
+    @Override
+    public void removeCellEditorListener(CellEditorListener l) {
+        tableCellEditorSupport.removeCellEditorListener(l);
+    }
+
+    @Override
+    public void cancelCellEditing() {
+        restoreOriginalValue();
+        tableCellEditorSupport.fireEditingCanceled();
+    }
+
+    @Override
+    public Object getCellEditorValue() {
+        return getText();
+    }
+
+    /**
+    * Returns true if <code>anEvent</code> is <b>not</b> a <code>MouseEvent</code>.  Otherwise, it
+    * returns true if the necessary number of clicks have occurred, and returns false otherwise.
+    *
+    * @param   anEvent         the event
+    * @return  true  if cell is ready for editing, false otherwise
+    * @see #shouldSelectCell
+    */
+    @Override
+    public boolean isCellEditable(EventObject anEvent) {
+        if (anEvent instanceof MouseEvent) {
+            return ((MouseEvent) anEvent).getClickCount() >= 1;
+        }
+        return true;
+    }
+
+    @Override
+    public boolean shouldSelectCell(EventObject anEvent) {
+        if (anEvent instanceof MouseEvent) {
+            MouseEvent e = (MouseEvent) anEvent;
+            return e.getID() != MouseEvent.MOUSE_DRAGGED;
+        }
+        return true;
+    }
+
+    @Override
+    public boolean stopCellEditing() {
+        tableCellEditorSupport.fireEditingStopped();
+        return true;
+    }
+
+    @Override
+    public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
+        setText(value == null ? "" : value.toString());
+        rememberOriginalValue(getText());
+        return this;
+    }
 }
Index: src/org/openstreetmap/josm/gui/tagging/ac/AutoCompListener.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/ac/AutoCompListener.java	(revision 18366)
+++ src/org/openstreetmap/josm/gui/tagging/ac/AutoCompListener.java	(working copy)
@@ -4,15 +4,19 @@
 import java.util.EventListener;
 
 /**
- * The listener interface for receiving autoComp events.
- * The class that is interested in processing an autoComp event
- * implements this interface, and the object created with that
- * class is registered with a component, using the component's
- * <code>addAutoCompListener</code> method. When the autoComp event
- * occurs, that object's <code>autoCompPerformed</code> method is
- * invoked.
+ * The listener interface for receiving AutoCompEvent events.
+ * <p>
+ * The class that is interested in processing an {@link AutoCompEvent} implements this interface,
+ * and the object created with that class is registered with an autocompleting component using the
+ * autocompleting component's {@link AutoCompTextField#addAutoCompListener addAutoCompListener}
+ * method.
+ * <p>
+ * Before the autocompletion searches for candidates, the listener's {@code autoCompBefore} method
+ * is invoked. It can be used to initialize the {@link AutoCompComboBoxModel}. After the
+ * autocompletion occured the listener's {@code autoCompPerformed} method is invoked. It is used eg.
+ * for adjusting the selection of an {@link AutoCompComboBox} after its {@link AutoCompTextField}
+ * has autocompleted.
  *
- * @see AutoCompEvent
  * @since 18221
  */
 public interface AutoCompListener extends EventListener {
Index: src/org/openstreetmap/josm/gui/tagging/ac/AutoCompletionManager.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/ac/AutoCompletionManager.java	(revision 18366)
+++ src/org/openstreetmap/josm/gui/tagging/ac/AutoCompletionManager.java	(working copy)
@@ -6,8 +6,8 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
+import java.util.EnumSet;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
@@ -16,6 +16,7 @@
 import java.util.Set;
 import java.util.function.Function;
 import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 import org.openstreetmap.josm.data.osm.DataSet;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
@@ -39,17 +40,20 @@
 import org.openstreetmap.josm.gui.layer.LayerManager.LayerOrderChangeEvent;
 import org.openstreetmap.josm.gui.layer.LayerManager.LayerRemoveEvent;
 import org.openstreetmap.josm.gui.layer.OsmDataLayer;
+import org.openstreetmap.josm.gui.tagging.presets.Item;
+import org.openstreetmap.josm.gui.tagging.presets.KeyedItem;
+import org.openstreetmap.josm.gui.tagging.presets.Role;
 import org.openstreetmap.josm.gui.tagging.presets.TaggingPreset;
+import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetType;
 import org.openstreetmap.josm.gui.tagging.presets.TaggingPresets;
-import org.openstreetmap.josm.gui.tagging.presets.items.Roles.Role;
 import org.openstreetmap.josm.tools.CheckParameterUtil;
 import org.openstreetmap.josm.tools.MultiMap;
 import org.openstreetmap.josm.tools.Utils;
 
 /**
- * AutoCompletionManager holds a cache of keys with a list of
- * possible auto completion values for each key.
- *
+ * AutoCompletionManager holds a cache of keys with a list of possible auto completion values for
+ * each key.
+ * <p>
  * Each DataSet can be assigned one AutoCompletionManager instance such that
  * <ol>
  *   <li>any key used in a tag in the data set is part of the key list in the cache</li>
@@ -56,12 +60,9 @@
  *   <li>any value used in a tag for a specific key is part of the autocompletion list of this key</li>
  * </ol>
  *
- * Building up auto completion lists should not
- * slow down tabbing from input field to input field. Looping through the complete
- * data set in order to build up the auto completion list for a specific input
- * field is not efficient enough, hence this cache.
- *
- * TODO: respect the relation type for member role autocompletion
+ * Building up auto completion lists should not slow down tabbing from input field to input field.
+ * Looping through the complete data set in order to build up the auto completion list for a
+ * specific input field is not efficient enough, hence this cache.
  */
 public class AutoCompletionManager implements DataSetListener {
 
@@ -105,6 +106,12 @@
         }
     }
 
+    /**
+     * Compares two AutoCompletionItems alphabetically.
+     */
+    public static final Comparator<AutoCompletionItem> ALPHABETIC_COMPARATOR =
+        (ac1, ac2) -> String.CASE_INSENSITIVE_ORDER.compare(ac1.getValue(), ac2.getValue());
+
     /** If the dirty flag is set true, a rebuild is necessary. */
     protected boolean dirty;
     /** The data set that is managed */
@@ -115,32 +122,22 @@
      * only accessed by getTagCache(), rebuild() and cachePrimitiveTags()
      * use getTagCache() accessor
      */
-    protected MultiMap<String, String> tagCache;
+    protected final MultiMap<String, String> TAG_CACHE = new MultiMap<>();
 
     /**
-     * the same as tagCache but for the preset keys and values can be accessed directly
-     */
-    static final MultiMap<String, String> PRESET_TAG_CACHE = new MultiMap<>();
-
-    /**
      * Cache for tags that have been entered by the user.
      */
     static final Set<UserInputTag> USER_INPUT_TAG_CACHE = new LinkedHashSet<>();
 
     /**
-     * the cached list of member roles
-     * only accessed by getRoleCache(), rebuild() and cacheRelationMemberRoles()
-     * use getRoleCache() accessor
+     * The cached relations by {@link #getRelationType(Map) relation type}.
      */
-    protected Set<String> roleCache;
+    protected final MultiMap<String, Relation> RELATION_CACHE = new MultiMap<>();
 
-    /**
-     * the same as roleCache but for the preset roles can be accessed directly
-     */
-    static final Set<String> PRESET_ROLE_CACHE = new HashSet<>();
-
     private static final Map<DataSet, AutoCompletionManager> INSTANCES = new HashMap<>();
 
+    private static final Map<String, String> EMPTY_MAP = Collections.emptyMap();
+
     /**
      * Constructs a new {@code AutoCompletionManager}.
      * @param ds data set
@@ -156,15 +153,15 @@
             rebuild();
             dirty = false;
         }
-        return tagCache;
+        return TAG_CACHE;
     }
 
-    protected Set<String> getRoleCache() {
+    protected MultiMap<String, Relation> getRelationCache() {
         if (dirty) {
             rebuild();
             dirty = false;
         }
-        return roleCache;
+        return RELATION_CACHE;
     }
 
     /**
@@ -171,8 +168,8 @@
      * initializes the cache from the primitives in the dataset
      */
     protected void rebuild() {
-        tagCache = new MultiMap<>();
-        roleCache = new HashSet<>();
+        TAG_CACHE.clear();
+        RELATION_CACHE.clear();
         cachePrimitives(ds.allNonDeletedCompletePrimitives());
     }
 
@@ -180,7 +177,8 @@
         for (OsmPrimitive primitive : primitives) {
             cachePrimitiveTags(primitive);
             if (primitive instanceof Relation) {
-                cacheRelationMemberRoles((Relation) primitive);
+                Relation rel = (Relation) primitive;
+                RELATION_CACHE.put(getRelationType(rel.getKeys()), rel);
             }
         }
     }
@@ -192,23 +190,41 @@
      * @param primitive an OSM primitive
      */
     protected void cachePrimitiveTags(OsmPrimitive primitive) {
-        primitive.visitKeys((p, key, value) -> tagCache.put(key, value));
+        primitive.visitKeys((p, key, value) -> TAG_CACHE.put(key, value));
     }
 
     /**
-     * Caches all member roles of the relation <code>relation</code>
+     * Returns the relation type.
+     * <p>
+     * This is used to categorize the relations in the dataset.  A relation with the keys:
+     * <ul>
+     * <li>type=route
+     * <li>route=hiking
+     * </ul>
+     * will return a relation type of {@code "route.hiking"}.
      *
-     * @param relation the relation
+     * @param tags the tags on the relation
+     * @return the relation type or {@code ""}
      */
-    protected void cacheRelationMemberRoles(Relation relation) {
-        for (RelationMember m: relation.getMembers()) {
-            if (m.hasRole()) {
-                roleCache.add(m.getRole());
-            }
-        }
+    private String getRelationType(Map<String, String> tags) {
+        String type = tags.get("type");
+        if (type == null) return "";
+        String subtype = tags.get(type);
+        if (subtype == null) return type;
+        return type + "." + subtype;
     }
 
     /**
+     * Construct a role out of a relation member
+     *
+     * @param member the relation member
+     * @return the Role
+     */
+    protected Role mkRole(RelationMember member) {
+        return new Role(member.getRole(), EnumSet.of(TaggingPresetType.forPrimitiveType(member.getDisplayType())));
+    }
+
+    /**
      * Remembers user input for the given key/value.
      * @param key Tag key
      * @param value Tag value
@@ -259,22 +275,44 @@
     }
 
     /**
-     * Replies the list of member roles
+     * Returns a collection of all member roles in the dataset.
+     * <p>
+     * Member roles are distinct on role name and primitive type they apply to. So there will be a
+     * role "platform" for nodes and a role "platform" for ways.
      *
-     * @return the list of member roles
+     * @return the collection of member roles
      */
-    public List<String> getMemberRoles() {
-        return new ArrayList<>(getRoleCache());
+    public Set<Role> getAllMemberRoles() {
+        return getRelationCache().getAllValues().stream()
+            .flatMap(rel -> rel.getMembers().stream()).map(r -> mkRole(r)).collect(Collectors.toSet());
     }
 
     /**
+     * Returns a collection of all roles in the dataset for one relation type.
+     * <p>
+     * Member roles are distinct on role name and primitive type they apply to. So there will be a
+     * role "platform" for nodes and a role "platform" for ways.
+     *
+     * @param relationType the {@link #getRelationType(Map) relation type}
+     * @return the collection of member roles
+     */
+    public Set<Role> getMemberRoles(String relationType) {
+        Set<Relation> relations = getRelationCache().get(relationType);
+        if (relations == null)
+            return Collections.emptySet();
+        return relations.stream().flatMap(rel -> rel.getMembers().stream()).map(r -> mkRole(r)).collect(Collectors.toSet());
+    }
+
+    /**
      * Populates the {@link AutoCompletionList} with the currently cached member roles.
      *
      * @param list the list to populate
      */
     public void populateWithMemberRoles(AutoCompletionList list) {
-        list.add(TaggingPresets.getPresetRoles(), AutoCompletionPriority.IS_IN_STANDARD);
-        list.add(getRoleCache(), AutoCompletionPriority.IS_IN_DATASET);
+        list.add(TaggingPresets.getPresetRoles().stream().map(r -> r.getKey())
+            .collect(Collectors.toList()), AutoCompletionPriority.IS_IN_STANDARD);
+        list.add(getAllMemberRoles().stream().map(role -> role.getKey())
+            .collect(Collectors.toSet()), AutoCompletionPriority.IS_IN_DATASET);
     }
 
     /**
@@ -292,9 +330,8 @@
         Collection<TaggingPreset> presets = r != null ? TaggingPresets.getMatchingPresets(null, r.getKeys(), false) : Collections.emptyList();
         if (r != null && !Utils.isEmpty(presets)) {
             for (TaggingPreset tp : presets) {
-                if (tp.roles != null) {
-                    list.add(Utils.transform(tp.roles.roles, (Function<Role, String>) x -> x.key), AutoCompletionPriority.IS_IN_STANDARD);
-                }
+                list.add(Utils.transform(tp.getAllRoles(),
+                    (Function<Item, String>) x -> ((Role) x).getKey()), AutoCompletionPriority.IS_IN_STANDARD);
             }
             list.add(r.getMemberRoles(), AutoCompletionPriority.IS_IN_DATASET);
         } else {
@@ -303,6 +340,186 @@
     }
 
     /**
+     * Merges two or more {@code Map<String, AutoCompletionPriority>}. The result will have the
+     * priorities merged.
+     *
+     * @param maps two or more maps to merge
+     * @return the merged map
+     */
+    @SafeVarargs
+    public static final Map<String, AutoCompletionPriority> merge(Map<String, AutoCompletionPriority>... maps) {
+        return Stream.of(maps).flatMap(m -> m.entrySet().stream())
+            .collect(Collectors.toMap(Entry::getKey, Entry::getValue, AutoCompletionPriority::mergeWith));
+    }
+
+    /**
+     * Returns key suggestions for a given relation type.
+     * <p>
+     * Returns all keys in the dataset used on a given {@link #getRelationType(Map) relation type}.
+     *
+     * @param tags current tags in the tag editor panel, used to determine the relation type
+     * @return the suggestions
+     */
+    public Map<String, AutoCompletionPriority> getKeysForRelation(Map<String, String> tags) {
+        Map<String, AutoCompletionPriority> map = new HashMap<>();
+        Set<Relation> relations = tags != null ? getRelationCache().get(getRelationType(tags)) : getRelationCache().getAllValues();
+        if (relations == null)
+            return map;
+        return relations.stream().flatMap(rel -> rel.getKeys().entrySet().stream()).map(e -> e.getKey())
+            .collect(Collectors.toMap(k -> k, v -> AutoCompletionPriority.IS_IN_DATASET, AutoCompletionPriority::mergeWith));
+    }
+
+    /**
+     * Returns value suggestions for a given relation type and key.
+     * <p>
+     * Returns all values in the dataset used with a given key on a given
+     * {@link #getRelationType(Map) relation type}.
+     *
+     * @param tags current tags in the tag editor panel, used to determine the relation type
+     * @param key the key to get values for
+     * @return the suggestions
+     */
+    public Map<String, AutoCompletionPriority> getValuesForRelation(Map<String, String> tags, String key) {
+        Map<String, AutoCompletionPriority> map = new HashMap<>();
+        Set<Relation> relations = tags != null ? getRelationCache().get(getRelationType(tags)) : getRelationCache().getAllValues();
+        if (relations == null)
+            return map;
+        return relations.stream().map(rel -> rel.get(key)).filter(e -> e != null)
+            .collect(Collectors.toMap(k -> k, v -> AutoCompletionPriority.IS_IN_DATASET, AutoCompletionPriority::mergeWith));
+    }
+
+    /**
+     * Returns role suggestions for a given relation type.
+     * <p>
+     * Returns all roles in the dataset for a given {@link TaggingPresetType role type} used with a given
+     * {@link #getRelationType(Map) relation type}.
+     *
+     * @param tags current tags in the tag editor panel, used to determine the relation type
+     * @param roleTypes all roles returned will match all of the types in this set.
+     * @return the suggestions
+     */
+    public Map<String, AutoCompletionPriority> getRolesForRelation(Map<String, String> tags, EnumSet<TaggingPresetType> roleTypes) {
+        Map<String, AutoCompletionPriority> map = new HashMap<>();
+        Set<Relation> relations = tags != null ? getRelationCache().get(getRelationType(tags)) : getRelationCache().getAllValues();
+        if (relations == null)
+            return map;
+        return relations.stream().flatMap(rel -> rel.getMembers().stream())
+            .map(member -> mkRole(member)).filter(role -> role.appliesToAll(roleTypes))
+            .collect(Collectors.toMap(k -> k.getKey(), v -> AutoCompletionPriority.IS_IN_DATASET, AutoCompletionPriority::mergeWith));
+    }
+
+    /**
+     * Returns all presets of type {@code types} matched by {@code tags}.
+     *
+     * @param types the preset types to include, (node / way / relation ...) or null to include all types
+     * @param tags match presets using these tags or null to match all presets
+     * @return the matched presets
+     */
+    private Collection<TaggingPreset> getPresets(Collection<TaggingPresetType> types, Map<String, String> tags) {
+        if (tags == null)
+            tags = EMPTY_MAP;
+
+        Collection<TaggingPreset> presets = TaggingPresets.getMatchingPresets(types, tags, false);
+        if (presets.isEmpty()) {
+            presets = TaggingPresets.getTaggingPresets();
+        }
+        return presets;
+    }
+
+    /**
+     * Returns all keys found in the presets matched by {@code tags}.
+     *
+     * @param types the preset types to include, (node / way / relation ...) or null to include all types
+     * @param tags match presets using these tags or null to match all presets
+     * @return the suggested keys
+     * @since xxx
+     */
+    public Map<String, AutoCompletionPriority> getPresetKeys(Collection<TaggingPresetType> types, Map<String, String> tags) {
+        Map<String, AutoCompletionPriority> map = new HashMap<>();
+
+        for (TaggingPreset preset : getPresets(types, tags)) {
+            for (Item item : preset.getAllItems()) {
+                if (item instanceof KeyedItem) {
+                    map.merge(((KeyedItem) item).getKey(), AutoCompletionPriority.IS_IN_STANDARD, AutoCompletionPriority::mergeWith);
+                }
+            }
+        }
+        return map;
+    }
+
+    /**
+     * Returns all values for {@code key} found in the presets matched by {@code tags}.
+     *
+     * @param types the preset types to include, (node / way / relation ...) or null to include all types
+     * @param tags match presets using these tags or null to match all presets
+     * @param key the key to return values for
+     * @return the suggested values
+     * @since xxx
+     */
+    public Map<String, AutoCompletionPriority> getPresetValues(Collection<TaggingPresetType> types, Map<String, String> tags, String key) {
+        Map<String, AutoCompletionPriority> map = new HashMap<>();
+
+        for (TaggingPreset preset : getPresets(types, tags)) {
+            for (Item item : preset.getAllItems()) {
+                if (item instanceof KeyedItem) {
+                    KeyedItem keyedItem = (KeyedItem) item;
+                    if (keyedItem.getKey().equals(key)) {
+                        for (String value : keyedItem.getValues()) {
+                            map.merge(value, AutoCompletionPriority.IS_IN_STANDARD, AutoCompletionPriority::mergeWith);
+                        }
+                    }
+                }
+            }
+        }
+        return map;
+    }
+
+    /**
+     * Returns all roles found in the presets matched by {@code tags}.
+     *
+     * @param tags match presets using these tags or null to match all presets
+     * @param roleTypes the role types to include, (node / way / relation ...) or null to include all types
+     * @return the suggested roles
+     * @since xxx
+     */
+    public Map<String, AutoCompletionPriority> getPresetRoles(Map<String, String> tags, Collection<TaggingPresetType> roleTypes) {
+        Map<String, AutoCompletionPriority> map = new HashMap<>();
+
+        for (TaggingPreset preset : getPresets(EnumSet.of(TaggingPresetType.RELATION), tags)) {
+            for (Role role : preset.getAllRoles()) {
+                if (role.appliesToAll(roleTypes))
+                    map.merge(role.getKey(), AutoCompletionPriority.IS_IN_STANDARD, AutoCompletionPriority::mergeWith);
+            }
+        }
+        return map;
+    }
+
+    /**
+     * Returns all cached {@link AutoCompletionItem}s for given keys.
+     *
+     * @param keys retrieve the items for these keys
+     * @return the currently cached items, sorted by priority and alphabet
+     * @since 18221
+     */
+    public List<AutoCompletionItem> getAllValuesForKeys(List<String> keys) {
+        Map<String, AutoCompletionPriority> map = new HashMap<>();
+
+        for (String key : keys) {
+            for (String value : TaggingPresets.getPresetValues(key)) {
+                map.merge(value, AutoCompletionPriority.IS_IN_STANDARD, AutoCompletionPriority::mergeWith);
+            }
+            for (String value : getDataValues(key)) {
+                map.merge(value, AutoCompletionPriority.IS_IN_DATASET, AutoCompletionPriority::mergeWith);
+            }
+            for (String value : getUserInputValues(key)) {
+                map.merge(value, AutoCompletionPriority.UNKNOWN, AutoCompletionPriority::mergeWith);
+            }
+        }
+        return map.entrySet().stream().map(e -> new AutoCompletionItem(e.getKey(), e.getValue()))
+            .sorted(ALPHABETIC_COMPARATOR).collect(Collectors.toList());
+    }
+
+    /**
      * Populates the an {@link AutoCompletionList} with the currently cached tag keys
      *
      * @param list the list to populate
@@ -345,30 +562,6 @@
     }
 
     /**
-     * Returns all cached {@link AutoCompletionItem}s for given keys.
-     *
-     * @param keys retrieve the items for these keys
-     * @return the currently cached items, sorted by priority and alphabet
-     * @since 18221
-     */
-    public List<AutoCompletionItem> getAllForKeys(List<String> keys) {
-        Map<String, AutoCompletionPriority> map = new HashMap<>();
-
-        for (String key : keys) {
-            for (String value : TaggingPresets.getPresetValues(key)) {
-                map.merge(value, AutoCompletionPriority.IS_IN_STANDARD, AutoCompletionPriority::mergeWith);
-            }
-            for (String value : getDataValues(key)) {
-                map.merge(value, AutoCompletionPriority.IS_IN_DATASET, AutoCompletionPriority::mergeWith);
-            }
-            for (String value : getUserInputValues(key)) {
-                map.merge(value, AutoCompletionPriority.UNKNOWN, AutoCompletionPriority::mergeWith);
-            }
-        }
-        return map.entrySet().stream().map(e -> new AutoCompletionItem(e.getKey(), e.getValue())).sorted().collect(Collectors.toList());
-    }
-
-    /**
      * Returns the currently cached tag keys.
      * @return a set of tag keys
      * @since 12859
@@ -502,8 +695,8 @@
                     ds.removeDataSetListener(AutoCompletionManager.this);
                     MainApplication.getLayerManager().removeLayerChangeListener(this);
                     dirty = true;
-                    tagCache = null;
-                    roleCache = null;
+                    TAG_CACHE.clear();
+                    RELATION_CACHE.clear();
                     ds = null;
                 }
             }
Index: src/org/openstreetmap/josm/gui/tagging/ac/DefaultAutoCompListener.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/ac/DefaultAutoCompListener.java	(nonexistent)
+++ src/org/openstreetmap/josm/gui/tagging/ac/DefaultAutoCompListener.java	(working copy)
@@ -0,0 +1,62 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.tagging.ac;
+
+import javax.swing.event.PopupMenuEvent;
+import javax.swing.event.PopupMenuListener;
+
+/**
+ * A default autocompletion listener.
+ * @param <E> the type of the {@code AutoCompComboBox<E>} or {@code AutoCompTextField<E>}
+ */
+public class DefaultAutoCompListener<E> implements AutoCompListener, PopupMenuListener {
+    protected void updateAutoCompModel(AutoCompComboBoxModel<E> model) {
+    }
+
+    @Override
+    public void autoCompBefore(AutoCompEvent e) {
+        AutoCompTextField<E> tf = toTextField(e);
+        String savedText = tf.getText();
+        updateAutoCompModel(tf.getModel());
+        tf.setText(savedText);
+    }
+
+    @Override
+    public void autoCompPerformed(AutoCompEvent e) {
+    }
+
+    @Override
+    public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
+        AutoCompComboBox<E> cb = toComboBox(e);
+        String savedText = cb.getText();
+        updateAutoCompModel(cb.getModel());
+        cb.setText(savedText);
+    }
+
+    @Override
+    public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
+    }
+
+    @Override
+    public void popupMenuCanceled(PopupMenuEvent e) {
+    }
+
+    /**
+     * Returns the AutoCompTextField that sent the request.
+     * @param e The AutoCompEvent
+     * @return the AutoCompTextField
+     */
+    @SuppressWarnings("unchecked")
+    public AutoCompTextField<E> toTextField(AutoCompEvent e) {
+        return (AutoCompTextField<E>) e.getSource();
+    }
+
+    /**
+     * Returns the AutoCompComboBox that sent the request.
+     * @param e The AutoCompEvent
+     * @return the AutoCompComboBox
+     */
+    @SuppressWarnings("unchecked")
+    public AutoCompComboBox<E> toComboBox(PopupMenuEvent e) {
+        return (AutoCompComboBox<E>) e.getSource();
+    }
+}
Index: src/org/openstreetmap/josm/gui/tagging/presets/Check.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/Check.java	(nonexistent)
+++ src/org/openstreetmap/josm/gui/tagging/presets/Check.java	(working copy)
@@ -0,0 +1,165 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.tagging.presets;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import javax.swing.JPanel;
+
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.OsmUtils;
+import org.openstreetmap.josm.data.osm.Tag;
+import org.openstreetmap.josm.gui.widgets.IconTextCheckBox;
+import org.openstreetmap.josm.gui.widgets.QuadStateCheckBox;
+import org.openstreetmap.josm.tools.GBC;
+
+/**
+ * Checkbox type.
+ */
+final class Check extends KeyedItem {
+
+    /** the value to set when checked (default is "yes") */
+    private final String valueOn;
+    /** the value to set when unchecked (default is "no") */
+    private final String valueOff;
+    /** whether the off value is disabled in the dialog, i.e., only unset or yes are provided */
+    private final boolean disableOff;
+    /** "on" or "off" or unset (default is unset) */
+    private final String default_; // only used for tagless objects
+
+    /**
+     * Private constructor. Use {@link #fromXML} instead.
+     * @param attributes the XML attributes
+     * @throws IllegalArgumentException on illegal attributes
+     */
+    private Check(Map<String, String> attributes) throws IllegalArgumentException {
+        super(attributes);
+        valueOn = attributes.getOrDefault("value_on", OsmUtils.TRUE_VALUE);
+        valueOff = attributes.getOrDefault("value_off", OsmUtils.FALSE_VALUE);
+        disableOff = Boolean.parseBoolean(attributes.get("disable_off"));
+        default_ = attributes.get("default");
+    }
+
+    /**
+     * Create this class from an XML element's attributes.
+     * @param attributes the XML attributes
+     * @return the new instance
+     * @throws IllegalArgumentException on invalid attributes
+     */
+    public static Check fromXML(Map<String, String> attributes) throws IllegalArgumentException {
+        return new Check(attributes);
+    }
+
+    @Override
+    boolean addToPanel(JPanel p, TaggingPresetInstance support) {
+
+        // find out if our key is already used in the selection.
+        final Usage usage = Usage.determineBooleanUsage(support.getSelected(), key);
+        final String oneValue = usage.map.isEmpty() ? null : usage.map.lastKey();
+        QuadStateCheckBox.State initialState;
+        Boolean def = "on".equals(default_) ? Boolean.TRUE : "off".equals(default_) ? Boolean.FALSE : null;
+
+        if (usage.map.size() < 2 && (oneValue == null || valueOn.equals(oneValue) || valueOff.equals(oneValue))) {
+            if (def != null && !PROP_FILL_DEFAULT.get()) {
+                // default is set and filling default values feature is disabled - check if all primitives are untagged
+                for (OsmPrimitive s : support.getSelected()) {
+                    if (s.hasKeys()) {
+                        def = null;
+                    }
+                }
+            }
+
+            // all selected objects share the same value which is either true or false or unset,
+            // we can display a standard check box.
+            initialState = valueOn.equals(oneValue) || Boolean.TRUE.equals(def)
+                    ? QuadStateCheckBox.State.SELECTED
+                    : valueOff.equals(oneValue) || Boolean.FALSE.equals(def)
+                    ? QuadStateCheckBox.State.NOT_SELECTED
+                    : QuadStateCheckBox.State.UNSET;
+
+        } else {
+            def = null;
+            // the objects have different values, or one or more objects have something
+            // else than true/false. we display a quad-state check box
+            // in "partial" state.
+            initialState = QuadStateCheckBox.State.PARTIAL;
+        }
+
+        final List<QuadStateCheckBox.State> allowedStates = new ArrayList<>(4);
+        if (QuadStateCheckBox.State.PARTIAL == initialState)
+            allowedStates.add(QuadStateCheckBox.State.PARTIAL);
+        allowedStates.add(QuadStateCheckBox.State.SELECTED);
+        if (!disableOff || valueOff.equals(oneValue))
+            allowedStates.add(QuadStateCheckBox.State.NOT_SELECTED);
+        allowedStates.add(QuadStateCheckBox.State.UNSET);
+
+        QuadStateCheckBox check;
+        check = new QuadStateCheckBox(icon == null ? localeText : null, initialState,
+                allowedStates.toArray(new QuadStateCheckBox.State[0]));
+        check.setPropertyText(key);
+        check.setState(check.getState()); // to update the tooltip text
+        check.setComponentPopupMenu(getPopupMenu());
+
+        if (icon != null) {
+            JPanel checkPanel = IconTextCheckBox.wrap(check, localeText, getIcon());
+            checkPanel.applyComponentOrientation(support.getDefaultComponentOrientation());
+            p.add(checkPanel, GBC.eol()); // Do not fill, see #15104
+        } else {
+            check.applyComponentOrientation(support.getDefaultComponentOrientation());
+            p.add(check, GBC.eol()); // Do not fill, see #15104
+        }
+        Instance instance = new Instance(check, initialState, def);
+        support.putInstance(this, instance);
+        check.addChangeListener(l -> support.fireItemValueModified(instance, key, instance.getValue()));
+        return true;
+    }
+
+    class Instance extends Item.Instance {
+        private QuadStateCheckBox checkbox;
+        private QuadStateCheckBox.State originalState;
+        private Boolean def;
+
+        Instance(QuadStateCheckBox checkbox, QuadStateCheckBox.State originalState, Boolean def) {
+            this.checkbox = checkbox;
+            this.originalState = originalState;
+            this.def = def;
+        }
+
+        @Override
+        public void addCommands(List<Tag> changedTags) {
+            // if the user hasn't changed anything, don't create a command.
+            if (def == null && (checkbox.getState() == originalState)) return;
+
+            // otherwise change things according to the selected value.
+            changedTags.add(new Tag(key, getValue()));
+        }
+
+        private String getValue() {
+            return checkbox.getState() == QuadStateCheckBox.State.SELECTED ? valueOn :
+            checkbox.getState() == QuadStateCheckBox.State.NOT_SELECTED ? valueOff :
+            null;
+        }
+    }
+
+    @Override
+    MatchType getDefaultMatch() {
+        return MatchType.NONE;
+    }
+
+    @Override
+    public Collection<String> getValues() {
+        return disableOff ? Arrays.asList(valueOn) : Arrays.asList(valueOn, valueOff);
+    }
+
+    @Override
+    public String toString() {
+        return "Check [key=" + key + ", text=" + text + ", "
+                + (localeText != null ? "locale_text=" + localeText + ", " : "")
+                + (valueOn != null ? "value_on=" + valueOn + ", " : "")
+                + (valueOff != null ? "value_off=" + valueOff + ", " : "")
+                + "default_=" + default_ + ']';
+    }
+}
Index: src/org/openstreetmap/josm/gui/tagging/presets/CheckGroup.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/CheckGroup.java	(nonexistent)
+++ src/org/openstreetmap/josm/gui/tagging/presets/CheckGroup.java	(working copy)
@@ -0,0 +1,66 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.tagging.presets;
+
+import java.awt.GridBagConstraints;
+import java.awt.GridLayout;
+import java.util.Map;
+
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+
+import org.openstreetmap.josm.tools.GBC;
+
+/**
+ * A group of {@link Check}s.
+ * @since 6114
+ */
+final class CheckGroup extends Container {
+    /**
+     * Number of columns (positive integer)
+     */
+    private final int columns;
+
+    /**
+     * Private constructor. Use {@link #fromXML} instead.
+     * @param attributes the XML attributes
+     * @throws IllegalArgumentException on illegal attributes
+     */
+    private CheckGroup(Map<String, String> attributes) throws IllegalArgumentException {
+        super(attributes);
+        columns = Integer.parseInt(attributes.getOrDefault("columns", "1"));
+    }
+
+    /**
+     * Create this class from an XML element's attributes.
+     * @param attributes the XML attributes
+     * @return the new instance
+     * @throws IllegalArgumentException on illegal attributes
+     */
+    public static CheckGroup fromXML(Map<String, String> attributes) throws IllegalArgumentException {
+        return new CheckGroup(attributes);
+    }
+
+    @Override
+    boolean addToPanel(JPanel p, TaggingPresetInstance support) {
+        int rows = (int) Math.ceil(items.size() / ((double) columns));
+        JPanel panel = new JPanel(new GridLayout(rows, columns));
+        addBorder(panel);
+
+        for (Item item : items) {
+            item.addToPanel(panel, support);
+        }
+        // fill remaining cells, see #20792
+        for (int i = items.size(); i < rows * columns; i++) {
+            panel.add(new JLabel());
+        }
+
+        panel.applyComponentOrientation(support.getDefaultComponentOrientation());
+        p.add(panel, GBC.eol().fill(GridBagConstraints.HORIZONTAL));
+        return false;
+    }
+
+    @Override
+    public String toString() {
+        return "CheckGroup [columns=" + columns + ']';
+    }
+}
Index: src/org/openstreetmap/josm/gui/tagging/presets/Chunk.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/Chunk.java	(nonexistent)
+++ src/org/openstreetmap/josm/gui/tagging/presets/Chunk.java	(working copy)
@@ -0,0 +1,50 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.tagging.presets;
+
+import java.util.Map;
+
+/**
+ * A collection of items to be inserted in place of a {@link Reference}.
+ */
+class Chunk extends Sequence {
+    private final String id;
+
+    /**
+     * Constructor.
+     * @param attributes the XML attributes
+     * @throws IllegalArgumentException on illegal attributes
+     */
+    Chunk(Map<String, String> attributes) throws IllegalArgumentException {
+        super(attributes);
+        id = attributes.get("id");
+    }
+
+    /**
+     * Create a {@code Chunk} from an XML element's attributes.
+     * @param attributes the XML attributes
+     * @return the {@code Chunk}
+     * @throws IllegalArgumentException on invalid attributes
+     */
+    public static Chunk fromXML(Map<String, String> attributes) throws IllegalArgumentException {
+        return new Chunk(attributes);
+    }
+
+    @Override
+    void fixup(Map<String, Chunk> chunks, Item parent) {
+        super.fixup(chunks, parent);
+        chunks.put(getId(), this);
+    }
+
+    /**
+     * Returns the chunk id.
+     * @return the chunk id
+     */
+    public String getId() {
+        return id;
+    }
+
+    @Override
+    public String toString() {
+        return "Chunk [id=" + id + "]";
+    }
+}
Index: src/org/openstreetmap/josm/gui/tagging/presets/CloneTaggingPresetHandler.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/CloneTaggingPresetHandler.java	(nonexistent)
+++ src/org/openstreetmap/josm/gui/tagging/presets/CloneTaggingPresetHandler.java	(working copy)
@@ -0,0 +1,54 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.tagging.presets;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import org.openstreetmap.josm.command.ChangePropertyCommand;
+import org.openstreetmap.josm.data.osm.DataSet;
+import org.openstreetmap.josm.data.osm.FilterModel;
+import org.openstreetmap.josm.data.osm.INode;
+import org.openstreetmap.josm.data.osm.IRelation;
+import org.openstreetmap.josm.data.osm.IWay;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.Tag;
+import org.openstreetmap.josm.tools.SubclassFilteredCollection;
+
+/**
+ * A handler that clones a selection into a new dataset.
+ * <p>
+ * Use this to apply temporary edits, eg. for the validator.
+ */
+public class CloneTaggingPresetHandler implements TaggingPresetHandler {
+    final DataSet ds = new DataSet();
+    final Collection<OsmPrimitive> selection;
+
+    /**
+     * Constructor
+     * @param selection the selection of primitives to edit
+     */
+    public CloneTaggingPresetHandler(Collection<OsmPrimitive> selection) {
+        Collection<OsmPrimitive> dependend = FilterModel.getAffectedPrimitives(selection);
+        Map<OsmPrimitive, OsmPrimitive> clonedMap = ds.clonePrimitives(
+            new SubclassFilteredCollection<>(dependend, INode.class::isInstance),
+            new SubclassFilteredCollection<>(dependend, IWay.class::isInstance),
+            new SubclassFilteredCollection<>(dependend, IRelation.class::isInstance)
+        );
+        this.selection = selection.stream().map(p -> clonedMap.get(p)).collect(Collectors.toList());
+    }
+
+    @Override
+    public void updateTags(List<Tag> changedTags) {
+        // we don't care about undo
+        for (Tag tag : changedTags) {
+            new ChangePropertyCommand(selection, tag.getKey(), tag.getValue()).executeCommand();
+        }
+    }
+
+    @Override
+    public Collection<OsmPrimitive> getPrimitives() {
+        return selection;
+    }
+}
Index: src/org/openstreetmap/josm/gui/tagging/presets/Combo.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/Combo.java	(nonexistent)
+++ src/org/openstreetmap/josm/gui/tagging/presets/Combo.java	(working copy)
@@ -0,0 +1,270 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.tagging.presets;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.Color;
+import java.awt.Cursor;
+import java.awt.Dimension;
+import java.awt.GridBagConstraints;
+import java.awt.Insets;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.ComponentAdapter;
+import java.awt.event.ComponentEvent;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.Map;
+
+import javax.swing.AbstractAction;
+import javax.swing.JButton;
+import javax.swing.JColorChooser;
+import javax.swing.JComponent;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+
+import org.openstreetmap.josm.data.tagging.ac.AutoCompletionItem;
+import org.openstreetmap.josm.data.tagging.ac.AutoCompletionPriority;
+import org.openstreetmap.josm.gui.MainApplication;
+import org.openstreetmap.josm.gui.mappaint.mapcss.CSSColors;
+import org.openstreetmap.josm.gui.tagging.ac.AutoCompComboBoxEditor;
+import org.openstreetmap.josm.gui.tagging.ac.AutoCompComboBoxModel;
+import org.openstreetmap.josm.gui.tagging.ac.AutoCompTextField;
+import org.openstreetmap.josm.gui.widgets.JosmComboBox;
+import org.openstreetmap.josm.gui.widgets.OrientationAction;
+import org.openstreetmap.josm.tools.ColorHelper;
+import org.openstreetmap.josm.tools.GBC;
+
+/**
+ * Combobox type.
+ */
+final class Combo extends ComboMultiSelect {
+
+    /**
+     * Whether the combo box is editable, which means that the user can add other values as text.
+     * Default is {@code true}. If {@code false} it is readonly, which means that the user can only select an item in the list.
+     */
+    private final boolean editable;
+
+    /**
+     * Private constructor. Use {@link #fromXML} instead.
+     * @param attributes the XML attributes
+     * @throws IllegalArgumentException on illegal attributes
+     */
+    private Combo(Map<String, String> attributes) throws IllegalArgumentException {
+        super(attributes);
+        editable = Boolean.parseBoolean(attributes.getOrDefault("editable", "true"));
+    }
+
+    /**
+     * Create this class from an XML element's attributes.
+     * @param attributes the XML attributes
+     * @return the new instance
+     * @throws IllegalArgumentException on invalid attributes
+     */
+    public static Combo fromXML(Map<String, String> attributes) throws IllegalArgumentException {
+        return new Combo(attributes);
+    }
+
+    static class ComponentListener extends ComponentAdapter {
+        JosmComboBox<PresetListEntry.Instance> combobox;
+
+        ComponentListener(JosmComboBox<PresetListEntry.Instance> combobox) {
+            this.combobox = combobox;
+        }
+
+        @Override
+        public void componentResized(ComponentEvent e) {
+            // Make multi-line JLabels the correct size
+            // Only needed if there is any short_description
+            JComponent component = (JComponent) e.getSource();
+            int width = component.getWidth();
+            if (width == 0)
+                width = 200;
+            Insets insets = component.getInsets();
+            width -= insets.left + insets.right + 10;
+            PresetListEntry.CellRenderer renderer = (PresetListEntry.CellRenderer) combobox.getRenderer();
+            renderer.setWidth(width);
+            combobox.setRenderer(null); // needed to make prop change fire
+            combobox.setRenderer(renderer);
+        }
+    }
+
+    @Override
+    String getDefaultDelimiter() {
+        return ",";
+    }
+
+    private void addEntry(AutoCompComboBoxModel<PresetListEntry.Instance> model, PresetListEntry.Instance instance) {
+        if (!seenValues.containsKey(instance.getValue())) {
+            model.addElement(instance);
+            seenValues.put(instance.getValue(), instance);
+        }
+    }
+
+    @Override
+    boolean addToPanel(JPanel p, TaggingPresetInstance support) {
+        Usage usage = Usage.determineTextUsage(support.getSelected(), key);
+        seenValues.clear();
+
+        // init the model
+        AutoCompComboBoxModel<PresetListEntry.Instance> dropDownModel =
+            new AutoCompComboBoxModel<>(Comparator.<PresetListEntry.Instance>naturalOrder());
+        JosmComboBox<PresetListEntry.Instance> combobox = new JosmComboBox<>(dropDownModel);
+        Instance instance = new Instance(combobox, usage);
+
+        if (!usage.hasUniqueValue() && !usage.unused()) {
+            addEntry(dropDownModel, PresetListEntry.ENTRY_DIFFERENT.newInstance(instance));
+        }
+        presetListEntries.forEach(e -> addEntry(dropDownModel, e.newInstance(instance)));
+        if (default_ != null) {
+            addEntry(dropDownModel, new PresetListEntry(this, default_).newInstance(instance));
+        }
+        addEntry(dropDownModel, PresetListEntry.ENTRY_EMPTY.newInstance(instance));
+
+        usage.map.forEach((value, count) -> {
+            addEntry(dropDownModel, new PresetListEntry(this, value).newInstance(instance));
+        });
+
+        AutoCompComboBoxEditor<AutoCompletionItem> editor = new AutoCompComboBoxEditor<>();
+        combobox.setEditor(editor);
+
+        // The default behaviour of JComboBox is to size the editor according to the tallest item in
+        // the dropdown list.  We don't want that to happen because we want to show taller items in
+        // the list than in the editor.  We can't use
+        // {@code combobox.setPrototypeDisplayValue(PresetListEntry.ENTRY_EMPTY);} because that would
+        // set a fixed cell height in JList.
+        combobox.setPreferredHeight(combobox.getPreferredSize().height);
+
+        // a custom cell renderer capable of displaying a short description text along with the
+        // value
+        combobox.setRenderer(new PresetListEntry.CellRenderer(combobox, combobox.getRenderer(), 200));
+        combobox.setEditable(editable);
+
+        AutoCompComboBoxModel<AutoCompletionItem> autoCompModel;
+        autoCompModel = new AutoCompComboBoxModel<>(Comparator.<AutoCompletionItem>naturalOrder());
+        TaggingPresetUtils.getAllForKeys(Arrays.asList(key)).forEach(autoCompModel::addElement);
+        getDisplayValues().forEach(s -> autoCompModel.addElement(new AutoCompletionItem(s, AutoCompletionPriority.IS_IN_STANDARD)));
+
+        AutoCompTextField<AutoCompletionItem> tf = editor.getEditorComponent();
+        tf.setModel(autoCompModel);
+
+        if (Item.DISPLAY_KEYS_AS_HINT.get()) {
+            combobox.setHint(key);
+        }
+        if (length > 0) {
+            tf.setMaxTextLength(length);
+        }
+
+        support.putInstance(this, instance);
+
+        JLabel label = addLabel(p);
+
+        if (key != null && ("colour".equals(key) || key.startsWith("colour:") || key.endsWith(":colour"))) {
+            p.add(combobox, GBC.std().fill(GridBagConstraints.HORIZONTAL));
+            JButton button = new JButton(new ChooseColorAction(instance));
+            button.setOpaque(true);
+            button.setBorderPainted(false);
+            Dimension size = combobox.getPreferredSize();
+            button.setPreferredSize(new Dimension(size.height, size.height));
+            button.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
+            p.add(button, GBC.eol());
+            ActionListener updateColor = ignore -> button.setBackground(instance.getColor());
+            updateColor.actionPerformed(null);
+            combobox.addActionListener(updateColor);
+        } else {
+            p.add(combobox, GBC.eol().fill(GridBagConstraints.HORIZONTAL));
+        }
+
+        String initialValue = instance.getInitialValue(usage, support);
+        PresetListEntry.Instance selItem = instance.find(initialValue);
+        if (selItem != null) {
+            combobox.setSelectedItem(selItem);
+        } else {
+            combobox.setText(initialValue);
+        }
+
+        combobox.addActionListener(l -> support.fireItemValueModified(instance, key, instance.getSelectedItem().getValue()));
+        combobox.addComponentListener(new ComponentListener(combobox));
+
+        label.setLabelFor(combobox);
+        combobox.setToolTipText(getKeyTooltipText());
+        combobox.applyComponentOrientation(OrientationAction.getValueOrientation(key));
+
+        seenValues.clear();
+        return true;
+    }
+
+    class Instance extends ComboMultiSelect.Instance {
+        JosmComboBox<PresetListEntry.Instance> combobox;
+
+        Instance(JosmComboBox<PresetListEntry.Instance> combobox, Usage usage) {
+            super(usage);
+            this.combobox = combobox;
+        }
+
+        /**
+         * Returns the value selected in the combobox or a synthetic value if a multiselect.
+         *
+         * @return the value
+         */
+        @Override
+        PresetListEntry.Instance getSelectedItem() {
+            Object sel = combobox.getSelectedItem();
+            if (sel instanceof PresetListEntry.Instance)
+                // selected from the dropdown
+                return (PresetListEntry.Instance) sel;
+            if (sel instanceof String) {
+                // free edit.  If the free edit corresponds to a known entry, use that entry.  This is
+                // to avoid that we write a display_value to the tag's value, eg. if the user did an
+                // undo.
+                PresetListEntry.Instance selItem = find((String) sel);
+                if (selItem != null)
+                    return selItem;
+                return new PresetListEntry(Combo.this, (String) sel).newInstance(this);
+            }
+            return PresetListEntry.ENTRY_EMPTY.newInstance(this);
+        }
+
+        /**
+         * Finds the PresetListEntry that matches value.
+         * <p>
+         * Looks in the model of the combobox for an element whose {@code value} matches {@code value}.
+         *
+         * @param value The value to match.
+         * @return The entry or null
+         */
+        PresetListEntry.Instance find(String value) {
+            return combobox.getModel().asCollection().stream().filter(o -> o.getValue().equals(value)).findAny().orElse(null);
+        }
+
+        void setColor(Color color) {
+            if (color != null) {
+                combobox.setSelectedItem(ColorHelper.color2html(color));
+            }
+        }
+
+        Color getColor() {
+            String colorString = getSelectedItem().getValue();
+            return colorString.startsWith("#")
+                    ? ColorHelper.html2color(colorString)
+                    : CSSColors.get(colorString);
+        }
+    }
+
+    static class ChooseColorAction extends AbstractAction {
+        private Instance instance;
+
+        ChooseColorAction(Instance instance) {
+            this.instance = instance;
+            putValue(SHORT_DESCRIPTION, tr("Choose a color"));
+        }
+
+        @Override
+        public void actionPerformed(ActionEvent e) {
+            Color color = instance.getColor();
+            color = JColorChooser.showDialog(MainApplication.getMainPanel(), tr("Choose a color"), color);
+            instance.setColor(color);
+        }
+    }
+}
Index: src/org/openstreetmap/josm/gui/tagging/presets/ComboMultiSelect.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/ComboMultiSelect.java	(nonexistent)
+++ src/org/openstreetmap/josm/gui/tagging/presets/ComboMultiSelect.java	(working copy)
@@ -0,0 +1,342 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.tagging.presets;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+
+import org.openstreetmap.josm.data.osm.Tag;
+import org.openstreetmap.josm.gui.widgets.OrientationAction;
+import org.openstreetmap.josm.tools.AlphanumComparator;
+import org.openstreetmap.josm.tools.GBC;
+import org.openstreetmap.josm.tools.Logging;
+
+/**
+ * Abstract superclass for combo box and multi-select list types.
+ */
+public abstract class ComboMultiSelect extends KeyedItem {
+    /** The context used for translating values */
+    final String valuesContext;
+    /** Disabled internationalisation for value to avoid mistakes, see #11696 */
+    final boolean valuesNoI18n;
+    /** The default value for the item. If not specified, the current value of the key is chosen as default (if applicable).*/
+    final String default_;
+    /** Whether to sort the values, defaults to true. */
+    private final boolean valuesSort;
+    /** Whether to offer display values for search via {@link TaggingPresetSelector} */
+    private final boolean valuesSearchable;
+    /**
+     * The character that separates values.
+     * In case of {@link Combo} the default is comma.
+     */
+    final char delimiter;
+
+    /**
+     * The standard entries in the combobox dropdown or multiselect list. These entries are defined
+     * in {@code defaultpresets.xml} (or in other custom preset files).
+     */
+    final List<PresetListEntry> presetListEntries = new ArrayList<>();
+
+    /** Helps avoid duplicate list entries */
+    final Map<String, PresetListEntry.Instance> seenValues = new HashMap<>();
+
+    private Set<String> valuesSet;
+    private Set<String> displayValuesSet = new HashSet<>();
+
+    /**
+     * Constructor.
+     * @param attributes the XML attributes
+     * @throws IllegalArgumentException on illegal attributes
+     */
+    ComboMultiSelect(Map<String, String> attributes) throws IllegalArgumentException {
+        super(attributes);
+        valuesContext = attributes.get("values_context");
+        valuesNoI18n = TaggingPresetUtils.parseBoolean(attributes.get("values_no_i18n"));
+        valuesSort = TaggingPresetUtils.parseBoolean(attributes.getOrDefault("values_sort", "on"));
+        valuesSearchable = TaggingPresetUtils.parseBoolean(attributes.get("values_searchable"));
+        default_ = attributes.get("default");
+        delimiter = attributes.getOrDefault("delimiter", getDefaultDelimiter()).charAt(0);
+
+        initPresetListEntries(attributes);
+    }
+
+    @Override
+    void endElement() {
+        super.endElement();
+        if (valuesSort && TaggingPresets.SORT_VALUES.get()) {
+            Collections.sort(presetListEntries,
+                (a, b) -> AlphanumComparator.getInstance().compare(a.getDisplayValue(this), b.getDisplayValue(this)));
+        }
+        valuesSet = presetListEntries.stream().map(PresetListEntry::getValue).collect(Collectors.toSet());
+        if (valuesSearchable) {
+            displayValuesSet = presetListEntries.stream().map(e -> e.getDisplayValue(this)).collect(Collectors.toSet());
+        }
+    }
+
+    @Override
+    public Set<String> getValues() {
+        return valuesSet;
+    }
+
+    /**
+     * Returns the values to display.
+     * @return the values to display
+     */
+    public Set<String> getDisplayValues() {
+        return displayValuesSet;
+    }
+
+    /**
+     * Returns the default delimiter used in multi-value attributes.
+     * @return the default delimiter
+     */
+    abstract String getDefaultDelimiter();
+
+    /**
+     * Adds the label to the panel
+     *
+     * @param p the panel
+     * @return the label
+     */
+    JLabel addLabel(JPanel p) {
+        final JLabel label = new JLabel(tr("{0}:", localeText));
+        addIcon(label);
+        label.setToolTipText(getKeyTooltipText());
+        label.setComponentPopupMenu(getPopupMenu());
+        label.applyComponentOrientation(OrientationAction.getDefaultComponentOrientation());
+        p.add(label, GBC.std().insets(0, 0, 10, 0));
+        return label;
+    }
+
+    private List<String> getValuesFromCode(String valuesFrom) {
+        // get the values from a Java function
+        String[] classMethod = valuesFrom.split("#", -1);
+        if (classMethod.length == 2) {
+            try {
+                Method method = Class.forName(classMethod[0]).getMethod(classMethod[1]);
+                // ComboMultiSelect method is public static String[] methodName()
+                int mod = method.getModifiers();
+                if (Modifier.isPublic(mod) && Modifier.isStatic(mod)
+                        && method.getReturnType().equals(String[].class) && method.getParameterTypes().length == 0) {
+                    return Arrays.asList((String[]) method.invoke(null));
+                } else {
+                    Logging.error(tr("Broken tagging preset \"{0}-{1}\" - Java method given in ''values_from'' is not \"{2}\"", key, text,
+                            "public static String[] methodName()"));
+                }
+            } catch (ReflectiveOperationException e) {
+                Logging.error(tr("Broken tagging preset \"{0}-{1}\" - Java method given in ''values_from'' threw {2} ({3})", key, text,
+                        e.getClass().getName(), e.getMessage()));
+                Logging.debug(e);
+            }
+        }
+        return null; // NOSONAR
+    }
+
+    /**
+     * Checks if list {@code a} is either null or the same length as list {@code b}.
+     *
+     * @param a The list to check
+     * @param b The other list
+     * @param name The name of the list for error reporting
+     * @return {@code a} if both lists have the same length or {@code null}
+     */
+    private List<String> checkListsSameLength(List<String> a, List<String> b, String name) {
+        if (a != null && a.size() != b.size()) {
+            Logging.error(tr("Broken tagging preset \"{0}-{1}\" - number of items in ''{2}'' must be the same as in ''values''",
+                            key, text, name));
+            Logging.error(tr("Detailed information: {0} <> {1}", a, b));
+            return null; // NOSONAR
+        }
+        return a;
+    }
+
+    private void initPresetListEntries(Map<String, String> attributes) {
+        /**
+         * A list of entries.
+         * The list has to be separated by commas (for the {@link Combo} box) or by the specified delimiter (for the {@link MultiSelect}).
+         * If a value contains the delimiter, the delimiter may be escaped with a backslash.
+         * If a value contains a backslash, it must also be escaped with a backslash. */
+        final String values = attributes.get("values");
+        /**
+         * To use instead of {@link #values} if the list of values has to be obtained with a Java method of this form:
+         * <p>{@code public static String[] getValues();}<p>
+         * The value must be: {@code full.package.name.ClassName#methodName}.
+         */
+        final String valuesFrom = attributes.get("values_from");
+        /**
+         * A list of entries that is displayed to the user.
+         * Must be the same number and order of entries as {@link #values} and editable must be false or not specified.
+         * For the delimiter character and escaping, see the remarks at {@link #values}.
+         */
+        final String displayValues = attributes.get("display_values");
+        /** The localized version of {@link #displayValues}. */
+        final String localeDisplayValues = attributes.get("locale_display_values");
+        /**
+         * A delimiter-separated list of texts to be displayed below each {@code display_value}.
+         * (Only if it is not possible to describe the entry in 2-3 words.)
+         * Instead of comma separated list instead using {@link #values}, {@link #displayValues} and {@link #shortDescriptions},
+         * the following form is also supported:<p>
+         * {@code <list_entry value="" display_value="" short_description="" icon="" icon_size="" />}
+         */
+        final String shortDescriptions = attributes.get("short_descriptions");
+        /** The localized version of {@link #shortDescriptions}. */
+        final String localeShortDescriptions = attributes.get("locale_short_descriptions");
+
+        List<String> valueList = null;
+        List<String> displayList = null;
+        List<String> localeDisplayList = null;
+
+        if (valuesFrom != null) {
+            valueList = getValuesFromCode(valuesFrom);
+        }
+
+        if (valueList == null) {
+            // get from {@code values} attribute
+            valueList = TaggingPresetUtils.splitEscaped(delimiter, values);
+        }
+        if (valueList == null) {
+            return;
+        }
+
+        if (!valuesNoI18n) {
+            localeDisplayList = TaggingPresetUtils.splitEscaped(delimiter, localeDisplayValues);
+            displayList = TaggingPresetUtils.splitEscaped(delimiter, displayValues);
+        }
+        List<String> localeShortDescriptionsList = TaggingPresetUtils.splitEscaped(delimiter, localeShortDescriptions);
+        List<String> shortDescriptionsList = TaggingPresetUtils.splitEscaped(delimiter, shortDescriptions);
+
+        displayList = checkListsSameLength(displayList, valueList, "display_values");
+        localeDisplayList = checkListsSameLength(localeDisplayList, valueList, "locale_display_values");
+        shortDescriptionsList = checkListsSameLength(shortDescriptionsList, valueList, "short_descriptions");
+        localeShortDescriptionsList = checkListsSameLength(localeShortDescriptionsList, valueList, "locale_short_descriptions");
+
+        for (int i = 0; i < valueList.size(); i++) {
+            Map<String, String> attribs = new HashMap<>();
+            attribs.put("value", valueList.get(i));
+            if (displayList != null)
+                attribs.put("display_value", displayList.get(i));
+            if (localeDisplayList != null)
+                attribs.put("locale_display_value", localeDisplayList.get(i));
+            if (shortDescriptionsList != null)
+                attribs.put("short_description", shortDescriptionsList.get(i));
+            if (localeShortDescriptionsList != null)
+                attribs.put("locale_short_description", localeShortDescriptionsList.get(i));
+            addItem(PresetListEntry.fromXML(attribs));
+        }
+    }
+
+    abstract class Instance extends Item.Instance {
+        /**
+         * Used to determine if the user has edited the value. This is not the same as the initial value
+         * shown in the component. The original value is the state of the data before the edit. The initial
+         * value may already be an edit suggested by the software.
+         */
+        String originalValue;
+        Usage usage;
+
+        Instance(Usage usage) {
+            this.usage = usage;
+        }
+
+        ComboMultiSelect getTemplate() {
+            return ComboMultiSelect.this;
+        }
+
+        @Override
+        public void addCommands(List<Tag> changedTags) {
+            String value = getSelectedItem().getValue();
+
+            // no change if same as before
+            if (value.equals(originalValue))
+                return;
+            changedTags.add(new Tag(key, value));
+
+            if (isUseLastAsDefault()) {
+                LAST_VALUES.put(key, value);
+            }
+        }
+
+        /**
+         * Returns the value selected in the combobox or a synthetic value if a multiselect.
+         *
+         * @return the value
+         */
+        abstract PresetListEntry.Instance getSelectedItem();
+
+        /**
+         * Returns the initial value to use for this preset.
+         * <p>
+         * The initial value is the value shown in the control when the preset dialog opens. For a
+         * discussion of all the options see the enclosed tickets.
+         *
+         * @param usage The key Usage
+         * @param support The support
+         * @return The initial value to use.
+         *
+         * @see "https://josm.openstreetmap.de/ticket/5564"
+         * @see "https://josm.openstreetmap.de/ticket/12733"
+         * @see "https://josm.openstreetmap.de/ticket/17324"
+         */
+        String getInitialValue(Usage usage, TaggingPresetInstance support) {
+            String initialValue = null;
+            originalValue = "";
+
+            if (usage.hasUniqueValue()) {
+                // all selected primitives have the same not empty value for this key
+                initialValue = usage.getFirst();
+                originalValue = initialValue;
+            } else if (!usage.unused()) {
+                // at least one primitive has a value for this key (but not all have the same one)
+                initialValue = DIFFERENT;
+                originalValue = initialValue;
+            } else if (!usage.hadKeys() || isForceUseLastAsDefault() || PROP_FILL_DEFAULT.get()) {
+                // at this point no primitive had any value for this key
+                if (!support.isPresetInitiallyMatches() && isUseLastAsDefault() && LAST_VALUES.containsKey(key)) {
+                    initialValue = LAST_VALUES.get(key);
+                } else {
+                    initialValue = default_;
+                }
+            }
+            return initialValue != null ? initialValue : "";
+        }
+    }
+
+    /**
+     * Adds a preset list entry.
+     * @param e list entry to add
+     */
+    @Override
+    void addItem(Item e) {
+        if (e instanceof PresetListEntry)
+            presetListEntries.add((PresetListEntry) e);
+    }
+
+    /**
+     * Adds a collection of preset list entries.
+     * @param e list entries to add
+     */
+    void addListEntries(Collection<PresetListEntry> e) {
+        for (PresetListEntry i : e) {
+            addItem(i);
+        }
+    }
+
+    @Override
+    MatchType getDefaultMatch() {
+        return MatchType.NONE;
+    }
+}
Index: src/org/openstreetmap/josm/gui/tagging/presets/Container.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/Container.java	(nonexistent)
+++ src/org/openstreetmap/josm/gui/tagging/presets/Container.java	(working copy)
@@ -0,0 +1,85 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.tagging.presets;
+
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.util.Map;
+
+import javax.swing.BorderFactory;
+import javax.swing.JPanel;
+import javax.swing.border.Border;
+import javax.swing.border.CompoundBorder;
+
+import org.openstreetmap.josm.tools.GBC;
+
+/**
+ * A sequence of {@link Item}s in a panel. The panel may have a title and border.
+ */
+public class Container extends Sequence {
+    /** The text to display */
+    final String text;
+    /** The context used for translating {@link #text} */
+    final String textContext;
+    /** The localized version of {@link #text} */
+    final String localeText;
+
+    /**
+     * Constructor.
+     * @param attributes the XML attributes
+     * @throws IllegalArgumentException on illegal attributes
+     */
+    Container(Map<String, String> attributes) throws IllegalArgumentException {
+        super(attributes);
+        String t = attributes.get("text");
+        text = t != null ? t : getDefaultText();
+        textContext = attributes.get("text_context");
+        localeText = TaggingPresetUtils.buildLocaleString(attributes.get("locale_text"), text, textContext);
+    }
+
+    /**
+     * Create this class from an XML element's attributes.
+     * @param attributes the XML attributes
+     * @return the new instance
+     * @throws IllegalArgumentException on illegal attributes
+     */
+    public static Container fromXML(Map<String, String> attributes) throws IllegalArgumentException {
+        return new Container(attributes);
+    }
+
+    String getDefaultText() {
+        return null;
+    }
+
+    void addBorder(JPanel panel) {
+        Border margin = BorderFactory.createEmptyBorder(10, 0, 0, 0);
+        if (localeText != null) {
+            Border border = BorderFactory.createTitledBorder(localeText);
+            Border padding = BorderFactory.createEmptyBorder(10, 10, 10, 10);
+            margin = new CompoundBorder(margin, new CompoundBorder(border, padding));
+        }
+        panel.setBorder(margin);
+    }
+
+    JPanel getPanel() {
+        JPanel panel = new JPanel(new GridBagLayout());
+        addBorder(panel);
+        return panel;
+    }
+
+    @Override
+    boolean addToPanel(JPanel p, TaggingPresetInstance support) {
+        if (items.isEmpty())
+            // do not add an empty panel
+            return false;
+        JPanel panel = getPanel();
+        super.addToPanel(panel, support);
+        panel.applyComponentOrientation(support.getDefaultComponentOrientation());
+        p.add(panel, GBC.eol().fill(GridBagConstraints.HORIZONTAL));
+        return false;
+    }
+
+    @Override
+    public String toString() {
+        return "Container";
+    }
+}
Index: src/org/openstreetmap/josm/gui/tagging/presets/DataSetTaggingPresetHandler.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/DataSetTaggingPresetHandler.java	(nonexistent)
+++ src/org/openstreetmap/josm/gui/tagging/presets/DataSetTaggingPresetHandler.java	(working copy)
@@ -0,0 +1,62 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.tagging.presets;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.util.Collection;
+import java.util.List;
+
+import org.openstreetmap.josm.command.ChangePropertyCommand;
+import org.openstreetmap.josm.command.Command;
+import org.openstreetmap.josm.command.SequenceCommand;
+import org.openstreetmap.josm.data.UndoRedoHandler;
+import org.openstreetmap.josm.data.osm.DataSet;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.Tag;
+import org.openstreetmap.josm.tools.StreamUtils;
+
+/**
+ * A TaggingPresetHandler that operates on a DataSet.
+ */
+public class DataSetTaggingPresetHandler implements TaggingPresetHandler {
+    Collection<OsmPrimitive> selection;
+
+    DataSetTaggingPresetHandler(DataSet dataSet) {
+        this.selection = dataSet.getSelected();
+    }
+
+    /**
+     * Constructor
+     * @param selection the selection of primitives to edit
+     */
+    public DataSetTaggingPresetHandler(Collection<OsmPrimitive> selection) {
+        this.selection = selection;
+    }
+
+    @Override
+    public void updateTags(List<Tag> changedTags) {
+        Command cmd = createCommand(selection, changedTags);
+        if (cmd != null) {
+            UndoRedoHandler.getInstance().add(cmd);
+        }
+    }
+
+    @Override
+    public Collection<OsmPrimitive> getPrimitives() {
+        return selection;
+    }
+
+    /**
+     * Create a command to change the given list of tags.
+     * @param sel The primitives to change the tags for
+     * @param changedTags The tags to change
+     * @return A command that changes the tags.
+     */
+    public static Command createCommand(Collection<OsmPrimitive> sel, List<Tag> changedTags) {
+        List<Command> cmds = changedTags.stream()
+                .map(tag -> new ChangePropertyCommand(sel, tag.getKey(), tag.getValue()))
+                .filter(cmd -> cmd.getObjectsNumber() > 0)
+                .collect(StreamUtils.toUnmodifiableList());
+        return cmds.isEmpty() ? null : SequenceCommand.wrapIfNeeded(tr("Change Tags"), cmds);
+    }
+}
Index: src/org/openstreetmap/josm/gui/tagging/presets/Item.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/Item.java	(nonexistent)
+++ src/org/openstreetmap/josm/gui/tagging/presets/Item.java	(working copy)
@@ -0,0 +1,159 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.tagging.presets;
+
+import java.util.List;
+import java.util.Map;
+import java.util.function.Predicate;
+
+import javax.swing.JMenu;
+import javax.swing.JPanel;
+
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.Tag;
+import org.openstreetmap.josm.data.preferences.BooleanProperty;
+
+/**
+ * Template class for preset dialog construction.
+ * <p>
+ * This class and all its subclasses are immutable.  They basically are in-memory representations of
+ * a {@code presets.xml} file.  Their main use is as templates to
+ * {@link #addToPanel create the swing components} of a preset dialog.
+ * <p>
+ * To every Item class there is a companion class {@link Instance} that holds all mutable data. An
+ * Instance is created along with every active component and registered with the preset
+ * {@link TaggingPresetInstance preset instance}.  The preset dialog calls the registered item
+ * instances {@link Instance#addCommands to save the user edits}.
+ * <p>
+ * All data access goes through the {@link TaggingPresetHandler}. By plugging in a different
+ * handler, the preset dialog can edit the JOSM {@code DataSet} or any other key/value data store.
+ *
+ * @since 6068
+ */
+public abstract class Item {
+    /**
+     * Display OSM keys as {@linkplain org.openstreetmap.josm.gui.widgets.OsmIdTextField#setHint hint}
+     */
+    static final BooleanProperty DISPLAY_KEYS_AS_HINT = new BooleanProperty("taggingpreset.display-keys-as-hint", true);
+
+    /**
+     * Constructor.
+     * @param attributes the XML attributes
+     * @throws IllegalArgumentException on illegal attributes
+     */
+    Item(Map<String, String> attributes) throws IllegalArgumentException {
+    }
+
+    /**
+     * Companion class to hold mutable data.
+     * <p>
+     * An instance of this class will be created by any template class that needs mutable instance
+     * data.
+     */
+    abstract static class Instance {
+        /**
+         * Called by {@link TaggingPreset#getChangedTags} to collect changes.  If the value in this
+         * item was changed by the user, this method should add its key and value to the list.
+         *
+         * @param changedTags The list to add to.
+         */
+        public void addCommands(List<Tag> changedTags) {
+        }
+    }
+
+    /**
+     * for use of {@link TaggingPresetReader} only
+     * @param s the content to set
+     */
+    void setContent(String s) {
+    }
+
+    /**
+     * for use of {@link TaggingPresetReader} only
+     */
+    void endElement() {
+    }
+
+    /**
+     * Adds an item to the container
+     * @param item the item to add
+     */
+    void addItem(Item item) {
+        // nothing to do as I'm no container
+    }
+
+    /**
+     * Called before item is removed.
+     * <p>
+     * Use to remove listeners, etc.
+     */
+    void destroy() {
+    }
+
+    /**
+     * Creates the Swing components for this preset item and adds them to the panel.
+     *
+     * @param p The panel where components must be added
+     * @param instance The preset instance
+     * @return {@code true} if this item adds semantic tagging elements, {@code false} otherwise.
+     */
+    boolean addToPanel(JPanel p, TaggingPresetInstance instance) {
+        return false;
+    }
+
+    /**
+     * Adds this item to the menu if it is a preset item.
+     * <p>
+     * This is overridden in {@link TaggingPreset} and descendants.
+     * @param parentMenu the parent menu
+     */
+    public void addToMenu(JMenu parentMenu) {
+    }
+
+    /**
+     * When this function is called, the item should add itself to the list if it satisfies the
+     * predicate.  If the item is a sequence it should also ask its children to do the same.
+     *
+     * @param list the list to add to
+     * @param p a predicate all added items must satisfy
+     * @param followReferences whether to follow references or not
+     */
+    void addToItemList(List<Item> list, Predicate<Item> p, boolean followReferences) {
+        if (p.test(this))
+            list.add(this);
+    }
+
+    /**
+     * When this function is called, the item should add itself to the list if it is an instance of
+     * {@code type}. If the item is a sequence it should also ask its children to do the same.
+     *
+     * @param <E> the type
+     * @param list the list to add to
+     * @param type the type
+     * @param followReferences whether to follow references or not
+     */
+    <E> void addToItemList(List<E> list, Class<E> type, boolean followReferences) {
+        if (type.isInstance(this))
+            list.add(type.cast(this));
+    }
+
+    /**
+     * Various fixups after the whole xml file has been read.
+     * <p>
+     * If you are a chunk, add yourself to the map.  If you are a reference, save the map for later.
+     *
+     * @param chunks the chunks map
+     * @param parent the parent item
+     */
+    void fixup(Map<String, Chunk> chunks, Item parent) {
+    }
+
+    /**
+     * Tests whether the tags match this item.
+     * Note that for a match, at least one positive and no negative is required.
+     * @param tags the tags of an {@link OsmPrimitive}
+     * @return {@code true} if matches (positive), {@code null} if neutral, {@code false} if mismatches (negative).
+     */
+    public Boolean matches(Map<String, String> tags) {
+        return null; // NOSONAR
+    }
+}
Index: src/org/openstreetmap/josm/gui/tagging/presets/ItemFactory.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/ItemFactory.java	(nonexistent)
+++ src/org/openstreetmap/josm/gui/tagging/presets/ItemFactory.java	(working copy)
@@ -0,0 +1,109 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.tagging.presets;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.text.MessageFormat;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.openstreetmap.josm.tools.TextTagParser;
+
+/**
+ * A factory for preset items.
+ */
+public final class ItemFactory {
+    @FunctionalInterface
+    private interface FromXML {
+        Item apply(Map<String, String> attributes) throws IllegalArgumentException;
+    }
+
+    /** The map from XML element name to template class constructor. */
+    private static Map<String, FromXML> fromXML = buildMap();
+
+    private ItemFactory() {
+    }
+
+    /**
+     * The factory method.
+     *
+     * @param localname build a preset item of this kind
+     * @param attributes the attributes of the item
+     * @return the item
+     */
+    public static Item build(String localname, Map<String, String> attributes) {
+        FromXML f = fromXML.get(localname);
+        Item item = null;
+        if (f != null)
+            item = f.apply(attributes);
+        if (item == null)
+            throw new IllegalArgumentException(tr("Unknown element {0}", localname));
+        return item;
+    }
+
+    /**
+     * A convenience factory method to ease testing.
+     *
+     * @param text The text describing the item in message format, eg.
+     * {@code key key=highway value=primary} or {@code checkgroup columns=4}
+     * @param objects parameters to message format
+     * @return a new item
+     */
+    public static Item build(String text, Object... objects) {
+        final String[] arr = MessageFormat.format(text, objects).split("\\s+", 2);
+        String localname = arr[0];
+        Map<String, String> attributes = new HashMap<>();
+        if (arr.length > 1)
+            attributes = TextTagParser.readTagsFromText(arr[1]);
+        return build(localname, attributes);
+    }
+
+    private static Map<String, FromXML> buildMap() {
+        // CHECKSTYLE.OFF: SingleSpaceSeparator
+        Map<String, FromXML> map = new HashMap<>();
+        map.put("chunk",          (FromXML) Chunk::fromXML);
+        map.put("reference",      (FromXML) Reference::fromXML);
+
+        map.put("presets",        (FromXML) Root::fromXML);
+        map.put("group",          (FromXML) TaggingPresetMenu::fromXML);
+        map.put("item",           (FromXML) TaggingPreset::fromXML);
+        map.put("separator",      (FromXML) TaggingPresetSeparator::fromXML);
+
+        map.put("check",          (FromXML) Check::fromXML);
+        map.put("checkgroup",     (FromXML) CheckGroup::fromXML);
+        map.put("combo",          (FromXML) Combo::fromXML);
+        map.put("container",      (FromXML) Container::fromXML);
+        map.put("item_separator", (FromXML) ItemSeparator::fromXML);
+        map.put("key",            (FromXML) Key::fromXML);
+        map.put("label",          (FromXML) Label::fromXML);
+        map.put("link",           (FromXML) Link::fromXML);
+        map.put("list_entry",     (FromXML) PresetListEntry::fromXML);
+        map.put("multiselect",    (FromXML) MultiSelect::fromXML);
+        map.put("optional",       (FromXML) Optional::fromXML);
+        map.put("preset_link",    (FromXML) PresetLink::fromXML);
+        map.put("role",           (FromXML) Role::fromXML);
+        map.put("roles",          (FromXML) Roles::fromXML);
+        map.put("sequence",       (FromXML) Sequence::fromXML);
+        map.put("space",          (FromXML) Space::fromXML);
+        map.put("text",           (FromXML) Text::fromXML);
+        // CHECKSTYLE.ON: SingleSpaceSeparator
+        return map;
+    }
+
+    /**
+     * Prepares an attribute map like the one used by the XML parser.
+     *
+     * @param attributes the attributes to set eg. ("key", "highway", "value", "primary")
+     * @return a map suitable to be passed to {@code fromXML}.
+     * @throws IllegalArgumentException on error in the attributes
+     */
+    static Map<String, String> attributesToMap(String... attributes) throws IllegalArgumentException {
+        if (attributes.length % 2 != 0)
+            throw new IllegalArgumentException();
+        Map<String, String> map = new HashMap<>();
+        for (int i = 0; i < attributes.length; i += 2) {
+            map.put(attributes[i], attributes[i + 1]);
+        }
+        return map;
+    }
+}
Index: src/org/openstreetmap/josm/gui/tagging/presets/ItemSeparator.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/ItemSeparator.java	(nonexistent)
+++ src/org/openstreetmap/josm/gui/tagging/presets/ItemSeparator.java	(working copy)
@@ -0,0 +1,41 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.tagging.presets;
+
+import java.util.Map;
+
+import javax.swing.JPanel;
+import javax.swing.JSeparator;
+
+import org.openstreetmap.josm.tools.GBC;
+
+/**
+ * Class used to represent a {@link JSeparator} inside tagging preset window.
+ * @since 6198
+ */
+final class ItemSeparator extends Item {
+
+    private ItemSeparator(Map<String, String> attributes) throws IllegalArgumentException {
+        super(attributes);
+    }
+
+    /**
+     * Create this class from an XML element's attributes.
+     * @param attributes the XML attributes (ignored)
+     * @return the new instance
+     * @throws IllegalArgumentException on invalid attributes
+     */
+    public static ItemSeparator fromXML(Map<String, String> attributes) throws IllegalArgumentException {
+        return new ItemSeparator(attributes);
+    }
+
+    @Override
+    boolean addToPanel(JPanel p, TaggingPresetInstance support) {
+        p.add(new JSeparator(), GBC.eol().fill(GBC.HORIZONTAL).insets(0, 5, 0, 5));
+        return false;
+    }
+
+    @Override
+    public String toString() {
+        return "ItemSeparator";
+    }
+}
Index: src/org/openstreetmap/josm/gui/tagging/presets/Key.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/Key.java	(nonexistent)
+++ src/org/openstreetmap/josm/gui/tagging/presets/Key.java	(working copy)
@@ -0,0 +1,86 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.tagging.presets;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import javax.swing.JPanel;
+
+import org.openstreetmap.josm.data.osm.Tag;
+
+/**
+ * Invisible type allowing to hardcode an OSM key/value from the preset definition.
+ */
+public final class Key extends KeyedItem {
+
+    /** The hardcoded value for key */
+    private final String value;
+
+    /**
+     * Private constructor. Use {@link #fromXML} instead.
+     * @param attributes the XML attributes
+     * @throws IllegalArgumentException on illegal attributes
+     */
+    private Key(Map<String, String> attributes) throws IllegalArgumentException {
+        super(attributes);
+        value = attributes.get("value");
+    }
+
+    /**
+     * Create a {@code Key} from an XML element's attributes.
+     * @param attributes the XML attributes
+     * @return the {@code Key}
+     * @throws IllegalArgumentException on invalid attributes
+     */
+    public static Key fromXML(Map<String, String> attributes) throws IllegalArgumentException {
+        return new Key(attributes);
+    }
+
+    /**
+     * Returns the value
+     * @return the value
+     */
+    public String getValue() {
+        return value;
+    }
+
+    @Override
+    boolean addToPanel(JPanel p, TaggingPresetInstance support) {
+        support.putInstance(this, new Instance());
+        return false;
+    }
+
+    class Instance extends Item.Instance {
+        @Override
+        public void addCommands(List<Tag> changedTags) {
+            changedTags.add(asTag());
+        }
+    }
+
+    /**
+     * Returns the {@link Tag} set by this item
+     * @return the tag
+     */
+    Tag asTag() {
+        return new Tag(key, value);
+    }
+
+    @Override
+    MatchType getDefaultMatch() {
+        return MatchType.KEY_VALUE_REQUIRED;
+    }
+
+    @Override
+    public Collection<String> getValues() {
+        return Collections.singleton(value);
+    }
+
+    @Override
+    public String toString() {
+        return "Key [key=" + key + ", value=" + value + ", text=" + text
+                + ", text_context=" + textContext + ", match=" + getMatchType()
+                + ']';
+    }
+}
Index: src/org/openstreetmap/josm/gui/tagging/presets/KeyedItem.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/KeyedItem.java	(nonexistent)
+++ src/org/openstreetmap/josm/gui/tagging/presets/KeyedItem.java	(working copy)
@@ -0,0 +1,240 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.tagging.presets;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.util.Collection;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.swing.JPopupMenu;
+
+import org.openstreetmap.josm.data.osm.Tag;
+import org.openstreetmap.josm.data.preferences.BooleanProperty;
+import org.openstreetmap.josm.gui.dialogs.properties.HelpTagAction;
+import org.openstreetmap.josm.gui.dialogs.properties.TaginfoAction;
+
+/**
+ * Preset item associated to an OSM key.
+ */
+public abstract class KeyedItem extends TextItem {
+
+    /** Last value of each key used in presets, used for prefilling corresponding fields */
+    static final Map<String, String> LAST_VALUES = new HashMap<>();
+    /** True if the default value should also be set on primitives that already have tags.  */
+    static final BooleanProperty PROP_FILL_DEFAULT = new BooleanProperty("taggingpreset.fill-default-for-tagged-primitives", false);
+    /** The constant value {@code "<different>"}. */
+    static final String DIFFERENT = "<different>";
+    /** Translation of {@code "<different>"}. */
+    static final String DIFFERENT_I18N = tr(DIFFERENT);
+
+    /** This specifies the property key that will be modified by the item. */
+    final String key;
+    /** The length of the text box (number of characters allowed). */
+    final int length;
+    /**
+     * Whether the last value is used as default.
+     * <ul>
+     * <li>false = 0: do not use the last value as default
+     * <li>true = 1: use the last value as default for primitives without any tag
+     * <li>force = 2: use the last value as default for all primitives.
+     * </ul>
+     * Default is "false".
+     */
+    final int useLastAsDefault;
+    /**
+     * Allows to change the matching process, i.e., determining whether the tags of an OSM object fit into this preset.
+     * If a preset fits then it is linked in the Tags/Membership dialog.<ul>
+     * <li>none: neutral, i.e., do not consider this item for matching</li>
+     * <li>key: positive if key matches, neutral otherwise</li>
+     * <li>key!: positive if key matches, negative otherwise</li>
+     * <li>keyvalue: positive if key and value matches, neutral otherwise</li>
+     * <li>keyvalue!: positive if key and value matches, negative otherwise</li></ul>
+     * Note that for a match, at least one positive and no negative is required.
+     * Default is "keyvalue!" for {@link Key} and "none" for {@link Text}, {@link Combo}, {@link MultiSelect} and {@link Check}.
+     */
+    final MatchType matchType;
+
+    /**
+     * Constructor.
+     * @param attributes the XML attributes
+     * @throws IllegalArgumentException on illegal attributes
+     */
+    KeyedItem(Map<String, String> attributes) throws IllegalArgumentException {
+        super(attributes);
+        key = attributes.get("key");
+        useLastAsDefault = getUseLastAsDefault(attributes.get("use_last_as_default"));
+        length = Integer.parseInt(attributes.getOrDefault("length", "0"));
+        matchType = setMatchType(attributes.get("match"));
+    }
+
+    MatchType setMatchType(String v) {
+        if (v != null)
+            return MatchType.ofString(v);
+        return MatchType.ofString(getDefaultMatch().getValue());
+    }
+
+    /**
+     * Enum denoting how a match (see {@link Item#matches}) is performed.
+     */
+    enum MatchType {
+
+        /** Neutral, i.e., do not consider this item for matching. */
+        NONE("none"),
+        /** Positive if key matches, neutral otherwise. */
+        KEY("key"),
+        /** Positive if key matches, negative otherwise. */
+        KEY_REQUIRED("key!"),
+        /** Positive if key and value matches, neutral otherwise. */
+        KEY_VALUE("keyvalue"),
+        /** Positive if key and value matches, negative otherwise. */
+        KEY_VALUE_REQUIRED("keyvalue!");
+
+        private final String value;
+
+        MatchType(String value) {
+            this.value = value;
+        }
+
+        /**
+         * Replies the associated textual value.
+         * @return the associated textual value
+         */
+        public String getValue() {
+            return value;
+        }
+
+        @Override
+        public String toString() {
+            return value;
+        }
+
+        /**
+         * Determines the {@code MatchType} for the given textual value.
+         * @param type the textual value
+         * @return the {@code MatchType} for the given textual value
+         */
+        public static MatchType ofString(String type) {
+            for (MatchType i : EnumSet.allOf(MatchType.class)) {
+                if (i.getValue().equals(type))
+                    return i;
+            }
+            throw new IllegalArgumentException(type + " is not allowed");
+        }
+    }
+
+    /**
+     * Returns the key
+     * @return the key
+     */
+    public String getKey() {
+        return key;
+    }
+
+    /**
+     * Returns the match type.
+     * @return the match type
+     */
+    public String getMatchType() {
+        return matchType.toString();
+    }
+
+    /**
+     * Determines whether key or key+value are required.
+     * @return whether key or key+value are required
+     */
+    public boolean isKeyRequired() {
+        return MatchType.KEY_REQUIRED == matchType || MatchType.KEY_VALUE_REQUIRED == matchType;
+    }
+
+    /**
+     * Returns the default match.
+     * @return the default match
+     */
+    abstract MatchType getDefaultMatch();
+
+    /**
+     * Returns the list of values.
+     * @return the list of values
+     */
+    public abstract Collection<String> getValues();
+
+    String getKeyTooltipText() {
+        return tr("This corresponds to the key ''{0}''", key);
+    }
+
+    @Override
+    public Boolean matches(Map<String, String> tags) {
+        switch (matchType) {
+        case NONE:
+            return null; // NOSONAR
+        case KEY:
+            return tags.containsKey(key) ? Boolean.TRUE : null;
+        case KEY_REQUIRED:
+            return tags.containsKey(key);
+        case KEY_VALUE:
+            return tags.containsKey(key) && getValues().contains(tags.get(key)) ? Boolean.TRUE : null;
+        case KEY_VALUE_REQUIRED:
+            return tags.containsKey(key) && getValues().contains(tags.get(key));
+        default:
+            throw new IllegalStateException();
+        }
+    }
+
+    /**
+     * Sets whether the last value is used as default.
+     * @param v Using "force" (2) enforces this behaviour also for already tagged objects. Default is "false" (0).
+     * @return the value as int
+     */
+    private int getUseLastAsDefault(String v) {
+        if ("force".equals(v)) {
+            return 2;
+        } else if ("true".equals(v)) {
+            return 1;
+        } else {
+            return 0;
+        }
+    }
+
+    /**
+     * Returns true if the last entered value should be used as default.
+     * <p>
+     * Note: never used in {@code defaultpresets.xml}.
+     *
+     * @return true if the last entered value should be used as default.
+     */
+    boolean isUseLastAsDefault() {
+        return useLastAsDefault > 0;
+    }
+
+    /**
+     * Returns true if the last entered value should be used as default also on primitives that
+     * already have tags.
+     * <p>
+     * Note: used for {@code addr:*} tags in {@code defaultpresets.xml}.
+     *
+     * @return true if see above
+     */
+    boolean isForceUseLastAsDefault() {
+        return useLastAsDefault == 2;
+    }
+
+    JPopupMenu getPopupMenu() {
+        Tag tag = new Tag(key, null);
+        JPopupMenu popupMenu = new JPopupMenu();
+        popupMenu.add(tr("Key: {0}", key)).setEnabled(false);
+        popupMenu.add(new HelpTagAction(() -> tag));
+        TaginfoAction taginfoAction = new TaginfoAction(() -> tag, () -> null);
+        popupMenu.add(taginfoAction.toTagHistoryAction());
+        popupMenu.add(taginfoAction);
+        return popupMenu;
+    }
+
+    @Override
+    public String toString() {
+        return "KeyedItem [key=" + key + ", text=" + text
+                + ", text_context=" + textContext + ", match=" + matchType
+                + ']';
+    }
+}
Index: src/org/openstreetmap/josm/gui/tagging/presets/Label.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/Label.java	(nonexistent)
+++ src/org/openstreetmap/josm/gui/tagging/presets/Label.java	(working copy)
@@ -0,0 +1,43 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.tagging.presets;
+
+import java.util.Map;
+
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+
+import org.openstreetmap.josm.tools.GBC;
+
+/**
+ * Label type.
+ */
+final class Label extends TextItem {
+
+    /**
+     * Private constructor. Use {@link #fromXML} instead.
+     * @param attributes the XML attributes
+     * @throws IllegalArgumentException on illegal attributes
+     */
+    private Label(Map<String, String> attributes) throws IllegalArgumentException {
+        super(attributes);
+    }
+
+    /**
+     * Create a {@code Label} from an XML element's attributes.
+     * @param attributes the XML attributes
+     * @return the {@code Label}
+     * @throws IllegalArgumentException on illegal attributes
+     */
+    public static Label fromXML(Map<String, String> attributes) throws IllegalArgumentException {
+        return new Label(attributes);
+    }
+
+    @Override
+    boolean addToPanel(JPanel p, TaggingPresetInstance support) {
+        JLabel label = new JLabel(localeText);
+        addIcon(label);
+        label.applyComponentOrientation(support.getDefaultComponentOrientation());
+        p.add(label, GBC.eol().fill(GBC.HORIZONTAL));
+        return true;
+    }
+}
Index: src/org/openstreetmap/josm/gui/tagging/presets/Link.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/Link.java	(nonexistent)
+++ src/org/openstreetmap/josm/gui/tagging/presets/Link.java	(working copy)
@@ -0,0 +1,117 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.tagging.presets;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.event.MouseEvent;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Optional;
+
+import javax.swing.JPanel;
+import javax.swing.SwingUtilities;
+
+import org.openstreetmap.josm.gui.dialogs.properties.HelpAction;
+import org.openstreetmap.josm.gui.widgets.UrlLabel;
+import org.openstreetmap.josm.spi.preferences.Config;
+import org.openstreetmap.josm.tools.GBC;
+import org.openstreetmap.josm.tools.LanguageInfo;
+
+/**
+ * Hyperlink type.
+ * @since 8863
+ */
+public final class Link extends TextItem {
+
+    /** The link to display. */
+    private final String href;
+    /** The localized version of {@link #href}. */
+    private final String localeHref;
+    /** The OSM wiki page to display. */
+    private final String wiki;
+
+    /**
+     * Private constructor. Use {@link #fromXML} instead.
+     * @param attributes the XML attributes
+     * @throws IllegalArgumentException on attribute error
+     */
+    private Link(Map<String, String> attributes) throws IllegalArgumentException {
+        super(attributes);
+        href = attributes.get("href");
+        localeHref = attributes.get("locale_href");
+        wiki = attributes.get("wiki");
+    }
+
+    /**
+     * Create a {@code Link} from an XML element's attributes.
+     * @param attributes the XML attributes
+     * @return the {@code Link}
+     * @throws IllegalArgumentException on invalid attributes
+     */
+    public static Link fromXML(Map<String, String> attributes) throws IllegalArgumentException {
+        return new Link(attributes);
+    }
+
+    @Override
+    String getDefaultText() {
+        return tr("More information about this feature");
+    }
+
+    @Override
+    boolean addToPanel(JPanel p, TaggingPresetInstance support) {
+        UrlLabel label = buildUrlLabel();
+        if (label != null) {
+            label.applyComponentOrientation(support.getDefaultComponentOrientation());
+            p.add(label, GBC.eol().insets(0, 10, 0, 0).fill(GBC.HORIZONTAL));
+        }
+        return false;
+    }
+
+    private UrlLabel buildUrlLabel() {
+        final String url = getUrl();
+        if (wiki != null) {
+            UrlLabel urlLabel = new UrlLabel(url, localeText, 2) {
+                @Override
+                public void mouseClicked(MouseEvent e) {
+                    if (SwingUtilities.isLeftMouseButton(e)) {
+                        // Open localized page if exists
+                        HelpAction.displayHelp(Arrays.asList(
+                                LanguageInfo.getWikiLanguagePrefix(LanguageInfo.LocaleType.OSM_WIKI) + wiki,
+                                wiki));
+                    } else {
+                        super.mouseClicked(e);
+                    }
+                }
+            };
+            addIcon(urlLabel);
+            return urlLabel;
+        } else if (href != null || localeHref != null) {
+            UrlLabel urlLabel = new UrlLabel(url, localeText, 2);
+            addIcon(urlLabel);
+            return urlLabel;
+        }
+        return null;
+    }
+
+    /**
+     * Returns the link URL.
+     * @return the link URL
+     * @since 15423
+     */
+    public String getUrl() {
+        if (wiki != null) {
+            return Config.getUrls().getOSMWiki() + "/wiki/" + wiki;
+        } else if (href != null || localeHref != null) {
+            return Optional.ofNullable(localeHref).orElse(href);
+        }
+        return null;
+    }
+
+    @Override
+    String fieldsToString() {
+        return super.fieldsToString()
+                + (wiki != null ? "wiki=" + wiki + ", " : "")
+                + (href != null ? "href=" + href + ", " : "")
+                + (localeHref != null ? "locale_href=" + localeHref + ", " : "");
+    }
+}
Index: src/org/openstreetmap/josm/gui/tagging/presets/MultiSelect.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/MultiSelect.java	(nonexistent)
+++ src/org/openstreetmap/josm/gui/tagging/presets/MultiSelect.java	(working copy)
@@ -0,0 +1,146 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.tagging.presets;
+
+import java.awt.Dimension;
+import java.awt.GridBagConstraints;
+import java.awt.Insets;
+import java.awt.Rectangle;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import javax.swing.DefaultListModel;
+import javax.swing.JLabel;
+import javax.swing.JList;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+
+import org.openstreetmap.josm.gui.widgets.OrientationAction;
+import org.openstreetmap.josm.tools.GBC;
+import org.openstreetmap.josm.tools.Logging;
+
+/**
+ * Multi-select list type.
+ */
+final class MultiSelect extends ComboMultiSelect {
+    /**
+     * Number of rows to display (positive integer, optional).
+     */
+    private final int rows;
+
+    /**
+     * Private constructor. Use {@link #fromXML} instead.
+     * @param attributes the XML attributes
+     * @throws IllegalArgumentException on illegal attributes
+     */
+    private MultiSelect(Map<String, String> attributes) throws IllegalArgumentException {
+        super(attributes);
+        rows = Integer.parseInt(attributes.getOrDefault("rows", "0"));
+    }
+
+    /**
+     * Create this class from an XML element's attributes.
+     * @param attributes the XML attributes
+     * @return the new instance
+     * @throws IllegalArgumentException on invalid attributes
+     */
+    public static MultiSelect fromXML(Map<String, String> attributes) throws IllegalArgumentException {
+        return new MultiSelect(attributes);
+    }
+
+    @Override
+    String getDefaultDelimiter() {
+        return ";";
+    }
+
+    private void addEntry(DefaultListModel<PresetListEntry.Instance> model, PresetListEntry.Instance instance) {
+        if (!seenValues.containsKey(instance.getValue())) {
+            model.addElement(instance);
+            seenValues.put(instance.getValue(), instance);
+        }
+    }
+
+    @Override
+    boolean addToPanel(JPanel p, TaggingPresetInstance support) {
+        Usage usage = Usage.determineTextUsage(support.getSelected(), key);
+        seenValues.clear();
+
+        DefaultListModel<PresetListEntry.Instance> model = new DefaultListModel<>();
+        JList<PresetListEntry.Instance> list = new JList<>(model);
+        Instance instance = new Instance(list, usage);
+
+        // disable if the selected primitives have different values
+        list.setEnabled(usage.hasUniqueValue() || usage.unused());
+
+        // Add values from the preset.
+        presetListEntries.forEach(e -> addEntry(model, e.newInstance(instance)));
+
+        support.putInstance(this, instance);
+        String initialValue = instance.getInitialValue(usage, support);
+        Logging.info("initialValue = {0}", initialValue);
+
+        // Add all values used in the selected primitives. This also adds custom values and makes
+        // sure we won't lose them.
+        usage.splitValues();
+        for (String value: usage.map.keySet()) {
+            addEntry(model, new PresetListEntry(this, value).newInstance(instance));
+        }
+
+        // Select the values in the initial value.
+        if (!initialValue.isEmpty() && !DIFFERENT.equals(initialValue)) {
+            for (String value : initialValue.split(";", -1)) {
+                PresetListEntry.Instance pi = new PresetListEntry(this, value).newInstance(instance);
+                addEntry(model, pi);
+                int i = model.indexOf(pi);
+                list.addSelectionInterval(i, i);
+                Logging.info("selecting: {0} at index {1}", value, i);
+            }
+        }
+
+        PresetListEntry.CellRenderer renderer = new PresetListEntry.CellRenderer(list, list.getCellRenderer(), 200);
+        list.setCellRenderer(renderer);
+        JLabel label = addLabel(p);
+        label.setLabelFor(list);
+        JScrollPane sp = new JScrollPane(list);
+
+        if (rows > 0) {
+            list.setVisibleRowCount(rows);
+            // setVisibleRowCount() only works when all cells have the same height, but sometimes we
+            // have icons of different sizes. Calculate the size of the first {@code rows} entries
+            // and size the scrollpane accordingly.
+            Rectangle r = list.getCellBounds(0, Math.min(rows, model.size() - 1));
+            if (r != null) {
+                Insets insets = list.getInsets();
+                r.width += insets.left + insets.right;
+                r.height += insets.top + insets.bottom;
+                insets = sp.getInsets();
+                r.width += insets.left + insets.right;
+                r.height += insets.top + insets.bottom;
+                sp.setPreferredSize(new Dimension(r.width, r.height));
+            }
+        }
+        p.add(sp, GBC.eol().fill(GridBagConstraints.HORIZONTAL));
+
+        list.addListSelectionListener(l -> support.fireItemValueModified(instance, key, instance.getSelectedItem().getValue()));
+        list.setToolTipText(getKeyTooltipText());
+        list.applyComponentOrientation(OrientationAction.getValueOrientation(key));
+
+        seenValues.clear();
+        return true;
+    }
+
+    class Instance extends ComboMultiSelect.Instance {
+        JList<PresetListEntry.Instance> list;
+
+        Instance(JList<PresetListEntry.Instance> list, Usage usage) {
+            super(usage);
+            this.list = list;
+        }
+
+        @Override
+        PresetListEntry.Instance getSelectedItem() {
+            return new PresetListEntry(MultiSelect.this, list.getSelectedValuesList()
+                .stream().map(e -> e.getValue()).distinct().sorted()
+                .collect(Collectors.joining(";"))).newInstance(this);
+        }
+    }
+}
Index: src/org/openstreetmap/josm/gui/tagging/presets/Optional.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/Optional.java	(nonexistent)
+++ src/org/openstreetmap/josm/gui/tagging/presets/Optional.java	(working copy)
@@ -0,0 +1,51 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.tagging.presets;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.GridBagLayout;
+import java.util.Map;
+
+import javax.swing.JPanel;
+
+/**
+ * Used to group optional attributes.
+ * @since 8863
+ */
+final class Optional extends Container {
+    /**
+     * Private constructor.
+     * @param attributes the XML attributes
+     * @throws IllegalArgumentException on illegal attributes
+     */
+    private Optional(Map<String, String> attributes) throws IllegalArgumentException {
+        super(attributes);
+    }
+
+    /**
+     * Create this class from an XML element's attributes.
+     * @param attributes the XML attributes
+     * @return the new instance
+     * @throws IllegalArgumentException on invalid attributes
+     */
+    public static Optional fromXML(Map<String, String> attributes) throws IllegalArgumentException {
+        return new Optional(attributes);
+    }
+
+    @Override
+    String getDefaultText() {
+        return tr("Optional Attributes:");
+    }
+
+    @Override
+    JPanel getPanel() {
+        JPanel panel = new JPanel(new GridBagLayout());
+        addBorder(panel);
+        return panel;
+    }
+
+    @Override
+    public String toString() {
+        return "Optional";
+    }
+}
Index: src/org/openstreetmap/josm/gui/tagging/presets/PresetLink.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/PresetLink.java	(nonexistent)
+++ src/org/openstreetmap/josm/gui/tagging/presets/PresetLink.java	(working copy)
@@ -0,0 +1,92 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.tagging.presets;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.util.Map;
+
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+
+import org.openstreetmap.josm.tools.GBC;
+
+/**
+ * Adds a link to another preset.
+ * @since 8863
+ */
+final class PresetLink extends TextItem {
+
+    /** The exact name of the preset to link to. Required. */
+    private final String presetName;
+
+    /**
+     * Private constructor. Use {@link #fromXML} instead.
+     * @param attributes the XML attributes
+     * @throws IllegalArgumentException on illegal attributes
+     */
+    private PresetLink(Map<String, String> attributes) throws IllegalArgumentException {
+        super(attributes);
+        presetName = attributes.get("preset_name");
+        if (presetName == null)
+            throw new IllegalArgumentException("attribute preset_name is required");
+    }
+
+    /**
+     * Create a {@code PresetLink} from an XML element's attributes.
+     * @param attributes the XML attributes
+     * @return the {@code PresetLink}
+     * @throws IllegalArgumentException on illegal attributes
+     */
+    public static PresetLink fromXML(Map<String, String> attributes) throws IllegalArgumentException {
+        return new PresetLink(attributes);
+    }
+
+    static final class TaggingPresetMouseAdapter extends MouseAdapter {
+        private final TaggingPreset preset;
+        private final TaggingPresetHandler handler;
+
+        TaggingPresetMouseAdapter(TaggingPreset preset, TaggingPresetHandler handler) {
+            this.preset = preset;
+            this.handler = handler;
+        }
+
+        @Override
+        public void mouseClicked(MouseEvent e) {
+            TaggingPresetDialog.showAndApply(preset, handler, false);
+        }
+    }
+
+    @Override
+    protected String getDefaultText() {
+        return tr("Edit also …");
+    }
+
+    /**
+     * Creates a label to be inserted above this link
+     * @return a label
+     */
+    JLabel createLabel() {
+        return new JLabel(localeText);
+    }
+
+    @Override
+    boolean addToPanel(JPanel p, TaggingPresetInstance support) {
+        for (TaggingPreset preset : TaggingPresets.getTaggingPresets()) {
+            if (presetName.equals(preset.getBaseName())) {
+                JLabel lbl = new TaggingPresetLabel(preset);
+                lbl.addMouseListener(new TaggingPresetMouseAdapter(preset, support.getHandler()));
+                lbl.applyComponentOrientation(support.getDefaultComponentOrientation());
+                p.add(lbl, GBC.eol().fill(GBC.HORIZONTAL));
+                break;
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public String toString() {
+        return "PresetLink [preset_name=" + presetName + ']';
+    }
+}
Index: src/org/openstreetmap/josm/gui/tagging/presets/PresetListEntry.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/PresetListEntry.java	(nonexistent)
+++ src/org/openstreetmap/josm/gui/tagging/presets/PresetListEntry.java	(working copy)
@@ -0,0 +1,323 @@
+// License: GPL. For details, see LICENSE file.
+
+package org.openstreetmap.josm.gui.tagging.presets;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+import static org.openstreetmap.josm.tools.I18n.trc;
+
+import java.awt.Component;
+import java.awt.Font;
+import java.util.Map;
+import java.util.Objects;
+
+import javax.swing.ImageIcon;
+import javax.swing.JLabel;
+import javax.swing.JList;
+import javax.swing.JPanel;
+import javax.swing.ListCellRenderer;
+
+import org.openstreetmap.josm.gui.widgets.JosmListCellRenderer;
+import org.openstreetmap.josm.tools.AlphanumComparator;
+import org.openstreetmap.josm.tools.Utils;
+
+/**
+ * Preset list entry.
+ * <p>
+ * Used for controls that offer a list of items to choose from like {@link Combo} and
+ * {@link MultiSelect}.
+ */
+class PresetListEntry extends Item {
+    /** Used to display an entry matching several different values. */
+    static final PresetListEntry ENTRY_DIFFERENT = new PresetListEntry(null, KeyedItem.DIFFERENT);
+    /** Used to display an empty entry used to clear values. */
+    static final PresetListEntry ENTRY_EMPTY = new PresetListEntry(null, "");
+
+    /**
+     * This is the value that is going to be written to the tag on the selected primitive(s). Except
+     * when the value is {@code "<different>"}, which is never written, or the value is empty, which
+     * deletes the tag.  {@code value} is never translated.
+     */
+    private final String value;
+    /** Text displayed to the user instead of {@link #value}. */
+    private final String displayValue;
+    /** Text to be displayed below {@link #displayValue} in the combobox list. */
+    private final String shortDescription;
+    /** The location of icon file to display */
+    private final String icon;
+    /** The size of displayed icon. If not set, default is size from icon file */
+    private final int iconSize;
+
+    /** The localized version of {@link #displayValue}. */
+    private final String localeDisplayValue;
+    /** The finallocalized version of {@link #shortDescription}. */
+    private final String localeShortDescription;
+    /** Tha cached icon */
+    private ImageIcon cachedIcon;
+
+    /**
+     * Private constructor. Use {@link #fromXML} instead.
+     * @param attributes the XML attributes
+     * @throws IllegalArgumentException on attribute error
+     */
+    private PresetListEntry(Map<String, String> attributes) throws IllegalArgumentException {
+        super(attributes);
+        value = attributes.get("value");
+        displayValue = attributes.get("display_value");
+        localeDisplayValue = attributes.get("locale_display_value");
+        shortDescription = attributes.get("short_description");
+        localeShortDescription = attributes.get("locale_short_description");
+        icon = attributes.get("icon");
+        iconSize = Integer.parseInt(attributes.getOrDefault("icon_size", "0"));
+    }
+
+    /**
+     * Create this class from an XML element's attributes.
+     * @param attributes the XML attributes
+     * @return the new instance
+     * @throws IllegalArgumentException on invalid attributes
+     */
+    public static PresetListEntry fromXML(Map<String, String> attributes) throws IllegalArgumentException {
+        return new PresetListEntry(attributes);
+    }
+
+    /**
+     * Convenience constructor.  Constructs a new {@code PresetListEntry}, initialized with a value.
+     *
+     * @param value value
+     * @param cms the ComboMultiSelect
+     */
+    PresetListEntry(ComboMultiSelect cms, String value) {
+        super(ItemFactory.attributesToMap());
+        this.value = value;
+        this.displayValue = value;
+        this.localeDisplayValue = value;
+        this.shortDescription = "";
+        this.localeShortDescription = "";
+        this.icon = null;
+        this.iconSize = 0;
+    }
+
+    /**
+     * Returns the value
+     * @return the value
+     */
+    String getValue() {
+        return value;
+    }
+
+    /**
+     * Returns the entry icon, if any.
+     * @return the entry icon, or {@code null}
+     */
+    ImageIcon getIcon() {
+        if (icon != null && cachedIcon == null) {
+            cachedIcon = TaggingPresetUtils.loadImageIcon(icon, TaggingPresetReader.getZipIcons(), iconSize);
+        }
+        return cachedIcon;
+    }
+
+    /**
+     * Returns the contents displayed in the current item view.
+     * @param cms the ComboMultiSelect
+     * @return the value to display
+     */
+    String getDisplayValue(ComboMultiSelect cms) {
+        if (cms.valuesNoI18n) {
+            return Utils.firstNonNull(PresetListEntry.this.value, " ");
+        }
+        return Utils.firstNonNull(
+            localeDisplayValue,
+            tr(displayValue),
+            trc(cms.valuesContext, value),
+            " "
+        );
+    }
+
+    /**
+     * Returns the short description to display.
+     * @return the short description to display
+     */
+    String getShortDescription() {
+        return Utils.firstNonNull(
+            localeShortDescription,
+            tr(shortDescription),
+            ""
+        );
+    }
+
+    /**
+     * Returns the tooltip for this entry.
+     * @param key the tag key
+     * @return the tooltip
+     */
+    String getToolTipText(String key) {
+        if (this.equals(ENTRY_DIFFERENT)) {
+            return tr("Keeps the original values of the selected objects unchanged.");
+        }
+        if (value != null && !value.isEmpty()) {
+            return tr("Sets the key ''{0}'' to the value ''{1}''.", key, value);
+        }
+        return tr("Clears the key ''{0}''.", key);
+    }
+
+    @Override
+    boolean addToPanel(JPanel p, TaggingPresetInstance support) {
+        return false;
+    }
+
+    /**
+     * Creates a new instance
+     * @param cmsInstance The ComboMultiSelect.Instance
+     * @return the new instance
+     */
+    Instance newInstance(ComboMultiSelect.Instance cmsInstance) {
+        return new Instance(cmsInstance);
+    }
+
+    class Instance implements Comparable<Instance> {
+        private String displayValue;
+        private String shortDescription;
+        private String toolTip;
+        private ImageIcon icon;
+        private Usage usage;
+
+        Instance(ComboMultiSelect.Instance cmsInstance) {
+            this.usage = cmsInstance.usage;
+            this.icon = PresetListEntry.this.getIcon();
+            ComboMultiSelect cms = cmsInstance.getTemplate();
+            this.toolTip = PresetListEntry.this.getToolTipText(cms.getKey());
+
+            displayValue = getDisplayValue(cms);
+            shortDescription = getShortDescription();
+        }
+
+        Instance(ComboMultiSelect.Instance cmsInstance, String value) {
+            this.usage = cmsInstance.usage;
+            this.displayValue = value;
+        }
+
+        String getValue() {
+            return PresetListEntry.this.getValue();
+        }
+
+        @Override
+        public int compareTo(Instance o) {
+            return AlphanumComparator.getInstance().compare(this.displayValue, o.displayValue);
+        }
+
+        // toString is mainly used to initialize the Editor
+        @Override
+        public String toString() {
+            if (this.getValue().equals(KeyedItem.DIFFERENT))
+                return displayValue;
+            return displayValue.replaceAll("\\s*<.*>\\s*", " "); // remove additional markup, e.g. <br>
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+            PresetListEntry.Instance that = (PresetListEntry.Instance) o;
+            return Objects.equals(getValue(), that.getValue());
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(getValue());
+        }
+
+        /**
+         * Returns how many selected primitives had this value set.
+         * @return see above
+         */
+        int getCount() {
+            Integer count = usage.map.get(value);
+            return count == null ? 0 : count;
+        }
+
+        /**
+         * Returns the contents displayed in the dropdown list.
+         *
+         * This is the contents that would be displayed in the current view plus a short description to
+         * aid the user.  The whole contents is wrapped to {@code width}.
+         *
+         * @param width the width in px
+         * @return HTML formatted contents
+         */
+        String getListDisplay(int width) {
+            Integer count = getCount();
+            String result = displayValue;
+
+            if (count > 0 && usage.getSelectedCount() > 1) {
+                result = tr("{0} ({1})", displayValue, count);
+            }
+
+            if (this.getValue().equals(KeyedItem.DIFFERENT)) {
+                return "<html><b>" + Utils.escapeReservedCharactersHTML(displayValue) + "</b></html>";
+            }
+
+            if (shortDescription.isEmpty()) {
+                // avoids a collapsed list entry if value == ""
+                if (result.isEmpty()) {
+                    return " ";
+                }
+                return result;
+            }
+
+            // RTL not supported in HTML. See: http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4866977
+            return String.format("<html><div style=\"width: %d\"><b>%s</b><p style=\"padding-left: 10\">%s</p></div></html>",
+                    width,
+                    result,
+                    Utils.escapeReservedCharactersHTML(shortDescription));
+        }
+    }
+
+    /**
+     * A list cell renderer that paints a short text in the current value pane and and a longer text
+     * in the dropdown list.
+     */
+    static class CellRenderer extends JosmListCellRenderer<PresetListEntry.Instance> {
+        int width;
+
+        CellRenderer(Component component, ListCellRenderer<? super PresetListEntry.Instance> renderer, int width) {
+            super(component, renderer);
+            setWidth(width);
+        }
+
+        /**
+         * Sets the width to format the dropdown list to
+         *
+         * Note: This is not the width of the list, but the width to which we format any multi-line
+         * label in the list.  We cannot use the list's width because at the time the combobox
+         * measures its items, it is not guaranteed that the list is already sized, the combobox may
+         * not even be layed out yet.  Set this to {@code combobox.getWidth()}
+         *
+         * @param width the width
+         */
+        public void setWidth(int width) {
+            if (width <= 0)
+                width = 200;
+            this.width = width - 20;
+        }
+
+        @Override
+        public JLabel getListCellRendererComponent(
+            JList<? extends PresetListEntry.Instance> list, PresetListEntry.Instance value,
+                int index, boolean isSelected, boolean cellHasFocus) {
+
+            JLabel l = (JLabel) renderer.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
+            l.setComponentOrientation(component.getComponentOrientation());
+            if (index != -1) {
+                // index -1 is set when measuring the size of the cell and when painting the
+                // editor-ersatz of a readonly combobox. fixes #6157
+                l.setText(value.getListDisplay(width));
+            }
+            if (value.getCount() > 0) {
+                l.setFont(l.getFont().deriveFont(Font.ITALIC + Font.BOLD));
+            }
+            l.setIcon(value.icon);
+            l.setToolTipText(value.toolTip);
+            return l;
+        }
+    }
+}
Index: src/org/openstreetmap/josm/gui/tagging/presets/ReadOnlyTaggingPresetHandler.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/ReadOnlyTaggingPresetHandler.java	(nonexistent)
+++ src/org/openstreetmap/josm/gui/tagging/presets/ReadOnlyTaggingPresetHandler.java	(working copy)
@@ -0,0 +1,34 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.tagging.presets;
+
+import java.util.Collection;
+import java.util.List;
+
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.Tag;
+
+/**
+ * A Read-Only TaggingPresetHandler that operates on a collection of primitives.
+ * <p>
+ * For testing purposes.
+ */
+public class ReadOnlyTaggingPresetHandler implements TaggingPresetHandler {
+    Collection<OsmPrimitive> selection;
+
+    /**
+     * Constructor
+     * @param selection the selection of primitives to edit
+     */
+    public ReadOnlyTaggingPresetHandler(Collection<OsmPrimitive> selection) {
+        this.selection = selection;
+    }
+
+    @Override
+    public void updateTags(List<Tag> changedTags) {
+    }
+
+    @Override
+    public Collection<OsmPrimitive> getPrimitives() {
+        return selection;
+    }
+}
Index: src/org/openstreetmap/josm/gui/tagging/presets/Reference.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/Reference.java	(nonexistent)
+++ src/org/openstreetmap/josm/gui/tagging/presets/Reference.java	(working copy)
@@ -0,0 +1,86 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.tagging.presets;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.util.List;
+import java.util.Map;
+import java.util.function.Predicate;
+
+import javax.swing.JPanel;
+
+/**
+ * A reference to be satisfied by a {@link Chunk}
+ */
+final class Reference extends Item {
+    private final String ref;
+    private Map<String, Chunk> chunks;
+
+    /**
+     * Private constructor.
+     * @param attributes the XML attributes
+     * @throws IllegalArgumentException on illegal attributes
+     */
+    private Reference(Map<String, String> attributes) throws IllegalArgumentException {
+        super(attributes);
+        ref = attributes.get("ref");
+    }
+
+    @Override
+    void fixup(Map<String, Chunk> chunks, Item parent) {
+        super.fixup(chunks, parent);
+        this.chunks = chunks;
+    }
+
+    /**
+     * Create a {@code Reference} from an XML element's attributes.
+     * @param attributes the XML attributes
+     * @return the {@code Reference}
+     * @throws IllegalArgumentException on invalid attributes
+     */
+    public static Reference fromXML(Map<String, String> attributes) throws IllegalArgumentException {
+        return new Reference(attributes);
+    }
+
+    @Override
+    void destroy() {
+        chunks = null;
+        super.destroy();
+    }
+
+    private Chunk getChunk() {
+        Chunk chunk = chunks.get(ref);
+        if (chunk == null)
+            throw new IllegalArgumentException(tr("Reference to undefined chunk: {0}", ref));
+        return chunk;
+    }
+
+    @Override
+    void addToItemList(List<Item> list, Predicate<Item> p, boolean followReferences) {
+        super.addToItemList(list, p, followReferences);
+        if (followReferences)
+            getChunk().addToItemList(list, p, followReferences);
+    }
+
+    @Override
+    <E> void addToItemList(List<E> list, Class<E> type, boolean followReferences) {
+        super.addToItemList(list, type, followReferences);
+        if (followReferences)
+            getChunk().addToItemList(list, type, followReferences);
+    }
+
+    @Override
+    boolean addToPanel(JPanel p, TaggingPresetInstance support) {
+        return getChunk().addToPanel(p, support);
+    }
+
+    @Override
+    public Boolean matches(Map<String, String> tags) {
+        return getChunk().matches(tags);
+    }
+
+    @Override
+    public String toString() {
+        return "Reference [ref=" + ref + "]";
+    }
+}
Index: src/org/openstreetmap/josm/gui/tagging/presets/Role.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/Role.java	(nonexistent)
+++ src/org/openstreetmap/josm/gui/tagging/presets/Role.java	(working copy)
@@ -0,0 +1,269 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.tagging.presets;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.util.Collection;
+import java.util.EnumSet;
+import java.util.Map;
+import java.util.Set;
+
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+
+import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
+import org.openstreetmap.josm.data.osm.search.SearchCompiler;
+import org.openstreetmap.josm.data.osm.search.SearchParseError;
+import org.openstreetmap.josm.data.osm.search.SearchSetting;
+import org.openstreetmap.josm.tools.GBC;
+import org.openstreetmap.josm.tools.ImageProvider;
+
+/**
+ * The <code>role</code> element in tagging preset definition.
+ *
+ * Information on a certain role, which is expected for the relation members.
+ */
+public class Role extends TextItem {
+    /** the right margin */
+    private static final int right = 10;
+
+    /** Role name used in a relation */
+    private final String key;
+    /** Presets types expected for this role */
+    private final EnumSet<TaggingPresetType> types;
+    /** Is the role name a regular expression */
+    private final boolean regexp;
+    /** How often must the element appear */
+    private final int count;
+    /** An expression (cf. search dialog) for objects of this role */
+    private final SearchCompiler.Match memberExpression;
+    /** Is this role required at least once in the relation? */
+    private final boolean required;
+
+    /**
+     * Private constructor. Use {@link #fromXML} instead.
+     * @param attributes the XML attributes
+     * @throws IllegalArgumentException on illegal attributes
+     */
+    private Role(Map<String, String> attributes) throws IllegalArgumentException {
+        super(attributes);
+        key = attributes.get("key");
+        types = TaggingPresetType.getOrDefault(attributes.get("type"), EnumSet.noneOf(TaggingPresetType.class));
+        regexp = Boolean.parseBoolean(attributes.getOrDefault("regexp", "false"));
+        count = Integer.parseInt(attributes.getOrDefault("count", "0"));
+        memberExpression = parseSearchExpression(attributes.get("member_expression"));
+        required = parseRequisite(attributes.getOrDefault("requisite", "optional"));
+    }
+
+    /**
+     * Convenience constructor (also for testing purposes)
+     * @param key the key
+     * @param types the tagging preset types this role applies to
+     */
+    public Role(String key, Set<TaggingPresetType> types) {
+        super(ItemFactory.attributesToMap());
+        this.key = key;
+        this.types = EnumSet.copyOf(types);
+        this.regexp = false;
+        this.count = 0;
+        this.memberExpression = null;
+        this.required = false;
+    }
+
+    /**
+     * Create this class from an XML element's attributes.
+     * @param attributes the XML attributes
+     * @return the new instance
+     * @throws IllegalArgumentException on invalid attributes
+     */
+    public static Role fromXML(Map<String, String> attributes) throws IllegalArgumentException {
+        return new Role(attributes);
+    }
+
+    /**
+     * Returns the key
+     * @return the key
+     */
+    public String getKey() {
+        return key;
+    }
+
+    /**
+     * Returns the member expression
+     * @return the member expression
+     */
+    public SearchCompiler.Match getMemberExpression() {
+        return memberExpression;
+    }
+
+    /**
+     * Sets whether this role is required at least once in the relation.
+     * @param str "required" or "optional"
+     * @return true if required
+     * @throws IllegalArgumentException if str is neither "required" or "optional"
+     */
+    private boolean parseRequisite(String str) throws IllegalArgumentException {
+        if ("required".equals(str)) {
+            return true;
+        } else if ("optional".equals(str)) {
+            return false;
+        }
+        throw new IllegalArgumentException(tr("Unknown requisite: {0}", str));
+    }
+
+    /**
+     * Sets an expression (cf. search dialog) for objects of this role
+     * @param memberExpression an expression (cf. search dialog) for objects of this role
+     * @return the match expression
+     * @throws IllegalArgumentException in case of parsing error
+     */
+    private SearchCompiler.Match parseSearchExpression(String memberExpression) throws IllegalArgumentException {
+        if (memberExpression == null)
+            return null;
+        try {
+            final SearchSetting searchSetting = new SearchSetting();
+            searchSetting.text = memberExpression;
+            searchSetting.caseSensitive = true;
+            searchSetting.regexSearch = true;
+            return SearchCompiler.compile(searchSetting);
+        } catch (SearchParseError ex) {
+            throw new IllegalArgumentException(tr("Illegal member expression: {0}", ex.getMessage()), ex);
+        }
+    }
+
+    /**
+     * Return either argument, the highest possible value or the lowest allowed value.  Used by
+     * relation checker.
+     * @param c count
+     * @return the highest possible value or the lowest allowed value
+     * @see #required
+     */
+    public long getValidCount(long c) {
+        if (count > 0 && !required)
+            return c != 0 ? count : 0;
+        else if (count > 0)
+            return count;
+        else if (!required)
+            return c != 0 ? c : 0;
+        else
+            return c != 0 ? c : 1;
+    }
+
+    /**
+     * Returns the preset types that this role may be applied to
+     * @return the preset types
+     */
+    public EnumSet<TaggingPresetType> getTypes() {
+        return EnumSet.copyOf(types);
+    }
+
+    /**
+     * Returns true if this role may be applied to the given preset type, eg. node / way ...
+     *
+     * @param presetType The preset type
+     * @return true if this role may be applied to the given preset type.
+     */
+    public boolean appliesTo(TaggingPresetType presetType) {
+        return types.contains(presetType);
+    }
+
+    /**
+     * Returns true if this role may be applied to the given primitive type, eg. node / way ...
+     *
+     * @param primitiveType The OSM primitive type
+     * @return true if this role may be applied to the given primitive type.
+     */
+    public boolean appliesTo(OsmPrimitiveType primitiveType) {
+        return types.contains(TaggingPresetType.forPrimitiveType(primitiveType));
+    }
+
+    /**
+     * Returns true if this role may be applied to all of the given preset types.
+     * <p>
+     * Returns true if {@code role.types} contains all elements of {@code types}.
+     *
+     * @param presetTypes The preset types.
+     * @return true if this role may be applied to all of the given preset types.
+     */
+    public boolean appliesToAll(Collection<TaggingPresetType> presetTypes) {
+        return types.containsAll(presetTypes);
+    }
+
+    /**
+     * Role if the given role matches this class (required to check regexp role types)
+     * @param role role to check
+     * @return <code>true</code> if role matches
+     * @since 11989
+     */
+    public boolean isRole(String role) {
+        if (regexp && role != null) { // pass null through, it will anyway fail
+            return role.matches(this.key);
+        }
+        return this.key.equals(role);
+    }
+
+    /**
+     * Adds this role to the given panel.
+     * @param p panel where to add this role
+     * @return {@code true}
+     */
+    @Override
+    boolean addToPanel(JPanel p, TaggingPresetInstance support) {
+        GBC std = GBC.std().insets(0, 0, right, 0);
+        GBC eol = GBC.eol().insets(0, 0, right, 0);
+
+        String cstring;
+        if (count > 0 && !required) {
+            cstring = "0,"+count;
+        } else if (count > 0) {
+            cstring = String.valueOf(count);
+        } else if (!required) {
+            cstring = "0-...";
+        } else {
+            cstring = "1-...";
+        }
+        p.add(new JLabel(localeText+':'), std);
+        p.add(new JLabel(key), std);
+        p.add(new JLabel(cstring), std);
+
+        JPanel typesPanel = new JPanel();
+        for (TaggingPresetType t : types) {
+            typesPanel.add(new JLabel(ImageProvider.get(t.getIconName())));
+        }
+        p.add(typesPanel, eol);
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ((key == null) ? 0 : key.hashCode());
+        result = prime * result + ((types == null) ? 0 : types.hashCode());
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj)
+            return true;
+        if (obj == null)
+            return false;
+        if (getClass() != obj.getClass())
+            return false;
+        Role other = (Role) obj;
+        if (key == null) {
+            if (other.key != null)
+                return false;
+        } else if (!key.equals(other.key))
+            return false;
+        if (!types.equals(other.types))
+            return false;
+        return true;
+    }
+
+    @Override
+    public String toString() {
+        return "Role [key=" + key + ", text=" + text + ']';
+    }
+}
Index: src/org/openstreetmap/josm/gui/tagging/presets/Roles.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/Roles.java	(nonexistent)
+++ src/org/openstreetmap/josm/gui/tagging/presets/Roles.java	(working copy)
@@ -0,0 +1,70 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.tagging.presets;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.util.Map;
+
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+
+import org.openstreetmap.josm.tools.GBC;
+
+/**
+ * The <code>roles</code> element in tagging presets definition.
+ * <p>
+ * A list of {@link Role} elements. Describes the roles that are expected for
+ * the members of a relation.
+ * <p>
+ * Used for data validation, auto completion, among others.
+ */
+final class Roles extends Container {
+    /** the right margin */
+    private static final int right = 10;
+
+    /**
+     * Private constructor. Use {@link #fromXML} instead.
+     * @param attributes the XML attributes
+     * @throws IllegalArgumentException on illegal attributes
+     */
+    private Roles(Map<String, String> attributes) throws IllegalArgumentException {
+        super(attributes);
+    }
+
+    /**
+     * Create this class from an XML element's attributes.
+     * @param attributes the XML attributes
+     * @return the new instance
+     * @throws IllegalArgumentException on invalid attributes
+     */
+    public static Roles fromXML(Map<String, String> attributes) throws IllegalArgumentException {
+        return new Roles(attributes);
+    }
+
+    @Override
+    String getDefaultText() {
+        return tr("Roles:");
+    }
+
+    @Override
+    JPanel getPanel() {
+        GBC std = GBC.std().insets(0, 0, right, 10);
+        GBC eol = GBC.eol().insets(0, 0, right, 10).fill(GridBagConstraints.HORIZONTAL);
+
+        JPanel panel = new JPanel(new GridBagLayout());
+        panel.setAlignmentX(0);
+        addBorder(panel);
+        panel.add(new JLabel(tr("Available roles")), std);
+        panel.add(new JLabel(tr("role")), std);
+        panel.add(new JLabel(tr("count")), std);
+        panel.add(new JLabel(tr("elements")), eol);
+        return panel;
+    }
+
+    @Override
+    public String toString() {
+        return "Roles";
+    }
+}
Index: src/org/openstreetmap/josm/gui/tagging/presets/Root.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/Root.java	(nonexistent)
+++ src/org/openstreetmap/josm/gui/tagging/presets/Root.java	(working copy)
@@ -0,0 +1,58 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.tagging.presets;
+
+import java.util.Map;
+
+import javax.swing.JPanel;
+
+/**
+ * The XML root element.  Corresponds to {@code <presets>}.
+ */
+public class Root extends Sequence {
+    /** The url of the XML resource. */
+    String url;
+
+    final String author;
+    final String version;
+    final String description;
+    final String shortDescription;
+    final String link;
+    final String iconName;
+    final String baseLanguage;
+
+    /**
+     * Constructor.
+     * @param attributes the XML attributes
+     * @throws IllegalArgumentException on illegal attributes
+     */
+    Root(Map<String, String> attributes) throws IllegalArgumentException {
+        super(attributes);
+        author = attributes.get("author");
+        version = attributes.get("version");
+        description = attributes.get("description");
+        shortDescription = attributes.get("shortdescription");
+        link = attributes.get("link");
+        iconName = attributes.get("icon");
+        baseLanguage = attributes.get("baselanguage");
+    }
+
+    /**
+     * Create a {@code Root}
+     * @param attributes the XML attributes
+     * @return the {@code Chunk}
+     * @throws IllegalArgumentException on invalid attributes
+     */
+    public static Root fromXML(Map<String, String> attributes) throws IllegalArgumentException {
+        return new Root(attributes);
+    }
+
+    @Override
+    boolean addToPanel(JPanel p, TaggingPresetInstance support) {
+        return false;
+    }
+
+    @Override
+    public String toString() {
+        return "Root [" + url + "]";
+    }
+}
Index: src/org/openstreetmap/josm/gui/tagging/presets/Sequence.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/Sequence.java	(nonexistent)
+++ src/org/openstreetmap/josm/gui/tagging/presets/Sequence.java	(working copy)
@@ -0,0 +1,163 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.tagging.presets;
+
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Predicate;
+
+import javax.swing.JMenu;
+import javax.swing.JPanel;
+
+/**
+ * A sequence of items {@link Item}s.
+ */
+public class Sequence extends Item {
+    /** The list of items in the sequence. */
+    final List<Item> items = new LinkedList<>();
+
+    /**
+     * Constructor.
+     * @param attributes the XML attributes
+     * @throws IllegalArgumentException on illegal attributes
+     */
+    Sequence(Map<String, String> attributes) throws IllegalArgumentException {
+        super(attributes);
+    }
+
+    /**
+     * Create this class from an XML element's attributes.
+     * @param attributes the XML attributes
+     * @return the new instance
+     * @throws IllegalArgumentException on illegal attributes
+     */
+    public static Sequence fromXML(Map<String, String> attributes) throws IllegalArgumentException {
+        return new Sequence(attributes);
+    }
+
+    /**
+     * Adds an item to the container
+     * @param item the item to add
+     */
+    @Override
+    public void addItem(Item item) {
+        items.add(item);
+    }
+
+    @Override
+    void destroy() {
+        super.destroy();
+        items.forEach(item -> item.destroy());
+    }
+
+    @Override
+    boolean addToPanel(JPanel p, TaggingPresetInstance instance) {
+        items.forEach(item -> item.addToPanel(p, instance));
+        return false;
+    }
+
+    @Override
+    public void addToMenu(JMenu parentMenu) {
+        items.forEach(item -> item.addToMenu(parentMenu));
+    }
+
+    /**
+     * Returns all items in this sequence. Convenience function.
+     *
+     * @return the list of all items
+     */
+    public List<Item> getAllItems() {
+        return getAllItems(true);
+    }
+
+    /**
+     * Returns all items in this sequence.
+     * <p>
+     * Use the followReference mode when getting all items inside a preset.  Do not follow
+     * references when getting all items inside a root element because you would get all items in
+     * chunks many times over.
+     *
+     * @param followReferences whether to follow references or not
+     * @return the list of all items
+     */
+    public List<Item> getAllItems(boolean followReferences) {
+        List<Item> result = new LinkedList<>();
+        items.forEach(item -> item.addToItemList(result, i -> true, followReferences));
+        return result;
+    }
+
+    /**
+     * Returns all items in this sequence that satisfy a predicate.
+     * @param p the predicate all items must satisfy
+     * @param followReferences whether to follow references or not
+     * @return the list of all items
+     */
+    public List<Item> getAllItems(Predicate<Item> p, boolean followReferences) {
+        List<Item> list = new LinkedList<>();
+        items.forEach(item -> item.addToItemList(list, p, followReferences));
+        return list;
+    }
+
+    /**
+     * Returns all items of a type in this sequence.
+     * @param <E> the type
+     * @param type the type
+     * @param followReferences whether to follow references or not
+     * @return the list of all items
+     */
+    public <E> List<E> getAllItems(Class<E> type, boolean followReferences) {
+        List<E> list = new LinkedList<>();
+        items.forEach(item -> item.addToItemList(list, type, followReferences));
+        return list;
+    }
+
+    /**
+     * Returns all roles in this sequence. Convenience function.
+     *
+     * @return the list of all roles
+     */
+    public List<Role> getAllRoles() {
+        return getAllRoles(true);
+    }
+
+    /**
+     * Returns all roles in this sequence.
+     * @param followReferences whether to follow references or not
+     * @return the list of all roles
+     */
+    public List<Role> getAllRoles(boolean followReferences) {
+        return getAllItems(Role.class, followReferences);
+    }
+
+    @Override
+    public void addToItemList(List<Item> list, Predicate<Item> p, boolean followReferences) {
+        super.addToItemList(list, p, followReferences);
+        items.forEach(item -> item.addToItemList(list, p, followReferences));
+    }
+
+    @Override
+    <E> void addToItemList(List<E> list, Class<E> type, boolean followReferences) {
+        super.addToItemList(list, type, followReferences);
+        items.forEach(item -> item.addToItemList(list, type, followReferences));
+    }
+
+    @Override
+    void fixup(Map<String, Chunk> chunks, Item parent) {
+        items.forEach(item -> item.fixup(chunks, parent));
+    }
+
+    @Override
+    public Boolean matches(Map<String, String> tags) {
+        for (Item item : items) {
+            if (Boolean.TRUE.equals(item.matches(tags))) {
+                return Boolean.TRUE;
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public String toString() {
+        return "Collection";
+    }
+}
Index: src/org/openstreetmap/josm/gui/tagging/presets/Space.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/Space.java	(nonexistent)
+++ src/org/openstreetmap/josm/gui/tagging/presets/Space.java	(working copy)
@@ -0,0 +1,39 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.tagging.presets;
+
+import java.util.Map;
+
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+
+import org.openstreetmap.josm.tools.GBC;
+
+/**
+ * A horizontal spacer.
+ */
+final class Space extends Item {
+    private Space(Map<String, String> attributes) throws IllegalArgumentException {
+        super(attributes);
+    }
+
+    /**
+     * Create this class from an XML element's attributes.
+     * @param attributes the XML attributes (ignored)
+     * @return the new instance
+     * @throws IllegalArgumentException on invalid attributes
+     */
+    public static Space fromXML(Map<String, String> attributes) throws IllegalArgumentException {
+        return new Space(attributes);
+    }
+
+    @Override
+    boolean addToPanel(JPanel p, TaggingPresetInstance support) {
+        p.add(new JLabel(" "), GBC.eol()); // space
+        return false;
+    }
+
+    @Override
+    public String toString() {
+        return "Space";
+    }
+}
Index: src/org/openstreetmap/josm/gui/tagging/presets/TaggingPreset.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/TaggingPreset.java	(revision 18366)
+++ src/org/openstreetmap/josm/gui/tagging/presets/TaggingPreset.java	(working copy)
@@ -2,380 +2,172 @@
 package org.openstreetmap.josm.gui.tagging.presets;
 
 import static org.openstreetmap.josm.tools.I18n.tr;
-import static org.openstreetmap.josm.tools.I18n.trc;
-import static org.openstreetmap.josm.tools.I18n.trn;
 
-import java.awt.Component;
 import java.awt.ComponentOrientation;
-import java.awt.Dimension;
 import java.awt.GridBagLayout;
-import java.awt.Insets;
 import java.awt.event.ActionEvent;
-import java.io.File;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.EnumSet;
-import java.util.LinkedHashSet;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
-import java.util.concurrent.CompletableFuture;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
 
 import javax.swing.AbstractAction;
 import javax.swing.Action;
-import javax.swing.ImageIcon;
 import javax.swing.JLabel;
-import javax.swing.JOptionPane;
+import javax.swing.JMenu;
+import javax.swing.JMenuItem;
 import javax.swing.JPanel;
 import javax.swing.JToggleButton;
-import javax.swing.SwingUtilities;
 
-import org.openstreetmap.josm.actions.AdaptableAction;
-import org.openstreetmap.josm.actions.CreateMultipolygonAction;
-import org.openstreetmap.josm.command.ChangePropertyCommand;
-import org.openstreetmap.josm.command.Command;
-import org.openstreetmap.josm.command.SequenceCommand;
-import org.openstreetmap.josm.data.UndoRedoHandler;
 import org.openstreetmap.josm.data.osm.DataSet;
 import org.openstreetmap.josm.data.osm.IPrimitive;
-import org.openstreetmap.josm.data.osm.OsmData;
 import org.openstreetmap.josm.data.osm.OsmDataManager;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
-import org.openstreetmap.josm.data.osm.Relation;
-import org.openstreetmap.josm.data.osm.RelationMember;
 import org.openstreetmap.josm.data.osm.Tag;
 import org.openstreetmap.josm.data.osm.Tagged;
-import org.openstreetmap.josm.data.osm.Way;
-import org.openstreetmap.josm.data.osm.search.SearchCompiler;
 import org.openstreetmap.josm.data.osm.search.SearchCompiler.Match;
-import org.openstreetmap.josm.data.osm.search.SearchParseError;
-import org.openstreetmap.josm.data.preferences.BooleanProperty;
-import org.openstreetmap.josm.gui.ExtendedDialog;
 import org.openstreetmap.josm.gui.MainApplication;
-import org.openstreetmap.josm.gui.Notification;
-import org.openstreetmap.josm.gui.dialogs.relation.RelationEditor;
-import org.openstreetmap.josm.gui.dialogs.relation.sort.RelationSorter;
-import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent;
-import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener;
 import org.openstreetmap.josm.gui.preferences.ToolbarPreferences;
-import org.openstreetmap.josm.gui.tagging.presets.items.Key;
-import org.openstreetmap.josm.gui.tagging.presets.items.Link;
-import org.openstreetmap.josm.gui.tagging.presets.items.Optional;
-import org.openstreetmap.josm.gui.tagging.presets.items.PresetLink;
-import org.openstreetmap.josm.gui.tagging.presets.items.Roles;
-import org.openstreetmap.josm.gui.tagging.presets.items.Space;
 import org.openstreetmap.josm.gui.util.GuiHelper;
 import org.openstreetmap.josm.tools.GBC;
 import org.openstreetmap.josm.tools.ImageProvider;
-import org.openstreetmap.josm.tools.ImageResource;
-import org.openstreetmap.josm.tools.Logging;
-import org.openstreetmap.josm.tools.Pair;
-import org.openstreetmap.josm.tools.StreamUtils;
 import org.openstreetmap.josm.tools.Utils;
-import org.openstreetmap.josm.tools.template_engine.ParseError;
 import org.openstreetmap.josm.tools.template_engine.TemplateEntry;
-import org.openstreetmap.josm.tools.template_engine.TemplateParser;
-import org.xml.sax.SAXException;
 
 /**
- * This class read encapsulate one tagging preset. A class method can
- * read in all predefined presets, either shipped with JOSM or that are
- * in the config directory.
+ * A template class to build preset dialogs.
+ * <p>
+ * This class is an immutable template class mainly used to build Swing dialogs. It also creates
+ * menu and toolbar entries.
+ * <p>
+ * This class is immutable and uses the companion class {@link TaggingPresetInstance} to store
+ * instance data.
  *
- * It is also able to construct dialogs out of preset definitions.
  * @since 294
  */
-public class TaggingPreset extends AbstractAction implements ActiveLayerChangeListener, AdaptableAction, Predicate<IPrimitive> {
-
-    /** The user pressed the "Apply" button */
-    public static final int DIALOG_ANSWER_APPLY = 1;
-    /** The user pressed the "New Relation" button */
-    public static final int DIALOG_ANSWER_NEW_RELATION = 2;
-    /** The user pressed the "Cancel" button */
-    public static final int DIALOG_ANSWER_CANCEL = 3;
-
-    /** The action key for optional tooltips */
-    public static final String OPTIONAL_TOOLTIP_TEXT = "Optional tooltip text";
-
-    /** Prefix of preset icon loading failure error message */
-    public static final String PRESET_ICON_ERROR_MSG_PREFIX = "Could not get presets icon ";
-
+public class TaggingPreset extends TaggingPresetBase implements Predicate<IPrimitive> {
+    /** Show the preset name and icon in the dialog if true */
+    private final boolean presetNameLabel;
+    /** The types this preset applies to. */
+    private final Set<TaggingPresetType> types;
     /**
-     * Defines whether the validator should be active in the preset dialog
-     * @see TaggingPresetValidation
-     */
-    public static final BooleanProperty USE_VALIDATOR = new BooleanProperty("taggingpreset.validator", false);
-
-    /**
-     * The preset group this preset belongs to.
-     */
-    public TaggingPresetMenu group;
-
-    /**
-     * The name of the tagging preset.
-     * @see #getRawName()
-     */
-    public String name;
-    /**
-     * The icon name assigned to this preset.
-     */
-    public String iconName;
-    /**
-     * Translation context for name
-     */
-    public String name_context;
-    /**
-     * A cache for the local name. Should never be accessed directly.
-     * @see #getLocaleName()
-     */
-    public String locale_name;
-    /**
-     * Show the preset name if true
-     */
-    public boolean preset_name_label;
-
-    /**
-     * The types as preparsed collection.
-     */
-    public transient Set<TaggingPresetType> types;
-    /**
-     * The list of preset items
-     */
-    public final transient List<TaggingPresetItem> data = new ArrayList<>(2);
-    /**
-     * The roles for this relation (if we are editing a relation). See:
-     * <a href="https://josm.openstreetmap.de/wiki/TaggingPresets#Tags">JOSM wiki</a>
-     */
-    public transient Roles roles;
-    /**
      * The name_template custom name formatter. See:
      * <a href="https://josm.openstreetmap.de/wiki/TaggingPresets#Attributes">JOSM wiki</a>
      */
-    public transient TemplateEntry nameTemplate;
+    private final TemplateEntry nameTemplate;
     /** The name_template_filter */
-    public transient Match nameTemplateFilter;
+    private final Match nameTemplateFilter;
     /** The match_expression */
-    public transient Match matchExpression;
+    private final Match matchExpression;
 
     /**
-     * True whenever the original selection given into createSelection was empty
+     * A store to persist information from one invocation of this preset's dialog to the next.
+     * <p>
+     * Example: The autoincrement setting of the street address preset.
      */
-    private boolean originalSelectionEmpty;
+    Map<String, Object> properties = new HashMap<>();
 
-    /** The completable future task of asynchronous icon loading */
-    private CompletableFuture<Void> iconFuture;
-
-    /** Support functions */
-    protected TaggingPresetItemGuiSupport itemGuiSupport;
-
     /**
      * Create an empty tagging preset. This will not have any items and
      * will be an empty string as text. createPanel will return null.
      * Use this as default item for "do not select anything".
+     *
+     * @param attributes the XML attributes
+     * @throws IllegalArgumentException on invalid attributes
      */
-    public TaggingPreset() {
-        updateEnabledState();
-    }
+    TaggingPreset(Map<String, String> attributes) throws IllegalArgumentException {
+        super(attributes);
 
-    /**
-     * Change the display name without changing the toolbar value.
-     */
-    public void setDisplayName() {
-        putValue(Action.NAME, getName());
-        putValue("toolbar", "tagging_" + getRawName());
-        putValue(OPTIONAL_TOOLTIP_TEXT, group != null ?
-                tr("Use preset ''{0}'' of group ''{1}''", getLocaleName(), group.getName()) :
-                    tr("Use preset ''{0}''", getLocaleName()));
+        presetNameLabel = TaggingPresetUtils.parseBoolean(attributes.getOrDefault("preset_name_label", "false"));
+        types = TaggingPresetType.getOrDefault(attributes.get("type"), EnumSet.allOf(TaggingPresetType.class));
+        nameTemplate = TaggingPresetUtils.parseTemplate(attributes.get("name_template"));
+        nameTemplateFilter = TaggingPresetUtils.parseSearchExpression(attributes.get("name_template_filter"));
+        matchExpression = TaggingPresetUtils.parseSearchExpression(attributes.get("match_expression"));
     }
 
     /**
-     * Gets the localized version of the name
-     * @return The name that should be displayed to the user.
+     * Create a {@code TaggingPreset} from an XML element's attributes.
+     * @param attributes the XML attributes
+     * @return the {@code TaggingPreset}
+     * @throws IllegalArgumentException on invalid attributes
      */
-    public String getLocaleName() {
-        if (locale_name == null) {
-            if (name_context != null) {
-                locale_name = trc(name_context, TaggingPresetItem.fixPresetString(name));
-            } else {
-                locale_name = tr(TaggingPresetItem.fixPresetString(name));
-            }
-        }
-        return locale_name;
+    public static TaggingPreset fromXML(Map<String, String> attributes) throws IllegalArgumentException {
+        return new TaggingPreset(attributes);
     }
 
-    /**
-     * Returns the translated name of this preset, prefixed with the group names it belongs to.
-     * @return the translated name of this preset, prefixed with the group names it belongs to
-     */
-    public String getName() {
-        return group != null ? group.getName() + '/' + getLocaleName() : getLocaleName();
+    @Override
+    void fixup(Map<String, Chunk> chunks, Item parent) {
+        super.fixup(chunks, parent);
+        action = new TaggingPresetAction();
+        iconFuture = TaggingPresetUtils.loadIcon(iconName, action);
     }
 
-    /**
-     * Returns the non translated name of this preset, prefixed with the (non translated) group names it belongs to.
-     * @return the non translated name of this preset, prefixed with the (non translated) group names it belongs to
-     */
-    public String getRawName() {
-        return group != null ? group.getRawName() + '/' + name : name;
+    @Override
+    public void addToMenu(JMenu parentMenu) {
+        JMenuItem menuItem = new JMenuItem(getAction());
+        menuItem.setText(getLocaleName());
+        parentMenu.add(menuItem);
     }
 
     /**
-     * Returns the preset icon (16px).
-     * @return The preset icon, or {@code null} if none defined
-     * @since 6403
+     * Returns whether the preset name should be shown in the dialog
+     * @return whether the preset name should be shown in the dialog
      */
-    public final ImageIcon getIcon() {
-        return getIcon(Action.SMALL_ICON);
+    public boolean getPresetNameLabel() {
+        return presetNameLabel;
     }
 
     /**
-     * Returns the preset icon (16 or 24px).
-     * @param key Key determining icon size: {@code Action.SMALL_ICON} for 16x, {@code Action.LARGE_ICON_KEY} for 24px
-     * @return The preset icon, or {@code null} if none defined
-     * @since 10849
+     * Returns the primitive types this preset applies to
+     * @return the set of types
      */
-    public final ImageIcon getIcon(String key) {
-        Object icon = getValue(key);
-        if (icon instanceof ImageIcon) {
-            return (ImageIcon) icon;
-        }
-        return null;
+    public Set<TaggingPresetType> getTypes() {
+        return Collections.unmodifiableSet(types);
     }
 
     /**
-     * Returns the {@link ImageResource} attached to this preset, if any.
-     * @return the {@code ImageResource} attached to this preset, or {@code null}
-     * @since 16060
+     * Returns the name template
+     * @return the name template
      */
-    public final ImageResource getImageResource() {
-        return ImageResource.getAttachedImageResource(this);
+    public TemplateEntry getNameTemplate() {
+        return nameTemplate;
     }
 
     /**
-     * Called from the XML parser to set the icon.
-     * The loading task is performed in the background in order to speedup startup.
-     * @param iconName icon name
+     * Returns the name template filter
+     * @return the name template filter
      */
-    public void setIcon(final String iconName) {
-        this.iconName = iconName;
-        if (iconName == null || !TaggingPresetReader.isLoadIcons()) {
-            return;
-        }
-        File arch = TaggingPresetReader.getZipIcons();
-        final Collection<String> s = TaggingPresets.ICON_SOURCES.get();
-        this.iconFuture = new CompletableFuture<>();
-        new ImageProvider(iconName)
-            .setDirs(s)
-            .setId("presets")
-            .setArchive(arch)
-            .setOptional(true)
-            .getResourceAsync(result -> {
-                if (result != null) {
-                    GuiHelper.runInEDT(() -> {
-                        try {
-                            result.attachImageIcon(this, true);
-                        } catch (IllegalArgumentException e) {
-                            Logging.warn(toString() + ": " + PRESET_ICON_ERROR_MSG_PREFIX + iconName);
-                            Logging.warn(e);
-                        } finally {
-                            iconFuture.complete(null);
-                        }
-                    });
-                } else {
-                    Logging.warn(toString() + ": " + PRESET_ICON_ERROR_MSG_PREFIX + iconName);
-                    iconFuture.complete(null);
-                }
-            });
+    public Match getNameTemplateFilter() {
+        return nameTemplateFilter;
     }
 
     /**
-     * Called from the XML parser to set the types this preset affects.
-     * @param types comma-separated primitive types ("node", "way", "relation" or "closedway")
-     * @throws SAXException if any SAX error occurs
-     * @see TaggingPresetType#fromString
+     * Creates a panel for this preset. This includes general information such as name and supported
+     * {@link TaggingPresetType types}. This includes the elements from the individual
+     * {@link Item items}.
+     * @param p the panel to add to
+     * @param support the support
+     * @return true if any elements where added
      */
-    public void setType(String types) throws SAXException {
-        this.types = TaggingPresetItem.getType(types);
-    }
+    @Override
+    boolean addToPanel(JPanel p, TaggingPresetInstance support) {
+        Collection<OsmPrimitive> selected = support.getSelected();
+        boolean hasElements = false;
 
-    /**
-     * Sets the name_template custom name formatter.
-     *
-     * @param template The format template
-     * @throws SAXException on template parse error
-     * @see <a href="https://josm.openstreetmap.de/wiki/TaggingPresets#name_templatedetails">JOSM wiki</a>
-     */
-    public void setName_template(String template) throws SAXException {
-        try {
-            this.nameTemplate = new TemplateParser(template).parse();
-        } catch (ParseError e) {
-            Logging.error("Error while parsing " + template + ": " + e.getMessage());
-            throw new SAXException(e);
-        }
-    }
-
-    /**
-     * Sets the name_template_filter.
-     *
-     * @param filter The search pattern
-     * @throws SAXException on search patern parse error
-     * @see <a href="https://josm.openstreetmap.de/wiki/TaggingPresets#name_templatedetails">JOSM wiki</a>
-     */
-    public void setName_template_filter(String filter) throws SAXException {
-        try {
-            this.nameTemplateFilter = SearchCompiler.compile(filter);
-        } catch (SearchParseError e) {
-            Logging.error("Error while parsing" + filter + ": " + e.getMessage());
-            throw new SAXException(e);
-        }
-    }
-
-    /**
-     * Sets the match_expression additional criteria for matching primitives.
-     *
-     * @param filter The search pattern
-     * @throws SAXException on search patern parse error
-     * @see <a href="https://josm.openstreetmap.de/wiki/TaggingPresets#Attributes">JOSM wiki</a>
-     */
-    public void setMatch_expression(String filter) throws SAXException {
-        try {
-            this.matchExpression = SearchCompiler.compile(filter);
-        } catch (SearchParseError e) {
-            Logging.error("Error while parsing" + filter + ": " + e.getMessage());
-            throw new SAXException(e);
-        }
-    }
-
-    private static class PresetPanel extends JPanel {
-        private boolean hasElements;
-
-        PresetPanel() {
-            super(new GridBagLayout());
-        }
-    }
-
-    /**
-     * Creates a panel for this preset. This includes general information such as name and supported {@link TaggingPresetType types}.
-     * This includes the elements from the individual {@link TaggingPresetItem items}.
-     *
-     * @param selected the selected primitives
-     * @return the newly created panel
-     */
-    public PresetPanel createPanel(Collection<OsmPrimitive> selected) {
-        PresetPanel p = new PresetPanel();
-
         final JPanel pp = new JPanel();
-        if (types != null) {
-            for (TaggingPresetType t : types) {
-                JLabel la = new JLabel(ImageProvider.get(t.getIconName()));
-                la.setToolTipText(tr("Elements of type {0} are supported.", tr(t.getName())));
-                pp.add(la);
-            }
+        for (TaggingPresetType t : types) {
+            JLabel la = new JLabel(ImageProvider.get(t.getIconName()));
+            la.setToolTipText(tr("Elements of type {0} are supported.", tr(t.getName())));
+            pp.add(la);
         }
-        final List<Tag> directlyAppliedTags = Utils.filteredCollection(data, Key.class).stream()
+        final List<Tag> directlyAppliedTags = Utils.filteredCollection(items, Key.class).stream()
                 .map(Key::asTag)
                 .collect(Collectors.toList());
         if (!directlyAppliedTags.isEmpty()) {
@@ -388,19 +180,16 @@
         pp.add(validationLabel);
 
         final int count = pp.getComponentCount();
-        if (preset_name_label) {
+        if (presetNameLabel) {
             p.add(new JLabel(getIcon(Action.LARGE_ICON_KEY)), GBC.std(0, 0).span(1, count > 0 ? 2 : 1).insets(0, 0, 5, 0));
         }
         if (count > 0) {
             p.add(pp, GBC.std(1, 0).span(GBC.REMAINDER));
         }
-        if (preset_name_label) {
+        if (presetNameLabel) {
             p.add(new JLabel(getName()), GBC.std(1, count > 0 ? 1 : 0).insets(5, 0, 0, 0).span(GBC.REMAINDER).fill(GBC.HORIZONTAL));
         }
 
-        boolean presetInitiallyMatches = !selected.isEmpty() && selected.stream().allMatch(this);
-        itemGuiSupport = TaggingPresetItemGuiSupport.create(presetInitiallyMatches, selected, this::getChangedTags);
-
         JPanel itemPanel = new JPanel(new GridBagLayout()) {
             /**
              * This hack allows the items to have their own orientation.
@@ -419,20 +208,20 @@
             }
         };
         JPanel linkPanel = new JPanel(new GridBagLayout());
-        TaggingPresetItem previous = null;
-        for (TaggingPresetItem i : data) {
+        Item previous = null;
+        for (Item i : items) {
             if (i instanceof Link) {
-                i.addToPanel(linkPanel, itemGuiSupport);
-                p.hasElements = true;
+                i.addToPanel(linkPanel, support);
+                hasElements = true;
             } else {
                 if (i instanceof PresetLink) {
                     PresetLink link = (PresetLink) i;
-                    if (!(previous instanceof PresetLink && Objects.equals(((PresetLink) previous).text, link.text))) {
+                    if (!(previous instanceof PresetLink && Objects.equals(((PresetLink) previous).getText(), link.getText()))) {
                         itemPanel.add(link.createLabel(), GBC.eol().insets(0, 8, 0, 0));
                     }
                 }
-                if (i.addToPanel(itemPanel, itemGuiSupport)) {
-                    p.hasElements = true;
+                if (i.addToPanel(itemPanel, support)) {
+                    hasElements = true;
                 }
             }
             previous = i;
@@ -444,21 +233,23 @@
             GuiHelper.setEnabledRec(itemPanel, false);
         }
 
-        if (selected.size() == 1 && USE_VALIDATOR.get()) {
-            itemGuiSupport.addListener((source, key, newValue) ->
-                    TaggingPresetValidation.validateAsync(selected.iterator().next(), validationLabel, getChangedTags()));
+        if (selected.size() == 1 && TaggingPresets.USE_VALIDATOR.get()) {
+            support.addListener((source, key, newValue) -> {
+                    TaggingPresetHandler handler = new CloneTaggingPresetHandler(selected);
+                    handler.updateTags(support.getChangedTags());
+                    TaggingPresetValidation.validateAsync(handler, validationLabel);
+            });
         }
 
-        // "Add toolbar button"
+        // add the "pin" button
         JToggleButton tb = new JToggleButton(new ToolbarButtonAction());
         tb.setFocusable(false);
         p.add(tb, GBC.std(1, 0).anchor(GBC.LINE_END));
 
         // Trigger initial updates once and only once
-        itemGuiSupport.setEnabled(true);
-        itemGuiSupport.fireItemValueModified(null, null, null);
-
-        return p;
+        support.setEnabled(true);
+        support.fireItemValueModified(null, null, null);
+        return hasElements;
     }
 
     /**
@@ -467,222 +258,48 @@
      * @return {@code true} if a dialog can be shown for this preset
      */
     public boolean isShowable() {
-        return data.stream().anyMatch(i -> !(i instanceof Optional || i instanceof Space || i instanceof Key));
+        return getAllItems(i -> !(i instanceof Container || i instanceof Space || i instanceof Key), true).size() > 0;
     }
 
     /**
      * Suggests a relation role for this primitive
+     * <p>
+     * Suggests a role when the primitive is added to a relation.
      *
      * @param osm The primitive
      * @return the suggested role or null
      */
     public String suggestRoleForOsmPrimitive(OsmPrimitive osm) {
-        if (roles != null && osm != null) {
-            return roles.roles.stream()
-                    .filter(i -> i.memberExpression != null && i.memberExpression.match(osm))
-                    .filter(i -> Utils.isEmpty(i.types) || i.types.contains(TaggingPresetType.forPrimitive(osm)))
-                    .findFirst()
-                    .map(i -> i.key)
-                    .orElse(null);
-        }
-        return null;
+        if (osm == null)
+            return null;
+        return getAllRoles().stream()
+                .filter(role -> role.getMemberExpression() != null && role.getMemberExpression().match(osm))
+                .filter(role -> role.appliesTo(TaggingPresetType.forPrimitive(osm)))
+                .findFirst()
+                .map(i -> i.getKey())
+                .orElse(null);
     }
 
-    @Override
-    public void actionPerformed(ActionEvent e) {
-        DataSet ds = OsmDataManager.getInstance().getEditDataSet();
-        if (ds == null) {
-            return;
-        }
-        showAndApply(ds.getSelected());
-    }
-
     /**
-     * {@linkplain #showDialog Show preset dialog}, apply changes
-     * @param primitives the primitives
-     */
-    public void showAndApply(Collection<OsmPrimitive> primitives) {
-        // Display dialog even if no data layer (used by preset-tagging-tester plugin)
-        Collection<OsmPrimitive> sel = createSelection(primitives);
-        int answer = showDialog(sel, supportsRelation());
-
-        if (!sel.isEmpty() && answer == DIALOG_ANSWER_APPLY) {
-            Command cmd = createCommand(sel, getChangedTags());
-            if (cmd != null) {
-                UndoRedoHandler.getInstance().add(cmd);
-            }
-        } else if (answer == DIALOG_ANSWER_NEW_RELATION) {
-            Relation calculated = null;
-            if (getChangedTags().stream().anyMatch(t -> "boundary".equals(t.get("type")) || "multipolygon".equals(t.get("type")))) {
-                Collection<Way> ways = Utils.filteredCollection(primitives, Way.class);
-                Pair<Relation, Relation> res = CreateMultipolygonAction.createMultipolygonRelation(ways, true);
-                if (res != null) {
-                    calculated = res.b;
-                }
-            }
-            final Relation r = calculated != null ? calculated : new Relation();
-            final Collection<RelationMember> members = new LinkedHashSet<>(r.getMembers());
-            for (Tag t : getChangedTags()) {
-                r.put(t.getKey(), t.getValue());
-            }
-            for (OsmPrimitive osm : primitives) {
-                if (r == calculated && osm instanceof Way)
-                    continue;
-                String role = suggestRoleForOsmPrimitive(osm);
-                RelationMember rm = new RelationMember(role == null ? "" : role, osm);
-                r.addMember(rm);
-                members.add(rm);
-            }
-            if (r.isMultipolygon() && r != calculated) {
-                r.setMembers(RelationSorter.sortMembersByConnectivity(r.getMembers()));
-            }
-            SwingUtilities.invokeLater(() -> RelationEditor.getEditor(
-                    MainApplication.getLayerManager().getEditLayer(), r, members).setVisible(true));
-        }
-        if (!primitives.isEmpty()) {
-            DataSet ds = primitives.iterator().next().getDataSet();
-            ds.setSelected(primitives); // force update
-        }
-    }
-
-    private static class PresetDialog extends ExtendedDialog {
-
-        /**
-         * Constructs a new {@code PresetDialog}.
-         * @param content the content that will be displayed in this dialog
-         * @param title the text that will be shown in the window titlebar
-         * @param icon the image to be displayed as the icon for this window
-         * @param disableApply whether to disable "Apply" button
-         * @param showNewRelation whether to display "New relation" button
-         */
-        PresetDialog(Component content, String title, ImageIcon icon, boolean disableApply, boolean showNewRelation) {
-            super(MainApplication.getMainFrame(), title,
-                    showNewRelation ?
-                            (new String[] {tr("Apply Preset"), tr("New relation"), tr("Cancel")}) :
-                            (new String[] {tr("Apply Preset"), tr("Cancel")}),
-                    true);
-            if (icon != null)
-                setIconImage(icon.getImage());
-            contentInsets = new Insets(10, 5, 0, 5);
-            if (showNewRelation) {
-                setButtonIcons("ok", "data/relation", "cancel");
-            } else {
-                setButtonIcons("ok", "cancel");
-            }
-            configureContextsensitiveHelp("/Menu/Presets", true);
-            setContent(content);
-            setDefaultButton(1);
-            setupDialog();
-            buttons.get(0).setEnabled(!disableApply);
-            buttons.get(0).setToolTipText(title);
-            // Prevent dialogs of being too narrow (fix #6261)
-            Dimension d = getSize();
-            if (d.width < 350) {
-                d.width = 350;
-                setSize(d);
-            }
-            super.showDialog();
-        }
-    }
-
-    /**
-     * Shows the preset dialog.
-     * @param sel selection
-     * @param showNewRelation whether to display "New relation" button
-     * @return the user choice after the dialog has been closed
-     */
-    public int showDialog(Collection<OsmPrimitive> sel, boolean showNewRelation) {
-        PresetPanel p = createPanel(sel);
-
-        int answer = 1;
-        boolean canCreateRelation = types == null || types.contains(TaggingPresetType.RELATION);
-        if (originalSelectionEmpty && !canCreateRelation) {
-            new Notification(
-                    tr("The preset <i>{0}</i> cannot be applied since nothing has been selected!", getLocaleName()))
-                    .setIcon(JOptionPane.WARNING_MESSAGE)
-                    .show();
-            return DIALOG_ANSWER_CANCEL;
-        } else if (sel.isEmpty() && !canCreateRelation) {
-            new Notification(
-                    tr("The preset <i>{0}</i> cannot be applied since the selection is unsuitable!", getLocaleName()))
-                    .setIcon(JOptionPane.WARNING_MESSAGE)
-                    .show();
-            return DIALOG_ANSWER_CANCEL;
-        } else if (p.getComponentCount() != 0 && (sel.isEmpty() || p.hasElements)) {
-            int size = sel.size();
-            String title = trn("Change {0} object", "Change {0} objects", size, size);
-            if (!showNewRelation && size == 0) {
-                if (originalSelectionEmpty) {
-                    title = tr("Nothing selected!");
-                } else {
-                    title = tr("Selection unsuitable!");
-                }
-            }
-
-            boolean disableApply = size == 0;
-            if (!disableApply) {
-                OsmData<?, ?, ?, ?> ds = sel.iterator().next().getDataSet();
-                disableApply = ds != null && ds.isLocked();
-            }
-            answer = new PresetDialog(p, title, preset_name_label ? null : (ImageIcon) getValue(Action.SMALL_ICON),
-                    disableApply, showNewRelation).getValue();
-        }
-        if (!showNewRelation && answer == 2)
-            return DIALOG_ANSWER_CANCEL;
-        else
-            return answer;
-    }
-
-    /**
-     * Removes all unsuitable OsmPrimitives from the given list
-     * @param participants List of possible OsmPrimitives to tag
-     * @return Cleaned list with suitable OsmPrimitives only
-     */
-    public Collection<OsmPrimitive> createSelection(Collection<OsmPrimitive> participants) {
-        originalSelectionEmpty = participants.isEmpty();
-        return participants.stream().filter(this::typeMatches).collect(Collectors.toList());
-    }
-
-    /**
-     * Gets a list of tags that are set by this preset.
+     * Gets the current (edited) state of the tags.
+     * @param support the support
      * @return The list of tags.
      */
-    public List<Tag> getChangedTags() {
-        List<Tag> result = new ArrayList<>();
-        data.forEach(i -> i.addCommands(result));
-        return result;
+    public List<Tag> getChangedTags(TaggingPresetInstance support) {
+        return support.getChangedTags();
     }
 
     /**
-     * Create a command to change the given list of tags.
-     * @param sel The primitives to change the tags for
-     * @param changedTags The tags to change
-     * @return A command that changes the tags.
+     * Return true if this preset applies to relations
+     * @return true if this preset applies to relations
      */
-    public static Command createCommand(Collection<OsmPrimitive> sel, List<Tag> changedTags) {
-        List<Command> cmds = changedTags.stream()
-                .map(tag -> new ChangePropertyCommand(sel, tag.getKey(), tag.getValue()))
-                .filter(cmd -> cmd.getObjectsNumber() > 0)
-                .collect(StreamUtils.toUnmodifiableList());
-        return cmds.isEmpty() ? null : SequenceCommand.wrapIfNeeded(tr("Change Tags"), cmds);
+    public boolean supportsRelation() {
+        return types.contains(TaggingPresetType.RELATION);
     }
 
-    private boolean supportsRelation() {
-        return types == null || types.contains(TaggingPresetType.RELATION);
-    }
-
-    protected final void updateEnabledState() {
-        setEnabled(OsmDataManager.getInstance().getEditDataSet() != null);
-    }
-
     @Override
-    public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) {
-        updateEnabledState();
-    }
-
-    @Override
     public String toString() {
-        return (types == null ? "" : types.toString()) + ' ' + name;
+        return "TaggingPreset " + types.toString() + " " + getName();
     }
 
     /**
@@ -701,12 +318,12 @@
      * @return <code>true</code> if all types match.
      */
     public boolean typeMatches(Collection<TaggingPresetType> t) {
-        return t == null || types == null || types.containsAll(t);
+        return t == null || types.containsAll(t);
     }
 
     /**
      * Determines whether this preset matches the given primitive, i.e.,
-     * whether the {@link #typeMatches(Collection) type matches} and the {@link TaggingPresetItem#matches(Map) tags match}.
+     * whether the {@link #typeMatches(Collection) type matches} and the {@link Item#matches(Map) tags match}.
      *
      * @param p the primitive
      * @return {@code true} if this preset matches the primitive
@@ -721,7 +338,7 @@
      * Determines whether this preset matches the parameters.
      *
      * @param t the preset types to include, see {@link #typeMatches(Collection)}
-     * @param tags the tags to perform matching on, see {@link TaggingPresetItem#matches(Map)}
+     * @param tags the tags to perform matching on, see {@link Item#matches(Map)}
      * @param onlyShowable whether the preset must be {@link #isShowable() showable}
      * @return {@code true} if this preset matches the parameters.
      */
@@ -731,13 +348,37 @@
         } else if (matchExpression != null && !matchExpression.match(Tagged.ofMap(tags))) {
             return false;
         } else {
-            return TaggingPresetItem.matches(data, tags);
+            return TaggingPresetUtils.matches(items, tags);
         }
     }
 
     /**
-     * Action that adds or removes the button on main toolbar
+     * An action that opens the preset dialog.
      */
+    public class TaggingPresetAction extends TaggingPresetBase.TaggingPresetBaseAction {
+        TaggingPresetAction() {
+            super();
+            putValue(Action.NAME, getName());
+            putValue("toolbar", "tagging_" + getRawName());
+            putValue(OPTIONAL_TOOLTIP_TEXT, tr("Use preset ''{0}''", getLocaleFullName()));
+            MainApplication.getLayerManager().addActiveLayerChangeListener(this);
+        }
+
+        @Override
+        public void actionPerformed(ActionEvent e) {
+            DataSet dataSet = OsmDataManager.getInstance().getEditDataSet();
+            if (dataSet == null) {
+                return;
+            }
+            Collection<OsmPrimitive> filtered = dataSet.getSelected().stream()
+                .filter(TaggingPreset.this::typeMatches).collect(Collectors.toList());
+            TaggingPresetDialog.showAndApply(TaggingPreset.this, new DataSetTaggingPresetHandler(filtered), supportsRelation());
+        }
+    }
+
+    /**
+     * Action that "pins" the preset to the main toolbar
+     */
     public class ToolbarButtonAction extends AbstractAction {
         private final int toolbarIndex;
 
@@ -755,28 +396,7 @@
 
         @Override
         public void actionPerformed(ActionEvent ae) {
-            String res = getToolbarString();
-            MainApplication.getToolbar().addCustomButton(res, toolbarIndex, true);
+            MainApplication.getToolbar().addCustomButton(getToolbarString(), toolbarIndex, true);
         }
     }
-
-    /**
-     * Gets a string describing this preset that can be used for the toolbar
-     * @return A String that can be passed on to the toolbar
-     * @see ToolbarPreferences#addCustomButton(String, int, boolean)
-     */
-    public String getToolbarString() {
-        ToolbarPreferences.ActionParser actionParser = new ToolbarPreferences.ActionParser(null);
-        return actionParser.saveAction(new ToolbarPreferences.ActionDefinition(this));
-    }
-
-    /**
-     * Returns the completable future task that performs icon loading, if any.
-     * @return the completable future task that performs icon loading, or null
-     * @since 14449
-     */
-    public CompletableFuture<Void> getIconLoadingTask() {
-        return iconFuture;
-    }
-
 }
Index: src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetBase.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetBase.java	(nonexistent)
+++ src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetBase.java	(working copy)
@@ -0,0 +1,248 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.tagging.presets;
+
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+
+import javax.swing.AbstractAction;
+import javax.swing.Action;
+import javax.swing.ImageIcon;
+
+import org.openstreetmap.josm.actions.AdaptableAction;
+import org.openstreetmap.josm.data.osm.OsmDataManager;
+import org.openstreetmap.josm.gui.MainApplication;
+import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent;
+import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener;
+import org.openstreetmap.josm.gui.preferences.ToolbarPreferences;
+import org.openstreetmap.josm.tools.ImageResource;
+
+/**
+ * Base class for templates to build preset menus.
+ * <p>
+ * This class is an immutable template class mainly used to build menues, toolbars and preset lists.
+ *
+ * @since xxx
+ */
+public abstract class TaggingPresetBase extends Sequence {
+    /** The action key for optional tooltips */
+    public static final String OPTIONAL_TOOLTIP_TEXT = "Optional tooltip text";
+    /** Prefix of preset icon loading failure error message */
+    public static final String PRESET_ICON_ERROR_MSG_PREFIX = "Could not get presets icon ";
+
+    /**
+     * The name of the tagging preset.
+     * @see #getRawName()
+     */
+    private final String name;
+    /** Translation context for name */
+    private final String nameContext;
+    /**
+     * A cache for the local name. Should never be accessed directly.
+     * @see #getLocaleName()
+     */
+    private final String localeName;
+    /** The icon name assigned to this preset. */
+    final String iconName;
+
+    /** The english full name of this preset, eg. {@code Highways/Streets/Motorway} */
+    String fullName;
+    /** The localized full name of this preset, eg. {@code Straßen/Straßen/Autobahn} */
+    String localeFullName;
+    /** The english group name of this preset, eg. {@code Highways/Streets} */
+    String groupName;
+    /** The localized group name of this preset, eg. {@code Straßen/Straßen} */
+    String localeGroupName;
+
+    TaggingPresetBaseAction action;
+    /** The completable future task of asynchronous icon loading. Used for testing. */
+    CompletableFuture<ImageResource> iconFuture;
+
+    /**
+     * Create an empty tagging preset. This will not have any items and
+     * will be an empty string as text. createPanel will return null.
+     * Use this as default item for "do not select anything".
+     *
+     * @param attributes the XML attributes
+     * @throws IllegalArgumentException on invalid attributes
+     */
+    TaggingPresetBase(Map<String, String> attributes) throws IllegalArgumentException {
+        super(attributes);
+
+        name = attributes.get("name");
+        nameContext = attributes.get("name_context");
+        localeName = TaggingPresetUtils.buildLocaleString(attributes.get("locale_name"), name, nameContext);
+        iconName = attributes.get("icon");
+    }
+
+    @Override
+    void fixup(Map<String, Chunk> chunks, Item parent) {
+        if (parent instanceof TaggingPresetBase) {
+            TaggingPresetBase p = (TaggingPresetBase) parent;
+            groupName = p.fullName;
+            localeGroupName = p.localeFullName;
+            fullName = p.fullName + "/" + name;
+            localeFullName = p.localeFullName + "/" + localeName;
+        } else {
+            groupName = "";
+            localeGroupName = "";
+            fullName = name;
+            localeFullName = localeName;
+        }
+        super.fixup(chunks, this);
+    }
+
+    @Override
+    void destroy() {
+        super.destroy();
+        action.removeListener();
+        action = null;
+        iconFuture = null;
+    }
+
+    /**
+     * Returns the untranslated name. eg. {@code Motorway}
+     *
+     * @return the name
+     */
+    public String getBaseName() {
+        return name;
+    }
+
+    /**
+     * Returns the localized version of the name. eg. {@code Autobahn}
+     *
+     * @return The name that should be displayed to the user.
+     */
+    public String getLocaleName() {
+        return localeName;
+    }
+
+    /**
+     * Returns the localized full name of this preset, eg. {@code Wege/Staßen/Autobahn}
+     * @return the localized full name
+     */
+    public String getName() {
+        return localeFullName;
+    }
+
+    /**
+     * Returns the localized full name of this preset, eg. {@code Straßen/Straßen/Autobahn}
+     * @return the localized full name
+     */
+    public String getLocaleFullName() {
+        return localeFullName;
+    }
+
+    /**
+     * Returns the full name of this preset, in English, eg. {@code Highways/Streets/Motorway}
+     * @return the full name
+     */
+    public String getRawName() {
+        return fullName;
+    }
+
+    /**
+     * Returns the group name of this preset, in English, eg. {@code Highways/Streets}
+     * @return the group name
+     */
+    public String getGroupName() {
+        return groupName;
+    }
+
+    /**
+     * Returns the localized group name of this preset, eg. {@code Straßen/Straßen}
+     * @return the localized group name
+     */
+    public String getLocaleGroupName() {
+        return localeGroupName;
+    }
+
+    /**
+     * Returns the preset icon (16 or 24px).
+     * @param size Key determining icon size: {@code Action.SMALL_ICON} for 16x, {@code Action.LARGE_ICON_KEY} for 24px
+     * @return The preset icon, or {@code null} if none defined
+     * @since 10849
+     */
+    public final ImageIcon getIcon(String size) {
+        Object icon = getAction().getValue(size);
+        if (icon instanceof ImageIcon) {
+            return (ImageIcon) icon;
+        }
+        return null;
+    }
+
+    /**
+     * Returns the preset icon (16px).
+     * @return The preset icon, or {@code null} if none defined
+     * @since 6403
+     */
+    public final ImageIcon getIcon() {
+        return getIcon(Action.SMALL_ICON);
+    }
+
+    /**
+     * Returns the icon name
+     * @return the icon name
+     */
+    public String getIconName() {
+        return iconName;
+    }
+
+    /**
+     * Returns the Action associated with this preset.
+     * @return the Action
+     */
+    public AbstractAction getAction() {
+        return action;
+    }
+
+    /**
+     * Gets a string describing this preset that can be used for the toolbar
+     * @return A String that can be passed on to the toolbar
+     * @see ToolbarPreferences#addCustomButton(String, int, boolean)
+     */
+    public String getToolbarString() {
+        ToolbarPreferences.ActionParser actionParser = new ToolbarPreferences.ActionParser(null);
+        return actionParser.saveAction(new ToolbarPreferences.ActionDefinition(getAction()));
+    }
+
+    /**
+     * Returns true if this preset's fullName matches the glob.
+     * @param glob the name to match. "/*" matches all names
+     * @return true if this preset's fullName matches the glob.
+     */
+    public boolean nameMatchesGlob(String glob) {
+        if (glob.endsWith("/*")) {
+            glob = glob.substring(0, glob.length() - 2);
+            return glob.equalsIgnoreCase(groupName);
+        }
+        return glob.equalsIgnoreCase(fullName);
+    }
+
+    /**
+     * An action that opens the preset dialog.
+     */
+    abstract static class TaggingPresetBaseAction extends AbstractAction implements ActiveLayerChangeListener, AdaptableAction {
+        TaggingPresetBaseAction() {
+            updateEnabledState();
+        }
+
+        @Override
+        public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) {
+            updateEnabledState();
+        }
+
+        final void updateEnabledState() {
+            setEnabled(OsmDataManager.getInstance().getEditDataSet() != null);
+        }
+
+        final void removeListener() {
+            try {
+                MainApplication.getLayerManager().removeActiveLayerChangeListener(this);
+            } catch (IllegalArgumentException e) {
+                // the test rig always cleans out the layers before we get a chance to unregister
+                return; // there must be at east one statement
+            }
+        }
+    }
+}
Index: src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetDialog.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetDialog.java	(nonexistent)
+++ src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetDialog.java	(working copy)
@@ -0,0 +1,185 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.tagging.presets;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+import static org.openstreetmap.josm.tools.I18n.trn;
+
+import java.awt.Component;
+import java.awt.Dimension;
+import java.awt.GridBagLayout;
+import java.awt.Insets;
+import java.util.Collection;
+import java.util.LinkedHashSet;
+
+import javax.swing.Action;
+import javax.swing.ImageIcon;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.SwingUtilities;
+
+import org.openstreetmap.josm.actions.CreateMultipolygonAction;
+import org.openstreetmap.josm.data.osm.DataSet;
+import org.openstreetmap.josm.data.osm.OsmData;
+import org.openstreetmap.josm.data.osm.OsmDataManager;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.Relation;
+import org.openstreetmap.josm.data.osm.RelationMember;
+import org.openstreetmap.josm.data.osm.Tag;
+import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.gui.ExtendedDialog;
+import org.openstreetmap.josm.gui.MainApplication;
+import org.openstreetmap.josm.gui.Notification;
+import org.openstreetmap.josm.gui.dialogs.relation.RelationEditor;
+import org.openstreetmap.josm.gui.dialogs.relation.sort.RelationSorter;
+import org.openstreetmap.josm.tools.Pair;
+import org.openstreetmap.josm.tools.Utils;
+
+/**
+ * A tagging preset dialog.
+ */
+public class TaggingPresetDialog extends ExtendedDialog {
+    /** The user pressed the "Apply" button */
+    private static final int DIALOG_ANSWER_APPLY = 1;
+    /** The user pressed the "New Relation" button */
+    private static final int DIALOG_ANSWER_NEW_RELATION = 2;
+    /** The user pressed the "Cancel" button */
+    private static final int DIALOG_ANSWER_CANCEL = 3;
+
+    /**
+     * Constructs a new {@code PresetDialog}.
+     * @param content the content that will be displayed in this dialog
+     * @param title the text that will be shown in the window titlebar
+     * @param icon the image to be displayed as the icon for this window
+     * @param disableApply whether to disable "Apply" button
+     * @param showNewRelation whether to display "New relation" button
+     */
+    TaggingPresetDialog(Component content, String title, ImageIcon icon, boolean disableApply, boolean showNewRelation) {
+        super(MainApplication.getMainFrame(), title,
+                showNewRelation ?
+                        (new String[] {tr("Apply Preset"), tr("New relation"), tr("Cancel")}) :
+                        (new String[] {tr("Apply Preset"), tr("Cancel")}),
+                true);
+        if (icon != null)
+            setIconImage(icon.getImage());
+        contentInsets = new Insets(10, 10, 0, 10);
+        if (showNewRelation) {
+            setButtonIcons("ok", "data/relation", "cancel");
+        } else {
+            setButtonIcons("ok", "cancel");
+        }
+        configureContextsensitiveHelp("/Menu/Presets", true);
+        setContent(content);
+        setDefaultButton(1);
+        setupDialog();
+        buttons.get(0).setEnabled(!disableApply);
+        buttons.get(0).setToolTipText(title);
+        // Prevent dialogs of being too narrow (fix #6261)
+        Dimension d = getSize();
+        if (d.width < 350) {
+            d.width = 350;
+            setSize(d);
+        }
+        super.showDialog();
+    }
+
+    /**
+     * Shows the preset dialog and applies changes
+     *
+     * @param preset the tagging preser
+     * @param handler the tagging preset handler
+     * @param showNewRelation if true adds a "new relation" button
+     */
+    public static void showAndApply(TaggingPreset preset, TaggingPresetHandler handler, boolean showNewRelation) {
+        TaggingPresetInstance instance = TaggingPresetInstance.create(preset, handler);
+
+        // get unfiltered selection
+        DataSet dataSet = OsmDataManager.getInstance().getEditDataSet();
+        if (dataSet == null) {
+            return;
+        }
+        Collection<OsmPrimitive> selected = dataSet.getSelected();
+        Collection<OsmPrimitive> filtered = instance.getSelected();
+
+        JPanel p = new JPanel(new GridBagLayout());
+        boolean hasElements = preset.addToPanel(p, instance);
+        boolean originalSelectionEmpty = selected.isEmpty();
+
+        int answer = 1;
+        boolean canCreateRelation = preset.supportsRelation();
+        if (originalSelectionEmpty && !canCreateRelation) {
+            new Notification(
+                    tr("The preset <i>{0}</i> cannot be applied since nothing has been selected!", preset.getLocaleName()))
+                    .setIcon(JOptionPane.WARNING_MESSAGE)
+                    .show();
+            answer = DIALOG_ANSWER_CANCEL;
+        } else if (filtered.isEmpty() && !canCreateRelation) {
+            new Notification(
+                    tr("The preset <i>{0}</i> cannot be applied since the selection is unsuitable!", preset.getLocaleName()))
+                    .setIcon(JOptionPane.WARNING_MESSAGE)
+                    .show();
+            answer = DIALOG_ANSWER_CANCEL;
+        } else if (p.getComponentCount() != 0 && (filtered.isEmpty() || hasElements)) {
+            int size = filtered.size();
+            String title = trn("Change {0} object", "Change {0} objects", size, size);
+            if (!showNewRelation && size == 0) {
+                if (originalSelectionEmpty) {
+                    title = tr("Nothing selected!");
+                } else {
+                    title = tr("Selection unsuitable!");
+                }
+            }
+
+            boolean disableApply = size == 0;
+            if (!disableApply) {
+                OsmData<?, ?, ?, ?> ds = filtered.iterator().next().getDataSet();
+                disableApply = ds != null && ds.isLocked();
+            }
+
+            // finally show the dialog
+            answer = new TaggingPresetDialog(p, title, preset.getPresetNameLabel() ? null : preset.getIcon(Action.SMALL_ICON),
+                    disableApply, showNewRelation).getValue();
+        }
+        if (!showNewRelation && answer == DIALOG_ANSWER_NEW_RELATION)
+            answer = DIALOG_ANSWER_CANCEL;
+
+        if (!handler.getPrimitives().isEmpty() && answer == DIALOG_ANSWER_APPLY) {
+            // write the changed tags
+            handler.updateTags(preset.getChangedTags(instance));
+        } else if (answer == DIALOG_ANSWER_NEW_RELATION) {
+            Relation calculated = null;
+            if (preset.getChangedTags(instance).stream()
+                    .anyMatch(t -> "boundary".equals(t.get("type")) || "multipolygon".equals(t.get("type")))) {
+                Collection<Way> ways = Utils.filteredCollection(selected, Way.class);
+                Pair<Relation, Relation> res = CreateMultipolygonAction.createMultipolygonRelation(ways, true);
+                if (res != null) {
+                    calculated = res.b;
+                }
+            }
+            final Relation r = calculated != null ? calculated : new Relation();
+            final Collection<RelationMember> members = new LinkedHashSet<>(r.getMembers());
+            for (Tag t : preset.getChangedTags(instance)) {
+                r.put(t.getKey(), t.getValue());
+            }
+            for (OsmPrimitive osm : selected) {
+                if (r == calculated && osm instanceof Way)
+                    continue;
+                String role = preset.suggestRoleForOsmPrimitive(osm);
+                RelationMember rm = new RelationMember(role == null ? "" : role, osm);
+                r.addMember(rm);
+                members.add(rm);
+            }
+            if (r.isMultipolygon() && r != calculated) {
+                r.setMembers(RelationSorter.sortMembersByConnectivity(r.getMembers()));
+            }
+            SwingUtilities.invokeLater(() -> RelationEditor.getEditor(
+                    MainApplication.getLayerManager().getEditLayer(), r, members).setVisible(true));
+        }
+        if (!selected.isEmpty()) {
+            DataSet ds = selected.iterator().next().getDataSet();
+            // check for null because if we were called from the relation editor, we are editing a
+            // copy that is not in the dataset.
+            if (ds != null)
+                ds.setSelected(selected); // force update
+        }
+    }
+}
Index: src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetHandler.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetHandler.java	(revision 18366)
+++ src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetHandler.java	(working copy)
@@ -8,19 +8,28 @@
 import org.openstreetmap.josm.data.osm.Tag;
 
 /**
- * This interface needs to be implemented in order to display a tagging preset. It allows the preset dialog to query the primitives it should
- * be displayed for and modify them.
+ * This bidirectional interface is the connection between a preset dialog and the backing store. The
+ * data store can be a JOSM {@code DataSet} or any other key/value store, eg. a {@code TagTable}.
  */
 public interface TaggingPresetHandler {
+
     /**
-     * Gets the selection the preset should be applied to.
+     * Returns the set of primitives to operate on.
+     * <p>
+     * This is called by the preset dialog to obtain initial values for the edit controls and by the
+     * {@link #updateTags} method. The returned set should not change between invocations.
+     *
      * @return A collection of primitives.
      */
-    Collection<OsmPrimitive> getSelection();
+    Collection<OsmPrimitive> getPrimitives();
 
     /**
-     * Update the given tags on the selection.
-     * @param tags The tags to update.
+     * Update the given tags on the primitives.
+     * <p>
+     * This method writes the tags in the given list to the primitives returned by
+     * {@link #getPrimitives}.
+     *
+     * @param tags The tags to write.
      */
     void updateTags(List<Tag> tags);
 }
Index: src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetInstance.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetInstance.java	(nonexistent)
+++ src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetInstance.java	(working copy)
@@ -0,0 +1,246 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.tagging.presets;
+
+import java.awt.ComponentOrientation;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.Tag;
+import org.openstreetmap.josm.data.osm.Tagged;
+import org.openstreetmap.josm.data.osm.search.SearchCompiler;
+import org.openstreetmap.josm.gui.widgets.OrientationAction;
+import org.openstreetmap.josm.tools.ListenerList;
+import org.openstreetmap.josm.tools.Utils;
+import org.openstreetmap.josm.tools.template_engine.TemplateEngineDataProvider;
+
+/**
+ * Companion class to TaggingPreset to hold instance data.
+ * <p>
+ * The {@link TaggingPreset} class is an immutable template class mainly used to build Swing
+ * dialogs.  But being immutable it cannot hold any of the data the user entered in the dialog.  An
+ * instance of this class is created along with every preset dialog.
+ *
+ * @since 17609
+ */
+public final class TaggingPresetInstance implements TemplateEngineDataProvider {
+    /** The preset template that created this instance. */
+    private final TaggingPreset preset;
+    /** The current TaggingPresetHandler */
+    private final TaggingPresetHandler handler;
+    /** The map from Item to Item.Instance. */
+    private final Map<Item, Item.Instance> instances = new HashMap<>();
+    /** True if all selected primitives matched this preset at the moment the dialog was openend. */
+    private final boolean presetInitiallyMatches;
+    /** Data change listeners */
+    private final ListenerList<ChangeListener> listeners = ListenerList.create();
+    /** whether to fire data changed events or not */
+    private boolean enabled;
+
+    private TaggingPresetInstance(TaggingPreset preset, TaggingPresetHandler handler) {
+        this.preset = preset;
+        this.handler = handler;
+        Collection<OsmPrimitive> selected = handler.getPrimitives();
+        this.presetInitiallyMatches = !selected.isEmpty() && selected.stream().allMatch(preset);
+    }
+
+    /**
+     * Creates a new {@code TaggingPresetInstance}
+     *
+     * @param preset the preset
+     * @param handler the preset handler
+     * @return the new {@code TaggingPresetInstance}
+     */
+    public static TaggingPresetInstance create(TaggingPreset preset, TaggingPresetHandler handler) {
+        return new TaggingPresetInstance(preset, handler);
+    }
+
+    /**
+     * Creates a new {@code TaggingPresetInstance} for testing purposes
+     *
+     * @param selected the selected primitives
+     * @return the new {@code TaggingPresetInstance}
+     */
+    public static TaggingPresetInstance createTest(OsmPrimitive... selected) {
+        return new TaggingPresetInstance(
+            (TaggingPreset) ItemFactory.build("item"),
+            new ReadOnlyTaggingPresetHandler(Arrays.asList(selected))
+        );
+    }
+
+    /**
+     * Returns the preset
+     * @return the preset
+     */
+    public TaggingPreset getPreset() {
+        return preset;
+    }
+
+    /**
+     * Returns the handler
+     * @return the handler
+     */
+    public TaggingPresetHandler getHandler() {
+        return handler;
+    }
+
+    /**
+     * Return the selected primitives.
+     * @return the selected primitives
+     */
+    public Collection<OsmPrimitive> getSelected() {
+        return handler.getPrimitives();
+    }
+
+    /**
+     * Return the instance for the item
+     * @param item the item
+     * @return the instance
+     */
+    public Item.Instance getInstance(Item item) {
+        return instances.get(item);
+    }
+
+    /**
+     * Returns the preset properties
+     * @return the preset properties
+     */
+    public Map<String, Object> getPresetProperties() {
+        return preset.properties;
+    }
+
+    /**
+     * Returns whether firing of events is enabled
+     *
+     * @return true if firing of events is enabled
+     */
+    public boolean isEnabled() {
+        return enabled;
+    }
+
+    /**
+     * Enables or disables the firing of events
+     *
+     * @param enabled fires if true
+     * @return the old state of enabled
+     */
+    public boolean setEnabled(boolean enabled) {
+        boolean oldEnabled = this.enabled;
+        this.enabled = enabled;
+        return oldEnabled;
+    }
+
+    /**
+     * Registers the instance of the preset item.
+     * <p>
+     * All presets have to register an instance if the item is editable.
+     *
+     * @param item the preset item
+     * @param instance the instance
+     */
+    public void putInstance(Item item, Item.Instance instance) {
+        instances.put(item, instance);
+    }
+
+    /**
+     * Interface to notify listeners that a preset item input as changed.
+     * @since 17610
+     */
+    public interface ChangeListener {
+        /**
+         * Notifies this listener that a preset item input as changed.
+         * @param source the source of this event
+         * @param key the tag key
+         * @param newValue the new tag value
+         */
+        void itemValueModified(Item.Instance source, String key, String newValue);
+    }
+
+    /**
+     * Returns true if all selected primitives matched this preset (before opening the dialog).
+     * <p>
+     * This usually means that the preset dialog was opened from the Tags / Memberships panel as
+     * opposed to being opened from the menu or toolbar or the preset search dialog.
+     *
+     * @return true if the preset initially matched
+     */
+    public boolean isPresetInitiallyMatches() {
+        return presetInitiallyMatches;
+    }
+
+    /**
+     * Gets the current (edited) state of the tags.
+     * @return The list of tags.
+     */
+    public List<Tag> getChangedTags() {
+        List<Tag> result = new ArrayList<>();
+        instances.forEach((item, instance) -> instance.addCommands(result));
+        return result;
+    }
+
+    /**
+     * Get tags with values as currently shown in the dialog.
+     * If exactly one primitive is selected, get all tags of it, then
+     * overwrite with the current values shown in the dialog.
+     * Else get only the tags shown in the dialog.
+     * @return Tags
+     */
+    public Tagged getTagged() {
+        if (getSelected().size() != 1) {
+            return Tagged.ofTags(getChangedTags());
+        }
+        // if there is only one primitive selected, get its tags
+        Tagged tagged = Tagged.ofMap(getSelected().iterator().next().getKeys());
+        // update changed tags
+        getChangedTags().forEach(tag -> tagged.put(tag));
+        return tagged;
+    }
+
+    @Override
+    public Collection<String> getTemplateKeys() {
+        return getTagged().keySet();
+    }
+
+    @Override
+    public Object getTemplateValue(String key, boolean special) {
+        String value = getTagged().get(key);
+        return Utils.isEmpty(value) ? null : value;
+    }
+
+    /**
+     * Returns the default component orientation by the user's locale
+     *
+     * @return the default component orientation
+     */
+    public ComponentOrientation getDefaultComponentOrientation() {
+        return OrientationAction.getDefaultComponentOrientation();
+    }
+
+    @Override
+    public boolean evaluateCondition(SearchCompiler.Match condition) {
+        return condition.match(getTagged());
+    }
+
+    /**
+     * Adds a new change listener
+     * @param listener the listener to add
+     */
+    public void addListener(ChangeListener listener) {
+        listeners.addListener(listener);
+    }
+
+    /**
+     * Notifies all listeners that a preset item input as changed.
+     * @param source the source of this event
+     * @param key the tag key
+     * @param newValue the new tag value
+     */
+    public void fireItemValueModified(Item.Instance source, String key, String newValue) {
+        if (enabled)
+            listeners.fireEvent(e -> e.itemValueModified(source, key, newValue));
+    }
+}
Index: src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetItem.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetItem.java	(revision 18366)
+++ src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetItem.java	(nonexistent)
@@ -1,183 +0,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.gui.tagging.presets;
-
-import static org.openstreetmap.josm.tools.I18n.tr;
-import static org.openstreetmap.josm.tools.I18n.trc;
-
-import java.io.File;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.EnumSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-import javax.swing.ImageIcon;
-import javax.swing.JPanel;
-
-import org.openstreetmap.josm.data.osm.DataSet;
-import org.openstreetmap.josm.data.osm.OsmDataManager;
-import org.openstreetmap.josm.data.osm.OsmPrimitive;
-import org.openstreetmap.josm.data.osm.Tag;
-import org.openstreetmap.josm.data.preferences.BooleanProperty;
-import org.openstreetmap.josm.data.tagging.ac.AutoCompletionItem;
-import org.openstreetmap.josm.gui.tagging.ac.AutoCompletingTextField;
-import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionList;
-import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionManager;
-import org.openstreetmap.josm.gui.util.LruCache;
-import org.openstreetmap.josm.tools.ImageProvider;
-import org.openstreetmap.josm.tools.Logging;
-import org.openstreetmap.josm.tools.Utils;
-import org.xml.sax.SAXException;
-
-/**
- * Class that represents single part of a preset - one field or text label that is shown to user
- * @since 6068
- */
-public abstract class TaggingPresetItem {
-
-    // cache the parsing of types using a LRU cache
-    private static final Map<String, Set<TaggingPresetType>> TYPE_CACHE = new LruCache<>(16);
-    /**
-     * Display OSM keys as {@linkplain org.openstreetmap.josm.gui.widgets.OsmIdTextField#setHint hint}
-     */
-    protected static final BooleanProperty DISPLAY_KEYS_AS_HINT = new BooleanProperty("taggingpreset.display-keys-as-hint", true);
-
-    protected void initAutoCompletionField(AutoCompletingTextField field, String... key) {
-        initAutoCompletionField(field, Arrays.asList(key));
-    }
-
-    protected void initAutoCompletionField(AutoCompletingTextField field, List<String> keys) {
-        DataSet data = OsmDataManager.getInstance().getEditDataSet();
-        if (data == null) {
-            return;
-        }
-        AutoCompletionList list = new AutoCompletionList();
-        AutoCompletionManager.of(data).populateWithTagValues(list, keys);
-        field.setAutoCompletionList(list);
-    }
-
-    /**
-     * Returns all cached {@link AutoCompletionItem}s for given keys.
-     *
-     * @param keys retrieve the items for these keys
-     * @return the currently cached items, sorted by priority and alphabet
-     * @since 18221
-     */
-    protected List<AutoCompletionItem> getAllForKeys(List<String> keys) {
-        DataSet data = OsmDataManager.getInstance().getEditDataSet();
-        if (data == null) {
-            return Collections.emptyList();
-        }
-        return AutoCompletionManager.of(data).getAllForKeys(keys);
-    }
-
-    /**
-     * Called by {@link TaggingPreset#createPanel} during tagging preset panel creation.
-     * All components defining this tagging preset item must be added to given panel.
-     *
-     * @param p The panel where components must be added
-     * @param support supporting class for creating the GUI
-     * @return {@code true} if this item adds semantic tagging elements, {@code false} otherwise.
-     */
-    protected abstract boolean addToPanel(JPanel p, TaggingPresetItemGuiSupport support);
-
-    /**
-     * Adds the new tags to apply to selected OSM primitives when the preset holding this item is applied.
-     * @param changedTags The list of changed tags to modify if needed
-     */
-    protected abstract void addCommands(List<Tag> changedTags);
-
-    /**
-     * Tests whether the tags match this item.
-     * Note that for a match, at least one positive and no negative is required.
-     * @param tags the tags of an {@link OsmPrimitive}
-     * @return {@code true} if matches (positive), {@code null} if neutral, {@code false} if mismatches (negative).
-     */
-    public Boolean matches(Map<String, String> tags) {
-        return null; // NOSONAR
-    }
-
-    protected static Set<TaggingPresetType> getType(String types) throws SAXException {
-        if (Utils.isEmpty(types)) {
-            throw new SAXException(tr("Unknown type: {0}", types));
-        }
-        if (TYPE_CACHE.containsKey(types))
-            return TYPE_CACHE.get(types);
-        Set<TaggingPresetType> result = EnumSet.noneOf(TaggingPresetType.class);
-        for (String type : types.split(",", -1)) {
-            try {
-                TaggingPresetType presetType = TaggingPresetType.fromString(type);
-                if (presetType != null) {
-                    result.add(presetType);
-                }
-            } catch (IllegalArgumentException e) {
-                throw new SAXException(tr("Unknown type: {0}", type), e);
-            }
-        }
-        TYPE_CACHE.put(types, result);
-        return result;
-    }
-
-    protected static String fixPresetString(String s) {
-        return s == null ? s : s.replace("'", "''");
-    }
-
-    protected static String getLocaleText(String text, String textContext, String defaultText) {
-        if (text == null) {
-            return defaultText;
-        } else if (textContext != null) {
-            return trc(textContext, fixPresetString(text));
-        } else {
-            return tr(fixPresetString(text));
-        }
-    }
-
-    protected static Integer parseInteger(String str) {
-        if (Utils.isEmpty(str))
-            return null;
-        try {
-            return Integer.valueOf(str);
-        } catch (NumberFormatException e) {
-            Logging.trace(e);
-        }
-        return null;
-    }
-
-    /**
-     * Loads a tagging preset icon
-     * @param iconName the icon name
-     * @param zipIcons zip file where the image is located
-     * @param maxSize maximum image size (or null)
-     * @return the requested image or null if the request failed
-     */
-    public static ImageIcon loadImageIcon(String iconName, File zipIcons, Integer maxSize) {
-        final Collection<String> s = TaggingPresets.ICON_SOURCES.get();
-        ImageProvider imgProv = new ImageProvider(iconName).setDirs(s).setId("presets").setArchive(zipIcons).setOptional(true);
-        if (maxSize != null && maxSize > 0) {
-            imgProv.setMaxSize(maxSize);
-        }
-        return imgProv.get();
-    }
-
-    /**
-     * Determine whether the given preset items match the tags
-     * @param data the preset items
-     * @param tags the tags to match
-     * @return whether the given preset items match the tags
-     * @since 9932
-     */
-    public static boolean matches(Iterable<? extends TaggingPresetItem> data, Map<String, String> tags) {
-        boolean atLeastOnePositiveMatch = false;
-        for (TaggingPresetItem item : data) {
-            Boolean m = item.matches(tags);
-            if (m != null && !m)
-                return false;
-            else if (m != null) {
-                atLeastOnePositiveMatch = true;
-            }
-        }
-        return atLeastOnePositiveMatch;
-    }
-}

Property changes on: src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetItem.java
___________________________________________________________________
Deleted: svn:eol-style
## -1 +0,0 ##
-native
\ No newline at end of property
Index: src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetItemGuiSupport.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetItemGuiSupport.java	(revision 18366)
+++ src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetItemGuiSupport.java	(nonexistent)
@@ -1,184 +0,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.gui.tagging.presets;
-
-import java.awt.ComponentOrientation;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.function.Supplier;
-
-import org.openstreetmap.josm.data.osm.OsmPrimitive;
-import org.openstreetmap.josm.data.osm.Tag;
-import org.openstreetmap.josm.data.osm.Tagged;
-import org.openstreetmap.josm.data.osm.search.SearchCompiler;
-import org.openstreetmap.josm.gui.widgets.OrientationAction;
-import org.openstreetmap.josm.tools.ListenerList;
-import org.openstreetmap.josm.tools.Utils;
-import org.openstreetmap.josm.tools.template_engine.TemplateEngineDataProvider;
-
-/**
- * Supporting class for creating the GUI for a preset item.
- *
- * @since 17609
- */
-public final class TaggingPresetItemGuiSupport implements TemplateEngineDataProvider {
-
-    private final Collection<OsmPrimitive> selected;
-    /** True if all selected primitives matched this preset at the moment it was openend. */
-    private final boolean presetInitiallyMatches;
-    private final Supplier<Collection<Tag>> changedTagsSupplier;
-    private final ListenerList<ChangeListener> listeners = ListenerList.create();
-
-    /** whether to fire events or not */
-    private boolean enabled;
-
-    /**
-     * Returns whether firing of events is enabled
-     *
-     * @return true if firing of events is enabled
-     */
-    public boolean isEnabled() {
-        return enabled;
-    }
-
-    /**
-     * Enables or disables the firing of events
-     *
-     * @param enabled fires if true
-     * @return the old state of enabled
-     */
-    public boolean setEnabled(boolean enabled) {
-        boolean oldEnabled = this.enabled;
-        this.enabled = enabled;
-        return oldEnabled;
-    }
-
-    /**
-     * Interface to notify listeners that a preset item input as changed.
-     * @since 17610
-     */
-    public interface ChangeListener {
-        /**
-         * Notifies this listener that a preset item input as changed.
-         * @param source the source of this event
-         * @param key the tag key
-         * @param newValue the new tag value
-         */
-        void itemValueModified(TaggingPresetItem source, String key, String newValue);
-    }
-
-    private TaggingPresetItemGuiSupport(
-            boolean presetInitiallyMatches, Collection<OsmPrimitive> selected, Supplier<Collection<Tag>> changedTagsSupplier) {
-        this.selected = selected;
-        this.presetInitiallyMatches = presetInitiallyMatches;
-        this.changedTagsSupplier = changedTagsSupplier;
-    }
-
-    /**
-     * Returns the selected primitives
-     *
-     * @return the selected primitives
-     */
-    public Collection<OsmPrimitive> getSelected() {
-        return selected;
-    }
-
-    /**
-     * Returns true if all selected primitives matched this preset (before opening the dialog).
-     * <p>
-     * This usually means that the preset dialog was opened from the Tags / Memberships panel as
-     * opposed to being opened by selection from the menu or toolbar or the search.
-     *
-     * @return true if the preset initially matched
-     */
-    public boolean isPresetInitiallyMatches() {
-        return presetInitiallyMatches;
-    }
-
-    /**
-     * Creates a new {@code TaggingPresetItemGuiSupport}
-     *
-     * @param presetInitiallyMatches whether the preset initially matched
-     * @param selected the selected primitives
-     * @param changedTagsSupplier the changed tags
-     * @return the new {@code TaggingPresetItemGuiSupport}
-     */
-    public static TaggingPresetItemGuiSupport create(
-            boolean presetInitiallyMatches, Collection<OsmPrimitive> selected, Supplier<Collection<Tag>> changedTagsSupplier) {
-        return new TaggingPresetItemGuiSupport(presetInitiallyMatches, selected, changedTagsSupplier);
-    }
-
-    /**
-     * Creates a new {@code TaggingPresetItemGuiSupport}
-     *
-     * @param presetInitiallyMatches whether the preset initially matched
-     * @param selected the selected primitives
-     * @return the new {@code TaggingPresetItemGuiSupport}
-     */
-    public static TaggingPresetItemGuiSupport create(
-            boolean presetInitiallyMatches, OsmPrimitive... selected) {
-        return new TaggingPresetItemGuiSupport(presetInitiallyMatches, Arrays.asList(selected), Collections::emptyList);
-    }
-
-    /**
-     * Get tags with values as currently shown in the dialog.
-     * If exactly one primitive is selected, get all tags of it, then
-     * overwrite with the current values shown in the dialog.
-     * Else get only the tags shown in the dialog.
-     * @return Tags
-     */
-    public Tagged getTagged() {
-        if (selected.size() != 1) {
-            return Tagged.ofTags(changedTagsSupplier.get());
-        }
-        // if there is only one primitive selected, get its tags
-        Tagged tagged = Tagged.ofMap(selected.iterator().next().getKeys());
-        // update changed tags
-        changedTagsSupplier.get().forEach(tag -> tagged.put(tag));
-        return tagged;
-    }
-
-    @Override
-    public Collection<String> getTemplateKeys() {
-        return getTagged().keySet();
-    }
-
-    @Override
-    public Object getTemplateValue(String key, boolean special) {
-        String value = getTagged().get(key);
-        return Utils.isEmpty(value) ? null : value;
-    }
-
-    /**
-     * Returns the default component orientation by the user's locale
-     *
-     * @return the default component orientation
-     */
-    public ComponentOrientation getDefaultComponentOrientation() {
-        return OrientationAction.getDefaultComponentOrientation();
-    }
-
-    @Override
-    public boolean evaluateCondition(SearchCompiler.Match condition) {
-        return condition.match(getTagged());
-    }
-
-    /**
-     * Adds a new change listener
-     * @param listener the listener to add
-     */
-    public void addListener(ChangeListener listener) {
-        listeners.addListener(listener);
-    }
-
-    /**
-     * Notifies all listeners that a preset item input as changed.
-     * @param source the source of this event
-     * @param key the tag key
-     * @param newValue the new tag value
-     */
-    public void fireItemValueModified(TaggingPresetItem source, String key, String newValue) {
-        if (enabled)
-            listeners.fireEvent(e -> e.itemValueModified(source, key, newValue));
-    }
-}
Index: src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetLabel.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetLabel.java	(revision 18366)
+++ src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetLabel.java	(working copy)
@@ -12,7 +12,7 @@
 
 /**
  * A hyperlink {@link JLabel}.
- * 
+ *
  * To indicate that the user can click on the text, it displays an appropriate
  * mouse cursor and dotted underline when the mouse is inside the hover area.
  */
Index: src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetMenu.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetMenu.java	(revision 18366)
+++ src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetMenu.java	(working copy)
@@ -8,21 +8,16 @@
 import java.awt.Point;
 import java.awt.PointerInfo;
 import java.awt.event.ActionEvent;
-import java.io.Serializable;
-import java.util.ArrayList;
-import java.util.Comparator;
-import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 
 import javax.swing.Action;
 import javax.swing.JMenu;
-import javax.swing.JMenuItem;
 import javax.swing.JPopupMenu;
-import javax.swing.JSeparator;
 
 import org.openstreetmap.josm.gui.MainApplication;
 import org.openstreetmap.josm.gui.MainFrame;
-import org.openstreetmap.josm.tools.AlphanumComparator;
+import org.openstreetmap.josm.gui.MenuScroller;
 import org.openstreetmap.josm.tools.Logging;
 
 /**
@@ -30,140 +25,100 @@
  * <p>
  * Used, to create the nested directory structure in the preset main menu entry.
  */
-public class TaggingPresetMenu extends TaggingPreset {
-    public JMenu menu; // set by TaggingPresets
-
-    private static class PresetTextComparator implements Comparator<JMenuItem>, Serializable {
-        private static final long serialVersionUID = 1L;
-        @Override
-        public int compare(JMenuItem o1, JMenuItem o2) {
-            if (MainApplication.getMenu().presetSearchAction.equals(o1.getAction()))
-                return -1;
-            else if (MainApplication.getMenu().presetSearchAction.equals(o2.getAction()))
-                return 1;
-            else
-                return AlphanumComparator.getInstance().compare(o1.getText(), o2.getText());
-        }
+public class TaggingPresetMenu extends TaggingPresetBase {
+    TaggingPresetMenu(Map<String, String> attributes) throws IllegalArgumentException {
+        super(attributes);
     }
 
     /**
-     * {@code TaggingPresetMenu} are considered equivalent if (and only if) their {@link #getRawName()} match.
+     * Create a {@code TaggingPresetMenu} from an XML element's attributes.
+     * @param attributes the XML attributes
+     * @return the {@code TaggingPresetMenu}
+     * @throws IllegalArgumentException on attribute errors
      */
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) return true;
-        if (o == null || getClass() != o.getClass()) return false;
-        TaggingPresetMenu that = (TaggingPresetMenu) o;
-        return Objects.equals(getRawName(), that.getRawName());
+    public static TaggingPresetMenu fromXML(Map<String, String> attributes) throws IllegalArgumentException {
+        return new TaggingPresetMenu(attributes);
     }
 
     @Override
-    public int hashCode() {
-        return Objects.hash(getRawName());
+    void fixup(Map<String, Chunk> chunks, Item parent) {
+        super.fixup(chunks, parent);
+        action = new TaggingPresetMenuAction();
+        iconFuture = TaggingPresetUtils.loadIcon(iconName, action);
     }
 
     @Override
-    public void setDisplayName() {
-        putValue(Action.NAME, getName());
-        /** Tooltips should be shown for the toolbar buttons, but not in the menu. */
-        putValue(OPTIONAL_TOOLTIP_TEXT, group != null ?
-                tr("Preset group {1} / {0}", getLocaleName(), group.getName()) :
-                    tr("Preset group {0}", getLocaleName()));
-        putValue("toolbar", "tagginggroup_" + getRawName());
-    }
+    public void addToMenu(JMenu parentMenu) {
+        JMenu subMenu = new JMenu(getAction());
+        subMenu.setText(getLocaleName());
+        parentMenu.add(subMenu);
 
-    private static Component copyMenuComponent(Component menuComponent) {
-        if (menuComponent instanceof JMenu) {
-            JMenu menu = (JMenu) menuComponent;
-            JMenu result = new JMenu(menu.getAction());
-            for (Component item:menu.getMenuComponents()) {
-                result.add(copyMenuComponent(item));
-            }
-            result.setText(menu.getText());
-            return result;
-        } else if (menuComponent instanceof JMenuItem) {
-            JMenuItem menuItem = (JMenuItem) menuComponent;
-            JMenuItem result = new JMenuItem(menuItem.getAction());
-            result.setText(menuItem.getText());
-            return result;
-        } else if (menuComponent instanceof JSeparator) {
-            return new JSeparator();
-        } else {
-            return menuComponent;
+        for (Item item : items) {
+            item.addToMenu(subMenu);
         }
+        if (subMenu.getItemCount() >= TaggingPresets.MIN_ELEMENTS_FOR_SCROLLER.get()) {
+            MenuScroller.setScrollerFor(subMenu);
+        }
     }
 
     @Override
-    public void actionPerformed(ActionEvent e) {
-        Object s = e.getSource();
-        if (menu != null && s instanceof Component) {
-            JPopupMenu pm = new JPopupMenu(getName());
-            for (Component c : menu.getMenuComponents()) {
-                pm.add(copyMenuComponent(c));
-            }
-            try {
-                PointerInfo pointerInfo = MouseInfo.getPointerInfo();
-                if (pointerInfo != null) {
-                    Point p = pointerInfo.getLocation();
-                    MainFrame parent = MainApplication.getMainFrame();
-                    if (parent.isShowing()) {
-                        pm.show(parent, p.x-parent.getX(), p.y-parent.getY());
-                    }
-                }
-            } catch (SecurityException ex) {
-                Logging.log(Logging.LEVEL_ERROR, "Unable to get mouse pointer info", ex);
-            }
-        }
+    public String toString() {
+        return "TaggingPresetMenu " + getName();
     }
 
     /**
-     * Sorts the menu items using the translated item text
+     * {@code TaggingPresetMenu} are considered equivalent if (and only if) their {@link #getRawName()} match.
      */
-    public void sortMenu() {
-        TaggingPresetMenu.sortMenu(this.menu);
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        TaggingPresetMenu that = (TaggingPresetMenu) o;
+        return Objects.equals(getRawName(), that.getRawName());
     }
 
+    @Override
+    public int hashCode() {
+        return Objects.hash(getRawName());
+    }
+
     /**
-     * Sorts the menu items using the translated item text
-     * @param menu menu to sort
+     * An action that opens this menu as a popup in the toolbar.
      */
-    public static void sortMenu(JMenu menu) {
-        Component[] items = menu.getMenuComponents();
-        PresetTextComparator comp = new PresetTextComparator();
-        List<JMenuItem> sortarray = new ArrayList<>();
-        int lastSeparator = 0;
-        for (int i = 0; i < items.length; i++) {
-            Object item = items[i];
-            if (item instanceof JMenu) {
-                sortMenu((JMenu) item);
-            }
-            if (item instanceof JMenuItem) {
-                sortarray.add((JMenuItem) item);
-                if (i == items.length-1) {
-                    handleMenuItem(menu, comp, sortarray, lastSeparator);
-                    sortarray = new ArrayList<>();
-                    lastSeparator = 0;
-                }
-            } else if (item instanceof JSeparator) {
-                handleMenuItem(menu, comp, sortarray, lastSeparator);
-                sortarray = new ArrayList<>();
-                lastSeparator = i;
-            }
+    public class TaggingPresetMenuAction extends TaggingPresetBase.TaggingPresetBaseAction {
+        TaggingPresetMenuAction() {
+            super();
+            putValue(Action.NAME, getName());
+            putValue("toolbar", "tagginggroup_" + getRawName());
+            /** Tooltips should be shown for the toolbar buttons, but not in the menu. */
+            putValue(OPTIONAL_TOOLTIP_TEXT, tr("Preset group ''{0}''", getLocaleFullName()));
+            MainApplication.getLayerManager().addActiveLayerChangeListener(this);
         }
-    }
 
-    private static void handleMenuItem(JMenu menu, PresetTextComparator comp, List<JMenuItem> sortarray, int lastSeparator) {
-        sortarray.sort(comp);
-        int pos = 0;
-        for (JMenuItem menuItem : sortarray) {
-            int oldPos;
-            if (lastSeparator == 0) {
-                oldPos = pos;
-            } else {
-                oldPos = pos+lastSeparator+1;
+        @Override
+        public void actionPerformed(ActionEvent e) {
+            if (e.getSource() instanceof Component) {
+                JMenu menu = new JMenu();
+                for (Item item : items) {
+                    item.addToMenu(menu);
+                }
+                JPopupMenu popupMenu = new JPopupMenu(getName());
+                for (Component menuItem : menu.getMenuComponents()) {
+                    popupMenu.add(menuItem);
+                }
+                try {
+                    PointerInfo pointerInfo = MouseInfo.getPointerInfo();
+                    if (pointerInfo != null) {
+                        Point p = pointerInfo.getLocation();
+                        MainFrame parent = MainApplication.getMainFrame();
+                        if (parent.isShowing()) {
+                            popupMenu.show(parent, p.x-parent.getX(), p.y-parent.getY());
+                        }
+                    }
+                } catch (SecurityException ex) {
+                    Logging.log(Logging.LEVEL_ERROR, "Unable to get mouse pointer info", ex);
+                }
             }
-            menu.add(menuItem, oldPos);
-            pos++;
         }
     }
 }
Index: src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetNameTemplateList.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetNameTemplateList.java	(revision 18366)
+++ src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetNameTemplateList.java	(working copy)
@@ -39,7 +39,7 @@
             Logging.debug("Building list of presets with name template");
             presetsWithPattern.clear();
             for (TaggingPreset tp : TaggingPresets.getTaggingPresets()) {
-                if (tp.nameTemplate != null) {
+                if (tp.getNameTemplate() != null) {
                     presetsWithPattern.add(tp);
                 }
             }
@@ -56,8 +56,8 @@
             for (TaggingPreset t : presetsWithPattern) {
                 Collection<TaggingPresetType> type = EnumSet.of(TaggingPresetType.forPrimitive(primitive));
                 if (t.typeMatches(type)) {
-                    if (t.nameTemplateFilter != null) {
-                        if (t.nameTemplateFilter.match(primitive))
+                    if (t.getNameTemplateFilter() != null) {
+                        if (t.getNameTemplateFilter().match(primitive))
                             return t;
                     } else if (t.matches(type, primitive.getKeys(), false)) {
                         return t;
Index: src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetReader.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetReader.java	(revision 18366)
+++ src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetReader.java	(working copy)
@@ -9,52 +9,53 @@
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.io.Reader;
-import java.util.ArrayDeque;
+import java.net.URI;
+import java.net.URISyntaxException;
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.Deque;
 import java.util.HashMap;
-import java.util.Iterator;
-import java.util.LinkedHashSet;
-import java.util.LinkedList;
-import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.Stack;
 
 import javax.swing.JOptionPane;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.stream.StreamSource;
+import javax.xml.validation.Schema;
+import javax.xml.validation.SchemaFactory;
+import javax.xml.validation.ValidatorHandler;
 
 import org.openstreetmap.josm.data.preferences.sources.PresetPrefHelper;
 import org.openstreetmap.josm.gui.MainApplication;
-import org.openstreetmap.josm.gui.tagging.presets.items.Check;
-import org.openstreetmap.josm.gui.tagging.presets.items.CheckGroup;
-import org.openstreetmap.josm.gui.tagging.presets.items.Combo;
-import org.openstreetmap.josm.gui.tagging.presets.items.ComboMultiSelect;
-import org.openstreetmap.josm.gui.tagging.presets.items.ItemSeparator;
-import org.openstreetmap.josm.gui.tagging.presets.items.Key;
-import org.openstreetmap.josm.gui.tagging.presets.items.Label;
-import org.openstreetmap.josm.gui.tagging.presets.items.Link;
-import org.openstreetmap.josm.gui.tagging.presets.items.MultiSelect;
-import org.openstreetmap.josm.gui.tagging.presets.items.Optional;
-import org.openstreetmap.josm.gui.tagging.presets.items.PresetLink;
-import org.openstreetmap.josm.gui.tagging.presets.items.PresetListEntry;
-import org.openstreetmap.josm.gui.tagging.presets.items.Roles;
-import org.openstreetmap.josm.gui.tagging.presets.items.Roles.Role;
-import org.openstreetmap.josm.gui.tagging.presets.items.Space;
-import org.openstreetmap.josm.gui.tagging.presets.items.Text;
 import org.openstreetmap.josm.io.CachedFile;
 import org.openstreetmap.josm.io.NetworkManager;
 import org.openstreetmap.josm.io.UTFInputStreamReader;
 import org.openstreetmap.josm.spi.preferences.Config;
 import org.openstreetmap.josm.tools.I18n;
+import org.openstreetmap.josm.tools.JosmRuntimeException;
+import org.openstreetmap.josm.tools.LanguageInfo;
 import org.openstreetmap.josm.tools.Logging;
 import org.openstreetmap.josm.tools.Stopwatch;
 import org.openstreetmap.josm.tools.Utils;
-import org.openstreetmap.josm.tools.XmlObjectParser;
+import org.openstreetmap.josm.tools.XmlParsingException;
+import org.openstreetmap.josm.tools.XmlUtils;
+import org.xml.sax.Attributes;
+import org.xml.sax.ContentHandler;
+import org.xml.sax.InputSource;
+import org.xml.sax.Locator;
+import org.xml.sax.SAXParseException;
 import org.xml.sax.SAXException;
+import org.xml.sax.XMLReader;
+import org.xml.sax.helpers.DefaultHandler;
+import org.xml.sax.helpers.XMLFilterImpl;
 
 /**
- * The tagging presets reader.
- * @since 6068
+ * The tagging presets XML file parser.
+ * <p>
+ * Parses an XML file and builds an in-memory tree of template classes. The template classes are
+ * then used to create preset dialogs and menus.
+ *
+ * @since xxx
  */
 public final class TaggingPresetReader {
 
@@ -77,335 +78,283 @@
      */
     public static final String SCHEMA_SOURCE = "resource://data/tagging-preset.xsd";
 
-    private static volatile File zipIcons;
-    private static volatile boolean loadIcons = true;
+    private static File zipIcons;
+    private static boolean loadIcons = true;
 
-    /**
-     * Holds a reference to a chunk of items/objects.
-     */
-    public static class Chunk {
-        /** The chunk id, can be referenced later */
-        public String id;
+    private static class Parser extends DefaultHandler {
+        private Root root;
+        private Stack<Item> stack = new Stack<>();
+        private StringBuilder characters;
+        private Locator locator;
+        private final String lang = LanguageInfo.getLanguageCodeXML();
 
+        public Root getRoot() {
+            return root;
+        }
+
         @Override
-        public String toString() {
-            return "Chunk [id=" + id + ']';
+        public void setDocumentLocator(Locator locator) {
+            this.locator = locator;
         }
-    }
 
-    /**
-     * Holds a reference to an earlier item/object.
-     */
-    public static class Reference {
-        /** Reference matching a chunk id defined earlier **/
-        public String ref;
+        private Map<String, String> getAttributes(Attributes a) {
+            Map<String, String> attributes = new HashMap<>();
+            for (int i = 0; i < a.getLength(); ++i) {
+                String name = a.getLocalName(i);
+                if (name.startsWith(lang)) {
+                    name = "locale_" + name.substring(lang.length());
+                }
+                attributes.put(a.getLocalName(i), a.getValue(i));
+            }
+            return attributes;
+        }
 
         @Override
-        public String toString() {
-            return "Reference [ref=" + ref + ']';
+        public void startElement(String ns, String lname, String qname, Attributes a) throws SAXException {
+            Item item = null;
+            Map<String, String> attributes = getAttributes(a);
+
+            try {
+                item = ItemFactory.build(lname, attributes);
+                if (item instanceof Root) {
+                    Root root = (Root) item;
+                    if (this.root == null) {
+                        root.url = locator.getSystemId();
+                        this.root = root;
+                    }
+                }
+                if (stack.size() > 0) {
+                    // add this item to the parent
+                    // do not put this into the constructor of Item or Chunks will not work
+                    stack.peek().addItem(item);
+                }
+            } catch (IllegalArgumentException e) {
+                throwException(e);
+            }
+            stack.push(item);
         }
-    }
 
-    static class HashSetWithLast<E> extends LinkedHashSet<E> {
-        private static final long serialVersionUID = 1L;
-        protected transient E last;
+        @Override
+        public void endElement(String ns, String lname, String qname) throws SAXException {
+            try {
+                Item item = stack.peek();
+                if (characters != null)
+                    item.setContent(characters.toString().trim());
+                item.endElement();
+            } catch (IllegalArgumentException e) {
+                throwException(e);
+            }
+            stack.pop();
+            characters = null;
+        }
 
         @Override
-        public boolean add(E e) {
-            last = e;
-            return super.add(e);
+        public void characters(char[] ch, int start, int length) {
+            if (characters == null)
+                characters = new StringBuilder(64); // lazily get a builder
+            characters.append(ch, start, length);
         }
 
         /**
-         * Returns the last inserted element.
-         * @return the last inserted element
+         * Rethrows an exception and adds location information
+         * @param e the exception without location information
+         * @throws XmlParsingException the exception with location information
          */
-        public E getLast() {
-            return last;
+        private void throwException(Exception e) throws XmlParsingException {
+            throw new XmlParsingException(e).rememberLocation(locator);
         }
-    }
 
-    /**
-     * Returns the set of preset source URLs.
-     * @return The set of preset source URLs.
-     */
-    public static Set<String> getPresetSources() {
-        return new PresetPrefHelper().getActiveUrls();
-    }
+        @Override
+        public void error(SAXParseException e) throws SAXException {
+            throwException(e);
+        }
 
-    private static XmlObjectParser buildParser() {
-        XmlObjectParser parser = new XmlObjectParser();
-        parser.mapOnStart("item", TaggingPreset.class);
-        parser.mapOnStart("separator", TaggingPresetSeparator.class);
-        parser.mapBoth("group", TaggingPresetMenu.class);
-        parser.map("text", Text.class);
-        parser.map("link", Link.class);
-        parser.map("preset_link", PresetLink.class);
-        parser.mapOnStart("optional", Optional.class);
-        parser.mapOnStart("roles", Roles.class);
-        parser.map("role", Role.class);
-        parser.mapBoth("checkgroup", CheckGroup.class);
-        parser.map("check", Check.class);
-        parser.map("combo", Combo.class);
-        parser.map("multiselect", MultiSelect.class);
-        parser.map("label", Label.class);
-        parser.map("space", Space.class);
-        parser.map("key", Key.class);
-        parser.map("list_entry", PresetListEntry.class);
-        parser.map("item_separator", ItemSeparator.class);
-        parser.mapBoth("chunk", Chunk.class);
-        parser.map("reference", Reference.class);
-        return parser;
+        @Override
+        public void fatalError(SAXParseException e) throws SAXException {
+            throwException(e);
+        }
     }
 
-    /**
-     * Reads all tagging presets from the input reader.
-     * @param in The input reader
-     * @param validate if {@code true}, XML validation will be performed
-     * @return collection of tagging presets
-     * @throws SAXException if any XML error occurs
-     */
-    public static Collection<TaggingPreset> readAll(Reader in, boolean validate) throws SAXException {
-        return readAll(in, validate, new HashSetWithLast<TaggingPreset>());
+    private static void start(final Reader in, final ContentHandler contentHandler, String url) throws SAXException, IOException {
+        try {
+            XMLReader reader = XmlUtils.newSafeSAXParser().getXMLReader();
+            reader.setContentHandler(contentHandler);
+            try {
+                // better performance on big files like defaultpresets.xml
+                reader.setProperty("http://apache.org/xml/properties/input-buffer-size", 8 * 1024);
+                // enable xinclude
+                reader.setFeature("http://apache.org/xml/features/xinclude", true);
+                // do not set xml:base, it doesn't validate
+                reader.setFeature("http://apache.org/xml/features/xinclude/fixup-base-uris", false);
+                // Do not load external DTDs (fix #8191)
+                reader.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
+            } catch (SAXException e) {
+                // Exception very unlikely to happen, so no need to translate this
+                Logging.log(Logging.LEVEL_ERROR, "Cannot set property or feature on SAX reader:", e);
+            }
+            InputSource is = new InputSource(in);
+            is.setSystemId(url);
+            reader.parse(is);
+        } catch (ParserConfigurationException e) {
+            throw new JosmRuntimeException(e);
+        }
     }
 
     /**
-     * Reads all tagging presets from the input reader.
-     * @param in The input reader
-     * @param validate if {@code true}, XML validation will be performed
-     * @param all the accumulator for parsed tagging presets
-     * @return the accumulator
-     * @throws SAXException if any XML error occurs
+     * This filter adds the default namespace
+     * {@code http://josm.openstreetmap.de/tagging-preset-1.0} to all elements that have none.
      */
-    static Collection<TaggingPreset> readAll(Reader in, boolean validate, HashSetWithLast<TaggingPreset> all) throws SAXException {
-        XmlObjectParser parser = buildParser();
+    private static class AddNamespaceFilter extends XMLFilterImpl {
+        private final String namespace;
 
-        /** to detect end of {@code <checkgroup>} */
-        CheckGroup lastcheckgroup = null;
-        /** to detect end of {@code <group>} */
-        TaggingPresetMenu lastmenu = null;
-        /** to detect end of reused {@code <group>} */
-        TaggingPresetMenu lastmenuOriginal = null;
-        Roles lastrole = null;
-        final List<Check> checks = new LinkedList<>();
-        final List<PresetListEntry> listEntries = new LinkedList<>();
-        final Map<String, List<Object>> byId = new HashMap<>();
-        final Deque<String> lastIds = new ArrayDeque<>();
-        /** lastIdIterators contains non empty iterators of items to be handled before obtaining the next item from the XML parser */
-        final Deque<Iterator<Object>> lastIdIterators = new ArrayDeque<>();
+        AddNamespaceFilter(String namespace) {
+            this.namespace = namespace;
+        }
 
-        if (validate) {
-            parser.startWithValidation(in, NAMESPACE, SCHEMA_SOURCE);
-        } else {
-            parser.start(in);
-        }
-        while (parser.hasNext() || !lastIdIterators.isEmpty()) {
-            final Object o;
-            if (!lastIdIterators.isEmpty()) {
-                // obtain elements from lastIdIterators with higher priority
-                o = lastIdIterators.peek().next();
-                if (!lastIdIterators.peek().hasNext()) {
-                    // remove iterator if is empty
-                    lastIdIterators.pop();
-                }
+        @Override
+        public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException {
+            if ("".equals(uri)) {
+                super.startElement(namespace, localName, qName, atts);
             } else {
-                o = parser.next();
+                super.startElement(uri, localName, qName, atts);
             }
-            Logging.trace("Preset object: {0}", o);
-            if (o instanceof Chunk) {
-                if (!lastIds.isEmpty() && ((Chunk) o).id.equals(lastIds.peek())) {
-                    // pop last id on end of object, don't process further
-                    lastIds.pop();
-                    ((Chunk) o).id = null;
-                } else {
-                    // if preset item contains an id, store a mapping for later usage
-                    String lastId = ((Chunk) o).id;
-                    lastIds.push(lastId);
-                    byId.put(lastId, new ArrayList<>());
-                }
-                continue;
-            } else if (!lastIds.isEmpty()) {
-                // add object to mapping for later usage
-                byId.get(lastIds.peek()).add(o);
-                continue;
-            }
-            if (o instanceof Reference) {
-                // if o is a reference, obtain the corresponding objects from the mapping,
-                // and iterate over those before consuming the next element from parser.
-                final String ref = ((Reference) o).ref;
-                if (byId.get(ref) == null) {
-                    throw new SAXException(tr("Reference {0} is being used before it was defined", ref));
-                }
-                Iterator<Object> it = byId.get(ref).iterator();
-                if (it.hasNext()) {
-                    lastIdIterators.push(it);
-                    if (lastIdIterators.size() > 100) {
-                        throw new SAXException(tr("Reference stack for {0} is too large", ref));
-                    }
-                } else {
-                    Logging.warn("Ignoring reference '"+ref+"' denoting an empty chunk");
-                }
-                continue;
-            }
-            if (!(o instanceof TaggingPresetItem) && !checks.isEmpty()) {
-                all.getLast().data.addAll(checks);
-                checks.clear();
-            }
-            if (o instanceof TaggingPresetMenu) {
-                TaggingPresetMenu tp = (TaggingPresetMenu) o;
-                if (tp == lastmenu || tp == lastmenuOriginal) {
-                    lastmenu = tp.group;
-                } else {
-                    tp.group = lastmenu;
-                    if (all.contains(tp)) {
-                        lastmenuOriginal = tp;
-                        tp = (TaggingPresetMenu) all.stream().filter(tp::equals).findFirst().orElse(tp);
-                        lastmenuOriginal.group = null;
-                    } else {
-                        tp.setDisplayName();
-                        all.add(tp);
-                        lastmenuOriginal = null;
-                    }
-                    lastmenu = tp;
-                }
-                lastrole = null;
-            } else if (o instanceof TaggingPresetSeparator) {
-                TaggingPresetSeparator tp = (TaggingPresetSeparator) o;
-                tp.group = lastmenu;
-                all.add(tp);
-                lastrole = null;
-            } else if (o instanceof TaggingPreset) {
-                TaggingPreset tp = (TaggingPreset) o;
-                tp.group = lastmenu;
-                tp.setDisplayName();
-                all.add(tp);
-                lastrole = null;
-            } else {
-                if (!all.isEmpty()) {
-                    if (o instanceof Roles) {
-                        all.getLast().data.add((TaggingPresetItem) o);
-                        if (all.getLast().roles != null) {
-                            throw new SAXException(tr("Roles cannot appear more than once"));
-                        }
-                        all.getLast().roles = (Roles) o;
-                        lastrole = (Roles) o;
-                        // #16458 - Make sure we don't duplicate role entries if used in a chunk/reference
-                        lastrole.roles.clear();
-                    } else if (o instanceof Role) {
-                        if (lastrole == null)
-                            throw new SAXException(tr("Preset role element without parent"));
-                        lastrole.roles.add((Role) o);
-                    } else if (o instanceof Check) {
-                        if (lastcheckgroup != null) {
-                            checks.add((Check) o);
-                        } else {
-                            all.getLast().data.add((TaggingPresetItem) o);
-                        }
-                    } else if (o instanceof PresetListEntry) {
-                        listEntries.add((PresetListEntry) o);
-                    } else if (o instanceof CheckGroup) {
-                        CheckGroup cg = (CheckGroup) o;
-                        if (cg == lastcheckgroup) {
-                            lastcheckgroup = null;
-                            all.getLast().data.add(cg);
-                            // Make sure list of checks is empty to avoid adding checks several times
-                            // when used in chunks (fix #10801)
-                            cg.checks.clear();
-                            cg.checks.addAll(checks);
-                            checks.clear();
-                        } else {
-                            lastcheckgroup = cg;
-                        }
-                    } else {
-                        if (!checks.isEmpty()) {
-                            all.getLast().data.addAll(checks);
-                            checks.clear();
-                        }
-                        all.getLast().data.add((TaggingPresetItem) o);
-                        if (o instanceof ComboMultiSelect) {
-                            ((ComboMultiSelect) o).addListEntries(listEntries);
-                        } else if (o instanceof Key && ((Key) o).value == null) {
-                            ((Key) o).value = ""; // Fix #8530
-                        }
-                        listEntries.clear();
-                        lastrole = null;
-                    }
-                } else
-                    throw new SAXException(tr("Preset sub element without parent"));
-            }
         }
-        if (!all.isEmpty() && !checks.isEmpty()) {
-            all.getLast().data.addAll(checks);
-            checks.clear();
-        }
-        return all;
     }
 
     /**
-     * Reads all tagging presets from the given source.
-     * @param source a given filename, URL or internal resource
-     * @param validate if {@code true}, XML validation will be performed
-     * @return collection of tagging presets
-     * @throws SAXException if any XML error occurs
-     * @throws IOException if any I/O error occurs
+     * Add validation filters to a parser
+     *
+     * @param parser the parser without validation
+     * @param namespace default namespace
+     * @param schemaUrl URL of XSD schema
+     * @return the new parser with validation
+     * @throws SAXException if any XML or I/O error occurs
      */
-    public static Collection<TaggingPreset> readAll(String source, boolean validate) throws SAXException, IOException {
-        return readAll(source, validate, new HashSetWithLast<TaggingPreset>());
+    public static ContentHandler buildParserWithValidation(Parser parser, String namespace, String schemaUrl) throws SAXException {
+        SchemaFactory factory = XmlUtils.newXmlSchemaFactory();
+        try (CachedFile cf = new CachedFile(schemaUrl);
+            InputStream mis = cf.getInputStream()) {
+            Schema schema = factory.newSchema(new StreamSource(mis));
+            ValidatorHandler validator = schema.newValidatorHandler();
+            validator.setContentHandler(parser);
+            validator.setErrorHandler(parser);
+
+            AddNamespaceFilter filter = new AddNamespaceFilter(namespace);
+            filter.setContentHandler(validator);
+            return filter;
+        } catch (IOException e) {
+            throw new SAXException(tr("Failed to load XML schema."), e);
+        }
     }
 
     /**
-     * Reads all tagging presets from the given source.
-     * @param source a given filename, URL or internal resource
+     * Reads all tagging presets from the given XML resource.
+     *
+     * @param url a given filename, URL or internal resource
      * @param validate if {@code true}, XML validation will be performed
-     * @param all the accumulator for parsed tagging presets
-     * @return the accumulator
+     * @return the root element of the resource
      * @throws SAXException if any XML error occurs
      * @throws IOException if any I/O error occurs
      */
-    static Collection<TaggingPreset> readAll(String source, boolean validate, HashSetWithLast<TaggingPreset> all)
-            throws SAXException, IOException {
-        Collection<TaggingPreset> tp;
-        Logging.debug("Reading presets from {0}", source);
+    public static Root read(String url, boolean validate) throws SAXException, IOException {
+        Logging.debug("Reading presets from {0}", url);
         Stopwatch stopwatch = Stopwatch.createStarted();
+        Parser parser = new Parser();
+
         try (
-            CachedFile cf = new CachedFile(source).setHttpAccept(PRESET_MIME_TYPES);
+            CachedFile cf = new CachedFile(url);
             // zip may be null, but Java 7 allows it: https://blogs.oracle.com/darcy/entry/project_coin_null_try_with
             InputStream zip = cf.findZipEntryInputStream("xml", "preset")
         ) {
+            cf.setHttpAccept(PRESET_MIME_TYPES);
             if (zip != null) {
                 zipIcons = cf.getFile();
                 I18n.addTexts(zipIcons);
             }
             try (InputStreamReader r = UTFInputStreamReader.create(zip == null ? cf.getInputStream() : zip)) {
-                tp = readAll(new BufferedReader(r), validate, all);
+                ContentHandler handler = parser;
+                if (validate) {
+                    handler = buildParserWithValidation((Parser) handler, NAMESPACE, SCHEMA_SOURCE);
+                }
+                start(new BufferedReader(r), handler, url);
             }
         }
+
+        Root patchRoot = readPatchFile(url, validate);
+        if (patchRoot != null)
+            parser.getRoot().items.addAll(patchRoot.items);
+
         Logging.debug(stopwatch.toString("Reading presets"));
-        return tp;
+        return parser.getRoot();
     }
 
     /**
-     * Reads all tagging presets from the given sources.
+     * Try to read a .local preset patch file.
+     * <p>
+     * A preset patch file has the same structure as the {@code defaultpresets.xml} file. All items
+     * in the root of the preset patch file will be appended to the root of the respective presets
+     * file. Chunks in the preset patch file will replace chunks with the same {@code id} in the
+     * presets file. The patch file must be placed in the {@code josmdir://} and have the same
+     * filename and extension with an added extension of {@code .local} eg.
+     * {@code <josmdir>/defaultpresets.xml.local}
+     *
+     * @param url a given filename, URL or internal resource
+     * @param validate if {@code true}, XML validation will be performed
+     * @return the root element of the resource
+     * @throws SAXException if any XML error occurs
+     * @throws IOException if any I/O error occurs
+     */
+
+    static Root readPatchFile(String url, boolean validate) throws SAXException, IOException {
+        try {
+            URI uri = new URI(url);
+            String fileName = new File(uri.getPath()).getName();
+            url = "josmdir://" + fileName + ".local";
+
+            Parser parser = new Parser();
+            try (CachedFile cf = new CachedFile(url)) {
+                Logging.debug("Reading local preset patches from {0}", cf.getFile().toPath());
+                try (InputStreamReader r = UTFInputStreamReader.create(cf.getInputStream())) {
+                    ContentHandler handler = parser;
+                    if (validate) {
+                        handler = buildParserWithValidation(parser, NAMESPACE, SCHEMA_SOURCE);
+                    }
+                    start(new BufferedReader(r), handler, url);
+                }
+            }
+            return parser.getRoot();
+        } catch (URISyntaxException e) {
+            Logging.error("readPatchFile: cannot parse url {0}", url);
+            return null;
+        } catch (IOException e) {
+            return null; // there is no local patch file, do nothing
+        }
+    }
+
+    /**
+     * Reads all tagging presets from the given XML resources. Convenience function.
+     *
      * @param sources Collection of tagging presets sources.
      * @param validate if {@code true}, presets will be validated against XML schema
-     * @return Collection of all presets successfully read
+     * @return the root elements of the XML resources
      */
-    public static Collection<TaggingPreset> readAll(Collection<String> sources, boolean validate) {
+    public static Collection<Root> readAll(Collection<String> sources, boolean validate) {
         return readAll(sources, validate, true);
     }
 
     /**
-     * Reads all tagging presets from the given sources.
+     * Reads all tagging presets from the given XML resources.
+     *
      * @param sources Collection of tagging presets sources.
      * @param validate if {@code true}, presets will be validated against XML schema
      * @param displayErrMsg if {@code true}, a blocking error message is displayed in case of I/O exception.
-     * @return Collection of all presets successfully read
+     * @return the root elements of the XML resources
      */
-    public static Collection<TaggingPreset> readAll(Collection<String> sources, boolean validate, boolean displayErrMsg) {
-        HashSetWithLast<TaggingPreset> allPresets = new HashSetWithLast<>();
+    public static Collection<Root> readAll(Collection<String> sources, boolean validate, boolean displayErrMsg) {
+        Collection<Root> result = new ArrayList<>();
         for (String source : sources) {
             try {
-                readAll(source, validate, allPresets);
+                result.add(read(source, validate));
             } catch (IOException e) {
                 Logging.log(Logging.LEVEL_ERROR, e);
                 Logging.error(source);
@@ -434,7 +383,7 @@
                 }
             }
         }
-        return allPresets;
+        return result;
     }
 
     /**
@@ -443,11 +392,19 @@
      * @param displayErrMsg if {@code true}, a blocking error message is displayed in case of I/O exception.
      * @return Collection of all presets successfully read
      */
-    public static Collection<TaggingPreset> readFromPreferences(boolean validate, boolean displayErrMsg) {
+    public static Collection<Root> readFromPreferences(boolean validate, boolean displayErrMsg) {
         return readAll(getPresetSources(), validate, displayErrMsg);
     }
 
     /**
+     * Returns the set of preset source URLs.
+     * @return The set of preset source URLs.
+     */
+    public static Set<String> getPresetSources() {
+        return new PresetPrefHelper().getActiveUrls();
+    }
+
+    /**
      * Returns the zip file where the icons are located
      * @return the zip file where the icons are located
      */
@@ -471,7 +428,6 @@
         TaggingPresetReader.loadIcons = loadIcons;
     }
 
-    private TaggingPresetReader() {
-        // Hide default constructor for utils classes
-    }
+    // fix checkstyle HideUtilityClassConstructor
+    private TaggingPresetReader() {}
 }
Index: src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetSearchDialog.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetSearchDialog.java	(revision 18366)
+++ src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetSearchDialog.java	(working copy)
@@ -54,7 +54,7 @@
         if (buttonIndex == 0) {
             TaggingPreset preset = selector.getSelectedPresetAndUpdateClassification();
             if (preset != null) {
-                preset.actionPerformed(null);
+                preset.getAction().actionPerformed(null);
             }
         }
         selector.savePreferences();
Index: src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetSelector.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetSelector.java	(revision 18366)
+++ src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetSelector.java	(working copy)
@@ -23,7 +23,6 @@
 import javax.swing.Action;
 import javax.swing.BoxLayout;
 import javax.swing.DefaultListCellRenderer;
-import javax.swing.Icon;
 import javax.swing.JCheckBox;
 import javax.swing.JLabel;
 import javax.swing.JList;
@@ -39,11 +38,6 @@
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
 import org.openstreetmap.josm.data.preferences.BooleanProperty;
 import org.openstreetmap.josm.gui.MainApplication;
-import org.openstreetmap.josm.gui.tagging.presets.items.ComboMultiSelect;
-import org.openstreetmap.josm.gui.tagging.presets.items.Key;
-import org.openstreetmap.josm.gui.tagging.presets.items.KeyedItem;
-import org.openstreetmap.josm.gui.tagging.presets.items.Roles;
-import org.openstreetmap.josm.gui.tagging.presets.items.Roles.Role;
 import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher;
 import org.openstreetmap.josm.gui.widgets.SearchTextResultListPanel;
 import org.openstreetmap.josm.tools.Destroyable;
@@ -77,7 +71,7 @@
                 boolean isSelected, boolean cellHasFocus) {
             JLabel result = (JLabel) def.getListCellRendererComponent(list, tp, index, isSelected, cellHasFocus);
             result.setText(tp.getName());
-            result.setIcon((Icon) tp.getValue(Action.SMALL_ICON));
+            result.setIcon(tp.getIcon(Action.SMALL_ICON));
             return result;
         }
     }
@@ -86,8 +80,11 @@
      * Computes the match ration of a {@link TaggingPreset} wrt. a searchString.
      */
     public static class PresetClassification implements Comparable<PresetClassification> {
+        /** The preset to classify */
         public final TaggingPreset preset;
+        /** how well this preset matches */
         public int classification;
+        /** where to put it in the list */
         public int favoriteIndex;
         private final Collection<String> groups;
         private final Collection<String> names;
@@ -98,28 +95,20 @@
             Set<String> groupSet = new HashSet<>();
             Set<String> nameSet = new HashSet<>();
             Set<String> tagSet = new HashSet<>();
-            TaggingPreset group = preset.group;
-            while (group != null) {
-                addLocaleNames(groupSet, group);
-                group = group.group;
+
+            for (String name : preset.getLocaleGroupName().split("/", -1)) {
+                addLocaleNames(groupSet, name);
             }
-            addLocaleNames(nameSet, preset);
-            for (TaggingPresetItem item: preset.data) {
+            addLocaleNames(nameSet, preset.getLocaleName());
+            for (Item item: preset.getAllItems()) {
                 if (item instanceof KeyedItem) {
-                    tagSet.add(((KeyedItem) item).key);
+                    tagSet.add(((KeyedItem) item).getKey());
+                    tagSet.addAll(((KeyedItem) item).getValues());
                     if (item instanceof ComboMultiSelect) {
-                        final ComboMultiSelect cms = (ComboMultiSelect) item;
-                        if (cms.values_searchable) {
-                            tagSet.addAll(cms.getDisplayValues());
-                        }
+                        tagSet.addAll(((ComboMultiSelect) item).getDisplayValues());
                     }
-                    if (item instanceof Key && ((Key) item).value != null) {
-                        tagSet.add(((Key) item).value);
-                    }
-                } else if (item instanceof Roles) {
-                    for (Role role : ((Roles) item).roles) {
-                        tagSet.add(role.key);
-                    }
+                } else if (item instanceof Role) {
+                    tagSet.add(((Role) item).getKey());
                 }
             }
             this.groups = Utils.toUnmodifiableList(groupSet);
@@ -127,8 +116,7 @@
             this.tags = Utils.toUnmodifiableList(tagSet);
         }
 
-        private static void addLocaleNames(Collection<String> collection, TaggingPreset preset) {
-            String locName = preset.getLocaleName();
+        private static void addLocaleNames(Collection<String> collection, String locName) {
             if (locName != null) {
                 Collections.addAll(collection, locName.toLowerCase(Locale.ENGLISH).split("\\s", -1));
             }
@@ -276,6 +264,16 @@
 
         private final List<PresetClassification> classifications = new ArrayList<>();
 
+        /**
+         * Returns a list of presets that match the search criteria.
+         *
+         * @param searchText the search text
+         * @param onlyApplicable only consider presets applicable to {@code presetTypes}
+         * @param inTags if true also search for words in tags
+         * @param presetTypes these preset types
+         * @param selectedPrimitives consider relation presets that can use the selected primitives as roles
+         * @return the list of presets
+         */
         public List<PresetClassification> getMatchingPresets(String searchText, boolean onlyApplicable, boolean inTags,
                 Set<TaggingPresetType> presetTypes, final Collection<? extends OsmPrimitive> selectedPrimitives) {
             final String[] groupWords;
@@ -292,6 +290,16 @@
             return getMatchingPresets(groupWords, nameWords, onlyApplicable, inTags, presetTypes, selectedPrimitives);
         }
 
+        /**
+         * Returns a list of presets that match the search criteria.
+         * @param groupWords search for these preset groups
+         * @param nameWords search for these preset names
+         * @param onlyApplicable only consider presets applicable to {@code presetTypes}
+         * @param inTags if true also search for words in tags
+         * @param presetTypes these preset types
+         * @param selectedPrimitives consider relation presets that can use the selected primitives as roles
+         * @return the list of presets
+         */
         public List<PresetClassification> getMatchingPresets(String[] groupWords, String[] nameWords, boolean onlyApplicable,
                 boolean inTags, Set<TaggingPresetType> presetTypes, final Collection<? extends OsmPrimitive> selectedPrimitives) {
 
@@ -303,10 +311,9 @@
                 if (onlyApplicable) {
                     boolean suitable = preset.typeMatches(presetTypes);
 
-                    if (!suitable && preset.types.contains(TaggingPresetType.RELATION)
-                            && preset.roles != null && !preset.roles.roles.isEmpty()) {
-                        suitable = preset.roles.roles.stream().anyMatch(
-                                object -> object.memberExpression != null && selectedPrimitives.stream().anyMatch(object.memberExpression));
+                    if (!suitable && preset.getTypes().contains(TaggingPresetType.RELATION)) {
+                        suitable = preset.getAllRoles().stream().anyMatch(
+                                role -> role.getMemberExpression() != null && selectedPrimitives.stream().anyMatch(role.getMemberExpression()));
                         // keep the preset to allow the creation of new relations
                     }
                     if (!suitable) {
@@ -360,10 +367,9 @@
          */
         public void loadPresets(Collection<TaggingPreset> presets) {
             for (TaggingPreset preset : presets) {
-                if (preset instanceof TaggingPresetSeparator || preset instanceof TaggingPresetMenu) {
-                    continue;
+                if (preset.getClass() == TaggingPreset.class) {
+                    classifications.add(new PresetClassification(preset));
                 }
-                classifications.add(new PresetClassification(preset));
             }
         }
 
Index: src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetSeparator.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetSeparator.java	(revision 18366)
+++ src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetSeparator.java	(working copy)
@@ -1,13 +1,38 @@
 // License: GPL. For details, see LICENSE file.
 package org.openstreetmap.josm.gui.tagging.presets;
 
+import java.util.Map;
+
+import javax.swing.JMenu;
+import javax.swing.JSeparator;
+
 /**
- * Class used to represent a {@link javax.swing.JSeparator} inside tagging preset menu.
+ * Class used to represent a {@link JSeparator} inside tagging preset menu.
  * @since 895
  */
-public class TaggingPresetSeparator extends TaggingPreset {
+final class TaggingPresetSeparator extends Item {
+
+    private TaggingPresetSeparator(Map<String, String> attributes) throws IllegalArgumentException {
+        super(attributes);
+    }
+
+    /**
+     * Create a {@code TaggingPresetSeparator} from an XML element's attributes.
+     * @param attributes the XML attributes
+     * @return the {@code TaggingPresetSeparator}
+     * @throws IllegalArgumentException on attribute errors
+     */
+    public static TaggingPresetSeparator fromXML(Map<String, String> attributes) throws IllegalArgumentException {
+        return new TaggingPresetSeparator(attributes);
+    }
+
     @Override
-    public void setDisplayName() {
-        // Do nothing
+    public void addToMenu(JMenu parentMenu) {
+        parentMenu.add(new JSeparator());
     }
+
+    @Override
+    public String toString() {
+        return "TaggingPresetSeparator";
+    }
 }
Index: src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetType.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetType.java	(revision 18366)
+++ src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetType.java	(working copy)
@@ -1,10 +1,15 @@
 // License: GPL. For details, see LICENSE file.
 package org.openstreetmap.josm.gui.tagging.presets;
 
+import static org.openstreetmap.josm.tools.I18n.tr;
+
 import java.util.Arrays;
+import java.util.EnumSet;
+import java.util.Map;
 
 import org.openstreetmap.josm.data.osm.IPrimitive;
 import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
+import org.openstreetmap.josm.gui.util.LruCache;
 
 /**
  * Enumeration of OSM primitive types associated with names and icons
@@ -24,6 +29,9 @@
     private final String iconName;
     private final String name;
 
+    /** LRU cache for the parsing of types */
+    private static final Map<String, EnumSet<TaggingPresetType>> TYPE_CACHE = new LruCache<>(16);
+
     TaggingPresetType(String iconName, String name) {
         this.iconName = iconName + ".svg";
         this.name = name;
@@ -83,4 +91,35 @@
                 .filter(t -> t.getName().equals(type))
                 .findFirst().orElse(null);
     }
+
+    /**
+     * Returns a set of types
+     *
+     * @param types the types as comma-separated string. eg. "node,way,relation"
+     * @param default_ the default value, returned if {@code types} is null
+     * @return the types as set
+     * @throws IllegalArgumentException on input error
+     */
+    public static EnumSet<TaggingPresetType> getOrDefault(String types, EnumSet<TaggingPresetType> default_) throws IllegalArgumentException {
+        if (types == null)
+            return default_;
+        if (types.isEmpty())
+            throw new IllegalArgumentException(tr("Unknown type: {0}", types));
+        if (TYPE_CACHE.containsKey(types))
+            return TYPE_CACHE.get(types);
+
+        EnumSet<TaggingPresetType> result = EnumSet.noneOf(TaggingPresetType.class);
+        for (String type : types.split(",", -1)) {
+            try {
+                TaggingPresetType presetType = fromString(type);
+                if (presetType != null) {
+                    result.add(presetType);
+                }
+            } catch (IllegalArgumentException e) {
+                throw new IllegalArgumentException(tr("Unknown type: {0}", type), e);
+            }
+        }
+        TYPE_CACHE.put(types, result);
+        return result;
+    }
 }
Index: src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetUtils.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetUtils.java	(nonexistent)
+++ src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetUtils.java	(working copy)
@@ -0,0 +1,322 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.tagging.presets;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+import static org.openstreetmap.josm.tools.I18n.trc;
+
+import java.awt.Component;
+import java.io.File;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import javax.swing.AbstractAction;
+import javax.swing.ImageIcon;
+import javax.swing.JMenu;
+import javax.swing.JMenuItem;
+import javax.swing.JSeparator;
+import javax.swing.SwingUtilities;
+
+import org.openstreetmap.josm.data.osm.DataSet;
+import org.openstreetmap.josm.data.osm.OsmDataManager;
+import org.openstreetmap.josm.data.osm.search.SearchCompiler;
+import org.openstreetmap.josm.data.osm.search.SearchParseError;
+import org.openstreetmap.josm.data.osm.search.SearchCompiler.Match;
+import org.openstreetmap.josm.data.tagging.ac.AutoCompletionItem;
+import org.openstreetmap.josm.gui.MainApplication;
+import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionManager;
+import org.openstreetmap.josm.tools.AlphanumComparator;
+import org.openstreetmap.josm.tools.ImageProvider;
+import org.openstreetmap.josm.tools.ImageResource;
+import org.openstreetmap.josm.tools.Logging;
+import org.openstreetmap.josm.tools.template_engine.ParseError;
+import org.openstreetmap.josm.tools.template_engine.TemplateEntry;
+import org.openstreetmap.josm.tools.template_engine.TemplateParser;
+
+/**
+ * Utility class for tagging presets.
+ */
+public final class TaggingPresetUtils {
+
+    private TaggingPresetUtils() {}
+
+    /**
+     * Replaces ' with ''
+     * @param s input
+     * @return output
+     */
+    public static String fixPresetString(String s) {
+        return s == null ? s : s.replace("'", "''");
+    }
+
+    /**
+     * Parse and compile a template.
+     *
+     * @param pattern The template pattern.
+     * @return the compiled template
+     * @throws IllegalArgumentException If an error occured while parsing.
+     */
+    public static TemplateEntry parseTemplate(String pattern) throws IllegalArgumentException { // NOPMD
+        if (pattern == null)
+            return null;
+        try {
+            return new TemplateParser(pattern).parse();
+        } catch (ParseError e) {
+            Logging.error("Error while parsing " + pattern + ": " + e.getMessage());
+            throw new IllegalArgumentException(e);
+        }
+    }
+
+    /**
+     * Sets the match_expression additional criteria for matching primitives.
+     *
+     * @param filter The search pattern
+     * @return the compiled expression
+
+     * @throws IllegalArgumentException on search pattern parse error
+     * @see <a href="https://josm.openstreetmap.de/wiki/TaggingPresets#Attributes">JOSM wiki</a>
+     */
+
+    public static Match parseSearchExpression(String filter) throws IllegalArgumentException {
+        if (filter == null)
+            return null;
+        try {
+            return SearchCompiler.compile(filter);
+        } catch (SearchParseError e) {
+            Logging.error("Error while parsing" + filter + ": " + e.getMessage());
+            throw new IllegalArgumentException(e);
+        }
+    }
+
+    /**
+     * Loads the icon asynchronously and puts it on the action.
+     * <p>
+     * The image resource is loaded in the background, and then the EDT is invoked to put the icon
+     * on the action.
+     * @param iconName the iconname
+     * @param action the action where to put the icon
+     *
+     * @return a future completed when the icon is put on the action
+     */
+    static CompletableFuture<ImageResource> loadIcon(String iconName, AbstractAction action) {
+        if (action != null && iconName != null && TaggingPresetReader.isLoadIcons()) {
+            return new ImageProvider(iconName)
+                .setDirs(TaggingPresets.ICON_SOURCES.get())
+                .setId("presets")
+                .setArchive(TaggingPresetReader.getZipIcons())
+                .setOptional(true)
+                .getResourceFuture()
+                .thenCompose((imageResource) -> {
+                    CompletableFuture<ImageResource> future = new CompletableFuture<>();
+                    SwingUtilities.invokeLater(() -> {
+                        if (imageResource != null)
+                            imageResource.attachImageIcon(action, true);
+                        future.complete(imageResource);
+                    });
+                    return future;
+                });
+        }
+        return CompletableFuture.<ImageResource>completedFuture(null);
+    }
+
+    /**
+     * Loads a tagging preset icon
+     * @param iconName the icon name
+     * @param zipIcons zip file where the image is located
+     * @param maxSize maximum image size (or null)
+     * @return the requested image or null if the request failed
+     */
+    public static ImageIcon loadImageIcon(String iconName, File zipIcons, Integer maxSize) {
+        final Collection<String> s = TaggingPresets.ICON_SOURCES.get();
+        ImageProvider imgProv = new ImageProvider(iconName).setDirs(s).setId("presets").setArchive(zipIcons).setOptional(true);
+        if (maxSize != null && maxSize > 0) {
+            imgProv.setMaxSize(maxSize);
+        }
+        return imgProv.get();
+    }
+
+    /**
+     * Localizes the preset name
+     *
+     * @param localeText the locale name from the attributes
+     * @param text the unlocalized name
+     * @param textContext the localization context
+     * @return The name that should be displayed to the user.
+     */
+    public static String buildLocaleString(String localeText, String text, String textContext) {
+        if (localeText != null)
+            return localeText;
+        text = fixPresetString(text);
+        if (textContext != null) {
+            return trc(textContext, text);
+        } else {
+            return tr(text);
+        }
+    }
+
+    /**
+     * Determine whether the given preset items match the tags
+     * @param data the preset items
+     * @param tags the tags to match
+     * @return whether the given preset items match the tags
+     * @since 9932
+     */
+    public static boolean matches(Iterable<? extends Item> data, Map<String, String> tags) {
+        boolean atLeastOnePositiveMatch = false;
+        for (Item item : data) {
+            Boolean m = item.matches(tags);
+            if (m != null && !m)
+                return false;
+            else if (m != null) {
+                atLeastOnePositiveMatch = true;
+            }
+        }
+        return atLeastOnePositiveMatch;
+    }
+
+    /**
+     * allow escaped comma in comma separated list:
+     * "A\, B\, C,one\, two" --&gt; ["A, B, C", "one, two"]
+     * @param delimiter the delimiter, e.g. a comma. separates the entries and
+     *      must be escaped within one entry
+     * @param s the string
+     * @return splitted items
+     */
+    public static List<String> splitEscaped(char delimiter, String s) {
+        if (s == null)
+            return null; // NOSONAR
+
+        List<String> result = new ArrayList<>();
+        boolean backslash = false;
+        StringBuilder item = new StringBuilder();
+        for (int i = 0; i < s.length(); i++) {
+            char ch = s.charAt(i);
+            if (backslash) {
+                item.append(ch);
+                backslash = false;
+            } else if (ch == '\\') {
+                backslash = true;
+            } else if (ch == delimiter) {
+                result.add(item.toString());
+                item.setLength(0);
+            } else {
+                item.append(ch);
+            }
+        }
+        if (item.length() > 0) {
+            result.add(item.toString());
+        }
+        return result;
+    }
+
+    /**
+     * Returns all cached {@link AutoCompletionItem}s for given keys.
+     *
+     * @param keys retrieve the items for these keys
+     * @return the currently cached items, sorted by priority and alphabet
+     * @since 18221
+     */
+    public static List<AutoCompletionItem> getAllForKeys(List<String> keys) {
+        DataSet data = OsmDataManager.getInstance().getEditDataSet();
+        if (data == null) {
+            return Collections.emptyList();
+        }
+        return AutoCompletionManager.of(data).getAllValuesForKeys(keys);
+    }
+
+    /**
+     * Parse a {@code String} into an {@code boolean}.
+     * @param s the string to parse
+     * @return the int
+     */
+    public static boolean parseBoolean(String s) {
+        return s != null
+                && !"0".equals(s)
+                && !s.startsWith("off")
+                && !s.startsWith("false")
+                && !s.startsWith("no");
+    }
+
+    /**
+     * Wait for all preset icons to load
+     * @param presets presets collection
+     * @param timeout timeout in seconds
+     * @throws InterruptedException if any thread is interrupted
+     * @throws ExecutionException if any thread throws
+     * @throws TimeoutException on timeout
+     */
+    public static void waitForIconsLoaded(Collection<TaggingPreset> presets, long timeout)
+        throws InterruptedException, ExecutionException, TimeoutException {
+
+        @SuppressWarnings("unchecked")
+        CompletableFuture<ImageResource>[] futures =
+            presets.stream().map(tp -> tp.iconFuture).toArray(CompletableFuture[]::new);
+
+        CompletableFuture.allOf(futures).get(timeout, TimeUnit.SECONDS);
+    }
+
+    /**
+     * Sorts the menu items using the translated item text
+     * @param menu menu to sort
+     */
+    public static void sortMenu(JMenu menu) {
+        Component[] items = menu.getMenuComponents();
+        PresetTextComparator comp = new PresetTextComparator();
+        List<JMenuItem> sortarray = new ArrayList<>();
+        int lastSeparator = 0;
+        for (int i = 0; i < items.length; i++) {
+            Object item = items[i];
+            if (item instanceof JMenu) {
+                sortMenu((JMenu) item);
+            }
+            if (item instanceof JMenuItem) {
+                sortarray.add((JMenuItem) item);
+                if (i == items.length-1) {
+                    handleMenuItem(menu, comp, sortarray, lastSeparator);
+                    sortarray = new ArrayList<>();
+                    lastSeparator = 0;
+                }
+            } else if (item instanceof JSeparator) {
+                handleMenuItem(menu, comp, sortarray, lastSeparator);
+                sortarray = new ArrayList<>();
+                lastSeparator = i;
+            }
+        }
+    }
+
+    private static void handleMenuItem(JMenu menu, PresetTextComparator comp, List<JMenuItem> sortarray, int lastSeparator) {
+        sortarray.sort(comp);
+        int pos = 0;
+        for (JMenuItem menuItem : sortarray) {
+            int oldPos;
+            if (lastSeparator == 0) {
+                oldPos = pos;
+            } else {
+                oldPos = pos+lastSeparator+1;
+            }
+            menu.add(menuItem, oldPos);
+            pos++;
+        }
+    }
+
+    private static class PresetTextComparator implements Comparator<JMenuItem>, Serializable {
+        private static final long serialVersionUID = 1L;
+        @Override
+        public int compare(JMenuItem o1, JMenuItem o2) {
+            if (MainApplication.getMenu().presetSearchAction.equals(o1.getAction()))
+                return -1;
+            else if (MainApplication.getMenu().presetSearchAction.equals(o2.getAction()))
+                return 1;
+            else
+                return AlphanumComparator.getInstance().compare(o1.getText(), o2.getText());
+        }
+    }
+}
Index: src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetValidation.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetValidation.java	(revision 18366)
+++ src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetValidation.java	(working copy)
@@ -1,24 +1,14 @@
 // License: GPL. For details, see LICENSE file.
 package org.openstreetmap.josm.gui.tagging.presets;
 
-import static java.util.Collections.singleton;
 import static org.openstreetmap.josm.tools.I18n.tr;
 
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collection;
 import java.util.List;
 
 import javax.swing.JLabel;
 
-import org.openstreetmap.josm.command.Command;
-import org.openstreetmap.josm.data.osm.DataSet;
-import org.openstreetmap.josm.data.osm.FilterModel;
-import org.openstreetmap.josm.data.osm.INode;
-import org.openstreetmap.josm.data.osm.IRelation;
-import org.openstreetmap.josm.data.osm.IWay;
-import org.openstreetmap.josm.data.osm.OsmPrimitive;
-import org.openstreetmap.josm.data.osm.Tag;
 import org.openstreetmap.josm.data.preferences.sources.ValidatorPrefHelper;
 import org.openstreetmap.josm.data.validation.OsmValidator;
 import org.openstreetmap.josm.data.validation.TestError;
@@ -27,31 +17,28 @@
 import org.openstreetmap.josm.gui.MainApplication;
 import org.openstreetmap.josm.gui.util.GuiHelper;
 import org.openstreetmap.josm.tools.Logging;
-import org.openstreetmap.josm.tools.SubclassFilteredCollection;
 import org.openstreetmap.josm.tools.Utils;
 
 /**
  * Validates the preset user input a the given primitive.
  */
-interface TaggingPresetValidation {
+public interface TaggingPresetValidation {
 
     /**
      * Asynchronously validates the user input for the given primitive.
-     * @param original the primitive
+     * @param handler the handler that holds the primitives to check
      * @param validationLabel the label for validation errors
-     * @param changedTags the list of tags that are set by this preset
      */
-    static void validateAsync(OsmPrimitive original, JLabel validationLabel, List<Tag> changedTags) {
-        OsmPrimitive primitive = applyChangedTags(original, changedTags);
-        MainApplication.worker.execute(() -> validate(primitive, validationLabel));
+    static void validateAsync(TaggingPresetHandler handler, JLabel validationLabel) {
+        MainApplication.worker.execute(() -> validate(handler, validationLabel));
     }
 
     /**
      * Validates the user input for the given primitive.
-     * @param primitive the primitive
+     * @param handler the handler that holds the primitives to check
      * @param validationLabel the label for validation errors
      */
-    static void validate(OsmPrimitive primitive, JLabel validationLabel) {
+    static void validate(TaggingPresetHandler handler, JLabel validationLabel) {
         try {
             MapCSSTagChecker mapCSSTagChecker = OsmValidator.getTest(MapCSSTagChecker.class);
             OpeningHourTest openingHourTest = OsmValidator.getTest(OpeningHourTest.class);
@@ -58,8 +45,8 @@
             OsmValidator.initializeTests(Arrays.asList(mapCSSTagChecker, openingHourTest));
 
             List<TestError> errors = new ArrayList<>();
-            openingHourTest.addErrorsForPrimitive(primitive, errors);
-            errors.addAll(mapCSSTagChecker.getErrorsForPrimitive(primitive, ValidatorPrefHelper.PREF_OTHER.get()));
+            openingHourTest.checkTaggingPresetHandler(handler, errors);
+            errors.addAll(mapCSSTagChecker.checkTaggingPresetHandler(handler, ValidatorPrefHelper.PREF_OTHER.get()));
 
             boolean visible = !errors.isEmpty();
             String toolTipText = "<html>" + Utils.joinAsHtmlUnorderedList(Utils.transform(errors, e ->
@@ -69,25 +56,8 @@
                 validationLabel.setToolTipText(toolTipText);
             });
         } catch (Exception e) {
-            Logging.warn("Failed to validate {0}", primitive);
+            Logging.warn("Failed to validate {0}", handler.getPrimitives().iterator().next());
             Logging.warn(e);
-        } finally {
-            primitive.getDataSet().clear();
         }
     }
-
-    static OsmPrimitive applyChangedTags(OsmPrimitive original, List<Tag> changedTags) {
-        DataSet ds = new DataSet();
-        Collection<OsmPrimitive> primitives = FilterModel.getAffectedPrimitives(singleton(original));
-        OsmPrimitive primitive = ds.clonePrimitives(
-                new SubclassFilteredCollection<>(primitives, INode.class::isInstance),
-                new SubclassFilteredCollection<>(primitives, IWay.class::isInstance),
-                new SubclassFilteredCollection<>(primitives, IRelation.class::isInstance))
-                .get(original);
-        Command command = TaggingPreset.createCommand(singleton(primitive), changedTags);
-        if (command != null) {
-            command.executeCommand();
-        }
-        return primitive;
-    }
 }
Index: src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresets.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresets.java	(revision 18366)
+++ src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresets.java	(working copy)
@@ -3,17 +3,19 @@
 
 import static org.openstreetmap.josm.tools.I18n.tr;
 
-import java.util.ArrayList;
+import java.io.IOException;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.function.Predicate;
 
 import javax.swing.JMenu;
-import javax.swing.JMenuItem;
-import javax.swing.JSeparator;
 
 import org.openstreetmap.josm.actions.PreferencesAction;
 import org.openstreetmap.josm.data.osm.IPrimitive;
@@ -22,16 +24,11 @@
 import org.openstreetmap.josm.data.preferences.ListProperty;
 import org.openstreetmap.josm.gui.MainApplication;
 import org.openstreetmap.josm.gui.MainMenu;
-import org.openstreetmap.josm.gui.MenuScroller;
 import org.openstreetmap.josm.gui.preferences.ToolbarPreferences;
 import org.openstreetmap.josm.gui.preferences.map.TaggingPresetPreference;
-import org.openstreetmap.josm.gui.tagging.presets.items.CheckGroup;
-import org.openstreetmap.josm.gui.tagging.presets.items.KeyedItem;
-import org.openstreetmap.josm.gui.tagging.presets.items.Roles;
-import org.openstreetmap.josm.gui.tagging.presets.items.Roles.Role;
-import org.openstreetmap.josm.tools.Logging;
 import org.openstreetmap.josm.tools.MultiMap;
 import org.openstreetmap.josm.tools.SubclassFilteredCollection;
+import org.xml.sax.SAXException;
 
 /**
  * Class holding Tagging Presets and allowing to manage them.
@@ -39,25 +36,29 @@
  */
 public final class TaggingPresets {
 
-    /** The collection of tagging presets */
-    private static final Collection<TaggingPreset> taggingPresets = new ArrayList<>();
+    /** The root elements of all XML files */
+    private static final Collection<Root> rootElements = new LinkedList<>();
 
-    /** cache for key/value pairs found in the preset */
+    /** caches the tags in all presets key->values */
+    private static final Map<String, TaggingPreset> PRESET_CACHE = new LinkedHashMap<>();
+    /** caches the tags in all presets key->values */
     private static final MultiMap<String, String> PRESET_TAG_CACHE = new MultiMap<>();
-    /** cache for roles found in the preset */
-    private static final Set<String> PRESET_ROLE_CACHE = new HashSet<>();
+    /** caches the roles in all presets key -> role */
+    private static final Set<Role> PRESET_ROLE_CACHE = new HashSet<>();
 
     /** The collection of listeners */
-    private static final Collection<TaggingPresetListener> listeners = new ArrayList<>();
+    private static final Collection<TaggingPresetListener> listeners = new LinkedList<>();
+    /** Custom icon sources */
+    public static final ListProperty ICON_SOURCES = new ListProperty("taggingpreset.icon.sources", null);
     /**
-     * Sort presets menu alphabetically
+     * Defines whether the validator should be active in the preset dialog
+     * @see TaggingPresetValidation
      */
-    public static final BooleanProperty SORT_MENU = new BooleanProperty("taggingpreset.sortvalues", true);
-    /**
-     * Custom icon sources
-     */
-    public static final ListProperty ICON_SOURCES = new ListProperty("taggingpreset.icon.sources", null);
-    private static final IntegerProperty MIN_ELEMENTS_FOR_SCROLLER = new IntegerProperty("taggingpreset.min-elements-for-scroller", 15);
+    public static final BooleanProperty USE_VALIDATOR = new BooleanProperty("taggingpreset.validator", false);
+    /** Sort preset values alphabetically in combos and menus */
+    public static final BooleanProperty SORT_VALUES = new BooleanProperty("taggingpreset.sortvalues", true);
+    /** No. of items a menu must have before using a scroller  */
+    public static final IntegerProperty MIN_ELEMENTS_FOR_SCROLLER = new IntegerProperty("taggingpreset.min-elements-for-scroller", 15);
 
     private TaggingPresets() {
         // Hide constructor for utility classes
@@ -64,18 +65,51 @@
     }
 
     /**
-     * Initializes tagging presets from preferences.
+     * Standard initialization during app startup. Obeys users prefs.
      */
+    public static void initialize() {
+        readFromPreferences();
+        initializeMenus();
+    }
+
+    /**
+     * Initializes tagging presets from user preferences.
+     */
     public static void readFromPreferences() {
-        taggingPresets.clear();
-        taggingPresets.addAll(TaggingPresetReader.readFromPreferences(false, false));
-        cachePresets(taggingPresets);
+        TaggingPresetReader.readFromPreferences(false, false).forEach(TaggingPresets::addRoot);
+        listeners.forEach(TaggingPresetListener::taggingPresetsModified);
     }
 
     /**
-     * Initialize the tagging presets (load and may display error)
+     * Deterministic initialization during test.
+     *
+     * @param presetsUrl the url of the presets file to load for testing
+     * @throws SAXException in case of parser errors
+     * @throws IOException if the url was not found
      */
-    public static void initialize() {
+    public static void testInitialize(String presetsUrl) throws SAXException, IOException {
+        addRoot(TaggingPresetReader.read(presetsUrl, false));
+        listeners.forEach(TaggingPresetListener::taggingPresetsModified);
+    }
+
+    /**
+     * Add a new root element
+     * @param root the new root element
+     */
+    public static void addRoot(Root root) {
+        Map<String, Chunk> chunks = new HashMap<>();
+        root.fixup(chunks, root);
+        rootElements.add(root);
+        cachePresets(root);
+    }
+
+    /**
+     * Initializes the preset menu and toolbar.
+     * <p>
+     * Builds the tagging presets menu and registers all preset actions with the application
+     * toolbar.
+     */
+    public static void initializeMenus() {
         MainMenu mainMenu = MainApplication.getMenu();
         JMenu presetsMenu = mainMenu.presetsMenu;
         if (presetsMenu.getComponentCount() == 0) {
@@ -86,47 +120,20 @@
             presetsMenu.addSeparator();
         }
 
-        readFromPreferences();
-        for (TaggingPreset tp: taggingPresets) {
-            if (!(tp instanceof TaggingPresetSeparator)) {
-                MainApplication.getToolbar().register(tp);
-                MainApplication.getLayerManager().addActiveLayerChangeListener(tp);
-            }
-        }
-        if (taggingPresets.isEmpty()) {
+        // register all presets with the application toolbar
+        ToolbarPreferences toolBar = MainApplication.getToolbar();
+        getAllItems(TaggingPresetBase.class).forEach(tp -> toolBar.register(tp.getAction()));
+        toolBar.refreshToolbarControl();
+
+        // add presets and groups to the presets menu
+        if (rootElements.isEmpty()) {
             presetsMenu.setVisible(false);
         } else {
-            Map<TaggingPresetMenu, JMenu> submenus = new HashMap<>();
-            for (final TaggingPreset p : taggingPresets) {
-                JMenu m = p.group != null ? submenus.get(p.group) : presetsMenu;
-                if (m == null && p.group != null) {
-                    Logging.error("No tagging preset submenu for " + p.group);
-                } else if (m == null) {
-                    Logging.error("No tagging preset menu. Tagging preset " + p + " won't be available there");
-                } else if (p instanceof TaggingPresetSeparator) {
-                    m.add(new JSeparator());
-                } else if (p instanceof TaggingPresetMenu) {
-                    JMenu submenu = new JMenu(p);
-                    submenu.setText(p.getLocaleName());
-                    ((TaggingPresetMenu) p).menu = submenu;
-                    submenus.put((TaggingPresetMenu) p, submenu);
-                    m.add(submenu);
-                } else {
-                    JMenuItem mi = new JMenuItem(p);
-                    mi.setText(p.getLocaleName());
-                    m.add(mi);
-                }
+            rootElements.forEach(e -> e.addToMenu(presetsMenu));
+            if (TaggingPresets.SORT_VALUES.get()) {
+                TaggingPresetUtils.sortMenu(presetsMenu);
             }
-            for (JMenu submenu : submenus.values()) {
-                if (submenu.getItemCount() >= MIN_ELEMENTS_FOR_SCROLLER.get()) {
-                    MenuScroller.setScrollerFor(submenu);
-                }
-            }
         }
-        if (SORT_MENU.get()) {
-            TaggingPresetMenu.sortMenu(presetsMenu);
-        }
-        listeners.forEach(TaggingPresetListener::taggingPresetsModified);
     }
 
     // Cannot implement Destroyable since this is static
@@ -137,70 +144,84 @@
      * @since 15582
      */
     public static void destroy() {
+        unInitializeMenus();
+        cleanUp();
+    }
+
+    static void unInitializeMenus() {
         ToolbarPreferences toolBar = MainApplication.getToolbar();
-        for (TaggingPreset tp: taggingPresets) {
-            toolBar.unregister(tp);
-            if (!(tp instanceof TaggingPresetSeparator)) {
-                MainApplication.getLayerManager().removeActiveLayerChangeListener(tp);
-            }
-        }
-        taggingPresets.clear();
+        if (toolBar != null)
+            getAllItems(TaggingPresetBase.class).forEach(tp -> toolBar.unregister(tp.getAction()));
+        MainMenu menu = MainApplication.getMenu();
+        if (menu != null)
+            menu.presetsMenu.removeAll();
+    }
+
+    static void cleanUp() {
+        PRESET_CACHE.clear();
         PRESET_TAG_CACHE.clear();
         PRESET_ROLE_CACHE.clear();
-        MainApplication.getMenu().presetsMenu.removeAll();
+        rootElements.forEach(Item::destroy);
+        rootElements.clear();
     }
 
     /**
-     * Initialize the cache for presets. This is done only once.
-     * @param presets Tagging presets to cache
+     * Initialize the cache with presets.
+     *
+     * @param root the root of the xml file
      */
-    public static void cachePresets(Collection<TaggingPreset> presets) {
-        for (final TaggingPreset p : presets) {
-            for (TaggingPresetItem item : p.data) {
-                cachePresetItem(p, item);
+    static void cachePresets(Root root) {
+        root.getAllItems(TaggingPreset.class, false).forEach(tp -> PRESET_CACHE.put(tp.fullName, tp));
+        PRESET_ROLE_CACHE.addAll(root.getAllItems(Role.class, false));
+        root.getAllItems(KeyedItem.class, false).forEach(item -> {
+            if (item.key != null && item.getValues() != null) {
+                PRESET_TAG_CACHE.putAll(item.key, item.getValues());
             }
-        }
+        });
     }
 
-    private static void cachePresetItem(TaggingPreset p, TaggingPresetItem item) {
-        if (item instanceof KeyedItem) {
-            KeyedItem ki = (KeyedItem) item;
-            if (ki.key != null && ki.getValues() != null) {
-                PRESET_TAG_CACHE.putAll(ki.key, ki.getValues());
-            }
-        } else if (item instanceof Roles) {
-            Roles r = (Roles) item;
-            for (Role i : r.roles) {
-                if (i.key != null) {
-                    PRESET_ROLE_CACHE.add(i.key);
-                }
-            }
-        } else if (item instanceof CheckGroup) {
-            for (KeyedItem check : ((CheckGroup) item).checks) {
-                cachePresetItem(p, check);
-            }
-        }
+    /**
+     * Returns all items that satisfy a given predicate.
+     * @param p the predicate all items must satisfy
+     * @return the items that satisfy the predicate
+     */
+    public static List<Item> getAllItems(Predicate<Item> p) {
+        List<Item> list = new LinkedList<>();
+        rootElements.forEach(r -> r.addToItemList(list, p, false));
+        return list;
     }
 
     /**
-     * Replies a new collection containing all tagging presets.
-     * @return a new collection containing all tagging presets. Empty if presets are not initialized (never null)
+     * Returns all items of a type.
+     * @param <E> the type
+     * @param type the type
+     * @return the list of all items
      */
+    public static <E> List<E> getAllItems(Class<E> type) {
+        List<E> list = new LinkedList<>();
+        rootElements.forEach(r -> r.addToItemList(list, type, false));
+        return list;
+    }
+
+    /**
+     * Returns all tagging presets.
+     * @return all tagging presets
+     */
     public static Collection<TaggingPreset> getTaggingPresets() {
-        return Collections.unmodifiableCollection(taggingPresets);
+        return Collections.unmodifiableCollection(PRESET_CACHE.values());
     }
 
     /**
-     * Replies a set of all roles in the tagging presets.
-     * @return a set of all roles in the tagging presets.
+     * Returns every role found in any preset.
+     * @return the roles
      */
-    public static Set<String> getPresetRoles() {
-        return Collections.unmodifiableSet(PRESET_ROLE_CACHE);
+    public static Collection<Role> getPresetRoles() {
+        return Collections.unmodifiableCollection(PRESET_ROLE_CACHE);
     }
 
     /**
-     * Replies a set of all keys in the tagging presets.
-     * @return a set of all keys in the tagging presets.
+     * Returns all keys seen in all tagging presets.
+     * @return the set of all keys
      */
     public static Set<String> getPresetKeys() {
         return Collections.unmodifiableSet(PRESET_TAG_CACHE.keySet());
@@ -207,9 +228,9 @@
     }
 
     /**
-     * Return set of values for a key in the tagging presets
+     * Returns all values seen in all presets for this key.
      * @param key the key
-     * @return set of values for a key in the tagging presets
+     * @return the set of all values
      */
     public static Set<String> getPresetValues(String key) {
         Set<String> values = PRESET_TAG_CACHE.get(key);
@@ -232,7 +253,7 @@
      * Replies a new collection of all presets matching the parameters.
      *
      * @param t the preset types to include
-     * @param tags the tags to perform matching on, see {@link TaggingPresetItem#matches(Map)}
+     * @param tags the tags to perform matching on, see {@link Item#matches(Map)}
      * @param onlyShowable whether only {@link TaggingPreset#isShowable() showable} presets should be returned
      * @return a new collection of all presets matching the parameters.
      * @see TaggingPreset#matches(Collection, Map, boolean)
@@ -256,16 +277,6 @@
     }
 
     /**
-     * Adds a list of tagging presets to the current list.
-     * @param presets The tagging presets to add
-     */
-    public static void addTaggingPresets(Collection<TaggingPreset> presets) {
-        if (presets != null && taggingPresets.addAll(presets)) {
-            listeners.forEach(TaggingPresetListener::taggingPresetsModified);
-        }
-    }
-
-    /**
      * Adds a tagging preset listener.
      * @param listener The listener to add
      */
Index: src/org/openstreetmap/josm/gui/tagging/presets/Text.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/Text.java	(nonexistent)
+++ src/org/openstreetmap/josm/gui/tagging/presets/Text.java	(working copy)
@@ -0,0 +1,311 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.tagging.presets;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.util.ArrayList;
+import java.awt.Color;
+import java.awt.GridBagLayout;
+import java.awt.Insets;
+import java.text.NumberFormat;
+import java.text.ParseException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import javax.swing.AbstractButton;
+import javax.swing.BorderFactory;
+import javax.swing.ButtonGroup;
+import javax.swing.JButton;
+import javax.swing.JComponent;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JToggleButton;
+
+import org.openstreetmap.josm.data.osm.Tag;
+import org.openstreetmap.josm.data.tagging.ac.AutoCompletionItem;
+import org.openstreetmap.josm.gui.tagging.ac.AutoCompComboBoxEditor;
+import org.openstreetmap.josm.gui.tagging.ac.AutoCompComboBoxModel;
+import org.openstreetmap.josm.gui.tagging.ac.AutoCompTextField;
+import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionManager;
+import org.openstreetmap.josm.gui.util.DocumentAdapter;
+import org.openstreetmap.josm.gui.widgets.JosmComboBox;
+import org.openstreetmap.josm.gui.widgets.OrientationAction;
+import org.openstreetmap.josm.tools.GBC;
+import org.openstreetmap.josm.tools.Logging;
+import org.openstreetmap.josm.tools.Utils;
+import org.openstreetmap.josm.tools.template_engine.TemplateEntry;
+
+/**
+ * Text field type.
+ */
+final class Text extends KeyedItem {
+    /**
+     * May contain a comma separated list of integer increments or decrements, e.g. "-2,-1,+1,+2".
+     * A button will be shown next to the text field for each value, allowing the user to select auto-increment with the given stepping.
+     * Auto-increment only happens if the user selects it. There is also a button to deselect auto-increment.
+     * Default is no auto-increment. Mutually exclusive with {@link KeyedItem#useLastAsDefault}.
+     */
+    private final String autoIncrement;
+    /** A comma separated list of alternative keys to use for autocompletion. */
+    private final String alternativeAutocompleteKeys;
+    /** A value template */
+    private final TemplateEntry valueTemplate;
+    /** The default value for the item. If not specified, the current value of the key is chosen as
+     * default (if applicable). Defaults to "". */
+    private final String default_;
+
+    /**
+     * Private constructor. Use {@link #fromXML} instead.
+     * @param attributes the XML attributes
+     * @throws IllegalArgumentException on illegal attributes
+     */
+    private Text(Map<String, String> attributes) throws IllegalArgumentException {
+        super(attributes);
+        valueTemplate = TaggingPresetUtils.parseTemplate(attributes.get("value_template"));
+        autoIncrement = attributes.get("auto_increment");
+        alternativeAutocompleteKeys = attributes.get("alternative_autocomplete_keys");
+        default_ = attributes.get("default");
+    }
+
+    /**
+     * Create a {@code Text} from an XML element's attributes.
+     * @param attributes the XML attributes
+     * @return the {@code Text}
+     * @throws IllegalArgumentException on invalid attributes
+     */
+    public static Text fromXML(Map<String, String> attributes) throws IllegalArgumentException {
+        return new Text(attributes);
+    }
+
+    @Override
+    boolean addToPanel(JPanel p, TaggingPresetInstance support) {
+        AutoCompComboBoxModel<AutoCompletionItem> model = new AutoCompComboBoxModel<>();
+        List<String> keys = new ArrayList<>();
+        keys.add(key);
+        if (alternativeAutocompleteKeys != null) {
+            for (String k : alternativeAutocompleteKeys.split(",", -1)) {
+                keys.add(k);
+            }
+        }
+        TaggingPresetUtils.getAllForKeys(keys).forEach(model::addElement);
+
+        AutoCompTextField<AutoCompletionItem> textField;
+        AutoCompComboBoxEditor<AutoCompletionItem> editor = null;
+
+        // find out if our key is already used in the selection.
+        Usage usage = Usage.determineTextUsage(support.getSelected(), key);
+
+        JComponent component;
+        if (usage.unused() || usage.hasUniqueValue()) {
+            textField = new AutoCompTextField<>();
+            component = textField;
+        } else {
+            // The selected primitives have different values for this key.   <b>Note:</b> this
+            // cannot be an AutoCompComboBox because the values in the dropdown are different from
+            // those we autocomplete on.
+            JosmComboBox<String> comboBox = new JosmComboBox<>();
+            comboBox.getModel().addAllElements(usage.map.keySet());
+            comboBox.setEditable(true);
+            editor = new AutoCompComboBoxEditor<>();
+            comboBox.setEditor(editor);
+            comboBox.getEditor().setItem(DIFFERENT_I18N);
+            textField = editor.getEditorComponent();
+            component = comboBox;
+        }
+        textField.setModel(model);
+
+        if (length > 0) {
+            textField.setMaxTextLength(length);
+        }
+        if (Item.DISPLAY_KEYS_AS_HINT.get()) {
+            textField.setHint(key);
+        }
+
+        Instance instance = new Instance(textField, support);
+        support.putInstance(this, instance);
+        instance.setInitialValue(usage, support);
+
+        instance.setupListeners(textField, support);
+
+        // if there's an auto_increment setting, then wrap the text field
+        // into a panel, appending a number of buttons.
+        // auto_increment has a format like -2,-1,1,2
+        // the text box being the first component in the panel is relied
+        // on in a rather ugly fashion further down.
+        if (autoIncrement != null) {
+            int autoIncrementSelected = getAutoIncrement(support);
+            ButtonGroup bg = new ButtonGroup();
+            JPanel pnl = new JPanel(new GridBagLayout());
+            pnl.add(component, GBC.std().fill(GBC.HORIZONTAL));
+
+            // first, one button for each auto_increment value
+            for (final String ai : autoIncrement.split(",", -1)) {
+                JToggleButton aibutton = new JToggleButton(ai);
+                aibutton.setToolTipText(tr("Select auto-increment of {0} for this field", ai));
+                aibutton.setMargin(new Insets(0, 0, 0, 0));
+                aibutton.setFocusable(false);
+                saveHorizontalSpace(aibutton);
+                bg.add(aibutton);
+                try {
+                    // TODO there must be a better way to parse a number like "+3" than this.
+                    final int buttonvalue = NumberFormat.getIntegerInstance().parse(ai.replace("+", "")).intValue();
+                    if (autoIncrementSelected == buttonvalue) aibutton.setSelected(true);
+                    aibutton.addActionListener(e -> setAutoIncrement(support, buttonvalue));
+                    pnl.add(aibutton, GBC.std());
+                } catch (ParseException ex) {
+                    Logging.error("Cannot parse auto-increment value of '" + ai + "' into an integer");
+                }
+            }
+
+            // an invisible toggle button for "release" of the button group
+            final JToggleButton clearbutton = new JToggleButton("X");
+            clearbutton.setVisible(false);
+            clearbutton.setFocusable(false);
+            bg.add(clearbutton);
+            // and its visible counterpart. - this mechanism allows us to
+            // have *no* button selected after the X is clicked, instead
+            // of the X remaining selected
+            JButton releasebutton = new JButton("X");
+            releasebutton.setToolTipText(tr("Cancel auto-increment for this field"));
+            releasebutton.setMargin(new Insets(0, 0, 0, 0));
+            releasebutton.setFocusable(false);
+            releasebutton.addActionListener(e -> {
+                setAutoIncrement(support, 0);
+                clearbutton.setSelected(true);
+            });
+            saveHorizontalSpace(releasebutton);
+            pnl.add(releasebutton, GBC.eol());
+            component = pnl;
+        }
+
+        final JLabel label = new JLabel(tr("{0}:", localeText));
+        addIcon(label);
+        label.setToolTipText(getKeyTooltipText());
+        label.setComponentPopupMenu(getPopupMenu());
+        label.setLabelFor(component);
+        p.add(label, GBC.std().insets(0, 0, 10, 0));
+        p.add(component, GBC.eol().fill(GBC.HORIZONTAL));
+        label.applyComponentOrientation(support.getDefaultComponentOrientation());
+        component.setToolTipText(getKeyTooltipText());
+        component.applyComponentOrientation(OrientationAction.getNamelikeOrientation(key));
+
+        return true;
+    }
+
+    private int getAutoIncrement(TaggingPresetInstance support) {
+        return (Integer) support.getPresetProperties().getOrDefault("autoincrement." + key, 0);
+    }
+
+    private void setAutoIncrement(TaggingPresetInstance support, int i) {
+        support.getPresetProperties().put("autoincrement." + key, i);
+    }
+
+    private static void saveHorizontalSpace(AbstractButton button) {
+        Insets insets = button.getBorder().getBorderInsets(button);
+        // Ensure the current look&feel does not waste horizontal space (as seen in Nimbus & Aqua)
+        if (insets != null && insets.left+insets.right > insets.top+insets.bottom) {
+            int min = Math.min(insets.top, insets.bottom);
+            button.setBorder(BorderFactory.createEmptyBorder(insets.top, min, insets.bottom, min));
+        }
+    }
+
+    @Override
+    MatchType getDefaultMatch() {
+        return MatchType.NONE;
+    }
+
+    @Override
+    public Collection<String> getValues() {
+        if (Utils.isEmpty(default_))
+            return Collections.emptyList();
+        return Collections.singleton(default_);
+    }
+
+    class Instance extends Item.Instance {
+        private AutoCompTextField<AutoCompletionItem> textField;
+        private String originalValue;
+        private Integer autoIncrementSelected;
+
+        Instance(AutoCompTextField<AutoCompletionItem> textField, TaggingPresetInstance support) {
+            this.textField = textField;
+            this.autoIncrementSelected = (Integer) support.getPresetProperties().getOrDefault("autoincrement." + key, 0);
+        }
+
+        @Override
+        public void addCommands(List<Tag> changedTags) {
+            // return if unchanged
+            String v = textField.getText();
+            if (v == null) {
+                Logging.error("No 'last value' support for component " + textField);
+                return;
+            }
+
+            v = Utils.removeWhiteSpaces(v);
+
+            if (isUseLastAsDefault() || autoIncrement != null) {
+                LAST_VALUES.put(key, v);
+            }
+            if (v.equals(originalValue) || (originalValue == null && v.isEmpty()))
+                return;
+
+            changedTags.add(new Tag(key, v));
+            AutoCompletionManager.rememberUserInput(key, v, true);
+        }
+
+        private void setInitialValue(Usage usage, TaggingPresetInstance support) {
+            if (usage.unused()) {
+                if (autoIncrementSelected != 0 && autoIncrement != null) {
+                    try {
+                        textField.setText(Integer.toString(Integer.parseInt(
+                                LAST_VALUES.get(key)) + autoIncrementSelected));
+                    } catch (NumberFormatException ex) {
+                        // Ignore - cannot auto-increment if last was non-numeric
+                        Logging.trace(ex);
+                    }
+                } else if (!usage.hadKeys() || PROP_FILL_DEFAULT.get() || isForceUseLastAsDefault()) {
+                    // selected osm primitives are untagged or filling default values feature is enabled
+                    if (!support.isPresetInitiallyMatches() && isUseLastAsDefault() && LAST_VALUES.containsKey(key)) {
+                        textField.setText(LAST_VALUES.get(key));
+                    } else {
+                        textField.setText(default_);
+                    }
+                } else {
+                    // selected osm primitives are tagged and filling default values feature is disabled
+                    textField.setText("");
+                }
+                originalValue = null;
+            } else if (usage.hasUniqueValue()) {
+                // all objects use the same value
+                textField.setText(usage.getFirst());
+                originalValue = usage.getFirst();
+            } else {
+                originalValue = DIFFERENT_I18N;
+            }
+        }
+
+        private void setupListeners(AutoCompTextField<AutoCompletionItem> textField, TaggingPresetInstance support) {
+            // value_templates don't work well with multiple selected items because,
+            // as the command queue is currently implemented, we can only save
+            // the same value to all selected primitives, which is probably not
+            // what you want.
+            if (valueTemplate == null || support.getSelected().size() > 1) { // only fire on normal fields
+                textField.getDocument().addDocumentListener(DocumentAdapter.create(ignore ->
+                        support.fireItemValueModified(this, key, textField.getText())));
+            } else { // only listen on calculated fields
+                support.addListener((source, key, newValue) -> {
+                    String valueTemplateText = valueTemplate.getText(support);
+                    Logging.trace("Evaluating value_template {0} for key {1} from {2} with new value {3} => {4}",
+                            valueTemplate, key, source, newValue, valueTemplateText);
+                    textField.setText(valueTemplateText);
+                    if (originalValue != null && !originalValue.equals(valueTemplateText)) {
+                        textField.setForeground(Color.RED);
+                    } else {
+                        textField.setForeground(Color.BLUE);
+                    }
+                });
+            }
+        }
+    }
+}
Index: src/org/openstreetmap/josm/gui/tagging/presets/TextItem.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/TextItem.java	(nonexistent)
+++ src/org/openstreetmap/josm/gui/tagging/presets/TextItem.java	(working copy)
@@ -0,0 +1,83 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.tagging.presets;
+
+import java.util.Map;
+
+import javax.swing.ImageIcon;
+import javax.swing.JLabel;
+import javax.swing.SwingConstants;
+
+/**
+ * A tagging preset item displaying a localizable text.
+ * @since 6190
+ */
+abstract class TextItem extends Item {
+
+    /** The text to display */
+    final String text;
+    /** The context used for translating {@link #text} */
+    final String textContext;
+    /** The localized version of {@link #text} */
+    final String localeText;
+    /** The location of icon file to display */
+    final String icon;
+    /** The size of displayed icon. If not set, default is 16px */
+    final int iconSize;
+
+    /**
+     * Constructor.
+     * @param attributes the XML attributes
+     * @throws IllegalArgumentException on illegal attributes
+     */
+    TextItem(Map<String, String> attributes) throws IllegalArgumentException {
+        super(attributes);
+        String v = attributes.get("text");
+        text = v != null ? v : getDefaultText();
+        textContext = attributes.get("text_context");
+        icon = attributes.get("icon");
+        iconSize = Integer.parseInt(attributes.getOrDefault("icon_size", "16"));
+        localeText = TaggingPresetUtils.buildLocaleString(attributes.get("locale_text"), text, textContext);
+    }
+
+    /**
+     * Returns the text
+     * @return teh text
+     */
+    String getText() {
+        return text;
+    }
+
+    String getDefaultText() {
+        return null;
+    }
+
+    String fieldsToString() {
+        return (text != null ? "text=" + text + ", " : "")
+                + (textContext != null ? "text_context=" + textContext + ", " : "")
+                + (localeText != null ? "locale_text=" + localeText : "");
+    }
+
+    /**
+     * Defines the label icon from this entry's icon
+     * @param label the component
+     * @since 17605
+     */
+    void addIcon(JLabel label) {
+        label.setIcon(getIcon());
+        label.setHorizontalAlignment(SwingConstants.LEADING);
+    }
+
+    /**
+     * Returns the entry icon, if any.
+     * @return the entry icon, or {@code null}
+     * @since 17605
+     */
+    ImageIcon getIcon() {
+        return icon == null ? null : TaggingPresetUtils.loadImageIcon(icon, TaggingPresetReader.getZipIcons(), iconSize);
+    }
+
+    @Override
+    public String toString() {
+        return getClass().getSimpleName() + " [" + fieldsToString() + ']';
+    }
+}
Index: src/org/openstreetmap/josm/gui/tagging/presets/Usage.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/Usage.java	(nonexistent)
+++ src/org/openstreetmap/josm/gui/tagging/presets/Usage.java	(working copy)
@@ -0,0 +1,123 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.tagging.presets;
+
+import java.util.Collection;
+import java.util.NoSuchElementException;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.OsmUtils;
+
+/**
+ * Usage information on a key
+ *
+ * TODO merge with {@link org.openstreetmap.josm.data.osm.TagCollection}
+ */
+public class Usage {
+    /** Usage count for all values used for this key */
+    public final SortedMap<String, Integer> map = new TreeMap<>();
+    private boolean hadKeys;
+    private boolean hadEmpty;
+    private int selectedCount;
+
+    /**
+     * Computes the tag usage for the given key from the given primitives
+     *
+     * @param sel the primitives
+     * @param key the key
+     * @return the tag usage
+     */
+    public static Usage determineTextUsage(Collection<OsmPrimitive> sel, String key) {
+        Usage returnValue = new Usage();
+        returnValue.selectedCount = sel.size();
+        for (OsmPrimitive s : sel) {
+            String v = s.get(key);
+            if (v != null) {
+                returnValue.map.merge(v, 1, Integer::sum);
+            } else {
+                returnValue.hadEmpty = true;
+            }
+            if (s.hasKeys()) {
+                returnValue.hadKeys = true;
+            }
+        }
+        return returnValue;
+    }
+
+    /**
+     * Computes the tag usage for the given key if the key has a boolean value
+     *
+     * @param sel the primitives
+     * @param key the key
+     * @return the tag usage
+     */
+    public static Usage determineBooleanUsage(Collection<OsmPrimitive> sel, String key) {
+        Usage returnValue = new Usage();
+        returnValue.selectedCount = sel.size();
+        for (OsmPrimitive s : sel) {
+            String booleanValue = OsmUtils.getNamedOsmBoolean(s.get(key));
+            if (booleanValue != null) {
+                returnValue.map.merge(booleanValue, 1, Integer::sum);
+            }
+        }
+        return returnValue;
+    }
+
+    /**
+     * Check if there is exactly one value for this key.
+     * @return <code>true</code> if there was exactly one value.
+     */
+    public boolean hasUniqueValue() {
+        return map.size() == 1 && !hadEmpty;
+    }
+
+    /**
+     * Check if this key was not used in any primitive
+     * @return <code>true</code> if it was unused.
+     */
+    public boolean unused() {
+        return map.isEmpty();
+    }
+
+    /**
+     * Get the first value available.
+     * @return The first value
+     * @throws NoSuchElementException if there is no such value.
+     */
+    public String getFirst() {
+        return map.firstKey();
+    }
+
+    /**
+     * Check if we encountered any primitive that had any keys
+     * @return <code>true</code> if any of the primitives had any tags.
+     */
+    public boolean hadKeys() {
+        return hadKeys;
+    }
+
+    /**
+     * Returns the number of primitives selected.
+     * @return the number of primitives selected.
+     */
+    public int getSelectedCount() {
+        return selectedCount;
+    }
+
+    /**
+     * Splits multiple values and adds their usage counts as single value.
+     * <p>
+     * A value of {@code regional;pizza} will increment the count of {@code regional} and of
+     * {@code pizza}.
+     */
+    public void splitValues() {
+        SortedMap<String, Integer> copy = new TreeMap<>(map);
+        copy.forEach((value, count) -> {
+            map.remove(value);
+            for (String v : value.split(";", -1)) {
+                map.merge(v, count, Integer::sum);
+            }
+        });
+    }
+}
Index: src/org/openstreetmap/josm/gui/tagging/presets/items/Check.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/items/Check.java	(revision 18366)
+++ src/org/openstreetmap/josm/gui/tagging/presets/items/Check.java	(nonexistent)
@@ -1,134 +0,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.gui.tagging.presets.items;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.List;
-
-import javax.swing.JPanel;
-
-import org.openstreetmap.josm.data.osm.OsmPrimitive;
-import org.openstreetmap.josm.data.osm.OsmUtils;
-import org.openstreetmap.josm.data.osm.Tag;
-import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItemGuiSupport;
-import org.openstreetmap.josm.gui.widgets.IconTextCheckBox;
-import org.openstreetmap.josm.gui.widgets.QuadStateCheckBox;
-import org.openstreetmap.josm.tools.GBC;
-
-/**
- * Checkbox type.
- */
-public class Check extends KeyedItem {
-
-    /** the value to set when checked (default is "yes") */
-    public String value_on = OsmUtils.TRUE_VALUE; // NOSONAR
-    /** the value to set when unchecked (default is "no") */
-    public String value_off = OsmUtils.FALSE_VALUE; // NOSONAR
-    /** whether the off value is disabled in the dialog, i.e., only unset or yes are provided */
-    public boolean disable_off; // NOSONAR
-    /** "on" or "off" or unset (default is unset) */
-    public String default_; // only used for tagless objects // NOSONAR
-
-    private QuadStateCheckBox check;
-    private QuadStateCheckBox.State initialState;
-    private Boolean def;
-
-    @Override
-    public boolean addToPanel(JPanel p, TaggingPresetItemGuiSupport support) {
-
-        // find out if our key is already used in the selection.
-        final Usage usage = determineBooleanUsage(support.getSelected(), key);
-        final String oneValue = usage.map.isEmpty() ? null : usage.map.lastKey();
-        def = "on".equals(default_) ? Boolean.TRUE : "off".equals(default_) ? Boolean.FALSE : null;
-
-        initializeLocaleText(null);
-
-        if (usage.map.size() < 2 && (oneValue == null || value_on.equals(oneValue) || value_off.equals(oneValue))) {
-            if (def != null && !PROP_FILL_DEFAULT.get()) {
-                // default is set and filling default values feature is disabled - check if all primitives are untagged
-                for (OsmPrimitive s : support.getSelected()) {
-                    if (s.hasKeys()) {
-                        def = null;
-                    }
-                }
-            }
-
-            // all selected objects share the same value which is either true or false or unset,
-            // we can display a standard check box.
-            initialState = value_on.equals(oneValue) || Boolean.TRUE.equals(def)
-                    ? QuadStateCheckBox.State.SELECTED
-                    : value_off.equals(oneValue) || Boolean.FALSE.equals(def)
-                    ? QuadStateCheckBox.State.NOT_SELECTED
-                    : QuadStateCheckBox.State.UNSET;
-
-        } else {
-            def = null;
-            // the objects have different values, or one or more objects have something
-            // else than true/false. we display a quad-state check box
-            // in "partial" state.
-            initialState = QuadStateCheckBox.State.PARTIAL;
-        }
-
-        final List<QuadStateCheckBox.State> allowedStates = new ArrayList<>(4);
-        if (QuadStateCheckBox.State.PARTIAL == initialState)
-            allowedStates.add(QuadStateCheckBox.State.PARTIAL);
-        allowedStates.add(QuadStateCheckBox.State.SELECTED);
-        if (!disable_off || value_off.equals(oneValue))
-            allowedStates.add(QuadStateCheckBox.State.NOT_SELECTED);
-        allowedStates.add(QuadStateCheckBox.State.UNSET);
-        check = new QuadStateCheckBox(icon == null ? locale_text : null, initialState,
-                allowedStates.toArray(new QuadStateCheckBox.State[0]));
-        check.setPropertyText(key);
-        check.setState(check.getState()); // to update the tooltip text
-        check.setComponentPopupMenu(getPopupMenu());
-
-        if (icon != null) {
-            JPanel checkPanel = IconTextCheckBox.wrap(check, locale_text, getIcon());
-            checkPanel.applyComponentOrientation(support.getDefaultComponentOrientation());
-            p.add(checkPanel, GBC.eol()); // Do not fill, see #15104
-        } else {
-            check.applyComponentOrientation(support.getDefaultComponentOrientation());
-            p.add(check, GBC.eol()); // Do not fill, see #15104
-        }
-        check.addChangeListener(l -> support.fireItemValueModified(this, key, getValue()));
-        return true;
-    }
-
-    @Override
-    public void addCommands(List<Tag> changedTags) {
-        // if the user hasn't changed anything, don't create a command.
-        if (def == null && check.getState() == initialState) return;
-
-        // otherwise change things according to the selected value.
-        changedTags.add(new Tag(key, getValue()));
-    }
-
-    protected String getValue() {
-        return check.getState() == QuadStateCheckBox.State.SELECTED ? value_on :
-            check.getState() == QuadStateCheckBox.State.NOT_SELECTED ? value_off :
-                null;
-    }
-
-    @Override
-    public MatchType getDefaultMatch() {
-        return MatchType.NONE;
-    }
-
-    @Override
-    public Collection<String> getValues() {
-        return disable_off ? Arrays.asList(value_on) : Arrays.asList(value_on, value_off);
-    }
-
-    @Override
-    public String toString() {
-        return "Check [key=" + key + ", text=" + text + ", "
-                + (locale_text != null ? "locale_text=" + locale_text + ", " : "")
-                + (value_on != null ? "value_on=" + value_on + ", " : "")
-                + (value_off != null ? "value_off=" + value_off + ", " : "")
-                + "default_=" + default_ + ", "
-                + (check != null ? "check=" + check + ", " : "")
-                + (initialState != null ? "initialState=" + initialState
-                        + ", " : "") + "def=" + def + ']';
-    }
-}
Index: src/org/openstreetmap/josm/gui/tagging/presets/items/CheckGroup.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/items/CheckGroup.java	(revision 18366)
+++ src/org/openstreetmap/josm/gui/tagging/presets/items/CheckGroup.java	(nonexistent)
@@ -1,74 +0,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.gui.tagging.presets.items;
-
-import java.awt.GridLayout;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Map;
-
-import javax.swing.JLabel;
-import javax.swing.JPanel;
-
-import org.openstreetmap.josm.data.osm.Tag;
-import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItem;
-import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItemGuiSupport;
-import org.openstreetmap.josm.tools.GBC;
-
-/**
- * A group of {@link Check}s.
- * @since 6114
- */
-public class CheckGroup extends TaggingPresetItem {
-
-    /**
-     * Number of columns (positive integer)
-     */
-    public short columns = 1; // NOSONAR
-
-    /**
-     * List of checkboxes
-     */
-    public final List<Check> checks = new LinkedList<>();
-
-    @Override
-    public boolean addToPanel(JPanel p, TaggingPresetItemGuiSupport support) {
-        int rows = (int) Math.ceil(checks.size() / ((double) columns));
-        JPanel panel = new JPanel(new GridLayout(rows, columns));
-
-        int i = 0;
-        for (Check check : checks) {
-            check.addToPanel(panel, support);
-            i++;
-        }
-        for (; i < rows * columns; i++) {
-            // fill remaining cells, see #20792
-            panel.add(new JLabel());
-        }
-
-        panel.applyComponentOrientation(support.getDefaultComponentOrientation());
-        p.add(panel, GBC.eol());
-        return false;
-    }
-
-    @Override
-    public void addCommands(List<Tag> changedTags) {
-        for (Check check : checks) {
-            check.addCommands(changedTags);
-        }
-    }
-
-    @Override
-    public Boolean matches(Map<String, String> tags) {
-        for (Check check : checks) {
-            if (Boolean.TRUE.equals(check.matches(tags))) {
-                return Boolean.TRUE;
-            }
-        }
-        return null;
-    }
-
-    @Override
-    public String toString() {
-        return "CheckGroup [columns=" + columns + ']';
-    }
-}
Index: src/org/openstreetmap/josm/gui/tagging/presets/items/Combo.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/items/Combo.java	(revision 18366)
+++ src/org/openstreetmap/josm/gui/tagging/presets/items/Combo.java	(nonexistent)
@@ -1,229 +0,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.gui.tagging.presets.items;
-
-import static org.openstreetmap.josm.tools.I18n.tr;
-
-import java.awt.Color;
-import java.awt.Cursor;
-import java.awt.Insets;
-import java.awt.event.ActionEvent;
-import java.awt.event.ActionListener;
-import java.awt.event.ComponentAdapter;
-import java.awt.event.ComponentEvent;
-import java.util.Arrays;
-import java.util.Comparator;
-
-import javax.swing.AbstractAction;
-import javax.swing.JButton;
-import javax.swing.JColorChooser;
-import javax.swing.JComponent;
-import javax.swing.JLabel;
-import javax.swing.JPanel;
-
-import org.openstreetmap.josm.data.tagging.ac.AutoCompletionItem;
-import org.openstreetmap.josm.data.tagging.ac.AutoCompletionPriority;
-import org.openstreetmap.josm.gui.MainApplication;
-import org.openstreetmap.josm.gui.mappaint.mapcss.CSSColors;
-import org.openstreetmap.josm.gui.tagging.ac.AutoCompComboBoxEditor;
-import org.openstreetmap.josm.gui.tagging.ac.AutoCompComboBoxModel;
-import org.openstreetmap.josm.gui.tagging.ac.AutoCompTextField;
-import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItem;
-import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItemGuiSupport;
-import org.openstreetmap.josm.gui.widgets.JosmComboBox;
-import org.openstreetmap.josm.gui.widgets.OrientationAction;
-import org.openstreetmap.josm.tools.ColorHelper;
-import org.openstreetmap.josm.tools.GBC;
-
-/**
- * Combobox type.
- */
-public class Combo extends ComboMultiSelect {
-
-    /**
-     * Whether the combo box is editable, which means that the user can add other values as text.
-     * Default is {@code true}. If {@code false} it is readonly, which means that the user can only select an item in the list.
-     */
-    public boolean editable = true; // NOSONAR
-    /** The length of the combo box (number of characters allowed). */
-    public int length; // NOSONAR
-
-    protected JosmComboBox<PresetListEntry> combobox;
-    protected AutoCompComboBoxModel<PresetListEntry> dropDownModel;
-    protected AutoCompComboBoxModel<AutoCompletionItem> autoCompModel;
-
-    class ComponentListener extends ComponentAdapter {
-        @Override
-        public void componentResized(ComponentEvent e) {
-            // Make multi-line JLabels the correct size
-            // Only needed if there is any short_description
-            JComponent component = (JComponent) e.getSource();
-            int width = component.getWidth();
-            if (width == 0)
-                width = 200;
-            Insets insets = component.getInsets();
-            width -= insets.left + insets.right + 10;
-            ComboMultiSelectListCellRenderer renderer = (ComboMultiSelectListCellRenderer) combobox.getRenderer();
-            renderer.setWidth(width);
-            combobox.setRenderer(null); // needed to make prop change fire
-            combobox.setRenderer(renderer);
-        }
-    }
-
-    /**
-     * Constructs a new {@code Combo}.
-     */
-    public Combo() {
-        delimiter = ',';
-    }
-
-    private void addEntry(PresetListEntry entry) {
-        if (!seenValues.containsKey(entry.value)) {
-            dropDownModel.addElement(entry);
-            seenValues.put(entry.value, entry);
-        }
-    }
-
-    @Override
-    protected boolean addToPanel(JPanel p, TaggingPresetItemGuiSupport support) {
-        initializeLocaleText(null);
-        usage = determineTextUsage(support.getSelected(), key);
-        seenValues.clear();
-        // get the standard values from the preset definition
-        initListEntries();
-
-        // init the model
-        dropDownModel = new AutoCompComboBoxModel<>(Comparator.<PresetListEntry>naturalOrder());
-
-        if (!usage.hasUniqueValue() && !usage.unused()) {
-            addEntry(PresetListEntry.ENTRY_DIFFERENT);
-        }
-        presetListEntries.forEach(this::addEntry);
-        if (default_ != null) {
-            addEntry(new PresetListEntry(default_, this));
-        }
-        addEntry(PresetListEntry.ENTRY_EMPTY);
-
-        usage.map.forEach((value, count) -> {
-            addEntry(new PresetListEntry(value, this));
-        });
-
-        combobox = new JosmComboBox<>(dropDownModel);
-        AutoCompComboBoxEditor<AutoCompletionItem> editor = new AutoCompComboBoxEditor<>();
-        combobox.setEditor(editor);
-
-        // The default behaviour of JComboBox is to size the editor according to the tallest item in
-        // the dropdown list.  We don't want that to happen because we want to show taller items in
-        // the list than in the editor.  We can't use
-        // {@code combobox.setPrototypeDisplayValue(PresetListEntry.ENTRY_EMPTY);} because that would
-        // set a fixed cell height in JList.
-        combobox.setPreferredHeight(combobox.getPreferredSize().height);
-
-        // a custom cell renderer capable of displaying a short description text along with the
-        // value
-        combobox.setRenderer(new ComboMultiSelectListCellRenderer(combobox, combobox.getRenderer(), 200, key));
-        combobox.setEditable(editable);
-
-        autoCompModel = new AutoCompComboBoxModel<>(Comparator.<AutoCompletionItem>naturalOrder());
-        getAllForKeys(Arrays.asList(key)).forEach(autoCompModel::addElement);
-        getDisplayValues().forEach(s -> autoCompModel.addElement(new AutoCompletionItem(s, AutoCompletionPriority.IS_IN_STANDARD)));
-
-        AutoCompTextField<AutoCompletionItem> tf = editor.getEditorComponent();
-        tf.setModel(autoCompModel);
-
-        if (TaggingPresetItem.DISPLAY_KEYS_AS_HINT.get()) {
-            combobox.setHint(key);
-        }
-        if (length > 0) {
-            tf.setMaxTextLength(length);
-        }
-
-        JLabel label = addLabel(p);
-
-        if (key != null && ("colour".equals(key) || key.startsWith("colour:") || key.endsWith(":colour"))) {
-            p.add(combobox, GBC.std().fill(GBC.HORIZONTAL)); // NOSONAR
-            JButton button = new JButton(new ChooseColorAction());
-            button.setOpaque(true);
-            button.setBorderPainted(false);
-            button.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
-            p.add(button, GBC.eol().fill(GBC.VERTICAL)); // NOSONAR
-            ActionListener updateColor = ignore -> button.setBackground(getColor());
-            updateColor.actionPerformed(null);
-            combobox.addActionListener(updateColor);
-        } else {
-            p.add(combobox, GBC.eol().fill(GBC.HORIZONTAL)); // NOSONAR
-        }
-
-        String initialValue = getInitialValue(usage, support);
-        PresetListEntry selItem = find(initialValue);
-        if (selItem != null) {
-            combobox.setSelectedItem(selItem);
-        } else {
-            combobox.setText(initialValue);
-        }
-
-        combobox.addActionListener(l -> support.fireItemValueModified(this, key, getSelectedItem().value));
-        combobox.addComponentListener(new ComponentListener());
-
-        label.setLabelFor(combobox);
-        combobox.setToolTipText(getKeyTooltipText());
-        combobox.applyComponentOrientation(OrientationAction.getValueOrientation(key));
-
-        return true;
-    }
-
-    /**
-     * Finds the PresetListEntry that matches value.
-     * <p>
-     * Looks in the model for an element whose {@code value} matches {@code value}.
-     *
-     * @param value The value to match.
-     * @return The entry or null
-     */
-    private PresetListEntry find(String value) {
-        return dropDownModel.asCollection().stream().filter(o -> o.value.equals(value)).findAny().orElse(null);
-    }
-
-    class ChooseColorAction extends AbstractAction {
-        ChooseColorAction() {
-            putValue(SHORT_DESCRIPTION, tr("Choose a color"));
-        }
-
-        @Override
-        public void actionPerformed(ActionEvent e) {
-            Color color = getColor();
-            color = JColorChooser.showDialog(MainApplication.getMainPanel(), tr("Choose a color"), color);
-            setColor(color);
-        }
-    }
-
-    protected void setColor(Color color) {
-        if (color != null) {
-            combobox.setSelectedItem(ColorHelper.color2html(color));
-        }
-    }
-
-    protected Color getColor() {
-        String colorString = getSelectedItem().value;
-        return colorString.startsWith("#")
-                ? ColorHelper.html2color(colorString)
-                : CSSColors.get(colorString);
-    }
-
-    @Override
-    protected PresetListEntry getSelectedItem() {
-        Object sel = combobox.getSelectedItem();
-        if (sel instanceof PresetListEntry)
-            // selected from the dropdown
-            return (PresetListEntry) sel;
-        if (sel instanceof String) {
-            // free edit.  If the free edit corresponds to a known entry, use that entry.  This is
-            // to avoid that we write a display_value to the tag's value, eg. if the user did an
-            // undo.
-            PresetListEntry selItem = dropDownModel.find((String) sel);
-            if (selItem != null)
-                return selItem;
-            return new PresetListEntry((String) sel, this);
-        }
-        return PresetListEntry.ENTRY_EMPTY;
-    }
-}
Index: src/org/openstreetmap/josm/gui/tagging/presets/items/ComboMultiSelect.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/items/ComboMultiSelect.java	(revision 18366)
+++ src/org/openstreetmap/josm/gui/tagging/presets/items/ComboMultiSelect.java	(nonexistent)
@@ -1,433 +0,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.gui.tagging.presets.items;
-
-import static org.openstreetmap.josm.tools.I18n.tr;
-
-import java.awt.Component;
-import java.awt.Font;
-import java.lang.reflect.Method;
-import java.lang.reflect.Modifier;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.TreeMap;
-import java.util.stream.Collectors;
-
-import javax.swing.JLabel;
-import javax.swing.JList;
-import javax.swing.JPanel;
-import javax.swing.ListCellRenderer;
-
-import org.openstreetmap.josm.data.osm.Tag;
-import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItemGuiSupport;
-import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetSelector;
-import org.openstreetmap.josm.gui.tagging.presets.TaggingPresets;
-import org.openstreetmap.josm.gui.widgets.JosmListCellRenderer;
-import org.openstreetmap.josm.gui.widgets.OrientationAction;
-import org.openstreetmap.josm.tools.AlphanumComparator;
-import org.openstreetmap.josm.tools.GBC;
-import org.openstreetmap.josm.tools.Logging;
-
-/**
- * Abstract superclass for combo box and multi-select list types.
- */
-public abstract class ComboMultiSelect extends KeyedItem {
-
-    /**
-     * A list of entries.
-     * The list has to be separated by commas (for the {@link Combo} box) or by the specified delimiter (for the {@link MultiSelect}).
-     * If a value contains the delimiter, the delimiter may be escaped with a backslash.
-     * If a value contains a backslash, it must also be escaped with a backslash. */
-    public String values; // NOSONAR
-    /**
-     * To use instead of {@link #values} if the list of values has to be obtained with a Java method of this form:
-     * <p>{@code public static String[] getValues();}<p>
-     * The value must be: {@code full.package.name.ClassName#methodName}.
-     */
-    public String values_from; // NOSONAR
-    /** The context used for translating {@link #values} */
-    public String values_context; // NOSONAR
-    /** Disabled internationalisation for value to avoid mistakes, see #11696 */
-    public boolean values_no_i18n; // NOSONAR
-    /** Whether to sort the values, defaults to true. */
-    public boolean values_sort = true; // NOSONAR
-    /**
-     * A list of entries that is displayed to the user.
-     * Must be the same number and order of entries as {@link #values} and editable must be false or not specified.
-     * For the delimiter character and escaping, see the remarks at {@link #values}.
-     */
-    public String display_values; // NOSONAR
-    /** The localized version of {@link #display_values}. */
-    public String locale_display_values; // NOSONAR
-    /**
-     * A delimiter-separated list of texts to be displayed below each {@code display_value}.
-     * (Only if it is not possible to describe the entry in 2-3 words.)
-     * Instead of comma separated list instead using {@link #values}, {@link #display_values} and {@link #short_descriptions},
-     * the following form is also supported:<p>
-     * {@code <list_entry value="" display_value="" short_description="" icon="" icon_size="" />}
-     */
-    public String short_descriptions; // NOSONAR
-    /** The localized version of {@link #short_descriptions}. */
-    public String locale_short_descriptions; // NOSONAR
-    /** The default value for the item. If not specified, the current value of the key is chosen as default (if applicable).*/
-    public String default_; // NOSONAR
-    /**
-     * The character that separates values.
-     * In case of {@link Combo} the default is comma.
-     * In case of {@link MultiSelect} the default is semicolon and this will also be used to separate selected values in the tag.
-     */
-    public char delimiter = ';'; // NOSONAR
-    /** whether the last value is used as default.
-     * Using "force" (2) enforces this behaviour also for already tagged objects. Default is "false" (0).*/
-    public byte use_last_as_default; // NOSONAR
-    /** whether to use values for search via {@link TaggingPresetSelector} */
-    public boolean values_searchable; // NOSONAR
-
-    /**
-     * The standard entries in the combobox dropdown or multiselect list. These entries are defined
-     * in {@code defaultpresets.xml} (or in other custom preset files).
-     */
-    protected final List<PresetListEntry> presetListEntries = new ArrayList<>();
-    /** Helps avoid duplicate list entries */
-    protected final Map<String, PresetListEntry> seenValues = new TreeMap<>();
-    protected Usage usage;
-    /** Used to see if the user edited the value. */
-    protected String originalValue;
-
-    /**
-     * A list cell renderer that paints a short text in the current value pane and and a longer text
-     * in the dropdown list.
-     */
-    static class ComboMultiSelectListCellRenderer extends JosmListCellRenderer<PresetListEntry> {
-        int width;
-        private String key;
-
-        ComboMultiSelectListCellRenderer(Component component, ListCellRenderer<? super PresetListEntry> renderer, int width, String key) {
-            super(component, renderer);
-            this.key = key;
-            setWidth(width);
-        }
-
-        /**
-         * Sets the width to format the dropdown list to
-         *
-         * Note: This is not the width of the list, but the width to which we format any multi-line
-         * label in the list.  We cannot use the list's width because at the time the combobox
-         * measures its items, it is not guaranteed that the list is already sized, the combobox may
-         * not even be layed out yet.  Set this to {@code combobox.getWidth()}
-         *
-         * @param width the width
-         */
-        public void setWidth(int width) {
-            if (width <= 0)
-                width = 200;
-            this.width = width - 20;
-        }
-
-        @Override
-        public JLabel getListCellRendererComponent(
-            JList<? extends PresetListEntry> list, PresetListEntry value, int index, boolean isSelected, boolean cellHasFocus) {
-
-            JLabel l = (JLabel) renderer.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
-            l.setComponentOrientation(component.getComponentOrientation());
-            if (index != -1) {
-                // index -1 is set when measuring the size of the cell and when painting the
-                // editor-ersatz of a readonly combobox. fixes #6157
-                l.setText(value.getListDisplay(width));
-            }
-            if (value.getCount() > 0) {
-                l.setFont(l.getFont().deriveFont(Font.ITALIC + Font.BOLD));
-            }
-            l.setIcon(value.getIcon());
-            l.setToolTipText(value.getToolTipText(key));
-            return l;
-        }
-    }
-
-    /**
-     * allow escaped comma in comma separated list:
-     * "A\, B\, C,one\, two" --&gt; ["A, B, C", "one, two"]
-     * @param delimiter the delimiter, e.g. a comma. separates the entries and
-     *      must be escaped within one entry
-     * @param s the string
-     * @return splitted items
-     */
-    public static List<String> splitEscaped(char delimiter, String s) {
-        if (s == null)
-            return null; // NOSONAR
-
-        List<String> result = new ArrayList<>();
-        boolean backslash = false;
-        StringBuilder item = new StringBuilder();
-        for (int i = 0; i < s.length(); i++) {
-            char ch = s.charAt(i);
-            if (backslash) {
-                item.append(ch);
-                backslash = false;
-            } else if (ch == '\\') {
-                backslash = true;
-            } else if (ch == delimiter) {
-                result.add(item.toString());
-                item.setLength(0);
-            } else {
-                item.append(ch);
-            }
-        }
-        if (item.length() > 0) {
-            result.add(item.toString());
-        }
-        return result;
-    }
-
-    /**
-     * Returns the value selected in the combobox or a synthetic value if a multiselect.
-     *
-     * @return the value
-     */
-    protected abstract PresetListEntry getSelectedItem();
-
-    @Override
-    public Collection<String> getValues() {
-        initListEntries();
-        return presetListEntries.stream().map(x -> x.value).collect(Collectors.toSet());
-    }
-
-    /**
-     * Returns the values to display.
-     * @return the values to display
-     */
-    public Collection<String> getDisplayValues() {
-        initListEntries();
-        return presetListEntries.stream().map(PresetListEntry::getDisplayValue).collect(Collectors.toList());
-    }
-
-    /**
-     * Adds the label to the panel
-     *
-     * @param p the panel
-     * @return the label
-     */
-    protected JLabel addLabel(JPanel p) {
-        final JLabel label = new JLabel(tr("{0}:", locale_text));
-        addIcon(label);
-        label.setToolTipText(getKeyTooltipText());
-        label.setComponentPopupMenu(getPopupMenu());
-        label.applyComponentOrientation(OrientationAction.getDefaultComponentOrientation());
-        p.add(label, GBC.std().insets(0, 0, 10, 0));
-        return label;
-    }
-
-    protected void initListEntries() {
-        if (presetListEntries.isEmpty()) {
-            initListEntriesFromAttributes();
-        }
-    }
-
-    private List<String> getValuesFromCode(String valuesFrom) {
-        // get the values from a Java function
-        String[] classMethod = valuesFrom.split("#", -1);
-        if (classMethod.length == 2) {
-            try {
-                Method method = Class.forName(classMethod[0]).getMethod(classMethod[1]);
-                // Check method is public static String[] methodName()
-                int mod = method.getModifiers();
-                if (Modifier.isPublic(mod) && Modifier.isStatic(mod)
-                        && method.getReturnType().equals(String[].class) && method.getParameterTypes().length == 0) {
-                    return Arrays.asList((String[]) method.invoke(null));
-                } else {
-                    Logging.error(tr("Broken tagging preset \"{0}-{1}\" - Java method given in ''values_from'' is not \"{2}\"", key, text,
-                            "public static String[] methodName()"));
-                }
-            } catch (ReflectiveOperationException e) {
-                Logging.error(tr("Broken tagging preset \"{0}-{1}\" - Java method given in ''values_from'' threw {2} ({3})", key, text,
-                        e.getClass().getName(), e.getMessage()));
-                Logging.debug(e);
-            }
-        }
-        return null; // NOSONAR
-    }
-
-    /**
-     * Checks if list {@code a} is either null or the same length as list {@code b}.
-     *
-     * @param a The list to check
-     * @param b The other list
-     * @param name The name of the list for error reporting
-     * @return {@code a} if both lists have the same length or {@code null}
-     */
-    private List<String> checkListsSameLength(List<String> a, List<String> b, String name) {
-        if (a != null && a.size() != b.size()) {
-            Logging.error(tr("Broken tagging preset \"{0}-{1}\" - number of items in ''{2}'' must be the same as in ''values''",
-                            key, text, name));
-            Logging.error(tr("Detailed information: {0} <> {1}", a, b));
-            return null; // NOSONAR
-        }
-        return a;
-    }
-
-    protected void initListEntriesFromAttributes() {
-        List<String> valueList = null;
-        List<String> displayList = null;
-        List<String> localeDisplayList = null;
-
-        if (values_from != null) {
-            valueList = getValuesFromCode(values_from);
-        }
-
-        if (valueList == null) {
-            // get from {@code values} attribute
-            valueList = splitEscaped(delimiter, values);
-        }
-        if (valueList == null) {
-            return;
-        }
-
-        if (!values_no_i18n) {
-            localeDisplayList = splitEscaped(delimiter, locale_display_values);
-            displayList = splitEscaped(delimiter, display_values);
-        }
-        List<String> localeShortDescriptionsList = splitEscaped(delimiter, locale_short_descriptions);
-        List<String> shortDescriptionsList = splitEscaped(delimiter, short_descriptions);
-
-        displayList = checkListsSameLength(displayList, valueList, "display_values");
-        localeDisplayList = checkListsSameLength(localeDisplayList, valueList, "locale_display_values");
-        shortDescriptionsList = checkListsSameLength(shortDescriptionsList, valueList, "short_descriptions");
-        localeShortDescriptionsList = checkListsSameLength(localeShortDescriptionsList, valueList, "locale_short_descriptions");
-
-        for (int i = 0; i < valueList.size(); i++) {
-            final PresetListEntry e = new PresetListEntry(valueList.get(i), this);
-            if (displayList != null)
-                e.display_value = displayList.get(i);
-            if (localeDisplayList != null)
-                e.locale_display_value = localeDisplayList.get(i);
-            if (shortDescriptionsList != null)
-                e.short_description = shortDescriptionsList.get(i);
-            if (localeShortDescriptionsList != null)
-                e.locale_short_description = localeShortDescriptionsList.get(i);
-            addListEntry(e);
-        }
-
-        if (values_sort && TaggingPresets.SORT_MENU.get()) {
-            Collections.sort(presetListEntries, (a, b) -> AlphanumComparator.getInstance().compare(a.getDisplayValue(), b.getDisplayValue()));
-        }
-    }
-
-    /**
-     * Returns the initial value to use for this preset.
-     * <p>
-     * The initial value is the value shown in the control when the preset dialog opens. For a
-     * discussion of all the options see the enclosed tickets.
-     *
-     * @param usage The key Usage
-     * @param support The support
-     * @return The initial value to use.
-     *
-     * @see "https://josm.openstreetmap.de/ticket/5564"
-     * @see "https://josm.openstreetmap.de/ticket/12733"
-     * @see "https://josm.openstreetmap.de/ticket/17324"
-     */
-    protected String getInitialValue(Usage usage, TaggingPresetItemGuiSupport support) {
-        String initialValue = null;
-        originalValue = "";
-
-        if (usage.hasUniqueValue()) {
-            // all selected primitives have the same not empty value for this key
-            initialValue = usage.getFirst();
-            originalValue = initialValue;
-        } else if (!usage.unused()) {
-            // at least one primitive has a value for this key (but not all have the same one)
-            initialValue = DIFFERENT;
-            originalValue = initialValue;
-        } else if (!usage.hadKeys() || isForceUseLastAsDefault() || PROP_FILL_DEFAULT.get()) {
-            // at this point no primitive had any value for this key
-            if (!support.isPresetInitiallyMatches() && isUseLastAsDefault() && LAST_VALUES.containsKey(key)) {
-                initialValue = LAST_VALUES.get(key);
-            } else {
-                initialValue = default_;
-            }
-        }
-        return initialValue != null ? initialValue : "";
-    }
-
-    @Override
-    public void addCommands(List<Tag> changedTags) {
-        String value = getSelectedItem().value;
-
-        // no change if same as before
-        if (value.equals(originalValue))
-            return;
-        changedTags.add(new Tag(key, value));
-
-        if (isUseLastAsDefault()) {
-            LAST_VALUES.put(key, value);
-        }
-    }
-
-    /**
-     * Sets whether the last value is used as default.
-     * @param v Using "force" (2) enforces this behaviour also for already tagged objects. Default is "false" (0).
-     */
-    public void setUse_last_as_default(String v) { // NOPMD
-        if ("force".equals(v)) {
-            use_last_as_default = 2;
-        } else if ("true".equals(v)) {
-            use_last_as_default = 1;
-        } else {
-            use_last_as_default = 0;
-        }
-    }
-
-    /**
-     * Returns true if the last entered value should be used as default.
-     * <p>
-     * Note: never used in {@code defaultpresets.xml}.
-     *
-     * @return true if the last entered value should be used as default.
-     */
-    protected boolean isUseLastAsDefault() {
-        return use_last_as_default > 0;
-    }
-
-    /**
-     * Returns true if the last entered value should be used as default also on primitives that
-     * already have tags.
-     * <p>
-     * Note: used for {@code addr:*} tags in {@code defaultpresets.xml}.
-     *
-     * @return true if see above
-     */
-    protected boolean isForceUseLastAsDefault() {
-        return use_last_as_default == 2;
-    }
-
-    /**
-     * Adds a preset list entry.
-     * @param e list entry to add
-     */
-    public void addListEntry(PresetListEntry e) {
-        presetListEntries.add(e);
-        // we need to fix the entries because the XML Parser
-        // {@link org.openstreetmap.josm.tools.XmlObjectParser.Parser#startElement} has used the
-        // default standard constructor for {@link PresetListEntry} if the list entry was defined
-        // using XML {@code <list_entry>}.
-        e.cms = this;
-    }
-
-    /**
-     * Adds a collection of preset list entries.
-     * @param e list entries to add
-     */
-    public void addListEntries(Collection<PresetListEntry> e) {
-        for (PresetListEntry i : e) {
-            addListEntry(i);
-        }
-    }
-
-    @Override
-    public MatchType getDefaultMatch() {
-        return MatchType.NONE;
-    }
-}
Index: src/org/openstreetmap/josm/gui/tagging/presets/items/ItemSeparator.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/items/ItemSeparator.java	(revision 18366)
+++ src/org/openstreetmap/josm/gui/tagging/presets/items/ItemSeparator.java	(nonexistent)
@@ -1,35 +0,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.gui.tagging.presets.items;
-
-import java.util.List;
-
-import javax.swing.JPanel;
-import javax.swing.JSeparator;
-
-import org.openstreetmap.josm.data.osm.Tag;
-import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItem;
-import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItemGuiSupport;
-import org.openstreetmap.josm.tools.GBC;
-
-/**
- * Class used to represent a {@link JSeparator} inside tagging preset window.
- * @since 6198
- */
-public class ItemSeparator extends TaggingPresetItem {
-
-    @Override
-    public boolean addToPanel(JPanel p, TaggingPresetItemGuiSupport support) {
-        p.add(new JSeparator(), GBC.eol().fill(GBC.HORIZONTAL).insets(0, 5, 0, 5));
-        return false;
-    }
-
-    @Override
-    public void addCommands(List<Tag> changedTags) {
-        // Do nothing
-    }
-
-    @Override
-    public String toString() {
-        return "ItemSeparator";
-    }
-}
Index: src/org/openstreetmap/josm/gui/tagging/presets/items/Key.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/items/Key.java	(revision 18366)
+++ src/org/openstreetmap/josm/gui/tagging/presets/items/Key.java	(nonexistent)
@@ -1,55 +0,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.gui.tagging.presets.items;
-
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-
-import javax.swing.JPanel;
-
-import org.openstreetmap.josm.data.osm.Tag;
-import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItemGuiSupport;
-
-/**
- * Invisible type allowing to hardcode an OSM key/value from the preset definition.
- */
-public class Key extends KeyedItem {
-
-    /** The hardcoded value for key */
-    public String value; // NOSONAR
-
-    @Override
-    public boolean addToPanel(JPanel p, TaggingPresetItemGuiSupport support) {
-        return false;
-    }
-
-    @Override
-    public void addCommands(List<Tag> changedTags) {
-        changedTags.add(asTag());
-    }
-
-    /**
-     * Returns the {@link Tag} set by this item
-     * @return the tag
-     */
-    public Tag asTag() {
-        return new Tag(key, value);
-    }
-
-    @Override
-    public MatchType getDefaultMatch() {
-        return MatchType.KEY_VALUE_REQUIRED;
-    }
-
-    @Override
-    public Collection<String> getValues() {
-        return Collections.singleton(value);
-    }
-
-    @Override
-    public String toString() {
-        return "Key [key=" + key + ", value=" + value + ", text=" + text
-                + ", text_context=" + text_context + ", match=" + match
-                + ']';
-    }
-}
Index: src/org/openstreetmap/josm/gui/tagging/presets/items/KeyedItem.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/items/KeyedItem.java	(revision 18366)
+++ src/org/openstreetmap/josm/gui/tagging/presets/items/KeyedItem.java	(nonexistent)
@@ -1,269 +0,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.gui.tagging.presets.items;
-
-import static org.openstreetmap.josm.tools.I18n.tr;
-
-import java.util.Collection;
-import java.util.EnumSet;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.SortedMap;
-import java.util.NoSuchElementException;
-import java.util.TreeMap;
-
-import javax.swing.JPopupMenu;
-
-import org.openstreetmap.josm.data.osm.OsmPrimitive;
-import org.openstreetmap.josm.data.osm.OsmUtils;
-import org.openstreetmap.josm.data.osm.Tag;
-import org.openstreetmap.josm.data.preferences.BooleanProperty;
-import org.openstreetmap.josm.gui.dialogs.properties.HelpTagAction;
-import org.openstreetmap.josm.gui.dialogs.properties.TaginfoAction;
-import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItem;
-
-/**
- * Preset item associated to an OSM key.
- */
-public abstract class KeyedItem extends TextItem {
-
-    /** The constant value {@code "<different>"}. */
-    protected static final String DIFFERENT = "<different>";
-    /** Translation of {@code "<different>"}. */
-    protected static final String DIFFERENT_I18N = tr(DIFFERENT);
-
-    /** True if the default value should also be set on primitives that already have tags.  */
-    protected static final BooleanProperty PROP_FILL_DEFAULT = new BooleanProperty("taggingpreset.fill-default-for-tagged-primitives", false);
-
-    /** Last value of each key used in presets, used for prefilling corresponding fields */
-    static final Map<String, String> LAST_VALUES = new HashMap<>();
-
-    /** This specifies the property key that will be modified by the item. */
-    public String key; // NOSONAR
-    /**
-     * Allows to change the matching process, i.e., determining whether the tags of an OSM object fit into this preset.
-     * If a preset fits then it is linked in the Tags/Membership dialog.<ul>
-     * <li>none: neutral, i.e., do not consider this item for matching</li>
-     * <li>key: positive if key matches, neutral otherwise</li>
-     * <li>key!: positive if key matches, negative otherwise</li>
-     * <li>keyvalue: positive if key and value matches, neutral otherwise</li>
-     * <li>keyvalue!: positive if key and value matches, negative otherwise</li></ul>
-     * Note that for a match, at least one positive and no negative is required.
-     * Default is "keyvalue!" for {@link Key} and "none" for {@link Text}, {@link Combo}, {@link MultiSelect} and {@link Check}.
-     */
-    public String match = getDefaultMatch().getValue(); // NOSONAR
-
-    /**
-     * Enum denoting how a match (see {@link TaggingPresetItem#matches}) is performed.
-     */
-    protected enum MatchType {
-
-        /** Neutral, i.e., do not consider this item for matching. */
-        NONE("none"),
-        /** Positive if key matches, neutral otherwise. */
-        KEY("key"),
-        /** Positive if key matches, negative otherwise. */
-        KEY_REQUIRED("key!"),
-        /** Positive if key and value matches, neutral otherwise. */
-        KEY_VALUE("keyvalue"),
-        /** Positive if key and value matches, negative otherwise. */
-        KEY_VALUE_REQUIRED("keyvalue!");
-
-        private final String value;
-
-        MatchType(String value) {
-            this.value = value;
-        }
-
-        /**
-         * Replies the associated textual value.
-         * @return the associated textual value
-         */
-        public String getValue() {
-            return value;
-        }
-
-        /**
-         * Determines the {@code MatchType} for the given textual value.
-         * @param type the textual value
-         * @return the {@code MatchType} for the given textual value
-         */
-        public static MatchType ofString(String type) {
-            for (MatchType i : EnumSet.allOf(MatchType.class)) {
-                if (i.getValue().equals(type))
-                    return i;
-            }
-            throw new IllegalArgumentException(type + " is not allowed");
-        }
-    }
-
-    /**
-     * Usage information on a key
-     *
-     * TODO merge with {@link org.openstreetmap.josm.data.osm.TagCollection}
-     */
-    public static class Usage {
-        /** Usage count for all values used for this key */
-        public final SortedMap<String, Integer> map = new TreeMap<>();
-        private boolean hadKeys;
-        private boolean hadEmpty;
-        private int selectedCount;
-
-        /**
-         * Check if there is exactly one value for this key.
-         * @return <code>true</code> if there was exactly one value.
-         */
-        public boolean hasUniqueValue() {
-            return map.size() == 1 && !hadEmpty;
-        }
-
-        /**
-         * Check if this key was not used in any primitive
-         * @return <code>true</code> if it was unused.
-         */
-        public boolean unused() {
-            return map.isEmpty();
-        }
-
-        /**
-         * Get the first value available.
-         * @return The first value
-         * @throws NoSuchElementException if there is no such value.
-         */
-        public String getFirst() {
-            return map.firstKey();
-        }
-
-        /**
-         * Check if we encountered any primitive that had any keys
-         * @return <code>true</code> if any of the primitives had any tags.
-         */
-        public boolean hadKeys() {
-            return hadKeys;
-        }
-
-        /**
-         * Returns the number of primitives selected.
-         * @return the number of primitives selected.
-         */
-        public int getSelectedCount() {
-            return selectedCount;
-        }
-
-        /**
-         * Splits multiple values and adds their usage counts as single value.
-         * <p>
-         * A value of {@code regional;pizza} will increment the count of {@code regional} and of
-         * {@code pizza}.
-         * @param delimiter The delimiter used for splitting.
-         * @return A new usage object with the new counts.
-         */
-        public Usage splitValues(String delimiter) {
-            Usage usage = new Usage();
-            usage.hadEmpty = hadEmpty;
-            usage.hadKeys = hadKeys;
-            usage.selectedCount = selectedCount;
-            map.forEach((value, count) -> {
-                for (String v : value.split(String.valueOf(delimiter), -1)) {
-                    usage.map.merge(v, count, Integer::sum);
-                }
-            });
-            return usage;
-        }
-    }
-
-    /**
-     * Computes the tag usage for the given key from the given primitives
-     * @param sel the primitives
-     * @param key the key
-     * @return the tag usage
-     */
-    public static Usage determineTextUsage(Collection<OsmPrimitive> sel, String key) {
-        Usage returnValue = new Usage();
-        returnValue.selectedCount = sel.size();
-        for (OsmPrimitive s : sel) {
-            String v = s.get(key);
-            if (v != null) {
-                returnValue.map.merge(v, 1, Integer::sum);
-            } else {
-                returnValue.hadEmpty = true;
-            }
-            if (s.hasKeys()) {
-                returnValue.hadKeys = true;
-            }
-        }
-        return returnValue;
-    }
-
-    protected static Usage determineBooleanUsage(Collection<OsmPrimitive> sel, String key) {
-        Usage returnValue = new Usage();
-        returnValue.selectedCount = sel.size();
-        for (OsmPrimitive s : sel) {
-            String booleanValue = OsmUtils.getNamedOsmBoolean(s.get(key));
-            if (booleanValue != null) {
-                returnValue.map.merge(booleanValue, 1, Integer::sum);
-            }
-        }
-        return returnValue;
-    }
-
-    /**
-     * Determines whether key or key+value are required.
-     * @return whether key or key+value are required
-     */
-    public boolean isKeyRequired() {
-        final MatchType type = MatchType.ofString(match);
-        return MatchType.KEY_REQUIRED == type || MatchType.KEY_VALUE_REQUIRED == type;
-    }
-
-    /**
-     * Returns the default match.
-     * @return the default match
-     */
-    public abstract MatchType getDefaultMatch();
-
-    /**
-     * Returns the list of values.
-     * @return the list of values
-     */
-    public abstract Collection<String> getValues();
-
-    protected String getKeyTooltipText() {
-        return tr("This corresponds to the key ''{0}''", key);
-    }
-
-    @Override
-    public Boolean matches(Map<String, String> tags) {
-        switch (MatchType.ofString(match)) {
-        case NONE:
-            return null; // NOSONAR
-        case KEY:
-            return tags.containsKey(key) ? Boolean.TRUE : null;
-        case KEY_REQUIRED:
-            return tags.containsKey(key);
-        case KEY_VALUE:
-            return tags.containsKey(key) && getValues().contains(tags.get(key)) ? Boolean.TRUE : null;
-        case KEY_VALUE_REQUIRED:
-            return tags.containsKey(key) && getValues().contains(tags.get(key));
-        default:
-            throw new IllegalStateException();
-        }
-    }
-
-    protected JPopupMenu getPopupMenu() {
-        Tag tag = new Tag(key, null);
-        JPopupMenu popupMenu = new JPopupMenu();
-        popupMenu.add(tr("Key: {0}", key)).setEnabled(false);
-        popupMenu.add(new HelpTagAction(() -> tag));
-        TaginfoAction taginfoAction = new TaginfoAction(() -> tag, () -> null);
-        popupMenu.add(taginfoAction.toTagHistoryAction());
-        popupMenu.add(taginfoAction);
-        return popupMenu;
-    }
-
-    @Override
-    public String toString() {
-        return "KeyedItem [key=" + key + ", text=" + text
-                + ", text_context=" + text_context + ", match=" + match
-                + ']';
-    }
-}
Index: src/org/openstreetmap/josm/gui/tagging/presets/items/Label.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/items/Label.java	(revision 18366)
+++ src/org/openstreetmap/josm/gui/tagging/presets/items/Label.java	(nonexistent)
@@ -1,25 +0,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.gui.tagging.presets.items;
-
-import javax.swing.JLabel;
-import javax.swing.JPanel;
-
-import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItemGuiSupport;
-import org.openstreetmap.josm.tools.GBC;
-
-/**
- * Label type.
- */
-public class Label extends TextItem {
-
-    @Override
-    public boolean addToPanel(JPanel p, TaggingPresetItemGuiSupport support) {
-        initializeLocaleText(null);
-        JLabel label = new JLabel(locale_text);
-        addIcon(label);
-        label.applyComponentOrientation(support.getDefaultComponentOrientation());
-        p.add(label, GBC.eol().fill(GBC.HORIZONTAL));
-        return true;
-    }
-
-}
Index: src/org/openstreetmap/josm/gui/tagging/presets/items/Link.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/items/Link.java	(revision 18366)
+++ src/org/openstreetmap/josm/gui/tagging/presets/items/Link.java	(nonexistent)
@@ -1,93 +0,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.gui.tagging.presets.items;
-
-import static org.openstreetmap.josm.tools.I18n.tr;
-
-import java.awt.event.MouseEvent;
-import java.util.Arrays;
-import java.util.Optional;
-
-import javax.swing.JPanel;
-import javax.swing.SwingUtilities;
-
-import org.openstreetmap.josm.gui.dialogs.properties.HelpAction;
-import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItemGuiSupport;
-import org.openstreetmap.josm.gui.widgets.UrlLabel;
-import org.openstreetmap.josm.spi.preferences.Config;
-import org.openstreetmap.josm.tools.GBC;
-import org.openstreetmap.josm.tools.LanguageInfo;
-
-/**
- * Hyperlink type.
- * @since 8863
- */
-public class Link extends TextItem {
-
-    /** The OSM wiki page to display. */
-    public String wiki; // NOSONAR
-
-    /** The link to display. */
-    public String href; // NOSONAR
-
-    /** The localized version of {@link #href}. */
-    public String locale_href; // NOSONAR
-
-    @Override
-    public boolean addToPanel(JPanel p, TaggingPresetItemGuiSupport support) {
-        initializeLocaleText(tr("More information about this feature"));
-        UrlLabel label = buildUrlLabel();
-        if (label != null) {
-            label.applyComponentOrientation(support.getDefaultComponentOrientation());
-            p.add(label, GBC.eol().insets(0, 10, 0, 0).fill(GBC.HORIZONTAL));
-        }
-        return false;
-    }
-
-    protected UrlLabel buildUrlLabel() {
-        final String url = getUrl();
-        if (wiki != null) {
-            UrlLabel urlLabel = new UrlLabel(url, locale_text, 2) {
-                @Override
-                public void mouseClicked(MouseEvent e) {
-                    if (SwingUtilities.isLeftMouseButton(e)) {
-                        // Open localized page if exists
-                        HelpAction.displayHelp(Arrays.asList(
-                                LanguageInfo.getWikiLanguagePrefix(LanguageInfo.LocaleType.OSM_WIKI) + wiki,
-                                wiki));
-                    } else {
-                        super.mouseClicked(e);
-                    }
-                }
-            };
-            addIcon(urlLabel);
-            return urlLabel;
-        } else if (href != null || locale_href != null) {
-            UrlLabel urlLabel = new UrlLabel(url, locale_text, 2);
-            addIcon(urlLabel);
-            return urlLabel;
-        }
-        return null;
-    }
-
-    /**
-     * Returns the link URL.
-     * @return the link URL
-     * @since 15423
-     */
-    public String getUrl() {
-        if (wiki != null) {
-            return Config.getUrls().getOSMWiki() + "/wiki/" + wiki;
-        } else if (href != null || locale_href != null) {
-            return Optional.ofNullable(locale_href).orElse(href);
-        }
-        return null;
-    }
-
-    @Override
-    protected String fieldsToString() {
-        return super.fieldsToString()
-                + (wiki != null ? "wiki=" + wiki + ", " : "")
-                + (href != null ? "href=" + href + ", " : "")
-                + (locale_href != null ? "locale_href=" + locale_href + ", " : "");
-    }
-}
Index: src/org/openstreetmap/josm/gui/tagging/presets/items/MultiSelect.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/items/MultiSelect.java	(revision 18366)
+++ src/org/openstreetmap/josm/gui/tagging/presets/items/MultiSelect.java	(nonexistent)
@@ -1,109 +0,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.gui.tagging.presets.items;
-
-import java.awt.Dimension;
-import java.awt.Insets;
-import java.awt.Rectangle;
-import java.util.stream.Collectors;
-
-import javax.swing.DefaultListModel;
-import javax.swing.JLabel;
-import javax.swing.JList;
-import javax.swing.JPanel;
-import javax.swing.JScrollPane;
-
-import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItemGuiSupport;
-import org.openstreetmap.josm.gui.widgets.OrientationAction;
-import org.openstreetmap.josm.tools.GBC;
-
-/**
- * Multi-select list type.
- */
-public class MultiSelect extends ComboMultiSelect {
-
-    /**
-     * Number of rows to display (positive integer, optional).
-     */
-    public short rows; // NOSONAR
-
-    /** The model for the JList */
-    protected final DefaultListModel<PresetListEntry> model = new DefaultListModel<>();
-    /** The swing component */
-    protected final JList<PresetListEntry> list = new JList<>(model);
-
-    private void addEntry(PresetListEntry entry) {
-        if (!seenValues.containsKey(entry.value)) {
-            model.addElement(entry);
-            seenValues.put(entry.value, entry);
-        }
-    }
-
-    @Override
-    protected boolean addToPanel(JPanel p, TaggingPresetItemGuiSupport support) {
-        initializeLocaleText(null);
-        usage = determineTextUsage(support.getSelected(), key);
-        seenValues.clear();
-        initListEntries();
-
-        model.clear();
-        // disable if the selected primitives have different values
-        list.setEnabled(usage.hasUniqueValue() || usage.unused());
-        String initialValue = getInitialValue(usage, support);
-
-        // Add values from the preset.
-        presetListEntries.forEach(this::addEntry);
-
-        // Add all values used in the selected primitives. This also adds custom values and makes
-        // sure we won't lose them.
-        usage = usage.splitValues(String.valueOf(delimiter));
-        for (String value: usage.map.keySet()) {
-            addEntry(new PresetListEntry(value, this));
-        }
-
-        // Select the values in the initial value.
-        if (!initialValue.isEmpty() && !DIFFERENT.equals(initialValue)) {
-            for (String value : initialValue.split(String.valueOf(delimiter), -1)) {
-                PresetListEntry e = new PresetListEntry(value, this);
-                addEntry(e);
-                int i = model.indexOf(e);
-                list.addSelectionInterval(i, i);
-            }
-        }
-
-        ComboMultiSelectListCellRenderer renderer = new ComboMultiSelectListCellRenderer(list, list.getCellRenderer(), 200, key);
-        list.setCellRenderer(renderer);
-        JLabel label = addLabel(p);
-        label.setLabelFor(list);
-        JScrollPane sp = new JScrollPane(list);
-
-        if (rows > 0) {
-            list.setVisibleRowCount(rows);
-            // setVisibleRowCount() only works when all cells have the same height, but sometimes we
-            // have icons of different sizes. Calculate the size of the first {@code rows} entries
-            // and size the scrollpane accordingly.
-            Rectangle r = list.getCellBounds(0, Math.min(rows, model.size() - 1));
-            if (r != null) {
-                Insets insets = list.getInsets();
-                r.width += insets.left + insets.right;
-                r.height += insets.top + insets.bottom;
-                insets = sp.getInsets();
-                r.width += insets.left + insets.right;
-                r.height += insets.top + insets.bottom;
-                sp.setPreferredSize(new Dimension(r.width, r.height));
-            }
-        }
-        p.add(sp, GBC.eol().fill(GBC.HORIZONTAL)); // NOSONAR
-
-        list.addListSelectionListener(l -> support.fireItemValueModified(this, key, getSelectedItem().value));
-        list.setToolTipText(getKeyTooltipText());
-        list.applyComponentOrientation(OrientationAction.getValueOrientation(key));
-
-        return true;
-    }
-
-    @Override
-    protected PresetListEntry getSelectedItem() {
-        return new PresetListEntry(list.getSelectedValuesList()
-            .stream().map(e -> e.value).distinct().sorted().collect(Collectors.joining(String.valueOf(delimiter))), this);
-    }
-}
Index: src/org/openstreetmap/josm/gui/tagging/presets/items/Optional.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/items/Optional.java	(revision 18366)
+++ src/org/openstreetmap/josm/gui/tagging/presets/items/Optional.java	(nonexistent)
@@ -1,29 +0,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.gui.tagging.presets.items;
-
-import static org.openstreetmap.josm.tools.I18n.tr;
-
-import javax.swing.JLabel;
-import javax.swing.JPanel;
-
-import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItemGuiSupport;
-import org.openstreetmap.josm.tools.GBC;
-
-/**
- * Used to group optional attributes.
- * @since 8863
- */
-public class Optional extends TextItem {
-
-    // TODO: Draw a box around optional stuff
-    @Override
-    public boolean addToPanel(JPanel p, TaggingPresetItemGuiSupport support) {
-        initializeLocaleText(tr("Optional Attributes:"));
-        JLabel label = new JLabel(locale_text);
-        label.applyComponentOrientation(support.getDefaultComponentOrientation());
-        p.add(new JLabel(" "), GBC.eol()); // space
-        p.add(label, GBC.eol());
-        p.add(new JLabel(" "), GBC.eol()); // space
-        return false;
-    }
-}
Index: src/org/openstreetmap/josm/gui/tagging/presets/items/PresetLink.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/items/PresetLink.java	(revision 18366)
+++ src/org/openstreetmap/josm/gui/tagging/presets/items/PresetLink.java	(nonexistent)
@@ -1,79 +0,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.gui.tagging.presets.items;
-
-import static org.openstreetmap.josm.tools.I18n.tr;
-
-import java.awt.event.MouseAdapter;
-import java.awt.event.MouseEvent;
-import java.util.Collection;
-import java.util.List;
-import java.util.Optional;
-
-import javax.swing.JLabel;
-import javax.swing.JPanel;
-
-import org.openstreetmap.josm.data.osm.OsmPrimitive;
-import org.openstreetmap.josm.data.osm.Tag;
-import org.openstreetmap.josm.gui.tagging.presets.TaggingPreset;
-import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItemGuiSupport;
-import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetLabel;
-import org.openstreetmap.josm.gui.tagging.presets.TaggingPresets;
-import org.openstreetmap.josm.tools.GBC;
-
-/**
- * Adds a link to another preset.
- * @since 8863
- */
-public class PresetLink extends TextItem {
-
-    static final class TaggingPresetMouseAdapter extends MouseAdapter {
-        private final TaggingPreset t;
-        private final Collection<OsmPrimitive> sel;
-
-        TaggingPresetMouseAdapter(TaggingPreset t, Collection<OsmPrimitive> sel) {
-            this.t = t;
-            this.sel = sel;
-        }
-
-        @Override
-        public void mouseClicked(MouseEvent e) {
-            t.showAndApply(sel);
-        }
-    }
-
-    /** The exact name of the preset to link to. Required. */
-    public String preset_name = ""; // NOSONAR
-
-    /**
-     * Creates a label to be inserted aboive this link
-     * @return a label
-     */
-    public JLabel createLabel() {
-        initializeLocaleText(tr("Edit also …"));
-        return new JLabel(locale_text);
-    }
-
-    @Override
-    public boolean addToPanel(JPanel p, TaggingPresetItemGuiSupport support) {
-        final String presetName = preset_name;
-        Optional<TaggingPreset> found = TaggingPresets.getTaggingPresets().stream().filter(preset -> presetName.equals(preset.name)).findFirst();
-        if (found.isPresent()) {
-            TaggingPreset t = found.get();
-            JLabel lbl = new TaggingPresetLabel(t);
-            lbl.addMouseListener(new TaggingPresetMouseAdapter(t, support.getSelected()));
-            lbl.applyComponentOrientation(support.getDefaultComponentOrientation());
-            p.add(lbl, GBC.eol().fill(GBC.HORIZONTAL));
-        }
-        return false;
-    }
-
-    @Override
-    public void addCommands(List<Tag> changedTags) {
-        // Do nothing
-    }
-
-    @Override
-    public String toString() {
-        return "PresetLink [preset_name=" + preset_name + ']';
-    }
-}
Index: src/org/openstreetmap/josm/gui/tagging/presets/items/PresetListEntry.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/items/PresetListEntry.java	(revision 18366)
+++ src/org/openstreetmap/josm/gui/tagging/presets/items/PresetListEntry.java	(nonexistent)
@@ -1,199 +0,0 @@
-// License: GPL. For details, see LICENSE file.
-
-package org.openstreetmap.josm.gui.tagging.presets.items;
-
-import static org.openstreetmap.josm.tools.I18n.tr;
-import static org.openstreetmap.josm.tools.I18n.trc;
-
-import java.util.Objects;
-
-import javax.swing.ImageIcon;
-
-import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItem;
-import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetReader;
-import org.openstreetmap.josm.tools.AlphanumComparator;
-import org.openstreetmap.josm.tools.Utils;
-
-/**
- * Preset list entry.
- * <p>
- * Used for controls that offer a list of items to choose from like {@link Combo} and
- * {@link MultiSelect}.
- */
-public class PresetListEntry implements Comparable<PresetListEntry> {
-    /** Used to display an entry matching several different values. */
-    protected static final PresetListEntry ENTRY_DIFFERENT = new PresetListEntry(KeyedItem.DIFFERENT, null);
-    /** Used to display an empty entry used to clear values. */
-    protected static final PresetListEntry ENTRY_EMPTY = new PresetListEntry("", null);
-
-    /**
-     * This is the value that is going to be written to the tag on the selected primitive(s). Except
-     * when the value is {@code "<different>"}, which is never written, or the value is empty, which
-     * deletes the tag.  {@code value} is never translated.
-     */
-    public String value; // NOSONAR
-    /** The ComboMultiSelect that displays the list */
-    public ComboMultiSelect cms; // NOSONAR
-    /** Text displayed to the user instead of {@link #value}. */
-    public String display_value; // NOSONAR
-    /** Text to be displayed below {@link #display_value} in the combobox list. */
-    public String short_description; // NOSONAR
-    /** The location of icon file to display */
-    public String icon; // NOSONAR
-    /** The size of displayed icon. If not set, default is size from icon file */
-    public short icon_size; // NOSONAR
-    /** The localized version of {@link #display_value}. */
-    public String locale_display_value; // NOSONAR
-    /** The localized version of {@link #short_description}. */
-    public String locale_short_description; // NOSONAR
-
-    private String cachedDisplayValue;
-    private String cachedShortDescription;
-    private ImageIcon cachedIcon;
-
-    /**
-     * Constructs a new {@code PresetListEntry}, uninitialized.
-     *
-     * Public default constructor is needed by {@link org.openstreetmap.josm.tools.XmlObjectParser.Parser#startElement}
-     */
-    public PresetListEntry() {
-    }
-
-    /**
-     * Constructs a new {@code PresetListEntry}, initialized with a value and
-     * {@link ComboMultiSelect} context.
-     *
-     * @param value value
-     * @param cms the ComboMultiSelect
-     */
-    public PresetListEntry(String value, ComboMultiSelect cms) {
-        this.value = value;
-        this.cms = cms;
-    }
-
-    /**
-     * Returns the contents displayed in the dropdown list.
-     *
-     * This is the contents that would be displayed in the current view plus a short description to
-     * aid the user.  The whole contents is wrapped to {@code width}.
-     *
-     * @param width the width in px
-     * @return HTML formatted contents
-     */
-    public String getListDisplay(int width) {
-        String displayValue = getDisplayValue();
-        Integer count = getCount();
-
-        if (count > 0 && cms.usage.getSelectedCount() > 1) {
-            displayValue = tr("{0} ({1})", displayValue, count);
-        }
-
-        if (this.equals(ENTRY_DIFFERENT)) {
-            return "<html><b>" + Utils.escapeReservedCharactersHTML(displayValue) + "</b></html>";
-        }
-
-        String shortDescription = getShortDescription();
-
-        if (shortDescription.isEmpty()) {
-            // avoids a collapsed list entry if value == ""
-            if (displayValue.isEmpty()) {
-                return " ";
-            }
-            return displayValue;
-        }
-
-        // RTL not supported in HTML. See: http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4866977
-        return String.format("<html><div style=\"width: %d\"><b>%s</b><p style=\"padding-left: 10\">%s</p></div></html>",
-                width,
-                displayValue,
-                Utils.escapeReservedCharactersHTML(shortDescription));
-    }
-
-    /**
-     * Returns the entry icon, if any.
-     * @return the entry icon, or {@code null}
-     */
-    public ImageIcon getIcon() {
-        if (icon != null && cachedIcon == null) {
-            cachedIcon = TaggingPresetItem.loadImageIcon(icon, TaggingPresetReader.getZipIcons(), (int) icon_size);
-        }
-        return cachedIcon;
-    }
-
-    /**
-     * Returns the contents displayed in the current item view.
-     * @return the value to display
-     */
-    public String getDisplayValue() {
-        if (cachedDisplayValue == null) {
-            if (cms != null && cms.values_no_i18n) {
-                cachedDisplayValue = Utils.firstNonNull(value, " ");
-            } else {
-                cachedDisplayValue = Utils.firstNonNull(
-                    locale_display_value, tr(display_value), trc(cms == null ? null : cms.values_context, value), " ");
-            }
-        }
-        return cachedDisplayValue;
-    }
-
-    /**
-     * Returns the short description to display.
-     * @return the short description to display
-     */
-    public String getShortDescription() {
-        if (cachedShortDescription == null) {
-            cachedShortDescription = Utils.firstNonNull(locale_short_description, tr(short_description), "");
-        }
-        return cachedShortDescription;
-    }
-
-    /**
-     * Returns the tooltip for this entry.
-     * @param key the tag key
-     * @return the tooltip
-     */
-    public String getToolTipText(String key) {
-        if (this.equals(ENTRY_DIFFERENT)) {
-            return tr("Keeps the original values of the selected objects unchanged.");
-        }
-        if (value != null && !value.isEmpty()) {
-            return tr("Sets the key ''{0}'' to the value ''{1}''.", key, value);
-        }
-        return tr("Clears the key ''{0}''.", key);
-    }
-
-    // toString is mainly used to initialize the Editor
-    @Override
-    public String toString() {
-        if (this.equals(ENTRY_DIFFERENT))
-            return getDisplayValue();
-        return getDisplayValue().replaceAll("\\s*<.*>\\s*", " "); // remove additional markup, e.g. <br>
-    }
-
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) return true;
-        if (o == null || getClass() != o.getClass()) return false;
-        PresetListEntry that = (PresetListEntry) o;
-        return Objects.equals(value, that.value);
-    }
-
-    @Override
-    public int hashCode() {
-        return Objects.hash(value);
-    }
-
-    /**
-     * Returns how many selected primitives had this value set.
-     * @return see above
-     */
-    public int getCount() {
-        Integer count = cms == null || cms.usage == null ? null : cms.usage.map.get(value);
-        return count == null ? 0 : count;
-    }
-
-    @Override
-    public int compareTo(PresetListEntry o) {
-        return AlphanumComparator.getInstance().compare(this.value, o.value);
-    }
-}
Index: src/org/openstreetmap/josm/gui/tagging/presets/items/Roles.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/items/Roles.java	(revision 18366)
+++ src/org/openstreetmap/josm/gui/tagging/presets/items/Roles.java	(nonexistent)
@@ -1,218 +0,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.gui.tagging.presets.items;
-
-import static org.openstreetmap.josm.tools.I18n.tr;
-
-import java.awt.GridBagLayout;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Set;
-
-import javax.swing.JLabel;
-import javax.swing.JPanel;
-
-import org.openstreetmap.josm.data.osm.Tag;
-import org.openstreetmap.josm.data.osm.search.SearchCompiler;
-import org.openstreetmap.josm.data.osm.search.SearchParseError;
-import org.openstreetmap.josm.data.osm.search.SearchSetting;
-import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItem;
-import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItemGuiSupport;
-import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetType;
-import org.openstreetmap.josm.tools.GBC;
-import org.openstreetmap.josm.tools.ImageProvider;
-import org.xml.sax.SAXException;
-
-/**
- * The <code>roles</code> element in tagging presets definition.
- * <p>
- * A list of {@link Role} elements. Describes the roles that are expected for
- * the members of a relation.
- * <p>
- * Used for data validation, auto completion, among others.
- */
-public class Roles extends TaggingPresetItem {
-
-    /**
-     * The <code>role</code> element in tagging preset definition.
-     *
-     * Information on a certain role, which is expected for the relation members.
-     */
-    public static class Role {
-        /** Presets types expected for this role */
-        public Set<TaggingPresetType> types; // NOSONAR
-        /** Role name used in a relation */
-        public String key; // NOSONAR
-        /** Is the role name a regular expression */
-        public boolean regexp; // NOSONAR
-        /** The text to display */
-        public String text; // NOSONAR
-        /** The context used for translating {@link #text} */
-        public String text_context; // NOSONAR
-        /** The localized version of {@link #text}. */
-        public String locale_text; // NOSONAR
-        /** An expression (cf. search dialog) for objects of this role */
-        public SearchCompiler.Match memberExpression; // NOSONAR
-        /** Is this role required at least once in the relation? */
-        public boolean required; // NOSONAR
-        /** How often must the element appear */
-        private short count;
-
-        /**
-         * Sets the presets types expected for this role.
-         * @param types comma-separated set of expected types
-         * @throws SAXException if an unknown type is detected
-         */
-        public void setType(String types) throws SAXException {
-            this.types = getType(types);
-        }
-
-        /**
-         * Sets whether this role is required at least once in the relation.
-         * @param str "required" or "optional"
-         * @throws SAXException if str is neither "required" or "optional"
-         */
-        public void setRequisite(String str) throws SAXException {
-            if ("required".equals(str)) {
-                required = true;
-            } else if (!"optional".equals(str))
-                throw new SAXException(tr("Unknown requisite: {0}", str));
-        }
-
-        /**
-         * Sets whether the role name is a regular expression.
-         * @param str "true" or "false"
-         * @throws SAXException if str is neither "true" or "false"
-         */
-        public void setRegexp(String str) throws SAXException {
-            if ("true".equals(str)) {
-                regexp = true;
-            } else if (!"false".equals(str))
-                throw new SAXException(tr("Unknown regexp value: {0}", str));
-        }
-
-        /**
-         * Sets an expression (cf. search dialog) for objects of this role
-         * @param memberExpression an expression (cf. search dialog) for objects of this role
-         * @throws SAXException in case of parsing error
-         */
-        public void setMember_expression(String memberExpression) throws SAXException {
-            try {
-                final SearchSetting searchSetting = new SearchSetting();
-                searchSetting.text = memberExpression;
-                searchSetting.caseSensitive = true;
-                searchSetting.regexSearch = true;
-                this.memberExpression = SearchCompiler.compile(searchSetting);
-            } catch (SearchParseError ex) {
-                throw new SAXException(tr("Illegal member expression: {0}", ex.getMessage()), ex);
-            }
-        }
-
-        /**
-         * Sets how often must the element appear.
-         * @param count how often must the element appear
-         */
-        public void setCount(String count) {
-            this.count = Short.parseShort(count);
-        }
-
-        /**
-         * Return either argument, the highest possible value or the lowest allowed value
-         * @param c count
-         * @return the highest possible value or the lowest allowed value
-         * @see #required
-         */
-        public long getValidCount(long c) {
-            if (count > 0 && !required)
-                return c != 0 ? count : 0;
-            else if (count > 0)
-                return count;
-            else if (!required)
-                return c != 0 ? c : 0;
-            else
-                return c != 0 ? c : 1;
-        }
-
-        /**
-         * Check if the given role matches this class (required to check regexp role types)
-         * @param role role to check
-         * @return <code>true</code> if role matches
-         * @since 11989
-         */
-        public boolean isRole(String role) {
-            if (regexp && role != null) { // pass null through, it will anyway fail
-                return role.matches(this.key);
-            }
-            return this.key.equals(role);
-        }
-
-        /**
-         * Adds this role to the given panel.
-         * @param p panel where to add this role
-         * @return {@code true}
-         */
-        public boolean addToPanel(JPanel p) {
-            String cstring;
-            if (count > 0 && !required) {
-                cstring = "0,"+count;
-            } else if (count > 0) {
-                cstring = String.valueOf(count);
-            } else if (!required) {
-                cstring = "0-...";
-            } else {
-                cstring = "1-...";
-            }
-            if (locale_text == null) {
-                locale_text = getLocaleText(text, text_context, null);
-            }
-            p.add(new JLabel(locale_text+':'), GBC.std().insets(0, 0, 10, 0));
-            p.add(new JLabel(key), GBC.std().insets(0, 0, 10, 0));
-            p.add(new JLabel(cstring), types == null ? GBC.eol() : GBC.std().insets(0, 0, 10, 0));
-            if (types != null) {
-                JPanel pp = new JPanel();
-                for (TaggingPresetType t : types) {
-                    pp.add(new JLabel(ImageProvider.get(t.getIconName())));
-                }
-                p.add(pp, GBC.eol());
-            }
-            return true;
-        }
-
-        @Override
-        public String toString() {
-            return "Role [key=" + key + ", text=" + text + ']';
-        }
-    }
-
-    /**
-     * List of {@link Role} elements.
-     */
-    public final List<Role> roles = new ArrayList<>(2);
-
-    @Override
-    public boolean addToPanel(JPanel p, TaggingPresetItemGuiSupport support) {
-        p.add(new JLabel(" "), GBC.eol()); // space
-        if (!roles.isEmpty()) {
-            JPanel proles = new JPanel(new GridBagLayout());
-            proles.add(new JLabel(tr("Available roles")), GBC.std().insets(0, 0, 10, 0));
-            proles.add(new JLabel(tr("role")), GBC.std().insets(0, 0, 10, 0));
-            proles.add(new JLabel(tr("count")), GBC.std().insets(0, 0, 10, 0));
-            proles.add(new JLabel(tr("elements")), GBC.eol());
-            for (Role i : roles) {
-                i.addToPanel(proles);
-            }
-            proles.applyComponentOrientation(support.getDefaultComponentOrientation());
-            p.add(proles, GBC.eol());
-        }
-        return false;
-    }
-
-    @Override
-    public void addCommands(List<Tag> changedTags) {
-        // Do nothing
-    }
-
-    @Override
-    public String toString() {
-        return "Roles [roles=" + roles + ']';
-    }
-}
Index: src/org/openstreetmap/josm/gui/tagging/presets/items/Space.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/items/Space.java	(revision 18366)
+++ src/org/openstreetmap/josm/gui/tagging/presets/items/Space.java	(nonexistent)
@@ -1,34 +0,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.gui.tagging.presets.items;
-
-import java.util.List;
-
-import javax.swing.JLabel;
-import javax.swing.JPanel;
-
-import org.openstreetmap.josm.data.osm.Tag;
-import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItem;
-import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItemGuiSupport;
-import org.openstreetmap.josm.tools.GBC;
-
-/**
- * Horizontal separator type.
- */
-public class Space extends TaggingPresetItem {
-
-    @Override
-    public boolean addToPanel(JPanel p, TaggingPresetItemGuiSupport support) {
-        p.add(new JLabel(" "), GBC.eol()); // space
-        return false;
-    }
-
-    @Override
-    public void addCommands(List<Tag> changedTags) {
-        // Do nothing
-    }
-
-    @Override
-    public String toString() {
-        return "Space";
-    }
-}
Index: src/org/openstreetmap/josm/gui/tagging/presets/items/Text.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/items/Text.java	(revision 18366)
+++ src/org/openstreetmap/josm/gui/tagging/presets/items/Text.java	(nonexistent)
@@ -1,305 +0,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.gui.tagging.presets.items;
-
-import static org.openstreetmap.josm.tools.I18n.tr;
-
-import java.awt.Color;
-import java.awt.Component;
-import java.awt.GridBagLayout;
-import java.awt.Insets;
-import java.text.NumberFormat;
-import java.text.ParseException;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-
-import javax.swing.AbstractButton;
-import javax.swing.BorderFactory;
-import javax.swing.ButtonGroup;
-import javax.swing.JButton;
-import javax.swing.JComponent;
-import javax.swing.JLabel;
-import javax.swing.JPanel;
-import javax.swing.JToggleButton;
-
-import org.openstreetmap.josm.data.osm.Tag;
-import org.openstreetmap.josm.data.tagging.ac.AutoCompletionItem;
-import org.openstreetmap.josm.gui.tagging.ac.AutoCompComboBoxEditor;
-import org.openstreetmap.josm.gui.tagging.ac.AutoCompComboBoxModel;
-import org.openstreetmap.josm.gui.tagging.ac.AutoCompTextField;
-import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionManager;
-import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItem;
-import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItemGuiSupport;
-import org.openstreetmap.josm.gui.util.DocumentAdapter;
-import org.openstreetmap.josm.gui.widgets.JosmComboBox;
-import org.openstreetmap.josm.gui.widgets.JosmTextField;
-import org.openstreetmap.josm.gui.widgets.OrientationAction;
-import org.openstreetmap.josm.tools.GBC;
-import org.openstreetmap.josm.tools.Logging;
-import org.openstreetmap.josm.tools.Utils;
-import org.openstreetmap.josm.tools.template_engine.ParseError;
-import org.openstreetmap.josm.tools.template_engine.TemplateEntry;
-import org.openstreetmap.josm.tools.template_engine.TemplateParser;
-import org.xml.sax.SAXException;
-
-/**
- * Text field type.
- */
-public class Text extends KeyedItem {
-
-    private static int auto_increment_selected; // NOSONAR
-
-    /** The default value for the item. If not specified, the current value of the key is chosen as default (if applicable). Defaults to "". */
-    public String default_; // NOSONAR
-    /** The original value */
-    public String originalValue; // NOSONAR
-    /** whether the last value is used as default. Using "force" enforces this behaviour also for already tagged objects. Default is "false".*/
-    public String use_last_as_default = "false"; // NOSONAR
-    /**
-     * May contain a comma separated list of integer increments or decrements, e.g. "-2,-1,+1,+2".
-     * A button will be shown next to the text field for each value, allowing the user to select auto-increment with the given stepping.
-     * Auto-increment only happens if the user selects it. There is also a button to deselect auto-increment.
-     * Default is no auto-increment. Mutually exclusive with {@link #use_last_as_default}.
-     */
-    public String auto_increment; // NOSONAR
-    /** The length of the text box (number of characters allowed). */
-    public short length; // NOSONAR
-    /** A comma separated list of alternative keys to use for autocompletion. */
-    public String alternative_autocomplete_keys; // NOSONAR
-
-    private JComponent value;
-    private transient TemplateEntry valueTemplate;
-
-    @Override
-    public boolean addToPanel(JPanel p, TaggingPresetItemGuiSupport support) {
-
-        AutoCompComboBoxModel<AutoCompletionItem> model = new AutoCompComboBoxModel<>();
-        List<String> keys = new ArrayList<>();
-        keys.add(key);
-        if (alternative_autocomplete_keys != null) {
-            for (String k : alternative_autocomplete_keys.split(",", -1)) {
-                keys.add(k);
-            }
-        }
-        getAllForKeys(keys).forEach(model::addElement);
-
-        AutoCompTextField<AutoCompletionItem> textField;
-        AutoCompComboBoxEditor<AutoCompletionItem> editor = null;
-
-        // find out if our key is already used in the selection.
-        Usage usage = determineTextUsage(support.getSelected(), key);
-
-        if (usage.unused() || usage.hasUniqueValue()) {
-            textField = new AutoCompTextField<>();
-        } else {
-            editor = new AutoCompComboBoxEditor<>();
-            textField = editor.getEditorComponent();
-        }
-        textField.setModel(model);
-        value = textField;
-
-        if (length > 0) {
-            textField.setMaxTextLength(length);
-        }
-        if (TaggingPresetItem.DISPLAY_KEYS_AS_HINT.get()) {
-            textField.setHint(key);
-        }
-        if (usage.unused()) {
-            if (auto_increment_selected != 0 && auto_increment != null) {
-                try {
-                    textField.setText(Integer.toString(Integer.parseInt(
-                            LAST_VALUES.get(key)) + auto_increment_selected));
-                } catch (NumberFormatException ex) {
-                    // Ignore - cannot auto-increment if last was non-numeric
-                    Logging.trace(ex);
-                }
-            } else if (!usage.hadKeys() || PROP_FILL_DEFAULT.get() || "force".equals(use_last_as_default)) {
-                // selected osm primitives are untagged or filling default values feature is enabled
-                if (!support.isPresetInitiallyMatches() && !"false".equals(use_last_as_default) && LAST_VALUES.containsKey(key)) {
-                    textField.setText(LAST_VALUES.get(key));
-                } else {
-                    textField.setText(default_);
-                }
-            } else {
-                // selected osm primitives are tagged and filling default values feature is disabled
-                textField.setText("");
-            }
-            value = textField;
-            originalValue = null;
-        } else if (usage.hasUniqueValue()) {
-            // all objects use the same value
-            textField.setText(usage.getFirst());
-            value = textField;
-            originalValue = usage.getFirst();
-        }
-        if (editor != null) {
-            // The selected primitives have different values for this key.   <b>Note:</b> this
-            // cannot be an AutoCompComboBox because the values in the dropdown are different from
-            // those we autocomplete on.
-            JosmComboBox<String> comboBox = new JosmComboBox<>();
-            comboBox.getModel().addAllElements(usage.map.keySet());
-            comboBox.setEditable(true);
-            comboBox.setEditor(editor);
-            comboBox.getEditor().setItem(DIFFERENT_I18N);
-            value = comboBox;
-            originalValue = DIFFERENT_I18N;
-        }
-        initializeLocaleText(null);
-
-        setupListeners(textField, support);
-
-        // if there's an auto_increment setting, then wrap the text field
-        // into a panel, appending a number of buttons.
-        // auto_increment has a format like -2,-1,1,2
-        // the text box being the first component in the panel is relied
-        // on in a rather ugly fashion further down.
-        if (auto_increment != null) {
-            ButtonGroup bg = new ButtonGroup();
-            JPanel pnl = new JPanel(new GridBagLayout());
-            pnl.add(value, GBC.std().fill(GBC.HORIZONTAL));
-
-            // first, one button for each auto_increment value
-            for (final String ai : auto_increment.split(",", -1)) {
-                JToggleButton aibutton = new JToggleButton(ai);
-                aibutton.setToolTipText(tr("Select auto-increment of {0} for this field", ai));
-                aibutton.setMargin(new Insets(0, 0, 0, 0));
-                aibutton.setFocusable(false);
-                saveHorizontalSpace(aibutton);
-                bg.add(aibutton);
-                try {
-                    // TODO there must be a better way to parse a number like "+3" than this.
-                    final int buttonvalue = NumberFormat.getIntegerInstance().parse(ai.replace("+", "")).intValue();
-                    if (auto_increment_selected == buttonvalue) aibutton.setSelected(true);
-                    aibutton.addActionListener(e -> auto_increment_selected = buttonvalue);
-                    pnl.add(aibutton, GBC.std());
-                } catch (ParseException ex) {
-                    Logging.error("Cannot parse auto-increment value of '" + ai + "' into an integer");
-                }
-            }
-
-            // an invisible toggle button for "release" of the button group
-            final JToggleButton clearbutton = new JToggleButton("X");
-            clearbutton.setVisible(false);
-            clearbutton.setFocusable(false);
-            bg.add(clearbutton);
-            // and its visible counterpart. - this mechanism allows us to
-            // have *no* button selected after the X is clicked, instead
-            // of the X remaining selected
-            JButton releasebutton = new JButton("X");
-            releasebutton.setToolTipText(tr("Cancel auto-increment for this field"));
-            releasebutton.setMargin(new Insets(0, 0, 0, 0));
-            releasebutton.setFocusable(false);
-            releasebutton.addActionListener(e -> {
-                auto_increment_selected = 0;
-                clearbutton.setSelected(true);
-            });
-            saveHorizontalSpace(releasebutton);
-            pnl.add(releasebutton, GBC.eol());
-            value = pnl;
-        }
-        final JLabel label = new JLabel(tr("{0}:", locale_text));
-        addIcon(label);
-        label.setToolTipText(getKeyTooltipText());
-        label.setComponentPopupMenu(getPopupMenu());
-        label.setLabelFor(value);
-        p.add(label, GBC.std().insets(0, 0, 10, 0));
-        p.add(value, GBC.eol().fill(GBC.HORIZONTAL));
-        label.applyComponentOrientation(support.getDefaultComponentOrientation());
-        value.setToolTipText(getKeyTooltipText());
-        value.applyComponentOrientation(OrientationAction.getNamelikeOrientation(key));
-        return true;
-    }
-
-    private static void saveHorizontalSpace(AbstractButton button) {
-        Insets insets = button.getBorder().getBorderInsets(button);
-        // Ensure the current look&feel does not waste horizontal space (as seen in Nimbus & Aqua)
-        if (insets != null && insets.left+insets.right > insets.top+insets.bottom) {
-            int min = Math.min(insets.top, insets.bottom);
-            button.setBorder(BorderFactory.createEmptyBorder(insets.top, min, insets.bottom, min));
-        }
-    }
-
-    private static String getValue(Component comp) {
-        if (comp instanceof JosmComboBox) {
-            return ((JosmComboBox<?>) comp).getEditorItemAsString();
-        } else if (comp instanceof JosmTextField) {
-            return ((JosmTextField) comp).getText();
-        } else if (comp instanceof JPanel) {
-            return getValue(((JPanel) comp).getComponent(0));
-        } else {
-            return null;
-        }
-    }
-
-    @Override
-    public void addCommands(List<Tag> changedTags) {
-
-        // return if unchanged
-        String v = getValue(value);
-        if (v == null) {
-            Logging.error("No 'last value' support for component " + value);
-            return;
-        }
-
-        v = Utils.removeWhiteSpaces(v);
-
-        if (!"false".equals(use_last_as_default) || auto_increment != null) {
-            LAST_VALUES.put(key, v);
-        }
-        if (v.equals(originalValue) || (originalValue == null && v.isEmpty()))
-            return;
-
-        changedTags.add(new Tag(key, v));
-        AutoCompletionManager.rememberUserInput(key, v, true);
-    }
-
-    @Override
-    public MatchType getDefaultMatch() {
-        return MatchType.NONE;
-    }
-
-    @Override
-    public Collection<String> getValues() {
-        if (Utils.isEmpty(default_))
-            return Collections.emptyList();
-        return Collections.singleton(default_);
-    }
-
-    /**
-     * Set the value template.
-     * @param pattern The value_template pattern.
-     * @throws SAXException If an error occured while parsing.
-     */
-    public void setValue_template(String pattern) throws SAXException { // NOPMD
-        try {
-            this.valueTemplate = new TemplateParser(pattern).parse();
-        } catch (ParseError e) {
-            Logging.error("Error while parsing " + pattern + ": " + e.getMessage());
-            throw new SAXException(e);
-        }
-    }
-
-    private void setupListeners(AutoCompTextField<AutoCompletionItem> textField, TaggingPresetItemGuiSupport support) {
-        // value_templates don't work well with multiple selected items because,
-        // as the command queue is currently implemented, we can only save
-        // the same value to all selected primitives, which is probably not
-        // what you want.
-        if (valueTemplate == null || support.getSelected().size() > 1) { // only fire on normal fields
-            textField.getDocument().addDocumentListener(DocumentAdapter.create(ignore ->
-                    support.fireItemValueModified(this, key, textField.getText())));
-        } else { // only listen on calculated fields
-            support.addListener((source, key, newValue) -> {
-                String valueTemplateText = valueTemplate.getText(support);
-                Logging.trace("Evaluating value_template {0} for key {1} from {2} with new value {3} => {4}",
-                        valueTemplate, key, source, newValue, valueTemplateText);
-                textField.setText(valueTemplateText);
-                if (originalValue != null && !originalValue.equals(valueTemplateText)) {
-                    textField.setForeground(Color.RED);
-                } else {
-                    textField.setForeground(Color.BLUE);
-                }
-            });
-        }
-    }
-}
Index: src/org/openstreetmap/josm/gui/tagging/presets/items/TextItem.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/items/TextItem.java	(revision 18366)
+++ src/org/openstreetmap/josm/gui/tagging/presets/items/TextItem.java	(nonexistent)
@@ -1,74 +0,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.gui.tagging.presets.items;
-
-import java.util.List;
-
-import org.openstreetmap.josm.data.osm.Tag;
-import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItem;
-import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetReader;
-
-import javax.swing.ImageIcon;
-import javax.swing.JLabel;
-import javax.swing.SwingConstants;
-
-/**
- * A tagging preset item displaying a localizable text.
- * @since 6190
- */
-public abstract class TextItem extends TaggingPresetItem {
-
-    /** The text to display */
-    public String text; // NOSONAR
-
-    /** The context used for translating {@link #text} */
-    public String text_context; // NOSONAR
-
-    /** The localized version of {@link #text} */
-    public String locale_text; // NOSONAR
-
-    /** The location of icon file to display */
-    public String icon; // NOSONAR
-    /** The size of displayed icon. If not set, default is 16px */
-    public short icon_size = 16; // NOSONAR
-
-    protected final void initializeLocaleText(String defaultText) {
-        if (locale_text == null) {
-            locale_text = getLocaleText(text, text_context, defaultText);
-        }
-    }
-
-    @Override
-    public void addCommands(List<Tag> changedTags) {
-        // Do nothing
-    }
-
-    protected String fieldsToString() {
-        return (text != null ? "text=" + text + ", " : "")
-                + (text_context != null ? "text_context=" + text_context + ", " : "")
-                + (locale_text != null ? "locale_text=" + locale_text : "");
-    }
-
-    /**
-     * Defines the label icon from this entry's icon
-     * @param label the component
-     * @since 17605
-     */
-    protected void addIcon(JLabel label) {
-        label.setIcon(getIcon());
-        label.setHorizontalAlignment(SwingConstants.LEADING);
-    }
-
-    /**
-     * Returns the entry icon, if any.
-     * @return the entry icon, or {@code null}
-     * @since 17605
-     */
-    public ImageIcon getIcon() {
-        return icon == null ? null : loadImageIcon(icon, TaggingPresetReader.getZipIcons(), (int) icon_size);
-    }
-
-    @Override
-    public String toString() {
-        return getClass().getSimpleName() + " [" + fieldsToString() + ']';
-    }
-}
Index: src/org/openstreetmap/josm/gui/widgets/JosmComboBox.java
===================================================================
--- src/org/openstreetmap/josm/gui/widgets/JosmComboBox.java	(revision 18366)
+++ src/org/openstreetmap/josm/gui/widgets/JosmComboBox.java	(working copy)
@@ -59,6 +59,8 @@
     /** greyed text to display in the editor when the selected value is empty */
     private String hint;
 
+    private boolean fakeWidth;
+
     /**
      * Creates a {@code JosmComboBox} with a {@link JosmComboBoxModel} data model.
      * The default data model is an empty list of objects.
@@ -302,6 +304,30 @@
     }
 
     /**
+     * Make popup wider than combobox.
+     */
+    @Override
+    public Dimension getSize() {
+        Dimension dim = super.getSize();
+        if (fakeWidth)
+            dim.width = Math.max(getPreferredSize().width, dim.width);
+        return dim;
+    }
+
+    /**
+     * Helper to make popup wider than combobox.
+     */
+    @Override
+    public void doLayout() {
+        try {
+            fakeWidth = false;
+            super.doLayout();
+        } finally {
+            fakeWidth = true;
+        }
+    }
+
+    /**
      * Get the dropdown list component
      *
      * @return the list or null
@@ -392,11 +418,10 @@
         int freeAbove = bounds.y - screenBounds.y;
         int freeBelow = (screenBounds.y + screenBounds.height) - (bounds.y + bounds.height);
 
-        try {
-            // First try an implementation-dependent method to get the exact number.
-            @SuppressWarnings("unchecked")
-            JList<E> jList = getList();
-
+        int maxRowCount = 8; // default
+        @SuppressWarnings("unchecked")
+        JList<E> jList = getList();
+        if (jList != null) {
             // Calculate the free space available on screen
             Insets insets = jList.getInsets();
             // A small fudge factor that accounts for the displacement of the popup relative to the
@@ -403,8 +428,9 @@
             // combobox and the popup shadow.
             int fudge = 4;
             int free = Math.max(freeAbove, freeBelow) - (insets.top + insets.bottom) - fudge;
-            if (jList.getParent() instanceof JScrollPane) {
-                JScrollPane scroller = (JScrollPane) jList.getParent();
+            Component parent = getParent();
+            if (parent instanceof JScrollPane) {
+                JScrollPane scroller = (JScrollPane) parent;
                 Border border = scroller.getViewportBorder();
                 if (border != null) {
                     insets = border.getBorderInsets(null);
@@ -427,11 +453,10 @@
                 if (h >= free)
                     break;
             }
-            setMaximumRowCount(i);
-            // Logging.debug("free = {0}, h = {1}, i = {2}, bounds = {3}, screenBounds = {4}", free, h, i, bounds, screenBounds);
-        } catch (Exception ex) {
-            setMaximumRowCount(8); // the default
+            maxRowCount = i;
         }
+        // Logging.debug("free = {0}, h = {1}, i = {2}, bounds = {3}, screenBounds = {4}", free, h, i, bounds, screenBounds);
+        setMaximumRowCount(maxRowCount);
     }
 
     @Override
Index: src/org/openstreetmap/josm/gui/widgets/JosmComboBoxModel.java
===================================================================
--- src/org/openstreetmap/josm/gui/widgets/JosmComboBoxModel.java	(revision 18366)
+++ src/org/openstreetmap/josm/gui/widgets/JosmComboBoxModel.java	(working copy)
@@ -205,6 +205,36 @@
     }
 
     /**
+     * Replaces all current elements with elements from the collection.
+     * <p>
+     * This is the same as {@link #removeAllElements} followed by {@link #addAllElements} but
+     * minimizes event firing and tries to keep the current selection.  Use this when all elements
+     * are reinitialized programmatically like in an {@code autoCompBefore} event.
+     *
+     * @param newElements The new elements.
+     */
+    public void replaceAllElements(Collection<E> newElements) {
+        Object oldSelected = selected;
+        int index0 = elements.size();
+        elements.clear();
+        newElements.forEach(e -> doAddElement(e));
+        int index1 = elements.size();
+        int index2 = Math.min(index0, index1);
+        if (0 < index2) {
+            fireContentsChanged(this, 0, index2 - 1);
+        }
+        if (index2 < index0) {
+            fireIntervalRemoved(this, index2, index0 - 1);
+        }
+        if (index2 < index1) {
+            fireIntervalAdded(this, index2, index1 - 1);
+        }
+        // re-select the old selection if possible
+        int index = elements.indexOf(oldSelected);
+        setSelectedItem(index == -1 ? null : getElementAt(index));
+    }
+
+    /**
      * Adds an element to the top of the list.
      * <p>
      * If the element is already in the model, moves it to the top.  If the model gets too big,
Index: src/org/openstreetmap/josm/gui/widgets/JosmTextField.java
===================================================================
--- src/org/openstreetmap/josm/gui/widgets/JosmTextField.java	(revision 18366)
+++ src/org/openstreetmap/josm/gui/widgets/JosmTextField.java	(working copy)
@@ -42,7 +42,7 @@
  */
 public class JosmTextField extends JTextField implements Destroyable, ComponentListener, FocusListener, PropertyChangeListener {
 
-    private final PopupMenuLauncher launcher;
+    private PopupMenuLauncher launcher;
     private String hint;
     private Icon icon;
     private Point iconPos;
@@ -241,6 +241,14 @@
     }
 
     /**
+     * Enables / disables the undo / redo functionality.
+     * @param undoRedo enable if true
+     */
+    public void enableUndoRedo(boolean undoRedo) {
+        launcher = TextContextualPopupMenu.enableMenuFor(this, undoRedo);
+    }
+
+    /**
      * Empties the internal undo manager.
      * @since 14977
      */
Index: src/org/openstreetmap/josm/tools/ImageProvider.java
===================================================================
--- src/org/openstreetmap/josm/tools/ImageProvider.java	(revision 18366)
+++ src/org/openstreetmap/josm/tools/ImageProvider.java	(working copy)
@@ -725,6 +725,20 @@
     }
 
     /**
+     * Returns a Future for the ImageResource.
+     *
+     * This method returns immediately and loads the image asynchronously.
+     *
+     * @return the future of the requested image
+     * @since xxx
+     */
+    public CompletableFuture<ImageResource> getResourceFuture() {
+        return isRemote()
+                ? CompletableFuture.supplyAsync(this::getResource, IMAGE_FETCHER)
+                : CompletableFuture.completedFuture(getResource());
+    }
+
+    /**
      * Load an image with a given file name.
      *
      * @param subdir subdirectory the image lies in
Index: src/org/openstreetmap/josm/tools/MultiMap.java
===================================================================
--- src/org/openstreetmap/josm/tools/MultiMap.java	(revision 18366)
+++ src/org/openstreetmap/josm/tools/MultiMap.java	(working copy)
@@ -124,6 +124,14 @@
     }
 
     /**
+     * Like getValues, but returns all values for all keys.
+     * @return the set of all values or an empty set
+     */
+    public Set<B> getAllValues() {
+        return map.entrySet().stream().flatMap(e -> e.getValue().stream()).collect(Collectors.toSet());
+    }
+
+    /**
      * Returns {@code true} if this map contains no key-value mappings.
      * @return {@code true} if this map contains no key-value mappings
      * @see Map#isEmpty()
Index: src/org/openstreetmap/josm/tools/OsmPrimitiveImageProvider.java
===================================================================
--- src/org/openstreetmap/josm/tools/OsmPrimitiveImageProvider.java	(revision 18366)
+++ src/org/openstreetmap/josm/tools/OsmPrimitiveImageProvider.java	(working copy)
@@ -22,7 +22,6 @@
 import org.openstreetmap.josm.gui.mappaint.styleelement.MapImage;
 import org.openstreetmap.josm.gui.mappaint.styleelement.NodeElement;
 import org.openstreetmap.josm.gui.mappaint.styleelement.StyleElement;
-import org.openstreetmap.josm.gui.tagging.presets.TaggingPreset;
 import org.openstreetmap.josm.gui.tagging.presets.TaggingPresets;
 
 /**
@@ -56,9 +55,10 @@
         // Check if the presets have icons for nodes/relations.
         if (primitive.isTagged() && (!options.contains(Options.NO_WAY_PRESETS) || OsmPrimitiveType.WAY != primitive.getType())) {
             final Optional<ImageResource> icon = TaggingPresets.getMatchingPresets(primitive).stream()
-                    .sorted(Comparator.comparing(p -> (p.iconName != null && p.iconName.contains("multipolygon"))
-                            || Utils.isEmpty(p.types) ? Integer.MAX_VALUE : p.types.size()))
-                    .map(TaggingPreset::getImageResource)
+                    .sorted(Comparator.comparing(p ->
+                        ((p.getIconName() != null && p.getIconName().contains("multipolygon")) || Utils.isEmpty(p.getTypes()))
+                             ? Integer.MAX_VALUE : p.getTypes().size()))
+                    .map(tp -> ImageResource.getAttachedImageResource(tp.getAction()))
                     .filter(Objects::nonNull)
                     .findFirst();
             if (icon.isPresent()) {
Index: src/org/openstreetmap/josm/tools/XmlParsingException.java
===================================================================
--- src/org/openstreetmap/josm/tools/XmlParsingException.java	(revision 18366)
+++ src/org/openstreetmap/josm/tools/XmlParsingException.java	(working copy)
@@ -5,6 +5,7 @@
 
 import org.xml.sax.Locator;
 import org.xml.sax.SAXException;
+import org.xml.sax.helpers.LocatorImpl;
 
 /**
  * An exception thrown during XML parsing, with known line and column.
@@ -11,8 +12,7 @@
  * @since 6906
  */
 public class XmlParsingException extends SAXException {
-    private int columnNumber;
-    private int lineNumber;
+    private LocatorImpl locator = new LocatorImpl();
 
     /**
      * Constructs a new {@code XmlParsingException}.
@@ -46,8 +46,7 @@
      */
     public XmlParsingException rememberLocation(Locator locator) {
         if (locator != null) {
-            this.columnNumber = locator.getColumnNumber();
-            this.lineNumber = locator.getLineNumber();
+            this.locator = new LocatorImpl(locator);
         }
         return this;
     }
@@ -55,12 +54,13 @@
     @Override
     public String getMessage() {
         String msg = super.getMessage();
-        if (lineNumber == 0 && columnNumber == 0)
+        if (getLineNumber() == 0 && getColumnNumber() == 0)
             return msg;
         if (msg == null) {
             msg = getClass().getName();
         }
-        return msg + ' ' + tr("(at line {0}, column {1})", lineNumber, columnNumber);
+        return msg + ' ' + tr("(in {0} at line {1}, column {2})",
+            locator.getSystemId(), getLineNumber(), getColumnNumber());
     }
 
     /**
@@ -68,7 +68,7 @@
      * @return the column number where the exception occurred
      */
     public int getColumnNumber() {
-        return columnNumber;
+        return locator.getColumnNumber();
     }
 
     /**
@@ -76,6 +76,6 @@
      * @return the line number where the exception occurred
      */
     public int getLineNumber() {
-        return lineNumber;
+        return locator.getLineNumber();
     }
 }
Index: test/functional/org/openstreetmap/josm/tools/ImageProviderTest.java
===================================================================
--- test/functional/org/openstreetmap/josm/tools/ImageProviderTest.java	(revision 18366)
+++ test/functional/org/openstreetmap/josm/tools/ImageProviderTest.java	(working copy)
@@ -6,6 +6,7 @@
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.openstreetmap.josm.gui.mappaint.MapCSSRendererTest.assertImageEquals;
+import static org.openstreetmap.josm.gui.tagging.presets.ItemFactory.build;
 
 import java.awt.Dimension;
 import java.awt.Image;
@@ -14,7 +15,6 @@
 import java.awt.image.BufferedImage;
 import java.io.File;
 import java.io.IOException;
-import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 import java.util.function.UnaryOperator;
@@ -34,9 +34,10 @@
 import org.openstreetmap.josm.TestUtils;
 import org.openstreetmap.josm.data.coor.LatLon;
 import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.gui.tagging.presets.Key;
+import org.openstreetmap.josm.gui.tagging.presets.Root;
 import org.openstreetmap.josm.gui.tagging.presets.TaggingPreset;
 import org.openstreetmap.josm.gui.tagging.presets.TaggingPresets;
-import org.openstreetmap.josm.gui.tagging.presets.items.Key;
 import org.openstreetmap.josm.testutils.JOSMTestRules;
 import org.xml.sax.SAXException;
 
@@ -134,17 +135,18 @@
      */
     @Test
     void testTicket19551() throws SAXException {
-        TaggingPreset badPreset = new TaggingPreset();
-        badPreset.setType("node,way,relation,closedway");
-        Key key = new Key();
-        key.key = "amenity";
-        key.value = "fuel";
-        badPreset.data.add(key);
-        TaggingPreset goodPreset = new TaggingPreset();
-        goodPreset.setType("node,way,relation,closedway");
-        goodPreset.data.add(key);
-        goodPreset.iconName = "stop";
-        TaggingPresets.addTaggingPresets(Arrays.asList(goodPreset, badPreset));
+        Root root = (Root) build("root");
+        Key key = (Key) build("key", "key", "amenity", "value", "fuel");
+
+        TaggingPreset badPreset = (TaggingPreset) build("item", "type", "node,way,relation,closedway");
+        badPreset.getAllItems().add(key);
+        root.addItem(badPreset);
+
+        TaggingPreset goodPreset = (TaggingPreset) build("item", "type", "node,way,relation,closedway", "icon_name", "stop");
+        goodPreset.getAllItems().add(key);
+        root.addItem(goodPreset);
+
+        TaggingPresets.addRoot(root);
         Node node = new Node(LatLon.ZERO);
         node.put("amenity", "fuel");
         assertDoesNotThrow(() -> OsmPrimitiveImageProvider.getResource(node, Collections.emptyList()));
Index: test/unit/org/openstreetmap/josm/data/osm/DefaultNameFormatterTest.java
===================================================================
--- test/unit/org/openstreetmap/josm/data/osm/DefaultNameFormatterTest.java	(revision 18366)
+++ test/unit/org/openstreetmap/josm/data/osm/DefaultNameFormatterTest.java	(working copy)
@@ -16,7 +16,6 @@
 
 import org.junit.jupiter.api.Test;
 import org.openstreetmap.josm.TestUtils;
-import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetReader;
 import org.openstreetmap.josm.gui.tagging.presets.TaggingPresets;
 import org.openstreetmap.josm.io.IllegalDataException;
 import org.openstreetmap.josm.io.OsmReader;
@@ -52,6 +51,9 @@
     @Test
     @SuppressFBWarnings(value = "ITA_INEFFICIENT_TO_ARRAY")
     void testTicket9632() throws IllegalDataException, IOException, SAXException {
+
+        DefaultNameFormatter f = DefaultNameFormatter.getInstance();
+
         String source = "presets/Presets_BicycleJunction-preset.xml";
         wireMockServer.stubFor(get(urlEqualTo("/" + source))
                 .willReturn(aResponse()
@@ -58,34 +60,34 @@
                     .withStatus(200)
                     .withHeader("Content-Type", "text/xml")
                     .withBodyFile(source)));
-        TaggingPresets.addTaggingPresets(TaggingPresetReader.readAll(wireMockServer.url(source), true));
 
-        Comparator<IRelation<?>> comparator = DefaultNameFormatter.getInstance().getRelationComparator();
+        TaggingPresets.testInitialize(wireMockServer.url(source));
+        Comparator<IRelation<?>> comparator = f.getRelationComparator();
 
         try (InputStream is = TestUtils.getRegressionDataStream(9632, "data.osm.zip")) {
             DataSet ds = OsmReader.parseDataSet(is, null);
 
+            // CHECKSTYLE.OFF: SingleSpaceSeparator
+            // CHECKSTYLE.OFF: ParenPad
+
             // Test with 3 known primitives causing the problem. Correct order is p1, p3, p2 with this preset
             Relation p1 = (Relation) ds.getPrimitiveById(2983382, OsmPrimitiveType.RELATION);
-            Relation p2 = (Relation) ds.getPrimitiveById(550315, OsmPrimitiveType.RELATION);
-            Relation p3 = (Relation) ds.getPrimitiveById(167042, OsmPrimitiveType.RELATION);
+            Relation p2 = (Relation) ds.getPrimitiveById( 550315, OsmPrimitiveType.RELATION);
+            Relation p3 = (Relation) ds.getPrimitiveById( 167042, OsmPrimitiveType.RELATION);
 
-            // route_master ("Bus 453", 6 members)
-            System.out.println("p1: "+DefaultNameFormatter.getInstance().format(p1)+" - "+p1);
-            // TMC ("A 6 Kaiserslautern - Mannheim [negative]", 123 members)
-            System.out.println("p2: "+DefaultNameFormatter.getInstance().format(p2)+" - "+p2);
-            // route(lcn Sal  Salier-Radweg(412 members)
-            System.out.println("p3: "+DefaultNameFormatter.getInstance().format(p3)+" - "+p3);
+            assertEquals("route_master (\"Bus 453\", 6 members)",                           f.format(p1));
+            assertEquals("TMC (\"A 6 Kaiserslautern - Mannheim [negative]\", 123 members)", f.format(p2));
+            assertEquals("route(lcn Sal  Salier-Radweg(412 members)",                       f.format(p3));
 
-            // CHECKSTYLE.OFF: SingleSpaceSeparator
-            assertEquals(comparator.compare(p1, p2), -1); // p1 < p2
-            assertEquals(comparator.compare(p2, p1),  1); // p2 > p1
+            assertEquals(-1, comparator.compare(p1, p2)); // p1 < p2
+            assertEquals( 1, comparator.compare(p2, p1)); // p2 > p1
+            assertEquals(-1, comparator.compare(p1, p3)); // p1 < p3
+            assertEquals( 1, comparator.compare(p3, p1)); // p3 > p1
+            assertEquals( 1, comparator.compare(p2, p3)); // p2 > p3
+            assertEquals(-1, comparator.compare(p3, p2)); // p3 < p2
 
-            assertEquals(comparator.compare(p1, p3), -1); // p1 < p3
-            assertEquals(comparator.compare(p3, p1),  1); // p3 > p1
-            assertEquals(comparator.compare(p2, p3),  1); // p2 > p3
-            assertEquals(comparator.compare(p3, p2), -1); // p3 < p2
             // CHECKSTYLE.ON: SingleSpaceSeparator
+            // CHECKSTYLE.ON: ParenPad
 
             Relation[] relations = new ArrayList<>(ds.getRelations()).toArray(new Relation[0]);
 
Index: test/unit/org/openstreetmap/josm/data/osm/search/SearchCompilerTest.java
===================================================================
--- test/unit/org/openstreetmap/josm/data/osm/search/SearchCompilerTest.java	(revision 18366)
+++ test/unit/org/openstreetmap/josm/data/osm/search/SearchCompilerTest.java	(working copy)
@@ -7,6 +7,7 @@
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.openstreetmap.josm.gui.tagging.presets.ItemFactory.build;
 
 import java.lang.reflect.Field;
 import java.nio.charset.StandardCharsets;
@@ -13,9 +14,7 @@
 import java.nio.file.Files;
 import java.nio.file.Paths;
 import java.time.Instant;
-import java.util.Arrays;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.Set;
 
 import org.junit.Assert;
@@ -39,11 +38,11 @@
 import org.openstreetmap.josm.data.osm.WayData;
 import org.openstreetmap.josm.data.osm.search.SearchCompiler.ExactKeyValue;
 import org.openstreetmap.josm.data.osm.search.SearchCompiler.Match;
+import org.openstreetmap.josm.gui.tagging.presets.Key;
+import org.openstreetmap.josm.gui.tagging.presets.Root;
 import org.openstreetmap.josm.gui.tagging.presets.TaggingPreset;
 import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetMenu;
-import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetType;
 import org.openstreetmap.josm.gui.tagging.presets.TaggingPresets;
-import org.openstreetmap.josm.gui.tagging.presets.items.Key;
 import org.openstreetmap.josm.testutils.annotations.BasicPreferences;
 import org.openstreetmap.josm.tools.Logging;
 
@@ -602,10 +601,9 @@
      */
     @Test
     void testPresetMultipleWords() {
-        TaggingPreset testPreset = new TaggingPreset();
-        testPreset.name = "Test Combined Preset Name";
-        testPreset.group = new TaggingPresetMenu();
-        testPreset.group.name = "TestGroupName";
+        TaggingPresetMenu testPresetMenu = newTaggingPresetMenu("TestGroupName");
+        TaggingPreset testPreset = newTaggingPreset("Test Combined Preset Name");
+        testPresetMenu.addItem(testPreset);
 
         String combinedPresetname = testPreset.getRawName();
         SearchSetting settings = new SearchSetting();
@@ -628,21 +626,22 @@
      */
     @Test
     void testPresetLookup() throws SearchParseError, NoSuchFieldException, IllegalAccessException {
-        TaggingPreset testPreset = new TaggingPreset();
-        testPreset.name = "Test Preset Name";
-        testPreset.group = new TaggingPresetMenu();
-        testPreset.group.name = "Test Preset Group Name";
+        Root root = newRoot();
 
-        String query = "preset:" +
-                "\"" + testPreset.getRawName() + "\"";
+        TaggingPresetMenu group = newTaggingPresetMenu("Test Preset Group Name");
+        root.addItem(group);
+
+        TaggingPreset testPreset = newTaggingPreset("Test Preset Name");
+        group.addItem(testPreset);
+
+        TaggingPresets.readFromPreferences();
+        TaggingPresets.addRoot(root);
+
+        String query = "preset:\"Test Preset Group Name/Test Preset Name\"";
         SearchSetting settings = new SearchSetting();
         settings.text = query;
         settings.mapCSSSearch = false;
 
-        // load presets and add the test preset
-        TaggingPresets.readFromPreferences();
-        TaggingPresets.addTaggingPresets(Collections.singletonList(testPreset));
-
         Match match = SearchCompiler.compile(settings);
 
         // access the private field where all matching presets are stored
@@ -667,29 +666,28 @@
      */
     @Test
     void testPresetLookupWildcard() throws SearchParseError, NoSuchFieldException, IllegalAccessException {
-        TaggingPresetMenu group = new TaggingPresetMenu();
-        group.name = "TestPresetGroup";
+        Root root = newRoot();
 
-        TaggingPreset testPreset1 = new TaggingPreset();
-        testPreset1.name = "TestPreset1";
-        testPreset1.group = group;
+        TaggingPresetMenu group = newTaggingPresetMenu("TestPresetGroup");
+        root.addItem(group);
 
-        TaggingPreset testPreset2 = new TaggingPreset();
-        testPreset2.name = "TestPreset2";
-        testPreset2.group = group;
+        TaggingPreset testPreset1 = newTaggingPreset("TestPreset1");
+        group.addItem(testPreset1);
 
-        TaggingPreset testPreset3 = new TaggingPreset();
-        testPreset3.name = "TestPreset3";
-        testPreset3.group = group;
+        TaggingPreset testPreset2 = newTaggingPreset("TestPreset2");
+        group.addItem(testPreset2);
 
-        String query = "preset:" + "\"" + group.getRawName() + "/*\"";
+        TaggingPreset testPreset3 = newTaggingPreset("TestPreset3");
+        group.addItem(testPreset3);
+
+        TaggingPresets.readFromPreferences();
+        TaggingPresets.addRoot(root);
+
+        String query = "preset:\"TestPresetGroup/*\"";
         SearchSetting settings = new SearchSetting();
         settings.text = query;
         settings.mapCSSSearch = false;
 
-        TaggingPresets.readFromPreferences();
-        TaggingPresets.addTaggingPresets(Arrays.asList(testPreset1, testPreset2, testPreset3));
-
         Match match = SearchCompiler.compile(settings);
 
         // access the private field where all matching presets are stored
@@ -717,19 +715,19 @@
         final String key = "test_key1";
         final String val = "test_val1";
 
-        Key key1 = new Key();
-        key1.key = key;
-        key1.value = val;
+        Root root = newRoot();
 
-        TaggingPreset testPreset = new TaggingPreset();
-        testPreset.name = presetName;
-        testPreset.types = Collections.singleton(TaggingPresetType.NODE);
-        testPreset.data.add(key1);
-        testPreset.group = new TaggingPresetMenu();
-        testPreset.group.name = presetGroupName;
+        TaggingPresetMenu group = newTaggingPresetMenu(presetGroupName);
+        root.addItem(group);
 
+        TaggingPreset testPreset = (TaggingPreset) build("item name={0} type=node", presetName);
+        group.addItem(testPreset);
+
+        Key key1 = (Key) build("key key={0} value={1}", key, val);
+        testPreset.addItem(key1);
+
         TaggingPresets.readFromPreferences();
-        TaggingPresets.addTaggingPresets(Collections.singleton(testPreset));
+        TaggingPresets.addRoot(root);
 
         String query = "preset:" + "\"" + testPreset.getRawName() + "\"";
 
@@ -763,12 +761,18 @@
         }
     }
 
+    private static Root newRoot() {
+        return (Root) build("presets");
+    }
+
     private static TaggingPreset newTaggingPreset(String name) {
-        TaggingPreset result = new TaggingPreset();
-        result.name = name;
-        return result;
+        return (TaggingPreset) build("item name={0}", name);
     }
 
+    private static TaggingPresetMenu newTaggingPresetMenu(String name) {
+        return (TaggingPresetMenu) build("group name={0}", name);
+    }
+
     /**
      * Search for {@code nodes:2}.
      * @throws SearchParseError if an error has been encountered while compiling
@@ -843,6 +847,7 @@
 
     /**
      * Non-regression test for JOSM #21463
+     * @throws SearchParseError in case of parser error
      */
     @Test
     void testNonRegression21463() throws SearchParseError {
Index: test/unit/org/openstreetmap/josm/data/validation/tests/OpeningHourTestTest.java
===================================================================
--- test/unit/org/openstreetmap/josm/data/validation/tests/OpeningHourTestTest.java	(revision 18366)
+++ test/unit/org/openstreetmap/josm/data/validation/tests/OpeningHourTestTest.java	(working copy)
@@ -9,8 +9,8 @@
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
+import java.io.IOException;
 import java.util.Arrays;
-import java.util.Collection;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Locale;
@@ -24,14 +24,14 @@
 import org.openstreetmap.josm.data.preferences.sources.ValidatorPrefHelper;
 import org.openstreetmap.josm.data.validation.Severity;
 import org.openstreetmap.josm.data.validation.TestError;
+import org.openstreetmap.josm.gui.tagging.presets.Item;
+import org.openstreetmap.josm.gui.tagging.presets.KeyedItem;
 import org.openstreetmap.josm.gui.tagging.presets.TaggingPreset;
-import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItem;
-import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetReader;
-import org.openstreetmap.josm.gui.tagging.presets.items.KeyedItem;
+import org.openstreetmap.josm.gui.tagging.presets.TaggingPresets;
 import org.openstreetmap.josm.testutils.annotations.BasicPreferences;
 import org.openstreetmap.josm.testutils.annotations.I18n;
 import org.openstreetmap.josm.tools.Logging;
-
+import org.xml.sax.SAXException;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
@@ -216,15 +216,16 @@
      * Tests that predefined values in presets are correct.
      */
     @Test
-    void testPresetValues() {
-        final Collection<TaggingPreset> presets = TaggingPresetReader.readFromPreferences(false, false);
+    void testPresetValues() throws SAXException, IOException {
+
+        TaggingPresets.testInitialize("resource://data/defaultpresets.xml");
         final Set<Tag> values = new LinkedHashSet<>();
-        for (final TaggingPreset p : presets) {
-            for (final TaggingPresetItem i : p.data) {
+        for (final TaggingPreset p : TaggingPresets.getTaggingPresets()) {
+            for (final Item i : p.getAllItems()) {
                 if (i instanceof KeyedItem &&
-                        Arrays.asList("opening_hours", "service_times", "collection_times").contains(((KeyedItem) i).key)) {
+                        Arrays.asList("opening_hours", "service_times", "collection_times").contains(((KeyedItem) i).getKey())) {
                     for (final String v : ((KeyedItem) i).getValues()) {
-                        values.add(new Tag(((KeyedItem) i).key, v));
+                        values.add(new Tag(((KeyedItem) i).getKey(), v));
                     }
                 }
             }
Index: test/unit/org/openstreetmap/josm/gui/dialogs/relation/GenericRelationEditorTest.java
===================================================================
--- test/unit/org/openstreetmap/josm/gui/dialogs/relation/GenericRelationEditorTest.java	(revision 18366)
+++ test/unit/org/openstreetmap/josm/gui/dialogs/relation/GenericRelationEditorTest.java	(working copy)
@@ -20,7 +20,6 @@
 import org.openstreetmap.josm.data.osm.Way;
 import org.openstreetmap.josm.gui.layer.OsmDataLayer;
 import org.openstreetmap.josm.gui.tagging.TagEditorPanel;
-import org.openstreetmap.josm.gui.tagging.ac.AutoCompletingTextField;
 import org.openstreetmap.josm.testutils.JOSMTestRules;
 import org.openstreetmap.josm.testutils.mockers.JOptionPaneSimpleMocker;
 
@@ -123,9 +122,6 @@
         OsmDataLayer layer = new OsmDataLayer(ds, "test", null);
         IRelationEditor re = newRelationEditor(relation, layer);
 
-        AutoCompletingTextField tfRole = GenericRelationEditor.buildRoleTextField(re);
-        assertNotNull(tfRole);
-
         TagEditorPanel tagEditorPanel = new TagEditorPanel(relation, null);
 
         JPanel top = GenericRelationEditor.buildTagEditorPanel(tagEditorPanel);
Index: test/unit/org/openstreetmap/josm/gui/dialogs/relation/MemberTableModelTest.java
===================================================================
--- test/unit/org/openstreetmap/josm/gui/dialogs/relation/MemberTableModelTest.java	(revision 18366)
+++ test/unit/org/openstreetmap/josm/gui/dialogs/relation/MemberTableModelTest.java	(working copy)
@@ -33,7 +33,7 @@
             }
 
             @Override
-            public Collection<OsmPrimitive> getSelection() {
+            public Collection<OsmPrimitive> getPrimitives() {
                 return Collections.<OsmPrimitive>singleton(n);
             }
         }).getRelationMemberForPrimitive(n));
Index: test/unit/org/openstreetmap/josm/gui/dialogs/relation/actions/AbstractRelationEditorActionTest.java
===================================================================
--- test/unit/org/openstreetmap/josm/gui/dialogs/relation/actions/AbstractRelationEditorActionTest.java	(revision 18366)
+++ test/unit/org/openstreetmap/josm/gui/dialogs/relation/actions/AbstractRelationEditorActionTest.java	(working copy)
@@ -12,6 +12,7 @@
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
 import org.openstreetmap.josm.data.osm.Relation;
 import org.openstreetmap.josm.data.osm.Tag;
+import org.openstreetmap.josm.data.tagging.ac.AutoCompletionItem;
 import org.openstreetmap.josm.gui.dialogs.relation.GenericRelationEditorTest;
 import org.openstreetmap.josm.gui.dialogs.relation.IRelationEditor;
 import org.openstreetmap.josm.gui.dialogs.relation.MemberTable;
@@ -20,7 +21,10 @@
 import org.openstreetmap.josm.gui.dialogs.relation.SelectionTableModel;
 import org.openstreetmap.josm.gui.layer.OsmDataLayer;
 import org.openstreetmap.josm.gui.tagging.TagEditorModel;
-import org.openstreetmap.josm.gui.tagging.ac.AutoCompletingTextField;
+import org.openstreetmap.josm.gui.tagging.ac.AutoCompComboBox;
+import org.openstreetmap.josm.gui.tagging.ac.AutoCompEvent;
+import org.openstreetmap.josm.gui.tagging.ac.AutoCompListener;
+import org.openstreetmap.josm.gui.tagging.ac.AutoCompTextField;
 import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetHandler;
 import org.openstreetmap.josm.testutils.JOSMTestRules;
 
@@ -31,7 +35,7 @@
  * @author Michael Zangl
  */
 @Disabled
-public abstract class AbstractRelationEditorActionTest {
+public abstract class AbstractRelationEditorActionTest implements AutoCompListener {
     /**
      * Platform for tooltips.
      */
@@ -46,13 +50,13 @@
     private IRelationEditor editor;
     private MemberTable memberTable;
     private MemberTableModel memberTableModel;
-    private AutoCompletingTextField tfRole;
+    private AutoCompTextField<AutoCompletionItem> tfRole;
     private TagEditorModel tagModel;
 
     protected final IRelationEditorActionAccess relationEditorAccess = new IRelationEditorActionAccess() {
 
         @Override
-        public AutoCompletingTextField getTextFieldRole() {
+        public AutoCompTextField<AutoCompletionItem> getTextFieldRole() {
             return tfRole;
         }
 
@@ -102,7 +106,7 @@
             }
 
             @Override
-            public Collection<OsmPrimitive> getSelection() {
+            public Collection<OsmPrimitive> getPrimitives() {
                 return Collections.<OsmPrimitive>singleton(orig);
             }
         });
@@ -109,8 +113,16 @@
         selectionTableModel = new SelectionTableModel(layer);
         selectionTable = new SelectionTable(selectionTableModel, memberTableModel);
         editor = GenericRelationEditorTest.newRelationEditor(orig, layer);
-        tfRole = new AutoCompletingTextField();
+        tfRole = new AutoCompTextField<>();
         tagModel = new TagEditorModel();
-        memberTable = new MemberTable(layer, editor.getRelation(), memberTableModel);
+        memberTable = new MemberTable(layer, new AutoCompComboBox<String>(), memberTableModel);
     }
+
+    @Override
+    public void autoCompBefore(AutoCompEvent e) {
+    }
+
+    @Override
+    public void autoCompPerformed(AutoCompEvent e) {
+    }
 }
Index: test/unit/org/openstreetmap/josm/gui/preferences/map/TaggingPresetPreferenceTestIT.java
===================================================================
--- test/unit/org/openstreetmap/josm/gui/preferences/map/TaggingPresetPreferenceTestIT.java	(revision 18366)
+++ test/unit/org/openstreetmap/josm/gui/preferences/map/TaggingPresetPreferenceTestIT.java	(working copy)
@@ -14,6 +14,8 @@
 import java.util.List;
 import java.util.Locale;
 import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
 
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.extension.RegisterExtension;
@@ -22,10 +24,10 @@
 import org.openstreetmap.josm.TestUtils;
 import org.openstreetmap.josm.data.preferences.sources.ExtendedSourceEntry;
 import org.openstreetmap.josm.gui.preferences.AbstractExtendedSourceEntryTestCase;
+import org.openstreetmap.josm.gui.tagging.presets.Link;
 import org.openstreetmap.josm.gui.tagging.presets.TaggingPreset;
-import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetReader;
-import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetsTest;
-import org.openstreetmap.josm.gui.tagging.presets.items.Link;
+import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetUtils;
+import org.openstreetmap.josm.gui.tagging.presets.TaggingPresets;
 import org.openstreetmap.josm.spi.preferences.Config;
 import org.openstreetmap.josm.testutils.JOSMTestRules;
 import org.openstreetmap.josm.tools.HttpClient;
@@ -108,35 +110,29 @@
 
     private void testPresets(Set<String> messages, ExtendedSourceEntry source, List<String> ignoredErrors)
             throws SAXException, IOException {
-        Collection<TaggingPreset> presets = TaggingPresetReader.readAll(source.url, true);
+        TaggingPresets.testInitialize(source.url);
+        Collection<TaggingPreset> presets = TaggingPresets.getTaggingPresets();
         assertFalse(presets.isEmpty());
-        TaggingPresetsTest.waitForIconLoading(presets);
+        try {
+            TaggingPresetUtils.waitForIconsLoaded(presets, 30);
+        } catch (InterruptedException | ExecutionException | TimeoutException e1) {
+            addOrIgnoreError(source, messages, e1.getMessage(), ignoredErrors);
+        }
         // check that links are correct and not redirections
-        presets.parallelStream().flatMap(x -> x.data.stream().filter(i -> i instanceof Link).map(i -> ((Link) i).getUrl())).forEach(u -> {
+        presets.parallelStream().flatMap(x -> x.getAllItems(Link.class, false).stream()).map(link -> link.getUrl()).forEach(url -> {
             try {
-                Response cr = HttpClient.create(new URL(u), "HEAD").setMaxRedirects(-1).connect();
+                Response cr = HttpClient.create(new URL(url), "HEAD").setMaxRedirects(-1).connect();
                 final int code = cr.getResponseCode();
                 if (HttpClient.isRedirect(code)) {
                     addOrIgnoreError(source, messages,
-                            "Found HTTP redirection for " + u + " -> " + code + " -> " + cr.getHeaderField("Location"), ignoredErrors);
+                            "Found HTTP redirection for " + url + " -> " + code + " -> " + cr.getHeaderField("Location"), ignoredErrors);
                 } else if (code >= 400) {
-                    addOrIgnoreError(source, messages, "Found HTTP error for " + u + " -> " + code, ignoredErrors);
+                    addOrIgnoreError(source, messages, "Found HTTP error for " + url + " -> " + code, ignoredErrors);
                 }
             } catch (IOException e) {
-                Logging.error(e);
+                addOrIgnoreError(source, messages, e.getMessage(), ignoredErrors);
             }
         });
-        Collection<String> errorsAndWarnings = Logging.getLastErrorAndWarnings();
-        boolean error = false;
-        for (String message : errorsAndWarnings) {
-            if (message.contains(TaggingPreset.PRESET_ICON_ERROR_MSG_PREFIX)) {
-                error = true;
-                addOrIgnoreError(source, messages, message, ignoredErrors);
-            }
-        }
-        if (error) {
-            Logging.clearLastErrorAndWarnings();
-        }
     }
 
     void addOrIgnoreError(ExtendedSourceEntry source, Set<String> messages, String message, List<String> ignoredErrors) {
Index: test/unit/org/openstreetmap/josm/gui/tagging/presets/CheckGroupTest.java
===================================================================
--- test/unit/org/openstreetmap/josm/gui/tagging/presets/CheckGroupTest.java	(nonexistent)
+++ test/unit/org/openstreetmap/josm/gui/tagging/presets/CheckGroupTest.java	(working copy)
@@ -0,0 +1,38 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.tagging.presets;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import javax.swing.JPanel;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.openstreetmap.josm.JOSMFixture;
+
+/**
+ * Unit tests of {@link CheckGroup} class.
+ */
+class CheckGroupTest {
+
+    /**
+     * Setup test.
+     */
+    @BeforeAll
+    public static void setUp() {
+        JOSMFixture.createUnitTestFixture().init();
+    }
+
+    /**
+     * Unit test for {@link CheckGroup#addToPanel}.
+     */
+    @Test
+    void testAddToPanel() {
+        CheckGroup cg = (CheckGroup) ItemFactory.build("checkgroup");
+        JPanel p = new JPanel();
+        assertEquals(0, p.getComponentCount());
+        assertFalse(cg.addToPanel(p, TaggingPresetInstance.createTest()));
+        assertTrue(p.getComponentCount() > 0);
+    }
+}
Index: test/unit/org/openstreetmap/josm/gui/tagging/presets/CheckTest.java
===================================================================
--- test/unit/org/openstreetmap/josm/gui/tagging/presets/CheckTest.java	(nonexistent)
+++ test/unit/org/openstreetmap/josm/gui/tagging/presets/CheckTest.java	(working copy)
@@ -0,0 +1,36 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.tagging.presets;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import javax.swing.JPanel;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.openstreetmap.josm.JOSMFixture;
+
+/**
+ * Unit tests of {@link Check} class.
+ */
+class CheckTest {
+
+    /**
+     * Setup test.
+     */
+    @BeforeAll
+    public static void setUp() {
+        JOSMFixture.createUnitTestFixture().init(true);
+    }
+
+    /**
+     * Unit test for {@link Check#addToPanel}.
+     */
+    @Test
+    void testAddToPanel() {
+        JPanel p = new JPanel();
+        assertEquals(0, p.getComponentCount());
+        assertTrue(ItemFactory.build("check").addToPanel(p, TaggingPresetInstance.createTest()));
+        assertTrue(p.getComponentCount() > 0);
+    }
+}
Index: test/unit/org/openstreetmap/josm/gui/tagging/presets/ComboTest.java
===================================================================
--- test/unit/org/openstreetmap/josm/gui/tagging/presets/ComboTest.java	(nonexistent)
+++ test/unit/org/openstreetmap/josm/gui/tagging/presets/ComboTest.java	(working copy)
@@ -0,0 +1,147 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.tagging.presets;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.awt.Color;
+
+import javax.swing.JPanel;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.openstreetmap.josm.JOSMFixture;
+import org.openstreetmap.josm.data.osm.OsmUtils;
+import org.openstreetmap.josm.tools.I18n;
+
+/**
+ * Unit tests of {@link Combo} class.
+ */
+class ComboTest {
+
+    /**
+     * Setup test.
+     */
+    @BeforeAll
+    public static void setUp() {
+        JOSMFixture.createUnitTestFixture().init(true);
+        I18n.init();
+        I18n.set("de");
+    }
+
+    /**
+     * Unit test for {@link Combo#addToPanel}.
+     */
+    @Test
+    void testAddToPanel() {
+        JPanel p = new JPanel();
+        assertEquals(0, p.getComponentCount());
+        assertTrue(ItemFactory.build("check").addToPanel(p, TaggingPresetInstance.createTest()));
+        assertTrue(p.getComponentCount() > 0);
+    }
+
+    void is(String expected, Combo combo, TaggingPresetInstance presetInstance) {
+        JPanel p = new JPanel();
+        combo.addToPanel(p, presetInstance);
+        Combo.Instance instance = (Combo.Instance) presetInstance.getInstance(combo);
+        assertEquals(expected, instance.getSelectedItem().getValue());
+    }
+
+    /**
+     * Unit test for {@link ComboMultiSelect#useLastAsDefault} and {@link ComboMultiSelect.Instance#getInitialValue}
+     */
+    @Test
+    void testUseLastAsDefault() {
+        KeyedItem.LAST_VALUES.clear();
+        KeyedItem.LAST_VALUES.put("addr:country", "AT");
+        Combo.PROP_FILL_DEFAULT.put(false);
+
+        TaggingPresetInstance way = TaggingPresetInstance.createTest(OsmUtils.createPrimitive("way"));
+        TaggingPresetInstance wayTagged = TaggingPresetInstance.createTest(OsmUtils.createPrimitive("way highway=residential"));
+        TaggingPresetInstance wayAT = TaggingPresetInstance.createTest(OsmUtils.createPrimitive("way addr:country=AT"));
+        TaggingPresetInstance waySI = TaggingPresetInstance.createTest(OsmUtils.createPrimitive("way addr:country=SI"));
+        TaggingPresetInstance waysATSI = TaggingPresetInstance.createTest(
+            OsmUtils.createPrimitive("way addr:country=AT"), OsmUtils.createPrimitive("way addr:country=SI"));
+
+        String desc = "combo key=addr:country values_from=java.util.Locale#getISOCountries";
+        Combo combo = (Combo) ItemFactory.build(desc);
+        combo.endElement();
+        is("", combo, way);
+        is("", combo, wayTagged);
+        is("AT", combo, wayAT);
+        is("SI", combo, waySI);
+        is(Combo.DIFFERENT, combo, waysATSI);
+
+        combo = (Combo) ItemFactory.build(desc + " default=AT");
+        combo.endElement();
+        is("AT", combo, way);
+        is("", combo, wayTagged);
+        is("AT", combo, wayAT);
+        is("SI", combo, waySI);
+        is(Combo.DIFFERENT, combo, waysATSI);
+
+        Combo.PROP_FILL_DEFAULT.put(true);
+
+        is("AT", combo, way);
+        is("AT", combo, wayTagged);
+        is("AT", combo, wayAT);
+        is("SI", combo, waySI);
+        is(Combo.DIFFERENT, combo, waysATSI);
+
+        Combo.PROP_FILL_DEFAULT.put(false);
+
+        combo = (Combo) ItemFactory.build(desc + " use_last_as_default=true");
+        combo.endElement();
+
+        is("AT", combo, way);
+        is("", combo, wayTagged);
+        is("AT", combo, wayAT);
+        is("SI", combo, waySI);
+        is(Combo.DIFFERENT, combo, waysATSI);
+
+        combo = (Combo) ItemFactory.build(desc + " use_last_as_default=force");
+        combo.endElement();
+
+        is("AT", combo, way);
+        is("AT", combo, wayTagged);
+        is("AT", combo, wayAT);
+        is("SI", combo, waySI);
+        is(Combo.DIFFERENT, combo, waysATSI);
+
+        KeyedItem.LAST_VALUES.clear();
+    }
+
+    @Test
+    void testColor() {
+        TaggingPresetInstance presetInstance = TaggingPresetInstance.createTest();
+        Combo combo = (Combo) ItemFactory.build("combo key=colour values=red;green;blue;black values_context=color delimiter=;");
+        combo.endElement();
+        combo.addToPanel(new JPanel(), presetInstance);
+        Combo.Instance comboInstance = (Combo.Instance) presetInstance.getInstance(combo);
+
+        assertEquals(5, comboInstance.combobox.getItemCount());
+
+        PresetListEntry.Instance i = comboInstance.find("red");
+        comboInstance.combobox.setSelectedItem(i);
+        assertEquals("red", comboInstance.getSelectedItem().getValue());
+        assertEquals("Rot", comboInstance.getSelectedItem().toString());
+        assertEquals(new Color(0xFF0000), comboInstance.getColor());
+
+        i = comboInstance.find("green");
+        comboInstance.combobox.setSelectedItem(i);
+        assertEquals("green", i.getValue());
+        assertEquals("Grün", i.toString());
+        assertEquals(new Color(0x008000), comboInstance.getColor());
+
+        comboInstance.combobox.setSelectedItem("#135");
+        assertEquals("#135", comboInstance.getSelectedItem().getValue());
+        assertEquals(new Color(0x113355), comboInstance.getColor());
+
+        comboInstance.combobox.setSelectedItem("#123456");
+        assertEquals("#123456", comboInstance.getSelectedItem().getValue());
+        assertEquals(new Color(0x123456), comboInstance.getColor());
+
+        comboInstance.setColor(new Color(0x448822));
+        assertEquals("#448822", comboInstance.getSelectedItem().getValue());
+    }
+}
Index: test/unit/org/openstreetmap/josm/gui/tagging/presets/ItemSeparatorTest.java
===================================================================
--- test/unit/org/openstreetmap/josm/gui/tagging/presets/ItemSeparatorTest.java	(nonexistent)
+++ test/unit/org/openstreetmap/josm/gui/tagging/presets/ItemSeparatorTest.java	(working copy)
@@ -0,0 +1,37 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.tagging.presets;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import javax.swing.JPanel;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.openstreetmap.josm.JOSMFixture;
+
+/**
+ * Unit tests of {@link ItemSeparator} class.
+ */
+class ItemSeparatorTest {
+
+    /**
+     * Setup test.
+     */
+    @BeforeAll
+    public static void setUp() {
+        JOSMFixture.createUnitTestFixture().init(true);
+    }
+
+    /**
+     * Unit test for {@link ItemSeparator#addToPanel}.
+     */
+    @Test
+    void testAddToPanel() {
+        JPanel p = new JPanel();
+        assertEquals(0, p.getComponentCount());
+        assertFalse(ItemFactory.build("item_separator").addToPanel(p, TaggingPresetInstance.createTest()));
+        assertTrue(p.getComponentCount() > 0);
+    }
+}
Index: test/unit/org/openstreetmap/josm/gui/tagging/presets/KeyTest.java
===================================================================
--- test/unit/org/openstreetmap/josm/gui/tagging/presets/KeyTest.java	(nonexistent)
+++ test/unit/org/openstreetmap/josm/gui/tagging/presets/KeyTest.java	(working copy)
@@ -0,0 +1,36 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.tagging.presets;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+import javax.swing.JPanel;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.openstreetmap.josm.JOSMFixture;
+
+/**
+ * Unit tests of {@link Key} class.
+ */
+class KeyTest {
+
+    /**
+     * Setup test.
+     */
+    @BeforeAll
+    public static void setUp() {
+        JOSMFixture.createUnitTestFixture().init();
+    }
+
+    /**
+     * Unit test for {@link Key#addToPanel}.
+     */
+    @Test
+    void testAddToPanel() {
+        JPanel p = new JPanel();
+        assertEquals(0, p.getComponentCount());
+        assertFalse(ItemFactory.build("key").addToPanel(p, TaggingPresetInstance.createTest()));
+        assertEquals(0, p.getComponentCount());
+    }
+}
Index: test/unit/org/openstreetmap/josm/gui/tagging/presets/LabelTest.java
===================================================================
--- test/unit/org/openstreetmap/josm/gui/tagging/presets/LabelTest.java	(nonexistent)
+++ test/unit/org/openstreetmap/josm/gui/tagging/presets/LabelTest.java	(working copy)
@@ -0,0 +1,36 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.tagging.presets;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import javax.swing.JPanel;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.openstreetmap.josm.JOSMFixture;
+
+/**
+ * Unit tests of {@link Label} class.
+ */
+class LabelTest {
+
+    /**
+     * Setup test.
+     */
+    @BeforeAll
+    public static void setUp() {
+        JOSMFixture.createUnitTestFixture().init();
+    }
+
+    /**
+     * Unit test for {@link Label#addToPanel}.
+     */
+    @Test
+    void testAddToPanel() {
+        JPanel p = new JPanel();
+        assertEquals(0, p.getComponentCount());
+        assertTrue(ItemFactory.build("label").addToPanel(p, TaggingPresetInstance.createTest()));
+        assertTrue(p.getComponentCount() > 0);
+    }
+}
Index: test/unit/org/openstreetmap/josm/gui/tagging/presets/LinkTest.java
===================================================================
--- test/unit/org/openstreetmap/josm/gui/tagging/presets/LinkTest.java	(nonexistent)
+++ test/unit/org/openstreetmap/josm/gui/tagging/presets/LinkTest.java	(working copy)
@@ -0,0 +1,49 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.tagging.presets;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.openstreetmap.josm.gui.tagging.presets.ItemFactory.build;
+
+import javax.swing.JPanel;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.openstreetmap.josm.JOSMFixture;
+import org.openstreetmap.josm.spi.preferences.Config;
+
+/**
+ * Unit tests of {@link Link} class.
+ */
+class LinkTest {
+
+    /**
+     * Setup test.
+     */
+    @BeforeAll
+    public static void setUp() {
+        JOSMFixture.createUnitTestFixture().init();
+    }
+
+    /**
+     * Unit test for {@link Link#addToPanel}.
+     */
+    @Test
+    void testAddToPanel() {
+        JPanel p = new JPanel();
+        Link l = (Link) build("link");
+        assertFalse(l.addToPanel(p, TaggingPresetInstance.createTest()));
+        assertEquals(0, p.getComponentCount());
+
+        p = new JPanel();
+        l = (Link) build("link href=" + Config.getUrls().getJOSMWebsite());
+        assertFalse(l.addToPanel(p, TaggingPresetInstance.createTest()));
+        assertTrue(p.getComponentCount() > 0);
+
+        p = new JPanel();
+        l = (Link) build("link locale_href=" + Config.getUrls().getJOSMWebsite());
+        assertFalse(l.addToPanel(p, TaggingPresetInstance.createTest()));
+        assertTrue(p.getComponentCount() > 0);
+    }
+}
Index: test/unit/org/openstreetmap/josm/gui/tagging/presets/MultiSelectTest.java
===================================================================
--- test/unit/org/openstreetmap/josm/gui/tagging/presets/MultiSelectTest.java	(nonexistent)
+++ test/unit/org/openstreetmap/josm/gui/tagging/presets/MultiSelectTest.java	(working copy)
@@ -0,0 +1,35 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.tagging.presets;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import javax.swing.JPanel;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.openstreetmap.josm.JOSMFixture;
+
+/**
+ * Unit tests of {@link MultiSelect} class.
+ */
+class MultiSelectTest {
+
+    /**
+     * Setup test.
+     */
+    @BeforeAll
+    public static void setUp() {
+        JOSMFixture.createUnitTestFixture().init();
+    }
+
+    /**
+     * Unit test for {@link MultiSelect#addToPanel}.
+     */
+    @Test
+    void testAddToPanel() {
+        JPanel p = new JPanel();
+        assertEquals(0, p.getComponentCount());
+        // cannot build a cms directly, only combo or multiselect
+        // assertTrue(p.getComponentCount() > 0);
+    }
+}
Index: test/unit/org/openstreetmap/josm/gui/tagging/presets/OptionalTest.java
===================================================================
--- test/unit/org/openstreetmap/josm/gui/tagging/presets/OptionalTest.java	(nonexistent)
+++ test/unit/org/openstreetmap/josm/gui/tagging/presets/OptionalTest.java	(working copy)
@@ -0,0 +1,38 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.tagging.presets;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import javax.swing.JPanel;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.openstreetmap.josm.JOSMFixture;
+
+/**
+ * Unit tests of {@link Optional} class.
+ */
+class OptionalTest {
+
+    /**
+     * Setup test.
+     */
+    @BeforeAll
+    public static void setUp() {
+        JOSMFixture.createUnitTestFixture().init();
+    }
+
+    /**
+     * Unit test for {@link Optional#addToPanel}.
+     */
+    @Test
+    void testAddToPanel() {
+        JPanel p = new JPanel();
+        assertEquals(0, p.getComponentCount());
+        assertFalse(ItemFactory.build("optional").addToPanel(p, TaggingPresetInstance.createTest()));
+        // do not add anything if there are no optional items
+        assertTrue(p.getComponentCount() == 0);
+    }
+}
Index: test/unit/org/openstreetmap/josm/gui/tagging/presets/PresetClassificationsTest.java
===================================================================
--- test/unit/org/openstreetmap/josm/gui/tagging/presets/PresetClassificationsTest.java	(revision 18366)
+++ test/unit/org/openstreetmap/josm/gui/tagging/presets/PresetClassificationsTest.java	(working copy)
@@ -5,7 +5,6 @@
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import java.io.IOException;
-import java.util.Collection;
 import java.util.Collections;
 import java.util.EnumSet;
 import java.util.List;
@@ -20,6 +19,7 @@
 import org.openstreetmap.josm.data.osm.Way;
 import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetSelector.PresetClassification;
 import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetSelector.PresetClassifications;
+import org.openstreetmap.josm.tools.Logging;
 import org.xml.sax.SAXException;
 
 /**
@@ -37,8 +37,8 @@
     @BeforeAll
     public static void setUp() throws IOException, SAXException {
         JOSMFixture.createUnitTestFixture().init();
-        final Collection<TaggingPreset> presets = TaggingPresetReader.readAll("resource://data/defaultpresets.xml", true);
-        classifications.loadPresets(presets);
+        TaggingPresets.testInitialize("resource://data/defaultpresets.xml");
+        classifications.loadPresets(TaggingPresets.getTaggingPresets());
     }
 
     private List<PresetClassification> getMatchingPresets(String searchText, OsmPrimitive w) {
@@ -47,7 +47,7 @@
     }
 
     private List<String> getMatchingPresetNames(String searchText, OsmPrimitive w) {
-        return getMatchingPresets(searchText, w).stream().map(x -> x.preset.name).collect(Collectors.toList());
+        return getMatchingPresets(searchText, w).stream().map(x -> x.preset.getBaseName()).collect(Collectors.toList());
     }
 
     /**
@@ -60,8 +60,10 @@
         w.addNode(n1);
         w.addNode(new Node());
         w.addNode(new Node());
+        Logging.info(getMatchingPresetNames("building", w).toString());
         assertFalse(getMatchingPresetNames("building", w).contains("Building"), "unclosed way should not match building preset");
         w.addNode(n1);
+        Logging.info(getMatchingPresetNames("building", w).toString());
         assertTrue(getMatchingPresetNames("building", w).contains("Building"), "closed way should match building preset");
     }
 
Index: test/unit/org/openstreetmap/josm/gui/tagging/presets/PresetLinkTest.java
===================================================================
--- test/unit/org/openstreetmap/josm/gui/tagging/presets/PresetLinkTest.java	(nonexistent)
+++ test/unit/org/openstreetmap/josm/gui/tagging/presets/PresetLinkTest.java	(working copy)
@@ -0,0 +1,39 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.tagging.presets;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import javax.swing.JPanel;
+
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.junit.jupiter.api.Test;
+import org.openstreetmap.josm.testutils.JOSMTestRules;
+
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+
+/**
+ * Unit tests of {@link PresetLink} class.
+ */
+class PresetLinkTest {
+
+    /**
+     * Setup test.
+     */
+    @RegisterExtension
+    @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
+    public JOSMTestRules rule = new JOSMTestRules().presets();
+
+    /**
+     * Unit test for {@link PresetLink#addToPanel}.
+     */
+    @Test
+    void testAddToPanel() {
+        PresetLink l = (PresetLink) ItemFactory.build("preset_link preset_name=River");
+        JPanel p = new JPanel();
+        assertEquals(0, p.getComponentCount());
+        assertFalse(l.addToPanel(p, TaggingPresetInstance.createTest()));
+        assertTrue(p.getComponentCount() > 0);
+    }
+}
Index: test/unit/org/openstreetmap/josm/gui/tagging/presets/PresetListEntryTest.java
===================================================================
--- test/unit/org/openstreetmap/josm/gui/tagging/presets/PresetListEntryTest.java	(nonexistent)
+++ test/unit/org/openstreetmap/josm/gui/tagging/presets/PresetListEntryTest.java	(working copy)
@@ -0,0 +1,55 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.tagging.presets;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import javax.swing.JPanel;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.openstreetmap.josm.JOSMFixture;
+
+/**
+ * Unit tests of {@link PresetListEntry} class.
+ */
+class PresetListEntryTest {
+
+    /**
+     * Setup test.
+     */
+    @BeforeAll
+    public static void setUp() {
+        JOSMFixture.createUnitTestFixture().init(true);
+    }
+
+    /**
+     * Non-regression test for ticket <a href="https://josm.openstreetmap.de/ticket/12416">#12416</a>.
+     */
+    @Test
+    void testTicket12416() {
+        Combo combo = (Combo) ItemFactory.build("combo key=foo");
+        PresetListEntry entry = (PresetListEntry) ItemFactory.build("list_entry value=");
+        combo.addItem(entry);
+
+        JPanel panel = new JPanel();
+        TaggingPresetInstance instance = TaggingPresetInstance.createTest();
+        combo.addToPanel(panel, instance);
+        assertTrue(entry.newInstance((Combo.Instance) instance.getInstance(combo)).getListDisplay(200).contains(" "));
+    }
+
+    /**
+     * Non-regression test for ticket <a href="https://josm.openstreetmap.de/ticket/21550">#21550</a>
+     */
+    @Test
+    void testTicket21550() {
+        Combo combo = (Combo) ItemFactory.build("combo key=foo");
+        PresetListEntry entry = (PresetListEntry) ItemFactory.build("list_entry value=");
+        combo.addItem(entry);
+
+        JPanel panel = new JPanel();
+        TaggingPresetInstance instance = TaggingPresetInstance.createTest();
+        combo.addToPanel(panel, instance);
+        assertDoesNotThrow(entry.newInstance((Combo.Instance) instance.getInstance(combo))::getCount);
+    }
+}
Index: test/unit/org/openstreetmap/josm/gui/tagging/presets/RolesTest.java
===================================================================
--- test/unit/org/openstreetmap/josm/gui/tagging/presets/RolesTest.java	(nonexistent)
+++ test/unit/org/openstreetmap/josm/gui/tagging/presets/RolesTest.java	(working copy)
@@ -0,0 +1,38 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.tagging.presets;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import javax.swing.JPanel;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.openstreetmap.josm.JOSMFixture;
+
+/**
+ * Unit tests of {@link Roles} class.
+ */
+class RolesTest {
+
+    /**
+     * Setup test.
+     */
+    @BeforeAll
+    public static void setUp() {
+        JOSMFixture.createUnitTestFixture().init();
+    }
+
+    /**
+     * Unit test for {@link Roles#addToPanel}.
+     */
+    @Test
+    void testAddToPanel() {
+        JPanel p = new JPanel();
+        assertEquals(0, p.getComponentCount());
+        assertFalse(ItemFactory.build("roles").addToPanel(p, TaggingPresetInstance.createTest()));
+        // do not add anything if there are no roles
+        assertTrue(p.getComponentCount() == 0);
+    }
+}
Index: test/unit/org/openstreetmap/josm/gui/tagging/presets/SpaceTest.java
===================================================================
--- test/unit/org/openstreetmap/josm/gui/tagging/presets/SpaceTest.java	(nonexistent)
+++ test/unit/org/openstreetmap/josm/gui/tagging/presets/SpaceTest.java	(working copy)
@@ -0,0 +1,37 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.tagging.presets;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import javax.swing.JPanel;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.openstreetmap.josm.JOSMFixture;
+
+/**
+ * Unit tests of {@link Space} class.
+ */
+class SpaceTest {
+
+    /**
+     * Setup test.
+     */
+    @BeforeAll
+    public static void setUp() {
+        JOSMFixture.createUnitTestFixture().init();
+    }
+
+    /**
+     * Unit test for {@link Space#addToPanel}.
+     */
+    @Test
+    void testAddToPanel() {
+        JPanel p = new JPanel();
+        assertEquals(0, p.getComponentCount());
+        assertFalse(ItemFactory.build("space").addToPanel(p, TaggingPresetInstance.createTest()));
+        assertTrue(p.getComponentCount() > 0);
+    }
+}
Index: test/unit/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetInstanceTest.java
===================================================================
--- test/unit/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetInstanceTest.java	(nonexistent)
+++ test/unit/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetInstanceTest.java	(working copy)
@@ -0,0 +1,39 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.tagging.presets;
+
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.openstreetmap.josm.data.osm.OsmUtils;
+import org.openstreetmap.josm.testutils.JOSMTestRules;
+import org.openstreetmap.josm.tools.template_engine.TemplateEntry;
+import org.openstreetmap.josm.tools.template_engine.TemplateParser;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * Unit tests of {@link TaggingPresetInstance}
+ */
+class TaggingPresetInstanceTest {
+
+    /**
+     * Setup rule
+     */
+    @RegisterExtension
+    @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
+    public JOSMTestRules test = new JOSMTestRules();
+
+    /**
+     * Tests {@link TemplateEntry} evaluation
+     * @throws Exception in case something goes wrong
+     */
+    @Test
+    void testTemplate() throws Exception {
+        TaggingPresetInstance instance = TaggingPresetInstance.createTest(
+            OsmUtils.createPrimitive("relation route=bus ref=42 name=xxx from=Foo to=Bar"));
+        TemplateEntry templateEntry = new TemplateParser("Bus {ref}: {from} -> {to}").parse();
+        assertEquals("Bus 42: Foo -> Bar", templateEntry.getText(instance));
+        templateEntry = new TemplateParser("?{route=train 'Train'|route=bus 'Bus'|'X'} {ref}: {from} -> {to}").parse();
+        assertEquals("Bus 42: Foo -> Bar", templateEntry.getText(instance));
+    }
+}
Index: test/unit/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetItemGuiSupportTest.java
===================================================================
--- test/unit/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetItemGuiSupportTest.java	(revision 18366)
+++ test/unit/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetItemGuiSupportTest.java	(nonexistent)
@@ -1,52 +0,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.gui.tagging.presets;
-
-import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.RegisterExtension;
-import org.openstreetmap.josm.data.osm.OsmPrimitive;
-import org.openstreetmap.josm.data.osm.OsmUtils;
-import org.openstreetmap.josm.data.osm.Tag;
-import org.openstreetmap.josm.testutils.JOSMTestRules;
-import org.openstreetmap.josm.tools.template_engine.TemplateEntry;
-import org.openstreetmap.josm.tools.template_engine.TemplateParser;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-/**
- * Unit tests of {@link TaggingPresetItemGuiSupport}
- */
-class TaggingPresetItemGuiSupportTest {
-
-    /**
-     * Setup rule
-     */
-    @RegisterExtension
-    @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
-    public JOSMTestRules test = new JOSMTestRules();
-
-    /**
-     * Tests {@link TemplateEntry} evaluation
-     */
-    @Test
-    void testTemplate() throws Exception {
-        ArrayList<Tag> tags = new ArrayList<>(Arrays.asList(
-                new Tag("route", "bus"),
-                new Tag("name", "xxx"),
-                new Tag("from", "Foo"),
-                new Tag("to", "Bar")));
-        Collection<OsmPrimitive> primitives = Collections.singleton(
-                OsmUtils.createPrimitive("relation ref=42"));
-
-        TaggingPresetItemGuiSupport support = TaggingPresetItemGuiSupport.create(false, primitives, () -> tags);
-        TemplateEntry templateEntry = new TemplateParser("Bus {ref}: {from} -> {to}").parse();
-        assertEquals("Bus 42: Foo -> Bar", templateEntry.getText(support));
-        templateEntry = new TemplateParser("?{route=train 'Train'|route=bus 'Bus'|'X'} {ref}: {from} -> {to}").parse();
-        assertEquals("Bus 42: Foo -> Bar", templateEntry.getText(support));
-    }
-}
Index: test/unit/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetReaderTest.java
===================================================================
--- test/unit/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetReaderTest.java	(revision 18366)
+++ test/unit/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetReaderTest.java	(working copy)
@@ -4,7 +4,6 @@
 import static org.CustomMatchers.hasSize;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.junit.jupiter.api.Assertions.fail;
 
 import java.io.IOException;
@@ -16,8 +15,6 @@
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.RegisterExtension;
 import org.openstreetmap.josm.TestUtils;
-import org.openstreetmap.josm.gui.tagging.presets.items.Check;
-import org.openstreetmap.josm.gui.tagging.presets.items.Key;
 import org.openstreetmap.josm.testutils.JOSMTestRules;
 import org.xml.sax.SAXException;
 
@@ -27,13 +24,12 @@
  * Unit tests of {@link TaggingPresetReader} class.
  */
 class TaggingPresetReaderTest {
-
     /**
-     * Setup rule
+     * Setup test.
      */
     @RegisterExtension
     @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
-    public JOSMTestRules test = new JOSMTestRules();
+    public JOSMTestRules rule = new JOSMTestRules();
 
     /**
      * #8954 - last checkbox in the preset is not added
@@ -43,12 +39,14 @@
     @Test
     void testTicket8954() throws SAXException, IOException {
         String presetfile = TestUtils.getRegressionDataFile(8954, "preset.xml");
-        final Collection<TaggingPreset> presets = TaggingPresetReader.readAll(presetfile, false);
+        TaggingPresets.testInitialize(presetfile);
+        final Collection<TaggingPreset> presets = TaggingPresets.getTaggingPresets();
         Assert.assertEquals("Number of preset items", 1, presets.size());
         final TaggingPreset preset = presets.iterator().next();
-        Assert.assertEquals("Number of entries", 1, preset.data.size());
-        final TaggingPresetItem item = preset.data.get(0);
+        Assert.assertEquals("Number of entries", 1, preset.getAllItems().size());
+        final Item item = preset.getAllItems().get(0);
         Assert.assertTrue("Entry is not checkbox", item instanceof Check);
+        TaggingPresets.cleanUp();
     }
 
     /**
@@ -58,12 +56,14 @@
      */
     @Test
     void testNestedChunks() throws SAXException, IOException {
-        final Collection<TaggingPreset> presets = TaggingPresetReader.readAll(TestUtils.getTestDataRoot() + "preset_chunk.xml", true);
+        TaggingPresets.testInitialize(TestUtils.getTestDataRoot() + "preset_chunk.xml");
+        final Collection<TaggingPreset> presets = TaggingPresets.getTaggingPresets();
         assertThat(presets, hasSize(1));
         final TaggingPreset abc = presets.iterator().next();
-        assertTrue(abc.data.stream().allMatch(Key.class::isInstance));
-        final List<String> keys = abc.data.stream().map(x -> ((Key) x).key).collect(Collectors.toList());
+
+        final List<String> keys = abc.getAllItems(Key.class, true).stream().map(x -> x.getKey()).collect(Collectors.toList());
         assertEquals("[A1, A2, A3, B1, B2, B3, C1, C2, C3]", keys.toString());
+        TaggingPresets.cleanUp();
     }
 
     /**
@@ -74,12 +74,13 @@
     @Test
     void testExternalEntityResolving() throws IOException {
         try {
-            TaggingPresetReader.readAll(TestUtils.getTestDataRoot() + "preset_external_entity.xml", true);
+            TaggingPresetReader.read(TestUtils.getTestDataRoot() + "preset_external_entity.xml", true);
             fail("Reading a file with external entities should throw an SAXParseException!");
         } catch (SAXException e) {
             String expected = "DOCTYPE is disallowed when the feature \"http://apache.org/xml/features/disallow-doctype-decl\" set to true.";
             assertEquals(expected, e.getMessage());
         }
+        TaggingPresets.cleanUp();
     }
 
     /**
@@ -91,7 +92,9 @@
     @Test
     void testReadDefaulPresets() throws SAXException, IOException {
         String presetfile = "resource://data/defaultpresets.xml";
-        final Collection<TaggingPreset> presets = TaggingPresetReader.readAll(presetfile, true);
+        TaggingPresets.testInitialize(presetfile);
+        final Collection<TaggingPreset> presets = TaggingPresets.getTaggingPresets();
         Assert.assertTrue("Default presets are empty", presets.size() > 0);
+        TaggingPresets.cleanUp();
     }
 }
Index: test/unit/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetSelectorTest.java
===================================================================
--- test/unit/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetSelectorTest.java	(revision 18366)
+++ test/unit/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetSelectorTest.java	(working copy)
@@ -28,8 +28,12 @@
      */
     @Test
     void testIsMatching() {
-        TaggingPreset preset = new TaggingPreset();
-        preset.name = "estação de bombeiros"; // fire_station in brazilian portuguese
+        // fire_station in brazilian portuguese
+        Root root = (Root) ItemFactory.build("presets");
+        TaggingPreset preset = (TaggingPreset) ItemFactory.build("item name=estação de bombeiros");
+        root.addItem(preset);
+        TaggingPresets.addRoot(root);
+
         PresetClassification pc = new PresetClassification(preset);
         assertEquals(0, pc.isMatchingName("foo"));
         assertTrue(pc.isMatchingName("estação") > 0);
Index: test/unit/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetTest.java
===================================================================
--- test/unit/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetTest.java	(revision 18366)
+++ test/unit/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetTest.java	(working copy)
@@ -3,16 +3,13 @@
 
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.openstreetmap.josm.gui.tagging.presets.ItemFactory.build;
 
-import java.util.EnumSet;
-
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.RegisterExtension;
 import org.openstreetmap.josm.data.osm.IPrimitive;
 import org.openstreetmap.josm.data.osm.OsmUtils;
-import org.openstreetmap.josm.data.osm.search.SearchCompiler;
 import org.openstreetmap.josm.data.osm.search.SearchParseError;
-import org.openstreetmap.josm.gui.tagging.presets.items.Key;
 import org.openstreetmap.josm.testutils.JOSMTestRules;
 
 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
@@ -35,20 +32,20 @@
      */
     @Test
     void test() throws SearchParseError {
-        Key key = new Key();
-        key.key = "railway";
-        key.value = "tram_stop";
-        TaggingPreset preset = new TaggingPreset();
-        preset.data.add(key);
+        Key key = (Key) build("key key=railway value=tram_stop");
 
+        TaggingPreset preset = (TaggingPreset) build("item");
+        preset.addItem(key);
         assertFalse(preset.test(OsmUtils.createPrimitive("node foo=bar")));
         assertTrue(preset.test(OsmUtils.createPrimitive("node railway=tram_stop")));
 
-        preset.types = EnumSet.of(TaggingPresetType.NODE);
+        preset = (TaggingPreset) build("item type=node");
+        preset.addItem(key);
         assertTrue(preset.test(OsmUtils.createPrimitive("node railway=tram_stop")));
         assertFalse(preset.test(OsmUtils.createPrimitive("way railway=tram_stop")));
 
-        preset.matchExpression = SearchCompiler.compile("-public_transport");
+        preset = (TaggingPreset) build("item type=node match_expression=-public_transport");
+        preset.addItem(key);
         assertTrue(preset.test(OsmUtils.createPrimitive("node railway=tram_stop")));
         assertFalse(preset.test(OsmUtils.createPrimitive("node railway=tram_stop public_transport=stop_position")));
     }
Index: test/unit/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetValidationTest.java
===================================================================
--- test/unit/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetValidationTest.java	(revision 18366)
+++ test/unit/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetValidationTest.java	(working copy)
@@ -4,7 +4,8 @@
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
-import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
 import java.util.Locale;
 
 import javax.swing.JLabel;
@@ -12,10 +13,8 @@
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.RegisterExtension;
-import org.openstreetmap.josm.data.osm.DataSet;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
 import org.openstreetmap.josm.data.osm.OsmUtils;
-import org.openstreetmap.josm.data.osm.Tag;
 import org.openstreetmap.josm.data.validation.OsmValidator;
 import org.openstreetmap.josm.testutils.JOSMTestRules;
 
@@ -46,9 +45,9 @@
     void testValidate() {
         JLabel label = new JLabel();
         OsmPrimitive primitive = OsmUtils.createPrimitive("way incline=10m width=1mm opening_hours=\"Mo-Fr 8-10\"");
-        new DataSet(primitive);
+        Collection<OsmPrimitive> selection = Collections.singletonList(primitive);
 
-        TaggingPresetValidation.validate(primitive, label);
+        TaggingPresetValidation.validate(new ReadOnlyTaggingPresetHandler(selection), label);
 
         // CHECKSTYLE.OFF: LineLength
         assertTrue(label.isVisible());
@@ -60,16 +59,4 @@
             "<li>suspicious tag combination (incline on suspicious object)</li></ul>", label.getToolTipText());
         // CHECKSTYLE.ON: LineLength
     }
-
-    /**
-     * Tests {@link TaggingPresetValidation#applyChangedTags}
-     */
-    @Test
-    void testApplyChangedTags() {
-        OsmPrimitive primitive = OsmUtils.createPrimitive("way incline=10m width=1mm opening_hours=\"Mo-Fr 8-10\"");
-        new DataSet(primitive);
-        OsmPrimitive clone = TaggingPresetValidation.applyChangedTags(primitive, Arrays.asList(new Tag("incline", "20m")));
-        assertEquals("20m", clone.get("incline"));
-        assertEquals("1mm", clone.get("width"));
-    }
 }
Index: test/unit/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetsTest.java
===================================================================
--- test/unit/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetsTest.java	(revision 18366)
+++ test/unit/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetsTest.java	(working copy)
@@ -1,16 +1,9 @@
 // License: GPL. For details, see LICENSE file.
 package org.openstreetmap.josm.gui.tagging.presets;
 
-import java.util.Collection;
-import java.util.Objects;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.RegisterExtension;
 import org.openstreetmap.josm.testutils.JOSMTestRules;
-import org.openstreetmap.josm.tools.Logging;
 
 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
 import net.trajano.commons.testing.UtilityClassTestUtil;
@@ -35,18 +28,4 @@
     void testUtilityClass() throws ReflectiveOperationException {
         UtilityClassTestUtil.assertUtilityClassWellDefined(TaggingPresets.class);
     }
-
-    /**
-     * Wait for asynchronous icon loading
-     * @param presets presets collection
-     */
-    public static void waitForIconLoading(Collection<TaggingPreset> presets) {
-        presets.parallelStream().map(TaggingPreset::getIconLoadingTask).filter(Objects::nonNull).forEach(t -> {
-            try {
-                t.get(30, TimeUnit.SECONDS);
-            } catch (InterruptedException | ExecutionException | TimeoutException e) {
-                Logging.error(e);
-            }
-        });
-    }
 }
Index: test/unit/org/openstreetmap/josm/gui/tagging/presets/TextTest.java
===================================================================
--- test/unit/org/openstreetmap/josm/gui/tagging/presets/TextTest.java	(nonexistent)
+++ test/unit/org/openstreetmap/josm/gui/tagging/presets/TextTest.java	(working copy)
@@ -0,0 +1,35 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.tagging.presets;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import javax.swing.JPanel;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.openstreetmap.josm.JOSMFixture;
+
+/**
+ * Unit tests of {@link Text} class.
+ */
+class TextTest {
+    /**
+     * Setup test.
+     */
+    @BeforeAll
+    public static void setUp() {
+        JOSMFixture.createUnitTestFixture().init(true);
+    }
+
+    /**
+     * Unit test for {@link Text#addToPanel}.
+     */
+    @Test
+    void testAddToPanel() {
+        JPanel p = new JPanel();
+        assertEquals(0, p.getComponentCount());
+        assertTrue(ItemFactory.build("text").addToPanel(p, TaggingPresetInstance.createTest()));
+        assertTrue(p.getComponentCount() > 0);
+    }
+}
Index: test/unit/org/openstreetmap/josm/gui/tagging/presets/items/CheckGroupTest.java
===================================================================
--- test/unit/org/openstreetmap/josm/gui/tagging/presets/items/CheckGroupTest.java	(revision 18366)
+++ test/unit/org/openstreetmap/josm/gui/tagging/presets/items/CheckGroupTest.java	(nonexistent)
@@ -1,39 +0,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.gui.tagging.presets.items;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-import javax.swing.JPanel;
-
-import org.junit.jupiter.api.BeforeAll;
-import org.junit.jupiter.api.Test;
-import org.openstreetmap.josm.JOSMFixture;
-import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItemGuiSupport;
-
-/**
- * Unit tests of {@link CheckGroup} class.
- */
-class CheckGroupTest {
-
-    /**
-     * Setup test.
-     */
-    @BeforeAll
-    public static void setUp() {
-        JOSMFixture.createUnitTestFixture().init();
-    }
-
-    /**
-     * Unit test for {@link CheckGroup#addToPanel}.
-     */
-    @Test
-    void testAddToPanel() {
-        CheckGroup cg = new CheckGroup();
-        JPanel p = new JPanel();
-        assertEquals(0, p.getComponentCount());
-        assertFalse(cg.addToPanel(p, TaggingPresetItemGuiSupport.create(false)));
-        assertTrue(p.getComponentCount() > 0);
-    }
-}

Property changes on: test/unit/org/openstreetmap/josm/gui/tagging/presets/items/CheckGroupTest.java
___________________________________________________________________
Deleted: svn:eol-style
## -1 +0,0 ##
-native
\ No newline at end of property
Index: test/unit/org/openstreetmap/josm/gui/tagging/presets/items/CheckTest.java
===================================================================
--- test/unit/org/openstreetmap/josm/gui/tagging/presets/items/CheckTest.java	(revision 18366)
+++ test/unit/org/openstreetmap/josm/gui/tagging/presets/items/CheckTest.java	(nonexistent)
@@ -1,38 +0,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.gui.tagging.presets.items;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-import javax.swing.JPanel;
-
-import org.junit.jupiter.api.extension.RegisterExtension;
-import org.junit.jupiter.api.Test;
-import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItemGuiSupport;
-import org.openstreetmap.josm.testutils.JOSMTestRules;
-
-import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
-
-/**
- * Unit tests of {@link Check} class.
- */
-class CheckTest {
-
-    /**
-     * Setup test.
-     */
-    @RegisterExtension
-    @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
-    public JOSMTestRules test = new JOSMTestRules().main();
-
-    /**
-     * Unit test for {@link Check#addToPanel}.
-     */
-    @Test
-    void testAddToPanel() {
-        JPanel p = new JPanel();
-        assertEquals(0, p.getComponentCount());
-        assertTrue(new Check().addToPanel(p, TaggingPresetItemGuiSupport.create(false)));
-        assertTrue(p.getComponentCount() > 0);
-    }
-}

Property changes on: test/unit/org/openstreetmap/josm/gui/tagging/presets/items/CheckTest.java
___________________________________________________________________
Deleted: svn:eol-style
## -1 +0,0 ##
-native
\ No newline at end of property
Index: test/unit/org/openstreetmap/josm/gui/tagging/presets/items/ComboTest.java
===================================================================
--- test/unit/org/openstreetmap/josm/gui/tagging/presets/items/ComboTest.java	(revision 18366)
+++ test/unit/org/openstreetmap/josm/gui/tagging/presets/items/ComboTest.java	(nonexistent)
@@ -1,151 +0,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.gui.tagging.presets.items;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-import java.awt.Color;
-
-import javax.swing.JPanel;
-
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.RegisterExtension;
-import org.openstreetmap.josm.data.osm.OsmPrimitive;
-import org.openstreetmap.josm.data.osm.OsmUtils;
-import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItemGuiSupport;
-import org.openstreetmap.josm.testutils.JOSMTestRules;
-
-import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
-
-/**
- * Unit tests of {@link Combo} class.
- */
-class ComboTest {
-
-    /**
-     * Setup test.
-     */
-    @RegisterExtension
-    @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
-    public JOSMTestRules test = new JOSMTestRules().preferences().main().i18n("de");
-
-    /**
-     * Unit test for {@link Combo#addToPanel}.
-     */
-    @Test
-    void testAddToPanel() {
-        JPanel p = new JPanel();
-        assertEquals(0, p.getComponentCount());
-        assertTrue(new Combo().addToPanel(p, TaggingPresetItemGuiSupport.create(false)));
-        assertTrue(p.getComponentCount() > 0);
-    }
-
-    /**
-     * Unit test for {@link ComboMultiSelect#use_last_as_default} and {@link ComboMultiSelect#getInitialValue}
-     */
-    @Test
-    void testUseLastAsDefault() {
-        Combo combo = new Combo();
-        combo.key = "addr:country";
-        combo.values_from = "java.util.Locale#getISOCountries";
-        OsmPrimitive way = OsmUtils.createPrimitive("way");
-        OsmPrimitive wayTagged = OsmUtils.createPrimitive("way highway=residential");
-        OsmPrimitive wayAT = OsmUtils.createPrimitive("way addr:country=AT");
-        OsmPrimitive waySI = OsmUtils.createPrimitive("way addr:country=SI");
-        KeyedItem.LAST_VALUES.clear();
-        KeyedItem.LAST_VALUES.put("addr:country", "AT");
-        Combo.PROP_FILL_DEFAULT.put(false);
-        combo.use_last_as_default = 0;
-
-        combo.addToPanel(new JPanel(), TaggingPresetItemGuiSupport.create(false, way));
-        assertEquals("", combo.getSelectedItem().value);
-        combo.addToPanel(new JPanel(), TaggingPresetItemGuiSupport.create(false, wayTagged));
-        assertEquals("", combo.getSelectedItem().value);
-        combo.addToPanel(new JPanel(), TaggingPresetItemGuiSupport.create(false, wayAT));
-        assertEquals("AT", combo.getSelectedItem().value);
-        combo.addToPanel(new JPanel(), TaggingPresetItemGuiSupport.create(false, waySI));
-        assertEquals("SI", combo.getSelectedItem().value);
-        combo.addToPanel(new JPanel(), TaggingPresetItemGuiSupport.create(false, wayAT, waySI));
-        assertEquals(Combo.DIFFERENT, combo.getSelectedItem().value);
-
-        combo.default_ = "AT";
-        combo.addToPanel(new JPanel(), TaggingPresetItemGuiSupport.create(false, way));
-        assertEquals("AT", combo.getSelectedItem().value);
-        combo.addToPanel(new JPanel(), TaggingPresetItemGuiSupport.create(false, wayTagged));
-        assertEquals("", combo.getSelectedItem().value);
-        combo.addToPanel(new JPanel(), TaggingPresetItemGuiSupport.create(false, wayAT));
-        assertEquals("AT", combo.getSelectedItem().value);
-        combo.addToPanel(new JPanel(), TaggingPresetItemGuiSupport.create(false, waySI));
-        assertEquals("SI", combo.getSelectedItem().value);
-        combo.addToPanel(new JPanel(), TaggingPresetItemGuiSupport.create(false, wayAT, waySI));
-        assertEquals(Combo.DIFFERENT, combo.getSelectedItem().value);
-
-        Combo.PROP_FILL_DEFAULT.put(true);
-        combo.addToPanel(new JPanel(), TaggingPresetItemGuiSupport.create(false, way));
-        assertEquals("AT", combo.getSelectedItem().value);
-        combo.addToPanel(new JPanel(), TaggingPresetItemGuiSupport.create(false, wayTagged));
-        assertEquals("AT", combo.getSelectedItem().value);
-        combo.addToPanel(new JPanel(), TaggingPresetItemGuiSupport.create(false, wayAT));
-        assertEquals("AT", combo.getSelectedItem().value);
-        combo.addToPanel(new JPanel(), TaggingPresetItemGuiSupport.create(false, waySI));
-        assertEquals("SI", combo.getSelectedItem().value);
-        combo.addToPanel(new JPanel(), TaggingPresetItemGuiSupport.create(false, wayAT, waySI));
-        assertEquals(Combo.DIFFERENT, combo.getSelectedItem().value);
-        Combo.PROP_FILL_DEFAULT.put(false);
-        combo.default_ = null;
-
-        combo.use_last_as_default = 1; // untagged objects only
-        combo.addToPanel(new JPanel(), TaggingPresetItemGuiSupport.create(false, way));
-        assertEquals("AT", combo.getSelectedItem().value);
-        combo.addToPanel(new JPanel(), TaggingPresetItemGuiSupport.create(false, wayTagged));
-        assertEquals("", combo.getSelectedItem().value);
-        combo.addToPanel(new JPanel(), TaggingPresetItemGuiSupport.create(false, wayAT));
-        assertEquals("AT", combo.getSelectedItem().value);
-        combo.addToPanel(new JPanel(), TaggingPresetItemGuiSupport.create(false, waySI));
-        assertEquals("SI", combo.getSelectedItem().value);
-        combo.addToPanel(new JPanel(), TaggingPresetItemGuiSupport.create(false, wayAT, waySI));
-        assertEquals(Combo.DIFFERENT, combo.getSelectedItem().value);
-
-        combo.use_last_as_default = 2; // "force" on tagged objects too
-        combo.addToPanel(new JPanel(), TaggingPresetItemGuiSupport.create(false, way));
-        assertEquals("AT", combo.getSelectedItem().value);
-        combo.addToPanel(new JPanel(), TaggingPresetItemGuiSupport.create(false, wayTagged));
-        assertEquals("AT", combo.getSelectedItem().value);
-        combo.addToPanel(new JPanel(), TaggingPresetItemGuiSupport.create(false, wayAT));
-        assertEquals("AT", combo.getSelectedItem().value);
-        combo.addToPanel(new JPanel(), TaggingPresetItemGuiSupport.create(false, waySI));
-        assertEquals("SI", combo.getSelectedItem().value);
-        combo.addToPanel(new JPanel(), TaggingPresetItemGuiSupport.create(false, wayAT, waySI));
-        assertEquals(Combo.DIFFERENT, combo.getSelectedItem().value);
-        combo.use_last_as_default = 0;
-
-        KeyedItem.LAST_VALUES.clear();
-    }
-
-    @Test
-    void testColor() {
-        Combo combo = new Combo();
-        combo.key = "colour";
-        combo.values = "red;green;blue;black";
-        combo.values_context = "color";
-        combo.delimiter = ';';
-        combo.addToPanel(new JPanel(), TaggingPresetItemGuiSupport.create(false));
-        assertEquals(5, combo.combobox.getItemCount());
-        combo.presetListEntries.stream().filter(e -> "red".equals(e.value)).findFirst().ifPresent(combo.combobox::setSelectedItem);
-        assertEquals("red", combo.getSelectedItem().value);
-        assertEquals("Rot", combo.getSelectedItem().toString());
-        assertEquals(new Color(0xFF0000), combo.getColor());
-        combo.presetListEntries.stream().filter(e -> "green".equals(e.value)).findFirst().ifPresent(combo.combobox::setSelectedItem);
-        assertEquals("green", combo.getSelectedItem().value);
-        assertEquals("Grün", combo.getSelectedItem().toString());
-        assertEquals(new Color(0x008000), combo.getColor());
-        combo.combobox.setSelectedItem("#135");
-        assertEquals("#135", combo.getSelectedItem().value);
-        assertEquals(new Color(0x113355), combo.getColor());
-        combo.combobox.setSelectedItem("#123456");
-        assertEquals("#123456", combo.getSelectedItem().value);
-        assertEquals(new Color(0x123456), combo.getColor());
-        combo.setColor(new Color(0x448822));
-        assertEquals("#448822", combo.getSelectedItem().value);
-    }
-}

Property changes on: test/unit/org/openstreetmap/josm/gui/tagging/presets/items/ComboTest.java
___________________________________________________________________
Deleted: svn:eol-style
## -1 +0,0 ##
-native
\ No newline at end of property
Index: test/unit/org/openstreetmap/josm/gui/tagging/presets/items/ItemSeparatorTest.java
===================================================================
--- test/unit/org/openstreetmap/josm/gui/tagging/presets/items/ItemSeparatorTest.java	(revision 18366)
+++ test/unit/org/openstreetmap/josm/gui/tagging/presets/items/ItemSeparatorTest.java	(nonexistent)
@@ -1,38 +0,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.gui.tagging.presets.items;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-import javax.swing.JPanel;
-
-import org.junit.jupiter.api.BeforeAll;
-import org.junit.jupiter.api.Test;
-import org.openstreetmap.josm.JOSMFixture;
-import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItemGuiSupport;
-
-/**
- * Unit tests of {@link ItemSeparator} class.
- */
-class ItemSeparatorTest {
-
-    /**
-     * Setup test.
-     */
-    @BeforeAll
-    public static void setUp() {
-        JOSMFixture.createUnitTestFixture().init();
-    }
-
-    /**
-     * Unit test for {@link ItemSeparator#addToPanel}.
-     */
-    @Test
-    void testAddToPanel() {
-        JPanel p = new JPanel();
-        assertEquals(0, p.getComponentCount());
-        assertFalse(new ItemSeparator().addToPanel(p, TaggingPresetItemGuiSupport.create(false)));
-        assertTrue(p.getComponentCount() > 0);
-    }
-}

Property changes on: test/unit/org/openstreetmap/josm/gui/tagging/presets/items/ItemSeparatorTest.java
___________________________________________________________________
Deleted: svn:eol-style
## -1 +0,0 ##
-native
\ No newline at end of property
Index: test/unit/org/openstreetmap/josm/gui/tagging/presets/items/KeyTest.java
===================================================================
--- test/unit/org/openstreetmap/josm/gui/tagging/presets/items/KeyTest.java	(revision 18366)
+++ test/unit/org/openstreetmap/josm/gui/tagging/presets/items/KeyTest.java	(nonexistent)
@@ -1,37 +0,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.gui.tagging.presets.items;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-
-import javax.swing.JPanel;
-
-import org.junit.jupiter.api.BeforeAll;
-import org.junit.jupiter.api.Test;
-import org.openstreetmap.josm.JOSMFixture;
-import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItemGuiSupport;
-
-/**
- * Unit tests of {@link Key} class.
- */
-class KeyTest {
-
-    /**
-     * Setup test.
-     */
-    @BeforeAll
-    public static void setUp() {
-        JOSMFixture.createUnitTestFixture().init();
-    }
-
-    /**
-     * Unit test for {@link Key#addToPanel}.
-     */
-    @Test
-    void testAddToPanel() {
-        JPanel p = new JPanel();
-        assertEquals(0, p.getComponentCount());
-        assertFalse(new Key().addToPanel(p, TaggingPresetItemGuiSupport.create(false)));
-        assertEquals(0, p.getComponentCount());
-    }
-}

Property changes on: test/unit/org/openstreetmap/josm/gui/tagging/presets/items/KeyTest.java
___________________________________________________________________
Deleted: svn:eol-style
## -1 +0,0 ##
-native
\ No newline at end of property
Index: test/unit/org/openstreetmap/josm/gui/tagging/presets/items/LabelTest.java
===================================================================
--- test/unit/org/openstreetmap/josm/gui/tagging/presets/items/LabelTest.java	(revision 18366)
+++ test/unit/org/openstreetmap/josm/gui/tagging/presets/items/LabelTest.java	(nonexistent)
@@ -1,37 +0,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.gui.tagging.presets.items;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-import javax.swing.JPanel;
-
-import org.junit.jupiter.api.BeforeAll;
-import org.junit.jupiter.api.Test;
-import org.openstreetmap.josm.JOSMFixture;
-import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItemGuiSupport;
-
-/**
- * Unit tests of {@link Label} class.
- */
-class LabelTest {
-
-    /**
-     * Setup test.
-     */
-    @BeforeAll
-    public static void setUp() {
-        JOSMFixture.createUnitTestFixture().init();
-    }
-
-    /**
-     * Unit test for {@link Label#addToPanel}.
-     */
-    @Test
-    void testAddToPanel() {
-        JPanel p = new JPanel();
-        assertEquals(0, p.getComponentCount());
-        assertTrue(new Label().addToPanel(p, TaggingPresetItemGuiSupport.create(false)));
-        assertTrue(p.getComponentCount() > 0);
-    }
-}

Property changes on: test/unit/org/openstreetmap/josm/gui/tagging/presets/items/LabelTest.java
___________________________________________________________________
Deleted: svn:eol-style
## -1 +0,0 ##
-native
\ No newline at end of property
Index: test/unit/org/openstreetmap/josm/gui/tagging/presets/items/LinkTest.java
===================================================================
--- test/unit/org/openstreetmap/josm/gui/tagging/presets/items/LinkTest.java	(revision 18366)
+++ test/unit/org/openstreetmap/josm/gui/tagging/presets/items/LinkTest.java	(nonexistent)
@@ -1,48 +0,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.gui.tagging.presets.items;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-import javax.swing.JPanel;
-
-import org.junit.jupiter.api.BeforeAll;
-import org.junit.jupiter.api.Test;
-import org.openstreetmap.josm.JOSMFixture;
-import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItemGuiSupport;
-import org.openstreetmap.josm.spi.preferences.Config;
-
-/**
- * Unit tests of {@link Link} class.
- */
-class LinkTest {
-
-    /**
-     * Setup test.
-     */
-    @BeforeAll
-    public static void setUp() {
-        JOSMFixture.createUnitTestFixture().init();
-    }
-
-    /**
-     * Unit test for {@link Link#addToPanel}.
-     */
-    @Test
-    void testAddToPanel() {
-        Link l = new Link();
-        JPanel p = new JPanel();
-        assertEquals(0, p.getComponentCount());
-        assertFalse(l.addToPanel(p, TaggingPresetItemGuiSupport.create(false)));
-        assertEquals(0, p.getComponentCount());
-
-        l.href = Config.getUrls().getJOSMWebsite();
-        assertFalse(l.addToPanel(p, TaggingPresetItemGuiSupport.create(false)));
-        assertTrue(p.getComponentCount() > 0);
-
-        l.locale_href = Config.getUrls().getJOSMWebsite();
-        assertFalse(l.addToPanel(p, TaggingPresetItemGuiSupport.create(false)));
-        assertTrue(p.getComponentCount() > 0);
-    }
-}

Property changes on: test/unit/org/openstreetmap/josm/gui/tagging/presets/items/LinkTest.java
___________________________________________________________________
Deleted: svn:eol-style
## -1 +0,0 ##
-native
\ No newline at end of property
Index: test/unit/org/openstreetmap/josm/gui/tagging/presets/items/MultiSelectTest.java
===================================================================
--- test/unit/org/openstreetmap/josm/gui/tagging/presets/items/MultiSelectTest.java	(revision 18366)
+++ test/unit/org/openstreetmap/josm/gui/tagging/presets/items/MultiSelectTest.java	(nonexistent)
@@ -1,37 +0,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.gui.tagging.presets.items;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-import javax.swing.JPanel;
-
-import org.junit.jupiter.api.BeforeAll;
-import org.junit.jupiter.api.Test;
-import org.openstreetmap.josm.JOSMFixture;
-import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItemGuiSupport;
-
-/**
- * Unit tests of {@link MultiSelect} class.
- */
-class MultiSelectTest {
-
-    /**
-     * Setup test.
-     */
-    @BeforeAll
-    public static void setUp() {
-        JOSMFixture.createUnitTestFixture().init();
-    }
-
-    /**
-     * Unit test for {@link MultiSelect#addToPanel}.
-     */
-    @Test
-    void testAddToPanel() {
-        JPanel p = new JPanel();
-        assertEquals(0, p.getComponentCount());
-        assertTrue(new MultiSelect().addToPanel(p, TaggingPresetItemGuiSupport.create(false)));
-        assertTrue(p.getComponentCount() > 0);
-    }
-}

Property changes on: test/unit/org/openstreetmap/josm/gui/tagging/presets/items/MultiSelectTest.java
___________________________________________________________________
Deleted: svn:eol-style
## -1 +0,0 ##
-native
\ No newline at end of property
Index: test/unit/org/openstreetmap/josm/gui/tagging/presets/items/OptionalTest.java
===================================================================
--- test/unit/org/openstreetmap/josm/gui/tagging/presets/items/OptionalTest.java	(revision 18366)
+++ test/unit/org/openstreetmap/josm/gui/tagging/presets/items/OptionalTest.java	(nonexistent)
@@ -1,38 +0,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.gui.tagging.presets.items;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-import javax.swing.JPanel;
-
-import org.junit.jupiter.api.BeforeAll;
-import org.junit.jupiter.api.Test;
-import org.openstreetmap.josm.JOSMFixture;
-import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItemGuiSupport;
-
-/**
- * Unit tests of {@link Optional} class.
- */
-class OptionalTest {
-
-    /**
-     * Setup test.
-     */
-    @BeforeAll
-    public static void setUp() {
-        JOSMFixture.createUnitTestFixture().init();
-    }
-
-    /**
-     * Unit test for {@link Optional#addToPanel}.
-     */
-    @Test
-    void testAddToPanel() {
-        JPanel p = new JPanel();
-        assertEquals(0, p.getComponentCount());
-        assertFalse(new Optional().addToPanel(p, TaggingPresetItemGuiSupport.create(false)));
-        assertTrue(p.getComponentCount() > 0);
-    }
-}

Property changes on: test/unit/org/openstreetmap/josm/gui/tagging/presets/items/OptionalTest.java
___________________________________________________________________
Deleted: svn:eol-style
## -1 +0,0 ##
-native
\ No newline at end of property
Index: test/unit/org/openstreetmap/josm/gui/tagging/presets/items/PresetLinkTest.java
===================================================================
--- test/unit/org/openstreetmap/josm/gui/tagging/presets/items/PresetLinkTest.java	(revision 18366)
+++ test/unit/org/openstreetmap/josm/gui/tagging/presets/items/PresetLinkTest.java	(nonexistent)
@@ -1,41 +0,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.gui.tagging.presets.items;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-import javax.swing.JPanel;
-
-import org.junit.jupiter.api.extension.RegisterExtension;
-import org.junit.jupiter.api.Test;
-import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItemGuiSupport;
-import org.openstreetmap.josm.testutils.JOSMTestRules;
-
-import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
-
-/**
- * Unit tests of {@link PresetLink} class.
- */
-class PresetLinkTest {
-
-    /**
-     * Setup test.
-     */
-    @RegisterExtension
-    @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
-    public JOSMTestRules rule = new JOSMTestRules().presets();
-
-    /**
-     * Unit test for {@link PresetLink#addToPanel}.
-     */
-    @Test
-    void testAddToPanel() {
-        PresetLink l = new PresetLink();
-        l.preset_name = "River";
-        JPanel p = new JPanel();
-        assertEquals(0, p.getComponentCount());
-        assertFalse(l.addToPanel(p, TaggingPresetItemGuiSupport.create(false)));
-        assertTrue(p.getComponentCount() > 0);
-    }
-}

Property changes on: test/unit/org/openstreetmap/josm/gui/tagging/presets/items/PresetLinkTest.java
___________________________________________________________________
Deleted: svn:eol-style
## -1 +0,0 ##
-native
\ No newline at end of property
Index: test/unit/org/openstreetmap/josm/gui/tagging/presets/items/PresetListEntryTest.java
===================================================================
--- test/unit/org/openstreetmap/josm/gui/tagging/presets/items/PresetListEntryTest.java	(revision 18366)
+++ test/unit/org/openstreetmap/josm/gui/tagging/presets/items/PresetListEntryTest.java	(nonexistent)
@@ -1,40 +0,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.gui.tagging.presets.items;
-
-import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-import org.junit.jupiter.api.BeforeAll;
-import org.junit.jupiter.api.Test;
-import org.openstreetmap.josm.JOSMFixture;
-
-/**
- * Unit tests of {@link PresetListEntry} class.
- */
-class PresetListEntryTest {
-
-    /**
-     * Setup test.
-     */
-    @BeforeAll
-    public static void setUp() {
-        JOSMFixture.createUnitTestFixture().init();
-    }
-
-    /**
-     * Non-regression test for ticket <a href="https://josm.openstreetmap.de/ticket/12416">#12416</a>.
-     */
-    @Test
-    void testTicket12416() {
-        assertTrue(new PresetListEntry("", null).getListDisplay(200).contains(" "));
-    }
-
-    /**
-     * Non-regression test for ticket <a href="https://josm.openstreetmap.de/ticket/21550">#21550</a>
-     */
-    @Test
-    void testTicket21550() {
-        final PresetListEntry entry = new PresetListEntry("", new Combo());
-        assertDoesNotThrow(entry::getCount);
-    }
-}

Property changes on: test/unit/org/openstreetmap/josm/gui/tagging/presets/items/PresetListEntryTest.java
___________________________________________________________________
Deleted: svn:eol-style
## -1 +0,0 ##
-native
\ No newline at end of property
Index: test/unit/org/openstreetmap/josm/gui/tagging/presets/items/RolesTest.java
===================================================================
--- test/unit/org/openstreetmap/josm/gui/tagging/presets/items/RolesTest.java	(revision 18366)
+++ test/unit/org/openstreetmap/josm/gui/tagging/presets/items/RolesTest.java	(nonexistent)
@@ -1,38 +0,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.gui.tagging.presets.items;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-import javax.swing.JPanel;
-
-import org.junit.jupiter.api.BeforeAll;
-import org.junit.jupiter.api.Test;
-import org.openstreetmap.josm.JOSMFixture;
-import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItemGuiSupport;
-
-/**
- * Unit tests of {@link Roles} class.
- */
-class RolesTest {
-
-    /**
-     * Setup test.
-     */
-    @BeforeAll
-    public static void setUp() {
-        JOSMFixture.createUnitTestFixture().init();
-    }
-
-    /**
-     * Unit test for {@link Roles#addToPanel}.
-     */
-    @Test
-    void testAddToPanel() {
-        JPanel p = new JPanel();
-        assertEquals(0, p.getComponentCount());
-        assertFalse(new Roles().addToPanel(p, TaggingPresetItemGuiSupport.create(false)));
-        assertTrue(p.getComponentCount() > 0);
-    }
-}

Property changes on: test/unit/org/openstreetmap/josm/gui/tagging/presets/items/RolesTest.java
___________________________________________________________________
Deleted: svn:eol-style
## -1 +0,0 ##
-native
\ No newline at end of property
Index: test/unit/org/openstreetmap/josm/gui/tagging/presets/items/SpaceTest.java
===================================================================
--- test/unit/org/openstreetmap/josm/gui/tagging/presets/items/SpaceTest.java	(revision 18366)
+++ test/unit/org/openstreetmap/josm/gui/tagging/presets/items/SpaceTest.java	(nonexistent)
@@ -1,38 +0,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.gui.tagging.presets.items;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-import javax.swing.JPanel;
-
-import org.junit.jupiter.api.BeforeAll;
-import org.junit.jupiter.api.Test;
-import org.openstreetmap.josm.JOSMFixture;
-import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItemGuiSupport;
-
-/**
- * Unit tests of {@link Space} class.
- */
-class SpaceTest {
-
-    /**
-     * Setup test.
-     */
-    @BeforeAll
-    public static void setUp() {
-        JOSMFixture.createUnitTestFixture().init();
-    }
-
-    /**
-     * Unit test for {@link Space#addToPanel}.
-     */
-    @Test
-    void testAddToPanel() {
-        JPanel p = new JPanel();
-        assertEquals(0, p.getComponentCount());
-        assertFalse(new Space().addToPanel(p, TaggingPresetItemGuiSupport.create(false)));
-        assertTrue(p.getComponentCount() > 0);
-    }
-}

Property changes on: test/unit/org/openstreetmap/josm/gui/tagging/presets/items/SpaceTest.java
___________________________________________________________________
Deleted: svn:eol-style
## -1 +0,0 ##
-native
\ No newline at end of property
Index: test/unit/org/openstreetmap/josm/gui/tagging/presets/items/TextTest.java
===================================================================
--- test/unit/org/openstreetmap/josm/gui/tagging/presets/items/TextTest.java	(revision 18366)
+++ test/unit/org/openstreetmap/josm/gui/tagging/presets/items/TextTest.java	(nonexistent)
@@ -1,38 +0,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.gui.tagging.presets.items;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-import javax.swing.JPanel;
-
-import org.junit.jupiter.api.extension.RegisterExtension;
-import org.junit.jupiter.api.Test;
-import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItemGuiSupport;
-import org.openstreetmap.josm.testutils.JOSMTestRules;
-
-import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
-
-/**
- * Unit tests of {@link Text} class.
- */
-class TextTest {
-
-    /**
-     * Setup test.
-     */
-    @RegisterExtension
-    @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
-    public JOSMTestRules test = new JOSMTestRules().main();
-
-    /**
-     * Unit test for {@link Text#addToPanel}.
-     */
-    @Test
-    void testAddToPanel() {
-        JPanel p = new JPanel();
-        assertEquals(0, p.getComponentCount());
-        assertTrue(new Text().addToPanel(p, TaggingPresetItemGuiSupport.create(false)));
-        assertTrue(p.getComponentCount() > 0);
-    }
-}

Property changes on: test/unit/org/openstreetmap/josm/gui/tagging/presets/items/TextTest.java
___________________________________________________________________
Deleted: svn:eol-style
## -1 +0,0 ##
-native
\ No newline at end of property
Index: test/unit/org/openstreetmap/josm/tools/OsmPrimitiveImageProviderTest.java
===================================================================
--- test/unit/org/openstreetmap/josm/tools/OsmPrimitiveImageProviderTest.java	(revision 18366)
+++ test/unit/org/openstreetmap/josm/tools/OsmPrimitiveImageProviderTest.java	(working copy)
@@ -7,6 +7,8 @@
 
 import java.awt.Dimension;
 import java.util.EnumSet;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
 
 import javax.swing.ImageIcon;
 
@@ -16,8 +18,8 @@
 import org.openstreetmap.josm.JOSMFixture;
 import org.openstreetmap.josm.data.osm.Node;
 import org.openstreetmap.josm.data.osm.OsmUtils;
+import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetUtils;
 import org.openstreetmap.josm.gui.tagging.presets.TaggingPresets;
-import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetsTest;
 import org.openstreetmap.josm.testutils.JOSMTestRules;
 import org.openstreetmap.josm.tools.OsmPrimitiveImageProvider.Options;
 
@@ -45,10 +47,13 @@
 
     /**
      * Unit test of {@link OsmPrimitiveImageProvider#getResource}.
+     * @throws InterruptedException if any thread is interrupted
+     * @throws ExecutionException if any thread throws
+     * @throws TimeoutException on timeout
      */
     @Test
-    void testGetResource() {
-        TaggingPresetsTest.waitForIconLoading(TaggingPresets.getTaggingPresets());
+    void testGetResource() throws InterruptedException, ExecutionException, TimeoutException {
+        TaggingPresetUtils.waitForIconsLoaded(TaggingPresets.getTaggingPresets(), 30);
 
         final EnumSet<Options> noDefault = EnumSet.of(Options.NO_DEFAULT);
         final Dimension iconSize = new Dimension(16, 16);
