Index: src/org/openstreetmap/josm/gui/conflict/pair/AbstractListMergeModel.java
===================================================================
--- src/org/openstreetmap/josm/gui/conflict/pair/AbstractListMergeModel.java	(revision 18220)
+++ src/org/openstreetmap/josm/gui/conflict/pair/AbstractListMergeModel.java	(working copy)
@@ -21,8 +21,6 @@
 import java.util.stream.Collectors;
 import java.util.stream.IntStream;
 
-import javax.swing.AbstractListModel;
-import javax.swing.ComboBoxModel;
 import javax.swing.DefaultListSelectionModel;
 import javax.swing.JOptionPane;
 import javax.swing.JTable;
@@ -41,6 +39,7 @@
 import org.openstreetmap.josm.gui.help.HelpUtil;
 import org.openstreetmap.josm.gui.util.ChangeNotifier;
 import org.openstreetmap.josm.gui.util.TableHelper;
+import org.openstreetmap.josm.gui.widgets.JosmComboBoxModel;
 import org.openstreetmap.josm.gui.widgets.OsmPrimitivesTableModel;
 import org.openstreetmap.josm.tools.CheckParameterUtil;
 import org.openstreetmap.josm.tools.Logging;
@@ -832,7 +831,7 @@
         return this.comparePairListModel;
     }
 
-    public class ComparePairListModel extends AbstractListModel<ComparePairType> implements ComboBoxModel<ComparePairType> {
+    public class ComparePairListModel extends JosmComboBoxModel<ComparePairType> {
 
         private int selectedIdx;
         private final List<ComparePairType> compareModes;
Index: src/org/openstreetmap/josm/gui/conflict/tags/MultiValueCellEditor.java
===================================================================
--- src/org/openstreetmap/josm/gui/conflict/tags/MultiValueCellEditor.java	(revision 18220)
+++ src/org/openstreetmap/josm/gui/conflict/tags/MultiValueCellEditor.java	(working copy)
@@ -12,7 +12,6 @@
 import java.util.concurrent.CopyOnWriteArrayList;
 
 import javax.swing.AbstractCellEditor;
-import javax.swing.DefaultComboBoxModel;
 import javax.swing.JLabel;
 import javax.swing.JList;
 import javax.swing.JTable;
@@ -21,6 +20,7 @@
 import javax.swing.table.TableCellEditor;
 
 import org.openstreetmap.josm.gui.widgets.JosmComboBox;
+import org.openstreetmap.josm.gui.widgets.JosmComboBoxModel;
 import org.openstreetmap.josm.tools.Logging;
 
 /**
@@ -49,7 +49,7 @@
 
     /** the combo box used as editor */
     private final JosmComboBox<Object> editor;
-    private final DefaultComboBoxModel<Object> editorModel;
+    private final JosmComboBoxModel<Object> editorModel;
     private final CopyOnWriteArrayList<NavigationListener> listeners;
 
     /**
@@ -86,7 +86,7 @@
      * Construct a new {@link MultiValueCellEditor}
      */
     public MultiValueCellEditor() {
-        editorModel = new DefaultComboBoxModel<>();
+        editorModel = new JosmComboBoxModel<>();
         editor = new JosmComboBox<Object>(editorModel) {
             @Override
             public void processKeyEvent(KeyEvent e) {
Index: src/org/openstreetmap/josm/gui/conflict/tags/MultiValueCellRenderer.java
===================================================================
--- src/org/openstreetmap/josm/gui/conflict/tags/MultiValueCellRenderer.java	(revision 18220)
+++ src/org/openstreetmap/josm/gui/conflict/tags/MultiValueCellRenderer.java	(working copy)
@@ -6,7 +6,6 @@
 import java.awt.Component;
 import java.awt.Font;
 
-import javax.swing.DefaultComboBoxModel;
 import javax.swing.ImageIcon;
 import javax.swing.JLabel;
 import javax.swing.JTable;
@@ -15,6 +14,7 @@
 
 import org.openstreetmap.josm.gui.conflict.ConflictColors;
 import org.openstreetmap.josm.gui.widgets.JosmComboBox;
+import org.openstreetmap.josm.gui.widgets.JosmComboBoxModel;
 import org.openstreetmap.josm.tools.ImageProvider;
 import org.openstreetmap.josm.tools.Logging;
 
@@ -26,7 +26,7 @@
 
     private final ImageIcon iconDecided;
     private final ImageIcon iconUndecided;
-    private final DefaultComboBoxModel<Object> model;
+    private final JosmComboBoxModel<Object> model;
     private final JosmComboBox<Object> cbDecisionRenderer;
 
     /**
@@ -36,7 +36,7 @@
         setOpaque(true);
         iconDecided = ImageProvider.get("dialogs/conflict", "tagconflictresolved");
         iconUndecided = ImageProvider.get("dialogs/conflict", "tagconflictunresolved");
-        model = new DefaultComboBoxModel<>();
+        model = new JosmComboBoxModel<>();
         cbDecisionRenderer = new JosmComboBox<>(model);
     }
 
Index: src/org/openstreetmap/josm/gui/dialogs/properties/TagEditHelper.java
===================================================================
--- src/org/openstreetmap/josm/gui/dialogs/properties/TagEditHelper.java	(revision 18220)
+++ src/org/openstreetmap/josm/gui/dialogs/properties/TagEditHelper.java	(working copy)
@@ -6,6 +6,7 @@
 
 import java.awt.BorderLayout;
 import java.awt.Component;
+import java.awt.ComponentOrientation;
 import java.awt.Container;
 import java.awt.Cursor;
 import java.awt.Dimension;
@@ -13,11 +14,9 @@
 import java.awt.Font;
 import java.awt.GridBagConstraints;
 import java.awt.GridBagLayout;
-import java.awt.datatransfer.Clipboard;
-import java.awt.datatransfer.Transferable;
 import java.awt.event.ActionEvent;
-import java.awt.event.FocusAdapter;
 import java.awt.event.FocusEvent;
+import java.awt.event.FocusListener;
 import java.awt.event.InputEvent;
 import java.awt.event.KeyEvent;
 import java.awt.event.MouseAdapter;
@@ -44,8 +43,6 @@
 import javax.swing.Action;
 import javax.swing.Box;
 import javax.swing.ButtonGroup;
-import javax.swing.ComboBoxModel;
-import javax.swing.DefaultListCellRenderer;
 import javax.swing.ImageIcon;
 import javax.swing.JCheckBoxMenuItem;
 import javax.swing.JComponent;
@@ -60,8 +57,9 @@
 import javax.swing.KeyStroke;
 import javax.swing.ListCellRenderer;
 import javax.swing.SwingUtilities;
+import javax.swing.event.PopupMenuEvent;
+import javax.swing.event.PopupMenuListener;
 import javax.swing.table.DefaultTableModel;
-import javax.swing.text.JTextComponent;
 
 import org.openstreetmap.josm.actions.JosmAction;
 import org.openstreetmap.josm.actions.search.SearchAction;
@@ -86,11 +84,14 @@
 import org.openstreetmap.josm.gui.ExtendedDialog;
 import org.openstreetmap.josm.gui.IExtendedDialog;
 import org.openstreetmap.josm.gui.MainApplication;
-import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils;
 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.AutoCompletionManager;
 import org.openstreetmap.josm.gui.util.GuiHelper;
 import org.openstreetmap.josm.gui.util.WindowGeometry;
+import org.openstreetmap.josm.gui.widgets.JosmListCellRenderer;
+import org.openstreetmap.josm.gui.widgets.OrientationAction;
 import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher;
 import org.openstreetmap.josm.io.XmlWriter;
 import org.openstreetmap.josm.tools.GBC;
@@ -115,7 +116,6 @@
     protected Collection<OsmPrimitive> sel;
 
     private String changedKey;
-    private String objKey;
 
     static final Comparator<AutoCompletionItem> DEFAULT_AC_ITEM_COMPARATOR =
             (o1, o2) -> String.CASE_INSENSITIVE_ORDER.compare(o1.getValue(), o2.getValue());
@@ -124,7 +124,6 @@
     public static final int DEFAULT_LRU_TAGS_NUMBER = 5;
     /** Maximum number of recent tags */
     public static final int MAX_LRU_TAGS_NUMBER = 30;
-
     /** Autocomplete keys by default */
     public static final BooleanProperty AUTOCOMPLETE_KEYS = new BooleanProperty("properties.autocomplete-keys", true);
     /** Autocomplete values by default */
@@ -192,6 +191,38 @@
     }
 
     /**
+     * A custom list cell renderer that adds the value count to some items.
+     */
+    static class TEHListCellRenderer extends JosmListCellRenderer<AutoCompletionItem> {
+        protected Map<String, Integer> map;
+
+        TEHListCellRenderer(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 there is a value count add it to the text
+            if (map != null) {
+                String text = value == null ? "" : value.toString();
+                count = map.get(text);
+                if (count != null) {
+                    value = new AutoCompletionItem(tr("{0} ({1})", text, count));
+                }
+            }
+            Component l = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
+            l.setComponentOrientation(component.getComponentOrientation());
+            if (count != null) {
+                l.setFont(l.getFont().deriveFont(Font.ITALIC + Font.BOLD));
+            }
+            return l;
+        }
+    }
+
+    /**
      * Constructs a new {@code TagEditHelper}.
      * @param tagTable tag table
      * @param propertyData table model
@@ -283,10 +314,7 @@
         if (Utils.isEmpty(sel))
             return;
 
-        String key = getDataKey(row);
-        objKey = key;
-
-        final IEditTagDialog editDialog = getEditTagDialog(row, focusOnKey, key);
+        final IEditTagDialog editDialog = getEditTagDialog(row, focusOnKey, getDataKey(row));
         editDialog.showDialog();
         if (editDialog.getValue() != 1)
             return;
@@ -443,29 +471,9 @@
         private final String key;
         private final transient Map<String, Integer> m;
         private final transient Comparator<AutoCompletionItem> usedValuesAwareComparator;
+        private final transient AutoCompletionManager autocomplete;
 
-        private final transient ListCellRenderer<AutoCompletionItem> cellRenderer = new ListCellRenderer<AutoCompletionItem>() {
-            private final DefaultListCellRenderer def = new DefaultListCellRenderer();
-            @Override
-            public Component getListCellRendererComponent(JList<? extends AutoCompletionItem> list,
-                    AutoCompletionItem value, int index, boolean isSelected, boolean cellHasFocus) {
-                Component c = def.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
-                if (c instanceof JLabel) {
-                    String str = value.getValue();
-                    if (valueCount.containsKey(objKey)) {
-                        Map<String, Integer> map = valueCount.get(objKey);
-                        if (map.containsKey(str)) {
-                            str = tr("{0} ({1})", str, map.get(str));
-                            c.setFont(c.getFont().deriveFont(Font.ITALIC + Font.BOLD));
-                        }
-                    }
-                    ((JLabel) c).setText(str);
-                }
-                return c;
-            }
-        };
-
-        protected EditTagDialog(String key, Map<String, Integer> map, final boolean initialFocusOnKey) {
+        protected EditTagDialog(String key, Map<String, Integer> map, boolean initialFocusOnKey) {
             super(MainApplication.getMainFrame(), trn("Change value?", "Change values?", map.size()), tr("OK"), tr("Cancel"));
             setButtonIcons("ok", "cancel");
             setCancelButton(2);
@@ -472,6 +480,7 @@
             configureContextsensitiveHelp("/Dialog/EditValue", true /* show help button */);
             this.key = key;
             this.m = map;
+            this.initialFocusOnKey = initialFocusOnKey;
 
             usedValuesAwareComparator = (o1, o2) -> {
                 boolean c1 = m.containsKey(o1.getValue());
@@ -492,18 +501,34 @@
 
             mainPanel.add(new JLabel(msg), BorderLayout.NORTH);
 
-            JPanel p = new JPanel(new GridBagLayout());
+            JPanel p = new JPanel(new GridBagLayout()) {
+                /**
+                 * This hack allows the comboboxes to have their own orientation.
+                 *
+                 * The problem is that
+                 * {@link org.openstreetmap.josm.gui.ExtendedDialog#showDialog ExtendedDialog} calls
+                 * {@code applyComponentOrientation} very late in the dialog construction process
+                 * thus overwriting the orientation the components have chosen for themselves.
+                 *
+                 * This stops the propagation of {@code applyComponentOrientation}, thus all
+                 * components may (and have to) set their own orientation.
+                 */
+                @Override
+                public void applyComponentOrientation(ComponentOrientation o) {
+                    setComponentOrientation(o);
+                }
+            };
             mainPanel.add(p, BorderLayout.CENTER);
 
-            AutoCompletionManager autocomplete = AutoCompletionManager.of(OsmDataManager.getInstance().getActiveDataSet());
+            autocomplete = AutoCompletionManager.of(OsmDataManager.getInstance().getActiveDataSet());
             List<AutoCompletionItem> keyList = autocomplete.getTagKeys(DEFAULT_AC_ITEM_COMPARATOR);
 
             keys = new AutoCompComboBox<>();
             keys.getModel().setComparator(Comparator.naturalOrder()); // according to Comparable
-            keys.setPrototypeDisplayValue(new AutoCompletionItem(key));
             keys.setEditable(true);
+            keys.setPrototypeDisplayValue(new AutoCompletionItem("dummy"));
             keys.getModel().addAllElements(keyList);
-            keys.setSelectedItem(key);
+            keys.setSelectedItemText(key);
 
             p.add(Box.createVerticalStrut(5), GBC.eol());
             p.add(new JLabel(tr("Key")), GBC.std());
@@ -516,37 +541,38 @@
 
             values = new AutoCompComboBox<>();
             values.getModel().setComparator(Comparator.naturalOrder());
-            values.setPrototypeDisplayValue(new AutoCompletionItem(selection));
-            values.setRenderer(cellRenderer);
+            values.setRenderer(new TEHListCellRenderer(values, values.getRenderer(), valueCount.get(key)));
             values.setEditable(true);
+            values.setPrototypeDisplayValue(new AutoCompletionItem("dummy"));
             values.getModel().addAllElements(valueList);
-            values.setSelectedItem(selection);
-            values.getEditor().setItem(selection);
+            values.setSelectedItemText(selection);
 
             p.add(Box.createVerticalStrut(5), GBC.eol());
             p.add(new JLabel(tr("Value")), GBC.std());
             p.add(Box.createHorizontalStrut(10), GBC.std());
             p.add(values, GBC.eol().fill(GBC.HORIZONTAL));
-            values.getEditor().addActionListener(e -> buttonAction(0, null));
-            addFocusAdapter(autocomplete, usedValuesAwareComparator);
+            p.add(Box.createVerticalStrut(2), GBC.eol());
 
-            addUpdateIconListener();
+            p.applyComponentOrientation(OrientationAction.getDefaultComponentOrientation());
+            keys.applyComponentOrientation(ComponentOrientation.LEFT_TO_RIGHT);
+            values.applyComponentOrientation(OrientationAction.getNamelikeOrientation(keys.getText()));
 
             setContent(mainPanel, false);
 
-            addWindowListener(new WindowAdapter() {
-                @Override
-                public void windowOpened(WindowEvent e) {
-                    if (initialFocusOnKey) {
-                        selectKeysComboBox();
-                    } else {
-                        selectValuesCombobox();
-                    }
-                }
-            });
+            addEventListeners();
         }
 
         @Override
+        public void autoCompBefore(AutoCompEvent e) {
+            updateValueModel(autocomplete, usedValuesAwareComparator);
+        }
+
+        @Override
+        public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
+            updateValueModel(autocomplete, usedValuesAwareComparator);
+        }
+
+        @Override
         public void performTagEdit() {
             String value = getEditItem(values);
             value = Normalizer.normalize(value, Normalizer.Form.NFC);
@@ -599,9 +625,15 @@
         }
     }
 
-    protected abstract class AbstractTagsDialog extends ExtendedDialog {
+    protected abstract class AbstractTagsDialog extends ExtendedDialog implements AutoCompListener, FocusListener, PopupMenuListener {
         protected AutoCompComboBox<AutoCompletionItem> keys;
         protected AutoCompComboBox<AutoCompletionItem> values;
+        protected boolean initialFocusOnKey = true;
+        /**
+         * The 'values' model is currently holding values for this key. Used for lazy-loading of
+         * values.
+         */
+        protected String currentValuesModelKey = "";
 
         AbstractTagsDialog(Component parent, String title, String... buttonTexts) {
             super(parent, title, buttonTexts);
@@ -622,6 +654,7 @@
 
             setRememberWindowGeometry(getClass().getName() + ".geometry",
                 WindowGeometry.centerInWindow(MainApplication.getMainFrame(), size));
+            keys.setFixedLocale(PROPERTY_FIX_TAG_LOCALE.get());
         }
 
         @Override
@@ -641,77 +674,76 @@
                     }
                     rememberWindowGeometry(geometry);
                 }
-                keys.setFixedLocale(PROPERTY_FIX_TAG_LOCALE.get());
                 updateOkButtonIcon();
             }
             super.setVisible(visible);
         }
 
-        private void selectACComboBoxSavingUnixBuffer(AutoCompComboBox<AutoCompletionItem> cb) {
-            // select combobox with saving unix system selection (middle mouse paste)
-            Clipboard sysSel = ClipboardUtils.getSystemSelection();
-            if (sysSel != null) {
-                Transferable old = ClipboardUtils.getClipboardContent(sysSel);
-                cb.requestFocusInWindow();
-                cb.getEditor().selectAll();
-                if (old != null) {
-                    sysSel.setContents(old, null);
-                }
-            } else {
-                cb.requestFocusInWindow();
-                cb.getEditor().selectAll();
+        /**
+         * Updates the values model if the key has changed
+         *
+         * @param autocomplete the autocompletion manager
+         * @param comparator sorting order for the items in the combo dropdown
+         */
+        protected void updateValueModel(AutoCompletionManager autocomplete, Comparator<AutoCompletionItem> comparator) {
+            String key = keys.getText();
+            if (!key.equals(currentValuesModelKey)) {
+                Logging.debug("updateValueModel: lazy loading values for key ''{0}''", key);
+                // key has changed, reload model
+                String savedText = values.getText();
+                values.getModel().removeAllElements();
+                values.getModel().addAllElements(autocomplete.getTagValues(getAutocompletionKeys(key), comparator));
+                values.applyComponentOrientation(OrientationAction.getNamelikeOrientation(key));
+                values.setSelectedItemText(savedText);
+                values.getEditor().selectAll();
+                currentValuesModelKey = key;
             }
         }
 
-        public void selectKeysComboBox() {
-            selectACComboBoxSavingUnixBuffer(keys);
+        protected void addEventListeners() {
+            // OK on Enter in values
+            values.getEditor().addActionListener(e -> buttonAction(0, null));
+            // update values orientation according to key
+            keys.getEditorComponent().addFocusListener(this);
+            // update the "values" data model before an autocomplete or list dropdown
+            values.getEditorComponent().addAutoCompListener(this);
+            values.addPopupMenuListener(this);
+            // set the initial focus to either combobox
+            addWindowListener(new WindowAdapter() {
+                @Override
+                public void windowOpened(WindowEvent e) {
+                    if (initialFocusOnKey) {
+                        keys.requestFocus();
+                    } else {
+                        values.requestFocus();
+                    }
+                }
+            });
         }
 
-        public void selectValuesCombobox() {
-            selectACComboBoxSavingUnixBuffer(values);
+        @Override
+        public void autoCompPerformed(AutoCompEvent e) {
         }
 
-        /**
-        * Create a focus handling adapter and apply in to the editor component of value
-        * autocompletion box.
-        * @param autocomplete Manager handling the autocompletion
-        * @param comparator Class to decide what values are offered on autocompletion
-        * @return The created adapter
-        */
-        protected FocusAdapter addFocusAdapter(final AutoCompletionManager autocomplete, final Comparator<AutoCompletionItem> comparator) {
-           // get the combo box' editor component
-           final JTextComponent editor = values.getEditorComponent();
-           // Refresh the values model when focus is gained
-           FocusAdapter focus = new FocusAdapter() {
-               @Override
-               public void focusGained(FocusEvent e) {
-                   Logging.trace("Focus gained by {0}, e={1}", values, e);
-                   String key = keys.getEditor().getItem().toString();
-                   List<AutoCompletionItem> correctItems = autocomplete.getTagValues(getAutocompletionKeys(key), comparator);
-                   ComboBoxModel<AutoCompletionItem> currentModel = values.getModel();
-                   final int size = correctItems.size();
-                   boolean valuesOK = size == currentModel.getSize()
-                           && IntStream.range(0, size).allMatch(i -> Objects.equals(currentModel.getElementAt(i), correctItems.get(i)));
-                   if (!valuesOK) {
-                       values.getModel().removeAllElements();
-                       values.getModel().addAllElements(correctItems);
-                   }
-                   if (!Objects.equals(key, objKey)) {
-                       values.getEditor().selectAll();
-                       objKey = key;
-                   }
-               }
-           };
-           editor.addFocusListener(focus);
-           return focus;
+        @Override
+        public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
         }
 
-        protected void addUpdateIconListener() {
-            keys.addActionListener(ignore -> updateOkButtonIcon());
-            values.addActionListener(ignore -> updateOkButtonIcon());
+        @Override
+        public void popupMenuCanceled(PopupMenuEvent e) {
         }
 
-        private void updateOkButtonIcon() {
+        @Override
+        public void focusGained(FocusEvent e) {
+        }
+
+        @Override
+        public void focusLost(FocusEvent e) {
+            // update the values combobox orientation if the key changed
+            values.applyComponentOrientation(OrientationAction.getNamelikeOrientation(keys.getText()));
+        }
+
+        protected void updateOkButtonIcon() {
             if (buttons.isEmpty()) {
                 return;
             }
@@ -745,12 +777,12 @@
 
     protected class AddTagsDialog extends AbstractTagsDialog {
         private final List<JosmAction> recentTagsActions = new ArrayList<>();
-        protected final transient FocusAdapter focus;
         private final JPanel mainPanel;
         private JPanel recentTagsPanel;
 
         // Counter of added commands for possible undo
         private int commandCount;
+        private final transient AutoCompletionManager autocomplete;
 
         protected AddTagsDialog() {
             super(MainApplication.getMainFrame(), tr("Add tag"), tr("OK"), tr("Cancel"));
@@ -758,51 +790,67 @@
             setCancelButton(2);
             configureContextsensitiveHelp("/Dialog/AddValue", true /* show help button */);
 
-            mainPanel = new JPanel(new GridBagLayout());
+            mainPanel = new JPanel(new GridBagLayout()) {
+                /**
+                 * This hack allows the comboboxes to have their own orientation.
+                 *
+                 * The problem is that
+                 * {@link org.openstreetmap.josm.gui.ExtendedDialog#showDialog ExtendedDialog} calls
+                 * {@code applyComponentOrientation} very late in the dialog construction process
+                 * thus overwriting the orientation the components have chosen for themselves.
+                 *
+                 * This stops the propagation of {@code applyComponentOrientation}, thus all
+                 * components may (and have to) set their own orientation.
+                 */
+                @Override
+                public void applyComponentOrientation(ComponentOrientation o) {
+                    setComponentOrientation(o);
+                }
+            };
+            mainPanel.add(new JLabel("<html>"+trn("This will change up to {0} object.",
+                "This will change up to {0} objects.", sel.size(), sel.size())
+                +"<br><br>"+tr("Please select a key")), GBC.eol().fill(GBC.HORIZONTAL));
+
             keys = new AutoCompComboBox<>();
+            keys.setPrototypeDisplayValue(new AutoCompletionItem("dummy"));
+            keys.setEditable(true);
+            keys.getModel().setComparator(Comparator.naturalOrder()); // according to Comparable
+            keys.setAutocompleteEnabled(AUTOCOMPLETE_KEYS.get());
+
+            mainPanel.add(keys, GBC.eop().fill(GBC.HORIZONTAL));
+            mainPanel.add(new JLabel(tr("Choose a value")), GBC.eol());
+
             values = new AutoCompComboBox<>();
-            keys.getModel().setComparator(Comparator.naturalOrder()); // according to Comparable
+            values.setPrototypeDisplayValue(new AutoCompletionItem("dummy"));
+            values.setEditable(true);
             values.getModel().setComparator(Comparator.naturalOrder());
-            keys.setPrototypeDisplayValue(new AutoCompletionItem("dummy"));
-            values.setPrototypeDisplayValue(new AutoCompletionItem("dummy"));
-            keys.setAutocompleteEnabled(AUTOCOMPLETE_KEYS.get());
             values.setAutocompleteEnabled(AUTOCOMPLETE_VALUES.get());
 
-            mainPanel.add(new JLabel("<html>"+trn("This will change up to {0} object.",
-                "This will change up to {0} objects.", sel.size(), sel.size())
-                +"<br><br>"+tr("Please select a key")), GBC.eol().fill(GBC.HORIZONTAL));
+            mainPanel.add(values, GBC.eop().fill(GBC.HORIZONTAL));
 
             cacheRecentTags();
-            AutoCompletionManager autocomplete = AutoCompletionManager.of(OsmDataManager.getInstance().getActiveDataSet());
+            autocomplete = AutoCompletionManager.of(OsmDataManager.getInstance().getActiveDataSet());
             List<AutoCompletionItem> keyList = autocomplete.getTagKeys(DEFAULT_AC_ITEM_COMPARATOR);
 
             // remove the object's tag keys from the list
             keyList.removeIf(item -> containsDataKey(item.getValue()));
 
-            keys.getModel().removeAllElements();
             keys.getModel().addAllElements(keyList);
-            keys.setEditable(true);
 
-            mainPanel.add(keys, GBC.eop().fill(GBC.HORIZONTAL));
+            updateValueModel(autocomplete, DEFAULT_AC_ITEM_COMPARATOR);
 
-            mainPanel.add(new JLabel(tr("Choose a value")), GBC.eol());
-            values.setEditable(true);
-            mainPanel.add(values, GBC.eop().fill(GBC.HORIZONTAL));
-
             // pre-fill first recent tag for which the key is not already present
             tags.stream()
                     .filter(tag -> !containsDataKey(tag.getKey()))
                     .findFirst()
                     .ifPresent(tag -> {
-                        keys.setSelectedItem(tag.getKey());
-                        values.setSelectedItem(tag.getValue());
+                        keys.setSelectedItemText(tag.getKey());
+                        values.setSelectedItemText(tag.getValue());
                     });
 
-            focus = addFocusAdapter(autocomplete, DEFAULT_AC_ITEM_COMPARATOR);
-            // fire focus event in advance or otherwise the popup list will be too small at first
-            focus.focusGained(new FocusEvent(this, FocusEvent.FOCUS_GAINED));
 
-            addUpdateIconListener();
+            keys.addActionListener(ignore -> updateOkButtonIcon());
+            values.addActionListener(ignore -> updateOkButtonIcon());
 
             // Add tag on Shift-Enter
             mainPanel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
@@ -812,7 +860,7 @@
                     public void actionPerformed(ActionEvent e) {
                         performTagAdding();
                         refreshRecentTags();
-                        selectKeysComboBox();
+                        keys.requestFocus();
                     }
                 });
 
@@ -819,9 +867,11 @@
             suggestRecentlyAddedTags();
 
             mainPanel.add(Box.createVerticalGlue(), GBC.eop().fill());
+            mainPanel.applyComponentOrientation(OrientationAction.getDefaultComponentOrientation());
+
             setContent(mainPanel, false);
 
-            selectKeysComboBox();
+            addEventListeners();
 
             popupMenu.add(new AbstractAction(tr("Set number of recently added tags")) {
                 @Override
@@ -848,6 +898,16 @@
             popupMenu.add(rememberLastTags);
         }
 
+        @Override
+        public void autoCompBefore(AutoCompEvent e) {
+            updateValueModel(autocomplete, DEFAULT_AC_ITEM_COMPARATOR);
+        }
+
+        @Override
+        public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
+            updateValueModel(autocomplete, DEFAULT_AC_ITEM_COMPARATOR);
+        }
+
         private JMenu buildMenuRecentExisting() {
             JMenu menu = new JMenu(tr("Recent tags with existing key"));
             TreeMap<RecentExisting, String> radios = new TreeMap<>();
@@ -971,11 +1031,11 @@
                         tr("Choose recent tag {0}", count), null, tr("Use this tag again"), sc, false) {
                     @Override
                     public void actionPerformed(ActionEvent e) {
-                        keys.setSelectedItem(t.getKey());
+                        keys.setSelectedItemText(t.getKey());
                         // fix #7951, #8298 - update list of values before setting value (?)
-                        focus.focusGained(new FocusEvent(AddTagsDialog.this, FocusEvent.FOCUS_GAINED));
-                        values.setSelectedItem(t.getValue());
-                        selectValuesCombobox();
+                        updateValueModel(autocomplete, DEFAULT_AC_ITEM_COMPARATOR);
+                        values.setSelectedItemText(t.getValue());
+                        values.requestFocus();
                     }
                 };
                 /* POSSIBLE SHORTCUTS: 1,2,3,4,5,6,7,8,9,0=10 */
@@ -988,7 +1048,7 @@
                         action.actionPerformed(null);
                         performTagAdding();
                         refreshRecentTags();
-                        selectKeysComboBox();
+                        keys.requestFocus();
                     }
                 };
                 recentTagsActions.add(action);
@@ -1036,7 +1096,7 @@
                                 // add tags on Shift-Click
                                 performTagAdding();
                                 refreshRecentTags();
-                                selectKeysComboBox();
+                                keys.requestFocus();
                             } else if (e.getClickCount() > 1) {
                                 // add tags and close window on double-click
                                 buttonAction(0, null); // emulate OK click and close the dialog
Index: src/org/openstreetmap/josm/gui/io/BasicUploadSettingsPanel.java
===================================================================
--- src/org/openstreetmap/josm/gui/io/BasicUploadSettingsPanel.java	(revision 18220)
+++ src/org/openstreetmap/josm/gui/io/BasicUploadSettingsPanel.java	(working copy)
@@ -107,7 +107,7 @@
         pnl.setBorder(BorderFactory.createTitledBorder(tr("Provide a brief comment for the changes you are uploading:")));
 
         hcbUploadComment.setToolTipText(tr("Enter an upload comment"));
-        hcbUploadComment.setMaxTextLength(Changeset.MAX_CHANGESET_TAG_LENGTH);
+        hcbUploadComment.getEditorComponent().setMaxTextLength(Changeset.MAX_CHANGESET_TAG_LENGTH);
         JTextField editor = hcbUploadComment.getEditorComponent();
         editor.getDocument().putProperty("tag", "comment");
         editor.addKeyListener(this);
@@ -146,7 +146,7 @@
         }
 
         hcbUploadSource.setToolTipText(tr("Enter a source"));
-        hcbUploadSource.setMaxTextLength(Changeset.MAX_CHANGESET_TAG_LENGTH);
+        hcbUploadSource.getEditorComponent().setMaxTextLength(Changeset.MAX_CHANGESET_TAG_LENGTH);
         JTextField editor = hcbUploadSource.getEditorComponent();
         editor.getDocument().putProperty("tag", "source");
         editor.addKeyListener(this);
Index: src/org/openstreetmap/josm/gui/io/OpenChangesetComboBoxModel.java
===================================================================
--- src/org/openstreetmap/josm/gui/io/OpenChangesetComboBoxModel.java	(revision 18220)
+++ src/org/openstreetmap/josm/gui/io/OpenChangesetComboBoxModel.java	(working copy)
@@ -4,13 +4,12 @@
 import java.util.ArrayList;
 import java.util.List;
 
-import javax.swing.DefaultComboBoxModel;
-
 import org.openstreetmap.josm.data.osm.Changeset;
 import org.openstreetmap.josm.data.osm.ChangesetCache;
 import org.openstreetmap.josm.data.osm.ChangesetCacheEvent;
 import org.openstreetmap.josm.data.osm.ChangesetCacheListener;
 import org.openstreetmap.josm.gui.util.GuiHelper;
+import org.openstreetmap.josm.gui.widgets.JosmComboBoxModel;
 import org.openstreetmap.josm.tools.Utils;
 
 /**
@@ -18,7 +17,7 @@
  * of open changesets kept in the {@link ChangesetCache}.
  *
  */
-public class OpenChangesetComboBoxModel extends DefaultComboBoxModel<Changeset> implements ChangesetCacheListener {
+public class OpenChangesetComboBoxModel extends JosmComboBoxModel<Changeset> implements ChangesetCacheListener {
     private final transient List<Changeset> changesets;
     private transient Changeset selectedChangeset;
 
@@ -78,7 +77,7 @@
     }
 
     @Override
-    public int getIndexOf(Object anObject) {
+    public int getIndexOf(Changeset anObject) {
         return changesets.indexOf(anObject);
     }
 
Index: src/org/openstreetmap/josm/gui/layer/geoimage/CorrelateGpxWithImages.java
===================================================================
--- src/org/openstreetmap/josm/gui/layer/geoimage/CorrelateGpxWithImages.java	(revision 18220)
+++ src/org/openstreetmap/josm/gui/layer/geoimage/CorrelateGpxWithImages.java	(working copy)
@@ -39,7 +39,6 @@
 import javax.swing.JOptionPane;
 import javax.swing.JPanel;
 import javax.swing.JSeparator;
-import javax.swing.MutableComboBoxModel;
 import javax.swing.SwingConstants;
 import javax.swing.event.ChangeEvent;
 import javax.swing.event.ChangeListener;
@@ -70,6 +69,7 @@
 import org.openstreetmap.josm.gui.layer.geoimage.SynchronizeTimeFromPhotoDialog.TimeZoneItem;
 import org.openstreetmap.josm.gui.layer.gpx.GpxDataHelper;
 import org.openstreetmap.josm.gui.widgets.JosmComboBox;
+import org.openstreetmap.josm.gui.widgets.JosmComboBoxModel;
 import org.openstreetmap.josm.gui.widgets.JosmTextField;
 import org.openstreetmap.josm.spi.preferences.Config;
 import org.openstreetmap.josm.tools.Destroyable;
@@ -85,7 +85,7 @@
  */
 public class CorrelateGpxWithImages extends AbstractAction implements ExpertModeChangeListener, Destroyable {
 
-    private static MutableComboBoxModel<GpxDataWrapper> gpxModel;
+    private static JosmComboBoxModel<GpxDataWrapper> gpxModel;
     private static boolean forceTags;
 
     private final transient GeoImageLayer yLayer;
@@ -423,7 +423,7 @@
      * @param nogdw Data wrapper with no GPX data
      */
     private void constructGpxModel(NoGpxDataWrapper nogdw) {
-        gpxModel = new DefaultComboBoxModel<>();
+        gpxModel = new JosmComboBoxModel<>();
         GpxDataWrapper defaultItem = null;
         for (AbstractModifiableLayer cur : MainApplication.getLayerManager().getLayersOfType(AbstractModifiableLayer.class)) {
             if (cur instanceof GpxDataContainer) {
Index: src/org/openstreetmap/josm/gui/preferences/display/LanguagePreference.java
===================================================================
--- src/org/openstreetmap/josm/gui/preferences/display/LanguagePreference.java	(revision 18220)
+++ src/org/openstreetmap/josm/gui/preferences/display/LanguagePreference.java	(working copy)
@@ -11,7 +11,6 @@
 import java.util.Locale;
 
 import javax.swing.Box;
-import javax.swing.DefaultComboBoxModel;
 import javax.swing.DefaultListCellRenderer;
 import javax.swing.JLabel;
 import javax.swing.JList;
@@ -24,6 +23,7 @@
 import org.openstreetmap.josm.gui.preferences.PreferenceSettingFactory;
 import org.openstreetmap.josm.gui.preferences.PreferenceTabbedPane;
 import org.openstreetmap.josm.gui.widgets.JosmComboBox;
+import org.openstreetmap.josm.gui.widgets.JosmComboBoxModel;
 import org.openstreetmap.josm.spi.preferences.Config;
 import org.openstreetmap.josm.tools.GBC;
 import org.openstreetmap.josm.tools.I18n;
@@ -81,7 +81,7 @@
                     LanguageInfo.getJOSMLocaleCode((Locale) langCombo.getSelectedItem()));
     }
 
-    private static class LanguageComboBoxModel extends DefaultComboBoxModel<Locale> {
+    private static class LanguageComboBoxModel extends JosmComboBoxModel<Locale> {
         private final List<Locale> data = new ArrayList<>();
 
         LanguageComboBoxModel() {
Index: src/org/openstreetmap/josm/gui/tagging/ac/AutoCompComboBox.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/ac/AutoCompComboBox.java	(revision 18220)
+++ src/org/openstreetmap/josm/gui/tagging/ac/AutoCompComboBox.java	(working copy)
@@ -1,35 +1,15 @@
 // License: GPL. For details, see LICENSE file.
 package org.openstreetmap.josm.gui.tagging.ac;
 
-import java.awt.datatransfer.Clipboard;
-import java.awt.datatransfer.StringSelection;
-import java.awt.datatransfer.Transferable;
-import java.awt.event.FocusEvent;
-import java.awt.event.FocusListener;
-import java.awt.event.KeyEvent;
-import java.awt.event.KeyListener;
 import java.awt.im.InputContext;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.LinkedList;
 import java.util.Locale;
-import java.util.Objects;
-import java.util.regex.Pattern;
 
-import javax.swing.JTextField;
-import javax.swing.SwingUtilities;
-import javax.swing.text.AbstractDocument;
-import javax.swing.text.AttributeSet;
-import javax.swing.text.BadLocationException;
-import javax.swing.text.DocumentFilter;
-import javax.swing.text.JTextComponent;
-import javax.swing.text.StyleConstants;
+import javax.swing.ComboBoxEditor;
 
-import org.openstreetmap.josm.gui.MainApplication;
-import org.openstreetmap.josm.gui.MapFrame;
-import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils;
 import org.openstreetmap.josm.gui.widgets.JosmComboBox;
-import org.openstreetmap.josm.spi.preferences.Config;
 import org.openstreetmap.josm.tools.Logging;
 
 /**
@@ -44,89 +24,13 @@
  * @param <E> the type of the combobox entries
  * @since 18173
  */
-public class AutoCompComboBox<E> extends JosmComboBox<E> implements KeyListener {
+public class AutoCompComboBox<E> extends JosmComboBox<E> implements AutoCompListener {
 
-    /** a regex that matches numbers */
-    private static final Pattern IS_NUMBER = Pattern.compile("^\\d+$");
-    /** true if the combobox should autocomplete */
-    private boolean autocompleteEnabled = true;
-    /** the editor will not accept text longer than this. -1 to disable */
-    private int maxTextLength = -1;
     /** force a different keyboard input locale for the editor */
     private boolean useFixedLocale;
-
-    /** Whether to autocomplete numbers */
-    private final boolean AUTOCOMPLETE_NUMBERS = !Config.getPref().getBoolean("autocomplete.dont_complete_numbers", true);
-
     private final transient InputContext privateInputContext = InputContext.getInstance();
 
-    static final class InnerFocusListener implements FocusListener {
-        private final JTextComponent editorComponent;
-
-        InnerFocusListener(JTextComponent editorComponent) {
-            this.editorComponent = editorComponent;
-        }
-
-        @Override
-        public void focusLost(FocusEvent e) {
-            MapFrame map = MainApplication.getMap();
-            if (map != null) {
-                map.keyDetector.setEnabled(true);
-            }
-        }
-
-        @Override
-        public void focusGained(FocusEvent e) {
-            MapFrame map = MainApplication.getMap();
-            if (map != null) {
-                map.keyDetector.setEnabled(false);
-            }
-            // save unix system selection (middle mouse paste)
-            Clipboard sysSel = ClipboardUtils.getSystemSelection();
-            if (sysSel != null) {
-                Transferable old = ClipboardUtils.getClipboardContent(sysSel);
-                editorComponent.selectAll();
-                if (old != null) {
-                    sysSel.setContents(old, null);
-                }
-            } else if (e != null && e.getOppositeComponent() != null) {
-                // Select all characters when the change of focus occurs inside JOSM only.
-                // When switching from another application, it is annoying, see #13747
-                editorComponent.selectAll();
-            }
-        }
-    }
-
     /**
-     * A {@link DocumentFilter} to limit the text length in the editor.
-     */
-    private class MaxLengthDocumentFilter extends DocumentFilter {
-        @Override
-        public void insertString(FilterBypass fb, int offset, String string, AttributeSet attr)
-                throws BadLocationException {
-            if (mustInsertOrReplace(fb, 0, string, attr)) {
-                super.insertString(fb, offset, string, attr);
-            }
-        }
-
-        @Override
-        public void replace(FilterBypass fb, int offset, int length, String string, AttributeSet attr)
-                throws BadLocationException {
-            if (mustInsertOrReplace(fb, length, string, attr)) {
-                super.replace(fb, offset, length, string, attr);
-            }
-        }
-
-        private boolean mustInsertOrReplace(FilterBypass fb, int length, String string, AttributeSet attr) {
-            int newLen = fb.getDocument().getLength() - length + ((string == null) ? 0 : string.length());
-            return (maxTextLength == -1 || newLen <= maxTextLength ||
-                    // allow longer text while composing characters or it will be hard to compose
-                    // the last characters before the limit
-                    ((attr != null) && attr.isDefined(StyleConstants.ComposedTextAttribute)));
-        }
-    }
-
-    /**
      * Constructs an {@code AutoCompletingComboBox}.
      */
     public AutoCompComboBox() {
@@ -140,18 +44,16 @@
      */
     public AutoCompComboBox(AutoCompComboBoxModel<E> model) {
         super(model);
-        Objects.requireNonNull(model, "A model cannot be null.");
+        setEditor(new AutoCompComboBoxEditor<E>());
         setEditable(true);
-        final JTextComponent editorComponent = getEditorComponent();
-        editorComponent.addFocusListener(new InnerFocusListener(editorComponent));
-        editorComponent.addKeyListener(this);
-        ((AbstractDocument) editorComponent.getDocument()).setDocumentFilter(new MaxLengthDocumentFilter());
+        getEditorComponent().setModel(model);
+        getEditorComponent().addAutoCompListener(this);
     }
 
     /**
      * Returns the {@link AutoCompComboBoxModel} currently used.
      *
-     * @return the model
+     * @return the model or null
      */
     @Override
     public AutoCompComboBoxModel<E> getModel() {
@@ -158,59 +60,41 @@
         return (AutoCompComboBoxModel<E>) dataModel;
     }
 
-    /**
-     * Autocompletes what the user typed in.
-     * <p>
-     * Gets the user input from the editor, finds the best matching item in the model, selects it in
-     * the list, sets the editor text, and highlights the autocompleted part. If there is no
-     * matching item, removes the list selection.
-     */
-    private void autocomplete() {
-        JTextField editor = getEditorComponent();
-        String prefix = editor.getText();
-        if (!AUTOCOMPLETE_NUMBERS && IS_NUMBER.matcher(prefix).matches())
-            return;
-
-        E item = getModel().findBestCandidate(prefix);
-        if (item != null) {
-            String text = item.toString();
-            // This calls setItem() if the selected item changed
-            // See: javax.swing.plaf.basic.BasicComboBoxUI.Handler.contentsChanged(ListDataEvent e)
-            setSelectedItem(item);
-            // set manually in case the selected item didn't change
-            editor.setText(text);
-            // select the autocompleted suffix in the editor
-            editor.select(prefix.length(), text.length());
-            // copy the whole autocompleted string to the unix system-wide selection (aka
-            // middle-click), else only the selected suffix would be copied
-            copyToSysSel(text);
-        } else {
-            setSelectedItem(null);
-            // avoid setItem because it selects the whole text (on windows only)
-            editor.setText(prefix);
+    @Override
+    public void setEditor(ComboBoxEditor newEditor) {
+        if (editor != null) {
+            editor.getEditorComponent().removePropertyChangeListener(this);
         }
+        super.setEditor(newEditor);
+        if (editor != null) {
+            // listen to orientation changes in the editor
+            editor.getEditorComponent().addPropertyChangeListener(this);
+        }
     }
 
     /**
-     * Copies a String to the UNIX system-wide selection (aka middle-click).
+     * Returns the editor component
      *
-     * @param s the string to copy
+     * @return the editor component
+     * @see ComboBoxEditor#getEditorComponent()
+     * @since xxx
      */
-    void copyToSysSel(String s) {
-        Clipboard sysSel = ClipboardUtils.getSystemSelection();
-        if (sysSel != null) {
-            Transferable transferable = new StringSelection(s);
-            sysSel.setContents(transferable, null);
-        }
+    @Override
+    @SuppressWarnings("unchecked")
+    public AutoCompTextField<E> getEditorComponent() {
+        return getEditor() == null ? null : (AutoCompTextField<E>) getEditor().getEditorComponent();
     }
 
     /**
-     * Sets the maximum text length.
+     * Selects the autocompleted item in the dropdown.
      *
-     * @param length the maximum text length in number of characters
+     * @param item the item selected for autocomplete
      */
-    public void setMaxTextLength(int length) {
-        maxTextLength = length;
+    private void autocomplete(Object item) {
+        // Save the text in case item is null, because setSelectedItem will erase it.
+        String savedText = getText();
+        setSelectedItem(item);
+        setText(savedText);
     }
 
     /**
@@ -262,15 +146,6 @@
     }
 
     /**
-     * Returns {@code true} if autocompletion is enabled.
-     *
-     * @return {@code true} if autocompletion is enabled.
-     */
-    public final boolean isAutocompleteEnabled() {
-        return autocompleteEnabled;
-    }
-
-    /**
      * Enables or disables the autocompletion.
      *
      * @param enabled {@code true} to enable autocompletion
@@ -278,9 +153,7 @@
      * @since 18173 (signature)
      */
     public boolean setAutocompleteEnabled(boolean enabled) {
-        boolean oldEnabled = this.autocompleteEnabled;
-        this.autocompleteEnabled = enabled;
-        return oldEnabled;
+        return getEditorComponent().setAutocompleteEnabled(enabled);
     }
 
     /**
@@ -318,32 +191,14 @@
         return super.getInputContext();
     }
 
-    /*
-     * The KeyListener interface
-     */
+    /** AutoCompListener Interface */
 
-    /**
-     * Listens to key events and eventually schedules an autocomplete.
-     *
-     * @param e the key event
-     */
     @Override
-    public void keyTyped(KeyEvent e) {
-        if (autocompleteEnabled
-                // and selection is at the end
-                && getEditorComponent().getSelectionEnd() == getEditorComponent().getText().length()
-                // and something visible was typed
-                && !Character.isISOControl(e.getKeyChar())) {
-            // We got the event before the editor component could see it. Let the editor do its job first.
-            SwingUtilities.invokeLater(() -> autocomplete());
-        }
+    public void autoCompBefore(AutoCompEvent e) {
     }
 
     @Override
-    public void keyPressed(KeyEvent e) {
+    public void autoCompPerformed(AutoCompEvent e) {
+        autocomplete(e.getItem());
     }
-
-    @Override
-    public void keyReleased(KeyEvent e) {
-    }
 }
Index: src/org/openstreetmap/josm/gui/tagging/ac/AutoCompComboBoxEditor.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/ac/AutoCompComboBoxEditor.java	(nonexistent)
+++ src/org/openstreetmap/josm/gui/tagging/ac/AutoCompComboBoxEditor.java	(working copy)
@@ -0,0 +1,28 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.tagging.ac;
+
+import org.openstreetmap.josm.gui.widgets.JosmComboBoxEditor;
+
+/**
+ * A {@link javax.swing.ComboBoxEditor} that uses an {@link AutoCompTextField}.
+ * <p>
+ * This lets us stick an {@code AutoCompTextField} into a {@link javax.swing.JComboBox}.  This is not
+ * used for {@link AutoCompComboBox}.
+ *
+ * @param <E> the type of the items in the editor
+ * @since xxx
+ */
+public class AutoCompComboBoxEditor<E> extends JosmComboBoxEditor {
+
+    @Override
+    protected AutoCompTextField<E> createEditorComponent() {
+        return new AutoCompTextField<>();
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public AutoCompTextField<E> getEditorComponent() {
+        // this cast holds unless somebody overrides createEditorComponent()
+        return (AutoCompTextField<E>) editor;
+    }
+}
Index: src/org/openstreetmap/josm/gui/tagging/ac/AutoCompComboBoxModel.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/ac/AutoCompComboBoxModel.java	(revision 18220)
+++ src/org/openstreetmap/josm/gui/tagging/ac/AutoCompComboBoxModel.java	(working copy)
@@ -1,20 +1,11 @@
 // License: GPL. For details, see LICENSE file.
 package org.openstreetmap.josm.gui.tagging.ac;
 
-import java.util.ArrayList;
-import java.util.Collection;
 import java.util.Comparator;
-import java.util.Iterator;
-import java.util.List;
 import java.util.Objects;
-import java.util.function.Function;
 
-import javax.swing.AbstractListModel;
-import javax.swing.MutableComboBoxModel;
+import org.openstreetmap.josm.gui.widgets.JosmComboBoxModel;
 
-import org.openstreetmap.josm.data.preferences.ListProperty;
-import org.openstreetmap.josm.spi.preferences.Config;
-
 /**
  * A data model for the {@link AutoCompComboBox}
  *
@@ -22,7 +13,7 @@
  * @param <E> The element type.
  * @since 18173
  */
-public class AutoCompComboBoxModel<E> extends AbstractListModel<E> implements MutableComboBoxModel<E>, Iterable<E> {
+public class AutoCompComboBoxModel<E> extends JosmComboBoxModel<E> {
 
     /**
      * The comparator used by {@link #findBestCandidate}
@@ -32,14 +23,7 @@
      * {@code E::toString}.
      */
     private Comparator<E> comparator;
-    /** The maximum number of elements to hold, -1 for no limit. Used for histories. */
-    private int maxSize = -1;
 
-    /** the elements shown in the dropdown */
-    protected ArrayList<E> elements = new ArrayList<>();
-    /** the selected element in the dropdown or null */
-    protected Object selected;
-
     /**
      * Constructs a new empty model with a default {@link #comparator}.
      */
@@ -73,177 +57,6 @@
     }
 
     /**
-     * Sets the maximum number of elements.
-     *
-     * @param size The maximal number of elements in the model.
-     */
-    public void setSize(int size) {
-        maxSize = size;
-    }
-
-    /**
-     * Returns a copy of the element list.
-     * @return a copy of the data
-     */
-    public Collection<E> asCollection() {
-        return new ArrayList<>(elements);
-    }
-
-    //
-    // interface java.lang.Iterable
-    //
-
-    @Override
-    public Iterator<E> iterator() {
-        return elements.iterator();
-    }
-
-    //
-    // interface javax.swing.MutableComboBoxModel
-    //
-
-    /**
-     * Adds an element to the end of the model. Does nothing if max size is already reached.
-     */
-    @Override
-    public void addElement(E element) {
-        if (element != null && (maxSize == -1 || getSize() < maxSize)) {
-            elements.add(element);
-        }
-    }
-
-    @Override
-    public void removeElement(Object elem) {
-        elements.remove(elem);
-    }
-
-    @Override
-    public void removeElementAt(int index) {
-        Object elem = getElementAt(index);
-        if (elem == selected) {
-            if (index == 0) {
-                setSelectedItem(getSize() == 1 ? null : getElementAt(index + 1));
-            } else {
-                setSelectedItem(getElementAt(index - 1));
-            }
-        }
-        elements.remove(index);
-        fireIntervalRemoved(this, index, index);
-    }
-
-    /**
-     * Adds an element at a specific index.
-     *
-     * @param element The element to add
-     * @param index Location to add the element
-     */
-    @Override
-    public void insertElementAt(E element, int index) {
-        if (maxSize != -1 && maxSize <= getSize()) {
-            removeElementAt(getSize() - 1);
-        }
-        elements.add(index, element);
-    }
-
-    //
-    // javax.swing.ComboBoxModel
-    //
-
-    /**
-     * Set the value of the selected item. The selected item may be null.
-     *
-     * @param elem The combo box value or null for no selection.
-     */
-    @Override
-    public void setSelectedItem(Object elem) {
-        if ((selected != null && !selected.equals(elem)) ||
-            (selected == null && elem != null)) {
-            selected = elem;
-            fireContentsChanged(this, -1, -1);
-        }
-    }
-
-    @Override
-    public Object getSelectedItem() {
-        return selected;
-    }
-
-    //
-    // javax.swing.ListModel
-    //
-
-    @Override
-    public int getSize() {
-        return elements.size();
-    }
-
-    @Override
-    public E getElementAt(int index) {
-        if (index >= 0 && index < elements.size())
-            return elements.get(index);
-        else
-            return null;
-    }
-
-    //
-    // end interfaces
-    //
-
-    /**
-     * Adds all elements from the collection.
-     *
-     * @param elems The elements to add.
-     */
-    public void addAllElements(Collection<E> elems) {
-        elems.forEach(e -> addElement(e));
-    }
-
-    /**
-     * Adds all elements from the collection of string representations.
-     *
-     * @param strings The string representation of the elements to add.
-     * @param buildE A {@link java.util.function.Function} that builds an {@code <E>} from a
-     *               {@code String}.
-     */
-    public void addAllElements(Collection<String> strings, Function<String, E> buildE) {
-        strings.forEach(s -> addElement(buildE.apply(s)));
-    }
-
-    /**
-     * 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,
-     * deletes the last element.
-     *
-     * @param newElement the element to add
-     * @return The element that is at the top now.
-     */
-    public E addTopElement(E newElement) {
-        // if the element is already at the top, do nothing
-        if (newElement.equals(getElementAt(0)))
-            return getElementAt(0);
-
-        removeElement(newElement);
-        insertElementAt(newElement, 0);
-        return newElement;
-    }
-
-    /**
-     * Empties the list.
-     */
-    public void removeAllElements() {
-        if (!elements.isEmpty()) {
-            int firstIndex = 0;
-            int lastIndex = elements.size() - 1;
-            elements.clear();
-            selected = null;
-            fireIntervalRemoved(this, firstIndex, lastIndex);
-        } else {
-            selected = null;
-        }
-    }
-
-    /**
      * Finds the best candidate for autocompletion.
      * <p>
      * Looks in the model for an element whose prefix matches {@code prefix}. If more than one
@@ -262,98 +75,4 @@
                            comparator.compare(x, y))
             .orElse(null);
     }
-
-    /**
-     * Gets a preference loader and saver.
-     *
-     * @param readE A {@link Function} that builds an {@code <E>} from a {@link String}.
-     * @param writeE A {@code Function} that serializes an {@code <E>} to a {@code String}
-     * @return The {@link Preferences} instance.
-     */
-    public Preferences prefs(Function<String, E> readE, Function<E, String> writeE) {
-        return new Preferences(readE, writeE);
-    }
-
-    /**
-     * Loads and saves the model to the JOSM preferences.
-     * <p>
-     * Obtainable through {@link #prefs}.
-     */
-    public final class Preferences {
-
-        /** A {@link Function} that builds an {@code <E>} from a {@code String}. */
-        private Function<String, E> readE;
-        /** A {@code Function} that serializes {@code <E>} to a {@code String}. */
-        private Function<E, String> writeE;
-
-        /**
-         * Private constructor
-         *
-         * @param readE A {@link Function} that builds an {@code <E>} from a {@code String}.
-         * @param writeE A {@code Function} that serializes an {@code <E>} to a {@code String}
-         */
-        private Preferences(Function<String, E> readE, Function<E, String> writeE) {
-            this.readE = readE;
-            this.writeE = writeE;
-        }
-
-        /**
-         * Loads the model from the JOSM preferences.
-         * @param key The preferences key
-         */
-        public void load(String key) {
-            removeAllElements();
-            addAllElements(Config.getPref().getList(key), readE);
-        }
-
-        /**
-         * Loads the model from the JOSM preferences.
-         *
-         * @param key The preferences key
-         * @param defaults A list of default values.
-         */
-        public void load(String key, List<String> defaults) {
-            removeAllElements();
-            addAllElements(Config.getPref().getList(key, defaults), readE);
-        }
-
-        /**
-         * Loads the model from the JOSM preferences.
-         *
-         * @param prop The property holding the strings.
-         */
-        public void load(ListProperty prop) {
-            removeAllElements();
-            addAllElements(prop.get(), readE);
-        }
-
-        /**
-         * Returns the model elements as list of strings.
-         *
-         * @return a list of strings
-         */
-        public List<String> asStringList() {
-            List<String> list = new ArrayList<>(getSize());
-            forEach(element -> list.add(writeE.apply(element)));
-            return list;
-        }
-
-        /**
-         * Saves the model to the JOSM preferences.
-         *
-        * @param key The preferences key
-        */
-        public void save(String key) {
-            Config.getPref().putList(key, asStringList());
-        }
-
-        /**
-         * Saves the model to the JOSM preferences.
-         *
-         * @param prop The property to write to.
-         */
-        public void save(ListProperty prop) {
-            prop.put(asStringList());
-        }
-    }
 }
Index: src/org/openstreetmap/josm/gui/tagging/ac/AutoCompEvent.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/ac/AutoCompEvent.java	(nonexistent)
+++ src/org/openstreetmap/josm/gui/tagging/ac/AutoCompEvent.java	(working copy)
@@ -0,0 +1,95 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.tagging.ac;
+
+import java.awt.AWTEvent;
+
+/**
+ * This event is generated by an AutoCompTextField when an autocomplete occured.
+ *
+ * @see AutoCompTextField
+ * @see AutoCompListener
+ * @since xxx
+ */
+
+public class AutoCompEvent extends AWTEvent {
+
+    /**
+     * The first number in the range of ids used for autoComp events.
+     */
+    public static final int AUTOCOMP_FIRST = 5900;
+
+    /**
+     * The last number in the range of ids used for autoComp events.
+     */
+    public static final int AUTOCOMP_LAST = 5901;
+
+    /**
+     * This event id indicates that an autocomp is about to start.
+     */
+    public static final int AUTOCOMP_BEFORE = AUTOCOMP_FIRST;
+
+    /**
+     * This event id indicates that an autocomp completed.
+     */
+    public static final int AUTOCOMP_DONE = AUTOCOMP_FIRST + 1;
+
+    /*
+     * JDK 1.1 serialVersionUID
+     */
+    private static final long serialVersionUID = 3745384758753475838L;
+
+    /** the selected autocomplete item */
+    private Object item;
+
+    /**
+     * Constructs a <code>AutoCompEvent</code> object.
+     * <p> This method throws an
+     * <code>IllegalArgumentException</code> if <code>source</code>
+     * is <code>null</code>.
+     *
+     * @param source The (<code>AutoCompTextField</code>) object that
+     *               originated the event
+     * @param id     An integer that identifies the event type.
+     *               For information on allowable values, see
+     *               the class description for {@link AutoCompEvent}
+     * @param item   The item selected for autocompletion.
+     * @throws IllegalArgumentException if <code>source</code> is null
+     * @see #getSource()
+     * @see #getID()
+     */
+    public AutoCompEvent(Object source, int id, Object item) {
+        super(source, id);
+        this.item = item;
+    }
+
+    /**
+     * Returns the item selected for autocompletion.
+     *
+     * @return the item selected for autocompletion
+     */
+    public Object getItem() {
+        return item;
+    }
+
+    /**
+     * Returns a parameter string identifying this text event.
+     * This method is useful for event-logging and for debugging.
+     *
+     * @return a string identifying the event and its attributes
+     */
+    @Override
+    public String paramString() {
+        String typeStr;
+        switch(id) {
+            case AUTOCOMP_BEFORE:
+                typeStr = "AUTOCOMP_BEFORE";
+                break;
+            case AUTOCOMP_DONE:
+                typeStr = "AUTOCOMP_DONE";
+                break;
+          default:
+                typeStr = "unknown type";
+        }
+        return typeStr;
+    }
+}
Index: src/org/openstreetmap/josm/gui/tagging/ac/AutoCompListener.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/ac/AutoCompListener.java	(nonexistent)
+++ src/org/openstreetmap/josm/gui/tagging/ac/AutoCompListener.java	(working copy)
@@ -0,0 +1,33 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.tagging.ac;
+
+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.
+ *
+ * @see AutoCompEvent
+ * @since xxx
+ */
+public interface AutoCompListener extends EventListener {
+
+    /**
+     * Invoked before an autocomplete.  You can use this to change the model.
+     *
+     * @param e an {@link AutoCompEvent}
+     */
+    void autoCompBefore(AutoCompEvent e);
+
+    /**
+     * Invoked after an autocomplete happened.
+     *
+     * @param e an {@link AutoCompEvent}
+     */
+    void autoCompPerformed(AutoCompEvent e);
+}
Index: src/org/openstreetmap/josm/gui/tagging/ac/AutoCompTextField.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/ac/AutoCompTextField.java	(nonexistent)
+++ src/org/openstreetmap/josm/gui/tagging/ac/AutoCompTextField.java	(working copy)
@@ -0,0 +1,339 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.tagging.ac;
+
+import java.awt.Component;
+import java.awt.datatransfer.Clipboard;
+import java.awt.datatransfer.StringSelection;
+import java.awt.datatransfer.Transferable;
+import java.awt.event.KeyEvent;
+import java.awt.event.KeyListener;
+import java.util.EventObject;
+import java.util.regex.Pattern;
+
+import javax.swing.JTable;
+import javax.swing.SwingUtilities;
+import javax.swing.event.CellEditorListener;
+import javax.swing.table.TableCellEditor;
+import javax.swing.text.AbstractDocument;
+
+import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils;
+import org.openstreetmap.josm.gui.util.CellEditorSupport;
+import org.openstreetmap.josm.gui.widgets.JosmTextField;
+import org.openstreetmap.josm.spi.preferences.Config;
+
+/**
+ * An auto-completing TextField.
+ * <p>
+ * When the user starts typing, this textfield will suggest the
+ * {@link AutoCompComboBoxModel#findBestCandidate best matching item} from its model.  The items in
+ * the model can be of any type while the items' {@code toString} values are used for
+ * autocompletion.
+ *
+ * @param <E> the type of items in the model
+ * @since xxx
+ */
+public class AutoCompTextField<E> extends JosmTextField implements TableCellEditor, KeyListener {
+
+    /** true if the combobox should autocomplete */
+    private boolean autocompleteEnabled = true;
+    /** a filter to enforce max. text length */
+    private MaxLengthDocumentFilter docFilter;
+    /** the model */
+    protected AutoCompComboBoxModel<E> model;
+    /** Whether to autocomplete numbers */
+    private final boolean AUTOCOMPLETE_NUMBERS = !Config.getPref().getBoolean("autocomplete.dont_complete_numbers", true);
+    /** a regex that matches numbers */
+    private static final Pattern IS_NUMBER = Pattern.compile("^\\d+$");
+
+    protected final void init() {
+        model = new AutoCompComboBoxModel<>();
+        docFilter = new MaxLengthDocumentFilter();
+        ((AbstractDocument) getDocument()).setDocumentFilter(docFilter);
+        addKeyListener(this);
+        tableCellEditorSupport = new CellEditorSupport(this);
+    }
+
+    /**
+     * Constructs a new {@code AutoCompTextField}.
+     */
+    public AutoCompTextField() {
+        this(0);
+    }
+
+    /**
+     * Constructs a new {@code AutoCompTextField}.
+     * @param model the model to use
+     */
+    public AutoCompTextField(AutoCompComboBoxModel<E> model) {
+        this(0);
+        this.model = model;
+    }
+
+    /**
+     * Constructs a new {@code AutoCompTextField}.
+     * @param columns the number of columns to use to calculate the preferred width;
+     * if columns is set to zero, the preferred width will be whatever naturally results from the component implementation
+     */
+    public AutoCompTextField(int columns) {
+        this(columns, true);
+    }
+
+    /**
+     * Constructs a new {@code AutoCompTextField}.
+     * @param columns the number of columns to use to calculate the preferred width;
+     * if columns is set to zero, the preferred width will be whatever naturally results from the component implementation
+     * @param undoRedo Enables or not Undo/Redo feature. Not recommended for table cell editors, unless each cell provides its own editor
+     */
+    public AutoCompTextField(int columns, boolean undoRedo) {
+        super(null, null, columns, undoRedo);
+        init();
+    }
+
+    /**
+     * Returns the {@link AutoCompComboBoxModel} currently used.
+     *
+     * @return the model
+     */
+    public AutoCompComboBoxModel<E> getModel() {
+        return model;
+    }
+
+    /**
+     * Sets the data model that the {@code AutoCompTextField} uses to obtain the list of items.
+     *
+     * @param model the {@link AutoCompComboBoxModel} that provides the list of items used for autocomplete
+     */
+    public void setModel(AutoCompComboBoxModel<E> model) {
+        AutoCompComboBoxModel<E> oldModel = this.model;
+        this.model = model;
+        firePropertyChange("model", oldModel, model);
+    }
+
+    /**
+     * Returns {@code true} if autocompletion is enabled.
+     *
+     * @return {@code true} if autocompletion is enabled.
+     */
+    public final boolean isAutocompleteEnabled() {
+        return autocompleteEnabled;
+    }
+
+    /**
+     * Enables or disables the autocompletion.
+     *
+     * @param enabled {@code true} to enable autocompletion
+     * @return {@code true} if autocomplete was enabled before calling this
+     */
+    public boolean setAutocompleteEnabled(boolean enabled) {
+        boolean oldEnabled = this.autocompleteEnabled;
+        this.autocompleteEnabled = enabled;
+        return oldEnabled;
+    }
+
+    /**
+     * Sets the maximum number of characters allowed.
+     * @param length maximum number of characters allowed
+     */
+    public void setMaxTextLength(int length) {
+        docFilter.setMaxLength(length);
+    }
+
+    /**
+     * Autocompletes what the user typed in.
+     * <p>
+     * Gets the user input from the editor, finds the best matching item in the model, sets the
+     * editor text to it, and highlights the autocompleted part. If there is no matching item, removes the
+     * list selection.
+     *
+     * @param oldText the text before the last keypress was processed
+     */
+    private void autocomplete(String oldText) {
+        String newText = getText();
+        if (getSelectionEnd() != newText.length())
+            // selection not at the end
+            return;
+        // if the user typed some control character (eg. Alt+A) the selection may still be there
+        String unSelected = newText.substring(0, getSelectionStart());
+        if (unSelected.length() <= oldText.length())
+            // do not autocomplete on control or deleted chars
+            return;
+        if (!AUTOCOMPLETE_NUMBERS && IS_NUMBER.matcher(newText).matches())
+            return;
+
+        fireAutoCompEvent(AutoCompEvent.AUTOCOMP_BEFORE, null);
+        E item = getModel().findBestCandidate(newText);
+        fireAutoCompEvent(AutoCompEvent.AUTOCOMP_DONE, item);
+
+        if (item != null) {
+            String text = item.toString();
+            setText(text);
+            // select the autocompleted suffix in the editor
+            select(newText.length(), text.length());
+            // copy the whole autocompleted string to the unix system-wide selection (aka
+            // middle-click), else only the selected suffix would be copied
+            copyToSysSel(text);
+        }
+    }
+
+    /**
+     * Copies a String to the UNIX system-wide selection (aka middle-click).
+     *
+     * @param s the string to copy
+     */
+    void copyToSysSel(String s) {
+        Clipboard sysSel = ClipboardUtils.getSystemSelection();
+        if (sysSel != null) {
+            Transferable transferable = new StringSelection(s);
+            sysSel.setContents(transferable, null);
+        }
+    }
+
+    /**
+     * Adds an AutoCompListener.
+     *
+     * @param l the AutoComp listener to be added
+     */
+    public synchronized void addAutoCompListener(AutoCompListener l) {
+        listenerList.add(AutoCompListener.class, l);
+    }
+
+    /**
+     * Removes the specified AutoCompListener.
+     *
+     * @param l the autoComp listener to be removed
+     */
+    public synchronized void removeActionListener(AutoCompListener l) {
+        if ((l != null) && (getAction() == l)) {
+            setAction(null);
+        } else {
+            listenerList.remove(AutoCompListener.class, l);
+        }
+    }
+
+    /**
+     * Returns an array of all the current <code>AutoCompListener</code>s.
+     *
+     * @return all of the <code>AutoCompListener</code>s added or an empty
+     *         array if no listeners have been added
+     */
+    public synchronized AutoCompListener[] getAutoCompListeners() {
+        return listenerList.getListeners(AutoCompListener.class);
+    }
+
+    /**
+     * Notifies all listeners that have registered interest for notification on this event type.
+     * The event instance is lazily created. The listener list is processed in last to first order.
+     *
+     * @param id The Autocomp event id
+     * @param item The item selected for autocompletion.
+     * @see javax.swing.event.EventListenerList
+     */
+    protected void fireAutoCompEvent(int id, Object item) {
+        // Guaranteed to return a non-null array
+        Object[] listeners = listenerList.getListenerList();
+        AutoCompEvent e = new AutoCompEvent(this, id, item);
+
+        // Process the listeners last to first, notifying
+        // those that are interested in this event
+        for (int i = listeners.length - 2; i >= 0; i -= 2) {
+            if (listeners[i] == AutoCompListener.class) {
+                switch (id) {
+                    case AutoCompEvent.AUTOCOMP_DONE:
+                        ((AutoCompListener) listeners[i + 1]).autoCompPerformed(e);
+                        break;
+                    case AutoCompEvent.AUTOCOMP_BEFORE:
+                        ((AutoCompListener) listeners[i + 1]).autoCompBefore(e);
+                        break;
+                }
+            }
+        }
+    }
+
+    /* ------------------------------------------------------------------------------------ */
+    /* KeyListener interface                                                                */
+    /* ------------------------------------------------------------------------------------ */
+
+    /**
+     * Listens to key events and eventually schedules an autocomplete.
+     *
+     * @param e the key event
+     */
+    @Override
+    public void keyTyped(KeyEvent e) {
+        if (autocompleteEnabled) {
+            // if selection is at the end
+            if (getSelectionEnd() == getText().length()) {
+                final String oldText = getText().substring(0, getSelectionStart());
+                // We got the event before the editor component could see it. Let the editor do its job first.
+                SwingUtilities.invokeLater(() -> autocomplete(oldText));
+            }
+        }
+    }
+
+    @Override
+    public void keyPressed(KeyEvent e) {
+    }
+
+    @Override
+    public void keyReleased(KeyEvent e) {
+    }
+
+    /* ------------------------------------------------------------------------------------ */
+    /* 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();
+    }
+
+    @Override
+    public boolean isCellEditable(EventObject anEvent) {
+        return true;
+    }
+
+    @Override
+    public boolean shouldSelectCell(EventObject anEvent) {
+        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/AutoCompletionManager.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/ac/AutoCompletionManager.java	(revision 18220)
+++ src/org/openstreetmap/josm/gui/tagging/ac/AutoCompletionManager.java	(working copy)
@@ -345,6 +345,30 @@
     }
 
     /**
+     * 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 xxx
+     */
+    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
Index: src/org/openstreetmap/josm/gui/tagging/ac/MaxLengthDocumentFilter.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/ac/MaxLengthDocumentFilter.java	(nonexistent)
+++ src/org/openstreetmap/josm/gui/tagging/ac/MaxLengthDocumentFilter.java	(working copy)
@@ -0,0 +1,48 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.tagging.ac;
+
+import javax.swing.text.AttributeSet;
+import javax.swing.text.BadLocationException;
+import javax.swing.text.DocumentFilter;
+import javax.swing.text.StyleConstants;
+
+/**
+ * A {@link DocumentFilter} to limit the text length in the editor.
+ */
+public class MaxLengthDocumentFilter extends DocumentFilter {
+    /** the document will not accept text longer than this. -1 to disable */
+    private int maxLength = -1;
+
+    /**
+     * Sets the maximum text length.
+     *
+     * @param length the maximum no. of charactes allowed in this document. -1 to disable
+     */
+    public void setMaxLength(int length) {
+        maxLength = length;
+    }
+
+    @Override
+    public void insertString(FilterBypass fb, int offset, String string, AttributeSet attr)
+            throws BadLocationException {
+        if (mustInsertOrReplace(fb, 0, string, attr)) {
+            super.insertString(fb, offset, string, attr);
+        }
+    }
+
+    @Override
+    public void replace(FilterBypass fb, int offset, int length, String string, AttributeSet attr)
+            throws BadLocationException {
+        if (mustInsertOrReplace(fb, length, string, attr)) {
+            super.replace(fb, offset, length, string, attr);
+        }
+    }
+
+    private boolean mustInsertOrReplace(FilterBypass fb, int length, String string, AttributeSet attr) {
+        int newLen = fb.getDocument().getLength() - length + ((string == null) ? 0 : string.length());
+        return (maxLength == -1 || newLen <= maxLength ||
+                // allow longer text while composing characters or it will be hard to compose
+                // the last characters before the limit
+                ((attr != null) && attr.isDefined(StyleConstants.ComposedTextAttribute)));
+    }
+}
Index: src/org/openstreetmap/josm/gui/tagging/presets/TaggingPreset.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/TaggingPreset.java	(revision 18220)
+++ src/org/openstreetmap/josm/gui/tagging/presets/TaggingPreset.java	(working copy)
@@ -6,6 +6,7 @@
 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;
@@ -15,7 +16,6 @@
 import java.util.Collection;
 import java.util.EnumSet;
 import java.util.LinkedHashSet;
-import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -90,10 +90,14 @@
  */
 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 */
@@ -119,6 +123,9 @@
      * 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.
@@ -125,6 +132,9 @@
      * @see #getLocaleName()
      */
     public String locale_name;
+    /**
+     * Show the preset name if true
+     */
     public boolean preset_name_label;
 
     /**
@@ -131,10 +141,23 @@
      * 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;
+    /** The name_template_filter */
     public transient Match nameTemplateFilter;
+    /** The match_expression */
     public transient Match matchExpression;
 
     /**
@@ -145,6 +168,9 @@
     /** 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.
@@ -276,15 +302,29 @@
         this.types = TaggingPresetItem.getType(types);
     }
 
-    public void setName_template(String pattern) throws SAXException {
+    /**
+     * 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(pattern).parse();
+            this.nameTemplate = new TemplateParser(template).parse();
         } catch (ParseError e) {
-            Logging.error("Error while parsing " + pattern + ": " + e.getMessage());
+            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);
@@ -294,6 +334,13 @@
         }
     }
 
+    /**
+     * 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);
@@ -320,7 +367,6 @@
      */
     public PresetPanel createPanel(Collection<OsmPrimitive> selected) {
         PresetPanel p = new PresetPanel();
-        List<Link> l = new LinkedList<>();
 
         final JPanel pp = new JPanel();
         if (types != null) {
@@ -354,30 +400,49 @@
         }
 
         boolean presetInitiallyMatches = !selected.isEmpty() && selected.stream().allMatch(this);
-        final TaggingPresetItemGuiSupport itemGuiSupport = TaggingPresetItemGuiSupport.create(
-                presetInitiallyMatches, selected, this::getChangedTags);
-        JPanel items = new JPanel(new GridBagLayout());
+        itemGuiSupport = TaggingPresetItemGuiSupport.create(presetInitiallyMatches, selected, this::getChangedTags);
+
+        JPanel itemPanel = new JPanel(new GridBagLayout()) {
+            /**
+             * This hack allows the items to have their own orientation.
+             *
+             * The problem is that
+             * {@link org.openstreetmap.josm.gui.ExtendedDialog#showDialog ExtendedDialog} calls
+             * {@code applyComponentOrientation} very late in the dialog construction process thus
+             * overwriting the orientation the components have chosen for themselves.
+             *
+             * This stops the propagation of {@code applyComponentOrientation}, thus all
+             * {@code TaggingPresetItem}s may (and have to) set their own orientation.
+             */
+            @Override
+            public void applyComponentOrientation(ComponentOrientation o) {
+                setComponentOrientation(o);
+            }
+        };
+        JPanel linkPanel = new JPanel(new GridBagLayout());
         TaggingPresetItem previous = null;
         for (TaggingPresetItem i : data) {
             if (i instanceof Link) {
-                l.add((Link) i);
+                i.addToPanel(linkPanel, itemGuiSupport);
                 p.hasElements = true;
             } else {
                 if (i instanceof PresetLink) {
                     PresetLink link = (PresetLink) i;
                     if (!(previous instanceof PresetLink && Objects.equals(((PresetLink) previous).text, link.text))) {
-                        items.add(link.createLabel(), GBC.eol().insets(0, 8, 0, 0));
+                        itemPanel.add(link.createLabel(), GBC.eol().insets(0, 8, 0, 0));
                     }
                 }
-                if (i.addToPanel(items, itemGuiSupport)) {
+                if (i.addToPanel(itemPanel, itemGuiSupport)) {
                     p.hasElements = true;
                 }
             }
             previous = i;
         }
-        p.add(items, GBC.eol().fill());
+        p.add(itemPanel, GBC.eol().fill());
+        p.add(linkPanel, GBC.eol().fill());
+
         if (selected.isEmpty() && !supportsRelation()) {
-            GuiHelper.setEnabledRec(items, false);
+            GuiHelper.setEnabledRec(itemPanel, false);
         }
 
         if (selected.size() == 1 && USE_VALIDATOR.get()) {
@@ -385,18 +450,15 @@
                     TaggingPresetValidation.validateAsync(selected.iterator().next(), validationLabel, getChangedTags()));
         }
 
-        // add Link
-        for (Link link : l) {
-            link.addToPanel(p, itemGuiSupport);
-        }
-
         // "Add toolbar button"
         JToggleButton tb = new JToggleButton(new ToolbarButtonAction());
         tb.setFocusable(false);
         p.add(tb, GBC.std(1, 0).anchor(GBC.LINE_END));
 
-        // Trigger initial updates
+        // Trigger initial updates once and only once
+        itemGuiSupport.setEnabled(true);
         itemGuiSupport.fireItemValueModified(null, null, null);
+
         return p;
     }
 
@@ -409,6 +471,12 @@
         return data.stream().anyMatch(i -> !(i instanceof Optional || i instanceof Space || i instanceof Key));
     }
 
+    /**
+     * Suggests a relation role for this primitive
+     *
+     * @param osm The primitive
+     * @return the suggested role or null
+     */
     public String suggestRoleForOsmPrimitive(OsmPrimitive osm) {
         if (roles != null && osm != null) {
             return roles.roles.stream()
Index: src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetItem.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetItem.java	(revision 18220)
+++ src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetItem.java	(working copy)
@@ -7,6 +7,7 @@
 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;
@@ -20,6 +21,7 @@
 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;
@@ -57,6 +59,21 @@
     }
 
     /**
+     * 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 xxx
+     */
+    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.
      *
Index: src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetItemGuiSupport.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetItemGuiSupport.java	(revision 18220)
+++ src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetItemGuiSupport.java	(working copy)
@@ -1,6 +1,7 @@
 // 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;
@@ -10,6 +11,7 @@
 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;
@@ -26,7 +28,31 @@
     private final Supplier<Collection<Tag>> changedTagsSupplier;
     private final ListenerList<ChangeListener> listeners = ListenerList.create();
 
+    /** whether to fire events or not */
+    private boolean enabled = false;
+
     /**
+     * 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
      */
@@ -119,6 +145,15 @@
         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());
@@ -139,6 +174,7 @@
      * @param newValue the new tag value
      */
     public void fireItemValueModified(TaggingPresetItem source, String key, String newValue) {
-        listeners.fireEvent(e -> e.itemValueModified(source, key, newValue));
+        if (enabled)
+            listeners.fireEvent(e -> e.itemValueModified(source, key, newValue));
     }
 }
Index: src/org/openstreetmap/josm/gui/tagging/presets/items/Check.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/items/Check.java	(revision 18220)
+++ src/org/openstreetmap/josm/gui/tagging/presets/items/Check.java	(working copy)
@@ -85,8 +85,10 @@
 
         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()));
Index: src/org/openstreetmap/josm/gui/tagging/presets/items/CheckGroup.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/items/CheckGroup.java	(revision 18220)
+++ src/org/openstreetmap/josm/gui/tagging/presets/items/CheckGroup.java	(working copy)
@@ -45,6 +45,7 @@
             panel.add(new JLabel());
         }
 
+        panel.applyComponentOrientation(support.getDefaultComponentOrientation());
         p.add(panel, GBC.eol());
         return false;
     }
Index: src/org/openstreetmap/josm/gui/tagging/presets/items/Combo.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/items/Combo.java	(revision 18220)
+++ src/org/openstreetmap/josm/gui/tagging/presets/items/Combo.java	(working copy)
@@ -5,19 +5,27 @@
 
 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.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.AutoCompletingTextField;
-import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionList;
+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;
@@ -35,10 +43,30 @@
      */
     public boolean editable = true; // NOSONAR
     /** The length of the combo box (number of characters allowed). */
-    public short length; // NOSONAR
+    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}.
      */
@@ -47,7 +75,7 @@
     }
 
     @Override
-    protected void addToPanelAnchor(JPanel p, String def, TaggingPresetItemGuiSupport support) {
+    protected JComponent addToPanelAnchor(JPanel p, String def, TaggingPresetItemGuiSupport support) {
         if (!usage.unused()) {
             for (String s : usage.values) {
                 presetListEntries.add(new PresetListEntry(s));
@@ -58,26 +86,38 @@
         }
         presetListEntries.add(new PresetListEntry(""));
 
-        combobox = new JosmComboBox<>(presetListEntries.toArray(new PresetListEntry[0]));
-        component = combobox;
-        combobox.setRenderer(getListCellRenderer());
-        combobox.setEditable(true); // fix incorrect height, see #6157
-        combobox.reinitialize(presetListEntries);
-        combobox.setEditable(editable); // see #6157
-        AutoCompletingTextField tf = new AutoCompletingTextField();
-        initAutoCompletionField(tf, key);
+        dropDownModel = new AutoCompComboBoxModel<PresetListEntry>(Comparator.naturalOrder());
+        autoCompModel = new AutoCompComboBoxModel<AutoCompletionItem>(Comparator.naturalOrder());
+        presetListEntries.forEach(dropDownModel::addElement);
+
+        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(new PresetListEntry(" "));} 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);
+
+        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()) {
-            tf.setHint(key);
+            combobox.setHint(key);
         }
         if (length > 0) {
-            tf.setMaxChars((int) length);
+            tf.setMaxTextLength(length);
         }
-        AutoCompletionList acList = tf.getAutoCompletionList();
-        if (acList != null) {
-            acList.add(getDisplayValues(), AutoCompletionPriority.IS_IN_STANDARD);
-        }
-        combobox.setEditor(tf);
-        combobox.setSelectedItem(getItemToSelect(def, support, false));
 
         if (key != null && ("colour".equals(key) || key.startsWith("colour:") || key.endsWith(":colour"))) {
             p.add(combobox, GBC.std().fill(GBC.HORIZONTAL));
@@ -92,7 +132,12 @@
         } else {
             p.add(combobox, GBC.eol().fill(GBC.HORIZONTAL));
         }
+
+        Object itemToSelect = getItemToSelect(default_, support, false);
+        combobox.setSelectedItemText(itemToSelect == null ? null : itemToSelect.toString());
         combobox.addActionListener(l -> support.fireItemValueModified(this, key, getSelectedValue()));
+        combobox.addComponentListener(new ComponentListener());
+        return combobox;
     }
 
     class ChooseColorAction extends AbstractAction {
Index: src/org/openstreetmap/josm/gui/tagging/presets/items/ComboMultiSelect.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/items/ComboMultiSelect.java	(revision 18220)
+++ src/org/openstreetmap/josm/gui/tagging/presets/items/ComboMultiSelect.java	(working copy)
@@ -5,8 +5,6 @@
 import static org.openstreetmap.josm.tools.I18n.trc;
 
 import java.awt.Component;
-import java.awt.Dimension;
-import java.awt.Font;
 import java.lang.reflect.Method;
 import java.lang.reflect.Modifier;
 import java.util.ArrayList;
@@ -13,13 +11,11 @@
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.Comparator;
 import java.util.List;
 import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.CopyOnWriteArraySet;
 import java.util.stream.Collectors;
-import java.util.stream.IntStream;
 
 import javax.swing.JComponent;
 import javax.swing.JLabel;
@@ -31,6 +27,8 @@
 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.GBC;
 import org.openstreetmap.josm.tools.Logging;
 import org.openstreetmap.josm.tools.Utils;
@@ -40,8 +38,6 @@
  */
 public abstract class ComboMultiSelect extends KeyedItem {
 
-    private static final Renderer RENDERER = new Renderer();
-
     /**
      * 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}).
@@ -92,62 +88,58 @@
     /** whether to use values for search via {@link TaggingPresetSelector} */
     public boolean values_searchable; // NOSONAR
 
-    protected JComponent component;
     protected final Set<PresetListEntry> presetListEntries = new CopyOnWriteArraySet<>();
     private boolean initialized;
     protected Usage usage;
     protected Object originalValue;
 
-    private static final class Renderer implements ListCellRenderer<PresetListEntry> {
+    /**
+     * 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;
 
-        private final JLabel lbl = new JLabel();
+        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 Component getListCellRendererComponent(JList<? extends PresetListEntry> list, PresetListEntry item, int index,
-                boolean isSelected, boolean cellHasFocus) {
+        public JLabel getListCellRendererComponent(
+            JList<? extends PresetListEntry> list, PresetListEntry value, int index, boolean isSelected, boolean cellHasFocus) {
 
-            if (list == null || item == null) {
-                return lbl;
+            JLabel l = (JLabel) renderer.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
+            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 (index == -1) {
-                // Take the longest element for the preferred width (#19321)
-                // We do not want the editor to have the maximum height of all entries. Return a dummy with bogus height.
-                IntStream.range(0, list.getModel().getSize())
-                        .mapToObj(i -> getListCellRendererComponent(list, list.getModel().getElementAt(i), i, isSelected, cellHasFocus))
-                        .map(Component::getPreferredSize)
-                        .max(Comparator.comparingInt(dim -> dim.width))
-                        .ifPresent(dim -> lbl.setPreferredSize(new Dimension(dim.width, 10)));
-                return lbl;
-            }
-
-            // Only return cached size, item is not shown
-            if (!list.isShowing() && item.preferredWidth != -1 && item.preferredHeight != -1) {
-                lbl.setPreferredSize(new Dimension(item.preferredWidth, item.preferredHeight));
-                return lbl;
-            }
-
-            lbl.setPreferredSize(null);
-
-            if (isSelected) {
-                lbl.setBackground(list.getSelectionBackground());
-                lbl.setForeground(list.getSelectionForeground());
+            String tt = value.value;
+            if (tt != null && !tt.isEmpty()) {
+                l.setToolTipText(tr("Sets the key ''{0}'' to the value ''{1}''.", key, tt));
             } else {
-                lbl.setBackground(list.getBackground());
-                lbl.setForeground(list.getForeground());
+                l.setToolTipText(tr("Clears the key ''{0}''.", key));
             }
-
-            lbl.setOpaque(true);
-            lbl.setFont(lbl.getFont().deriveFont(Font.PLAIN));
-            lbl.setText("<html>" + item.getListDisplay() + "</html>");
-            lbl.setIcon(item.getIcon());
-            lbl.setEnabled(list.isEnabled());
-
-            // Cache size
-            item.preferredWidth = (short) lbl.getPreferredSize().width;
-            item.preferredHeight = (short) lbl.getPreferredSize().height;
-
-            return lbl;
+            return l;
         }
     }
 
@@ -187,7 +179,7 @@
 
     protected abstract Object getSelectedItem();
 
-    protected abstract void addToPanelAnchor(JPanel p, String def, TaggingPresetItemGuiSupport support);
+    protected abstract JComponent addToPanelAnchor(JPanel p, String def, TaggingPresetItemGuiSupport support);
 
     @Override
     public Collection<String> getValues() {
@@ -218,10 +210,12 @@
         addIcon(label);
         label.setToolTipText(getKeyTooltipText());
         label.setComponentPopupMenu(getPopupMenu());
+        label.applyComponentOrientation(OrientationAction.getDefaultComponentOrientation());
         p.add(label, GBC.std().insets(0, 0, 10, 0));
-        addToPanelAnchor(p, default_, support);
+        JComponent component = addToPanelAnchor(p, default_, support);
         label.setLabelFor(component);
         component.setToolTipText(getKeyTooltipText());
+        component.applyComponentOrientation(OrientationAction.getValueOrientation(key));
 
         return true;
     }
@@ -454,10 +448,6 @@
         return presetListEntries.stream().filter(e -> Objects.equals(e.value, value)).findFirst().orElse(null);
     }
 
-    protected ListCellRenderer<PresetListEntry> getListCellRenderer() {
-        return RENDERER;
-    }
-
     @Override
     public MatchType getDefaultMatch() {
         return MatchType.NONE;
Index: src/org/openstreetmap/josm/gui/tagging/presets/items/Label.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/items/Label.java	(revision 18220)
+++ src/org/openstreetmap/josm/gui/tagging/presets/items/Label.java	(working copy)
@@ -17,6 +17,7 @@
         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 18220)
+++ src/org/openstreetmap/josm/gui/tagging/presets/items/Link.java	(working copy)
@@ -35,7 +35,11 @@
     @Override
     public boolean addToPanel(JPanel p, TaggingPresetItemGuiSupport support) {
         initializeLocaleText(tr("More information about this feature"));
-        Optional.ofNullable(buildUrlLabel()).ifPresent(label -> p.add(label, GBC.eol().insets(0, 10, 0, 0).fill(GBC.HORIZONTAL)));
+        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;
     }
 
Index: src/org/openstreetmap/josm/gui/tagging/presets/items/MultiSelect.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/items/MultiSelect.java	(revision 18220)
+++ src/org/openstreetmap/josm/gui/tagging/presets/items/MultiSelect.java	(working copy)
@@ -7,10 +7,10 @@
 import java.util.Set;
 import java.util.TreeSet;
 
+import javax.swing.JComponent;
 import javax.swing.JList;
 import javax.swing.JPanel;
 import javax.swing.JScrollPane;
-import javax.swing.ListCellRenderer;
 import javax.swing.ListModel;
 
 import org.openstreetmap.josm.data.osm.Tag;
@@ -30,12 +30,12 @@
     protected ConcatenatingJList list;
 
     @Override
-    protected void addToPanelAnchor(JPanel p, String def, TaggingPresetItemGuiSupport support) {
+    protected JComponent addToPanelAnchor(JPanel p, String def, TaggingPresetItemGuiSupport support) {
         list = new ConcatenatingJList(delimiter, presetListEntries.toArray(new PresetListEntry[0]));
-        component = list;
-        ListCellRenderer<PresetListEntry> renderer = getListCellRenderer();
+        ComboMultiSelectListCellRenderer renderer = new ComboMultiSelectListCellRenderer(list, list.getCellRenderer(), 200, key);
         list.setCellRenderer(renderer);
-        list.setSelectedItem(getItemToSelect(def, support, true));
+        Object itemToSelect = getItemToSelect(def, support, true);
+        list.setSelectedItem(itemToSelect == null ? null : new PresetListEntry(itemToSelect.toString()));
         JScrollPane sp = new JScrollPane(list);
         // if a number of rows has been specified in the preset,
         // modify preferred height of scroll pane to match that row count.
@@ -46,6 +46,7 @@
         }
         list.addListSelectionListener(l -> support.fireItemValueModified(this, key, getSelectedValue()));
         p.add(sp, GBC.eol().fill(GBC.HORIZONTAL));
+        return list;
     }
 
     @Override
Index: src/org/openstreetmap/josm/gui/tagging/presets/items/Optional.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/items/Optional.java	(revision 18220)
+++ src/org/openstreetmap/josm/gui/tagging/presets/items/Optional.java	(working copy)
@@ -18,9 +18,11 @@
     // TODO: Draw a box around optional stuff
     @Override
     public boolean addToPanel(JPanel p, TaggingPresetItemGuiSupport support) {
+        JLabel label = new JLabel(locale_text);
+        label.applyComponentOrientation(support.getDefaultComponentOrientation());
         initializeLocaleText(tr("Optional Attributes:"));
         p.add(new JLabel(" "), GBC.eol()); // space
-        p.add(new JLabel(locale_text), GBC.eol());
+        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 18220)
+++ src/org/openstreetmap/josm/gui/tagging/presets/items/PresetLink.java	(working copy)
@@ -61,6 +61,7 @@
             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;
Index: src/org/openstreetmap/josm/gui/tagging/presets/items/PresetListEntry.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/items/PresetListEntry.java	(revision 18220)
+++ src/org/openstreetmap/josm/gui/tagging/presets/items/PresetListEntry.java	(working copy)
@@ -35,11 +35,6 @@
     /** The localized version of {@link #short_description}. */
     public String locale_short_description; // NOSONAR
 
-    /** Cached width (currently only for Combo) to speed up preset dialog initialization */
-    public short preferredWidth = -1; // NOSONAR
-    /** Cached height (currently only for Combo) to speed up preset dialog initialization */
-    public short preferredHeight = -1; // NOSONAR
-
     /**
      * Constructs a new {@code PresetListEntry}, uninitialized.
      */
@@ -56,27 +51,34 @@
     }
 
     /**
-     * Returns HTML formatted contents.
+     * 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() {
-        if (value.equals(KeyedItem.DIFFERENT))
-            return "<b>" + Utils.escapeReservedCharactersHTML(KeyedItem.DIFFERENT) + "</b>";
+    public String getListDisplay(int width) {
+        if (value.equals(KeyedItem.DIFFERENT)) {
+            return "<b>" + KeyedItem.DIFFERENT + "</b>";
+        }
 
-        String displayValue = Utils.escapeReservedCharactersHTML(getDisplayValue());
         String shortDescription = getShortDescription(true);
+        String displayValue = getDisplayValue();
 
-        if (displayValue.isEmpty() && Utils.isEmpty(shortDescription))
-            return "&nbsp;";
+        if (shortDescription.isEmpty()) {
+            if (displayValue.isEmpty()) {
+                return " ";
+            }
+            return displayValue;
+        }
 
-        final StringBuilder res = new StringBuilder("<b>").append(displayValue).append("</b>");
-        if (!Utils.isEmpty(shortDescription)) {
-            // wrap in table to restrict the text width
-            res.append("<div style=\"width:300px; padding:0 0 5px 5px\">")
-               .append(shortDescription)
-               .append("</div>");
-        }
-        return res.toString();
+        // 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));
     }
 
     /**
@@ -88,7 +90,7 @@
     }
 
     /**
-     * Returns the value to display.
+     * Returns the contents of the current item view.
      * @return the value to display
      */
     public String getDisplayValue() {
@@ -101,9 +103,10 @@
      * @return the short description to display
      */
     public String getShortDescription(boolean translated) {
-        return translated
+        String shortDesc = translated
                 ? Utils.firstNonNull(locale_short_description, tr(short_description))
                         : short_description;
+        return shortDesc == null ? "" : shortDesc;
     }
 
     // toString is mainly used to initialize the Editor
@@ -112,7 +115,7 @@
         if (KeyedItem.DIFFERENT.equals(value))
             return KeyedItem.DIFFERENT;
         String displayValue = getDisplayValue();
-        return displayValue != null ? displayValue.replaceAll("<.*>", "") : ""; // remove additional markup, e.g. <br>
+        return displayValue != null ? displayValue.replaceAll("\\s*<.*>\\s*", " ") : ""; // remove additional markup, e.g. <br>
     }
 
     @Override
Index: src/org/openstreetmap/josm/gui/tagging/presets/items/Roles.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/items/Roles.java	(revision 18220)
+++ src/org/openstreetmap/josm/gui/tagging/presets/items/Roles.java	(working copy)
@@ -200,6 +200,7 @@
             for (Role i : roles) {
                 i.addToPanel(proles);
             }
+            proles.applyComponentOrientation(support.getDefaultComponentOrientation());
             p.add(proles, GBC.eol());
         }
         return false;
Index: src/org/openstreetmap/josm/gui/tagging/presets/items/Text.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/items/Text.java	(revision 18220)
+++ src/org/openstreetmap/josm/gui/tagging/presets/items/Text.java	(working copy)
@@ -3,6 +3,7 @@
 
 import static org.openstreetmap.josm.tools.I18n.tr;
 
+import java.util.ArrayList;
 import java.awt.Color;
 import java.awt.Component;
 import java.awt.GridBagLayout;
@@ -23,7 +24,10 @@
 import javax.swing.JToggleButton;
 
 import org.openstreetmap.josm.data.osm.Tag;
-import org.openstreetmap.josm.gui.tagging.ac.AutoCompletingTextField;
+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;
@@ -30,6 +34,7 @@
 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;
@@ -69,20 +74,37 @@
     @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);
-        AutoCompletingTextField textField = new AutoCompletingTextField();
-        if (alternative_autocomplete_keys != null) {
-            initAutoCompletionField(textField, (key + ',' + alternative_autocomplete_keys).split(",", -1));
+
+        if (usage.unused() || usage.hasUniqueValue()) {
+            textField = new AutoCompTextField<>();
         } else {
-            initAutoCompletionField(textField, key);
+            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 (length > 0) {
-            textField.setMaxChars((int) length);
-        }
         if (usage.unused()) {
             if (auto_increment_selected != 0 && auto_increment != null) {
                 try {
@@ -110,11 +132,14 @@
             textField.setText(usage.getFirst());
             value = textField;
             originalValue = usage.getFirst();
-        } else {
-            // the objects have different values
+        }
+        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<>(usage.values.toArray(new String[0]));
             comboBox.setEditable(true);
-            comboBox.setEditor(textField);
+            comboBox.setEditor(editor);
             comboBox.getEditor().setItem(DIFFERENT);
             value = comboBox;
             originalValue = DIFFERENT;
@@ -179,7 +204,9 @@
         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;
     }
 
@@ -252,7 +279,7 @@
         }
     }
 
-    private void setupListeners(AutoCompletingTextField textField, TaggingPresetItemGuiSupport support) {
+    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
@@ -265,7 +292,7 @@
                 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.setItem(valueTemplateText);
+                textField.setText(valueTemplateText);
                 if (originalValue != null && !originalValue.equals(valueTemplateText)) {
                     textField.setForeground(Color.RED);
                 } else {
Index: src/org/openstreetmap/josm/gui/widgets/AbstractIdTextField.java
===================================================================
--- src/org/openstreetmap/josm/gui/widgets/AbstractIdTextField.java	(revision 18220)
+++ src/org/openstreetmap/josm/gui/widgets/AbstractIdTextField.java	(working copy)
@@ -4,6 +4,7 @@
 import javax.swing.text.JTextComponent;
 
 import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils;
+import org.openstreetmap.josm.gui.tagging.ac.AutoCompTextField;
 import org.openstreetmap.josm.tools.Logging;
 import org.openstreetmap.josm.tools.Utils;
 
@@ -13,7 +14,7 @@
  * @param <T> The ID validator class
  * @since 5765
  */
-public abstract class AbstractIdTextField<T extends AbstractTextComponentValidator> extends JosmTextField {
+public abstract class AbstractIdTextField<T extends AbstractTextComponentValidator> extends AutoCompTextField<String> {
 
     protected final transient T validator;
 
Index: src/org/openstreetmap/josm/gui/widgets/JosmComboBox.java
===================================================================
--- src/org/openstreetmap/josm/gui/widgets/JosmComboBox.java	(revision 18220)
+++ src/org/openstreetmap/josm/gui/widgets/JosmComboBox.java	(working copy)
@@ -1,55 +1,77 @@
 // License: GPL. For details, see LICENSE file.
 package org.openstreetmap.josm.gui.widgets;
 
+
 import java.awt.Component;
+import java.awt.ComponentOrientation;
 import java.awt.Dimension;
-import java.awt.event.MouseAdapter;
-import java.awt.event.MouseEvent;
+import java.awt.Graphics;
+import java.awt.GraphicsConfiguration;
+import java.awt.Insets;
+import java.awt.Point;
+import java.awt.Rectangle;
+import java.awt.Toolkit;
 import java.beans.PropertyChangeEvent;
 import java.beans.PropertyChangeListener;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.List;
-import java.util.stream.Collectors;
-import java.util.stream.IntStream;
 
 import javax.swing.ComboBoxEditor;
-import javax.swing.ComboBoxModel;
-import javax.swing.DefaultComboBoxModel;
 import javax.swing.JComboBox;
 import javax.swing.JList;
+import javax.swing.JScrollPane;
 import javax.swing.JTextField;
-import javax.swing.plaf.basic.ComboPopup;
+import javax.swing.ListCellRenderer;
+import javax.swing.border.Border;
+import javax.swing.event.PopupMenuEvent;
+import javax.swing.event.PopupMenuListener;
 import javax.swing.text.JTextComponent;
 
-import org.openstreetmap.josm.gui.util.GuiHelper;
+import org.openstreetmap.josm.spi.preferences.Config;
 
 /**
- * Class overriding each {@link JComboBox} in JOSM to control consistently the number of displayed items at once.<br>
- * This is needed because of the default Java behaviour that may display the top-down list off the screen (see #7917).
+ * Base class for all comboboxes in JOSM.
+ * <p>
+ * This combobox will show as many rows as possible without covering the combox itself. It makes
+ * sure the list will never go outside the screen (see #7917). You may limit the number of rows
+ * shown with the configuration: {@code gui.combobox.maximum-row-count}.
+ * <p>
+ * This combobox uses a {@link JosmTextField} for its editor component.
+ *
  * @param <E> the type of the elements of this combo box
- *
  * @since 5429 (creation)
  * @since 7015 (generics for Java 7)
  */
-public class JosmComboBox<E> extends JComboBox<E> {
+public class JosmComboBox<E> extends JComboBox<E> implements PopupMenuListener, PropertyChangeListener {
+    /**
+     * Limits the number of rows that this combobox will show.
+     */
+    public static final String PROP_MAXIMUM_ROW_COUNT = "gui.combobox.maximum-row-count";
 
-    private final ContextMenuHandler handler = new ContextMenuHandler();
+    /** the configured maximum row count or null */
+    private Integer configMaximumRowCount = null;
 
     /**
-     * Creates a <code>JosmComboBox</code> with a default data model.
+     * The preferred height of the combobox when closed.  Use if the items in the list dropdown are
+     * taller than the item in the editor, as in some comboboxes in the preset dialog.  -1 to use
+     * the height of the tallest item in the list.
+     */
+    private int preferredHeight = -1;
+
+    /** greyed text to display in the editor when the selected value is empty */
+    private String hint;
+
+    /**
+     * Creates a {@code JosmComboBox} with a {@link JosmComboBoxModel} data model.
      * The default data model is an empty list of objects.
      * Use <code>addItem</code> to add items. By default the first item
      * in the data model becomes selected.
-     *
-     * @see DefaultComboBoxModel
      */
     public JosmComboBox() {
-        init(null);
+        super(new JosmComboBoxModel<E>());
+        init();
     }
 
     /**
-     * Creates a <code>JosmComboBox</code> with a default data model and
+     * Creates a {@code JosmComboBox} with a {@link JosmComboBoxModel} data model and
      * the specified prototype display value.
      * The default data model is an empty list of objects.
      * Use <code>addItem</code> to add items. By default the first item
@@ -59,53 +81,94 @@
      *      the maximum number of elements to be displayed at once before
      *      displaying a scroll bar
      *
-     * @see DefaultComboBoxModel
      * @since 5450
+     * @deprecated use {@link #setPrototypeDisplayValue} instead.
      */
+    @Deprecated
     public JosmComboBox(E prototypeDisplayValue) {
-        init(prototypeDisplayValue);
+        super(new JosmComboBoxModel<E>());
+        setPrototypeDisplayValue(prototypeDisplayValue);
+        init();
     }
 
     /**
-     * Creates a <code>JosmComboBox</code> that takes its items from an
-     * existing <code>ComboBoxModel</code>. Since the
-     * <code>ComboBoxModel</code> is provided, a combo box created using
-     * this constructor does not create a default combo box model and
-     * may impact how the insert, remove and add methods behave.
+     * Creates a {@code JosmComboBox} that takes it items from an existing {@link JosmComboBoxModel}
+     * data model.
      *
-     * @param aModel the <code>ComboBoxModel</code> that provides the
-     *      displayed list of items
-     * @see DefaultComboBoxModel
+     * @param aModel the model that provides the displayed list of items
      */
-    public JosmComboBox(ComboBoxModel<E> aModel) {
+    public JosmComboBox(JosmComboBoxModel<E> aModel) {
         super(aModel);
-        List<E> list = IntStream.range(0, aModel.getSize())
-                .mapToObj(aModel::getElementAt)
-                .collect(Collectors.toList());
-        init(findPrototypeDisplayValue(list));
+        init();
     }
 
     /**
-     * Creates a <code>JosmComboBox</code> that contains the elements
+     * Creates a {@code JosmComboBox} that takes it items from an existing {@link JosmComboBoxModel}
+     * data model and sets the specified prototype display value.
+     *
+     * @param aModel the model that provides the displayed list of items
+     * @param prototypeDisplayValue use this item to size the combobox (may be null)
+     * @deprecated use {@link #setPrototypeDisplayValue} instead.
+     */
+    @Deprecated
+    public JosmComboBox(JosmComboBoxModel<E> aModel, E prototypeDisplayValue) {
+        super(aModel);
+        setPrototypeDisplayValue(prototypeDisplayValue);
+        init();
+    }
+
+    /**
+     * Creates a {@code JosmComboBox} that contains the elements
      * in the specified array. By default the first item in the array
      * (and therefore the data model) becomes selected.
      *
      * @param items  an array of objects to insert into the combo box
-     * @see DefaultComboBoxModel
      */
     public JosmComboBox(E[] items) {
-        super(items);
-        init(findPrototypeDisplayValue(Arrays.asList(items)));
+        super(new JosmComboBoxModel<E>());
+        init();
+        for (E elem : items) {
+            getModel().addElement(elem);
+        }
     }
 
+    private void init() {
+        configMaximumRowCount = Config.getPref().getInt(PROP_MAXIMUM_ROW_COUNT, 9999);
+        setEditor(new JosmComboBoxEditor());
+        // listen when the popup shows up so we can maximize its height
+        addPopupMenuListener(this);
+    }
+
     /**
+     * Returns the {@link JosmComboBoxModel} currently used.
+     *
+     * @return the model or null
+     */
+    @Override
+    public JosmComboBoxModel<E> getModel() {
+        return (JosmComboBoxModel<E>) dataModel;
+    }
+
+    @Override
+    public void setEditor(ComboBoxEditor newEditor) {
+        if (editor != null) {
+            editor.getEditorComponent().removePropertyChangeListener(this);
+        }
+        super.setEditor(newEditor);
+        if (editor != null) {
+            // listen to orientation changes in the editor
+            editor.getEditorComponent().addPropertyChangeListener(this);
+        }
+    }
+
+    /**
      * Returns the editor component
      * @return the editor component
      * @see ComboBoxEditor#getEditorComponent()
      * @since 9484
      */
-    public JTextField getEditorComponent() {
-        return (JTextField) getEditor().getEditorComponent();
+    public JosmTextField getEditorComponent() {
+        return (JosmTextField) (editor == null ? null : editor.getEditorComponent());
     }
 
     /**
@@ -115,7 +178,8 @@
      * @since 18173
      */
     public String getText() {
-        return getEditorComponent().getText();
+        JosmTextField tf = getEditorComponent();
+        return tf == null ? null : tf.getText();
     }
 
     /**
@@ -125,183 +189,258 @@
      * @since 18173
      */
     public void setText(String value) {
-        getEditorComponent().setText(value);
+        JosmTextField tf = getEditorComponent();
+        if (tf != null)
+            tf.setText(value);
     }
 
     /**
-     * Finds the prototype display value to use among the given possible candidates.
-     * @param possibleValues The possible candidates that will be iterated.
-     * @return The value that needs the largest display height on screen.
-     * @since 5558
+     * Selects an item and/or sets text
+     *
+     * Selects the item whose {@code toString()} equals {@code text}. If an item could not be found,
+     * selects nothing and sets the text anyway.
+     *
+     * @param text the text to select and set
+     * @return the item or null
      */
-    protected final E findPrototypeDisplayValue(Collection<E> possibleValues) {
-        E result = null;
-        int maxHeight = -1;
-        if (possibleValues != null) {
-            // Remind old prototype to restore it later
-            E oldPrototype = getPrototypeDisplayValue();
-            // Get internal JList to directly call the renderer
-            @SuppressWarnings("rawtypes")
-            JList list = getList();
-            try {
-                // Index to give to renderer
-                int i = 0;
-                for (E value : possibleValues) {
-                    if (value != null) {
-                        // With a "classic" renderer, we could call setPrototypeDisplayValue(value) + getPreferredSize()
-                        // but not with TaggingPreset custom renderer that return a dummy height if index is equal to -1
-                        // So we explicitly call the renderer by simulating a correct index for the current value
-                        @SuppressWarnings("unchecked")
-                        Component c = getRenderer().getListCellRendererComponent(list, value, i, true, true);
-                        if (c != null) {
-                            // Get the real preferred size for the current value
-                            Dimension dim = c.getPreferredSize();
-                            if (dim.height > maxHeight) {
-                                // Larger ? This is our new prototype
-                                maxHeight = dim.height;
-                                result = value;
-                            }
-                        }
-                    }
-                    i++;
-                }
-            } finally {
-                // Restore original prototype
-                setPrototypeDisplayValue(oldPrototype);
-            }
-        }
-        return result;
+    public E setSelectedItemText(String text) {
+        E item = getModel().find(text);
+        setSelectedItem(item);
+        if (text == null || !text.equals(getText()))
+            setText(text);
+        return item;
     }
 
-    @SuppressWarnings("unchecked")
-    protected final JList<Object> getList() {
-        return IntStream.range(0, getUI().getAccessibleChildrenCount(this))
-                .mapToObj(i -> getUI().getAccessibleChild(this, i))
-                .filter(child -> child instanceof ComboPopup)
-                .findFirst()
-                .map(child -> ((ComboPopup) child).getList())
-                .orElse(null);
+    /* Hint handling */
+
+    /**
+     * Returns the hint text
+     * @return the hint text
+     */
+    public String getHint() {
+        return hint;
     }
 
     /**
-     * Set the prototypeCellValue property and calculate the height of the dropdown.
+     * Sets the hint to display when no text has been entered.
+     *
+     * @param hint the hint to set
+     * @return the old hint
+     * @since xxx
      */
+    public String setHint(String hint) {
+        String old = hint;
+        this.hint = hint;
+        JosmTextField tf = getEditorComponent();
+        if (tf != null)
+            tf.setHint(hint);
+        return old;
+    }
+
     @Override
-    public void setPrototypeDisplayValue(E prototype) {
-        if (prototype != null) {
-            super.setPrototypeDisplayValue(prototype);
-            int screenHeight = GuiHelper.getScreenSize().height;
-            // Compute maximum number of visible items based on the preferred size of the combo box.
-            // This assumes that items have the same height as the combo box, which is not granted by the look and feel
-            int maxsize = (screenHeight/getPreferredSize().height) / 2;
-            // If possible, adjust the maximum number of items with the real height of items
-            // It is not granted this works on every platform (tested OK on Windows)
-            JList<Object> list = getList();
-            if (list != null) {
-                if (!prototype.equals(list.getPrototypeCellValue())) {
-                    list.setPrototypeCellValue(prototype);
-                }
-                int height = list.getFixedCellHeight();
-                if (height > 0) {
-                    maxsize = (screenHeight/height) / 2;
-                }
-            }
-            setMaximumRowCount(Math.max(getMaximumRowCount(), maxsize));
+    public void setComponentOrientation(ComponentOrientation o) {
+        if (o.isLeftToRight() != getComponentOrientation().isLeftToRight()) {
+            super.setComponentOrientation(o);
+            getEditorComponent().setComponentOrientation(o);
+            // the button doesn't move over without this
+            revalidate();
         }
     }
 
-    protected final void init(E prototype) {
-        init(prototype, true);
+    /**
+     * Return true if the combobox should display the hint text.
+     *
+     * @return whether to display the hint text
+     * @since xxx
+     */
+    public boolean displayHint() {
+        return !isEditable() && hint != null && !hint.isEmpty() && getText().isEmpty(); // && !isFocusOwner();
     }
 
-    protected final void init(E prototype, boolean registerPropertyChangeListener) {
-        setPrototypeDisplayValue(prototype);
-        // Handle text contextual menus for editable comboboxes
-        if (registerPropertyChangeListener) {
-            addPropertyChangeListener("editable", handler);
-            addPropertyChangeListener("editor", handler);
-        }
+    /**
+     * Overrides the calculated height.  See: {@link #setPreferredHeight(int)}.
+     *
+     * @since xxx
+     */
+    @Override
+    public Dimension getPreferredSize() {
+        Dimension d = super.getPreferredSize();
+        if (preferredHeight != -1)
+            d.height = preferredHeight;
+        return d;
     }
 
-    protected class ContextMenuHandler extends MouseAdapter implements PropertyChangeListener {
+    /**
+     * Sets the preferred height of the combobox editor.
+     * <p>
+     * A combobox editor is automatically sized to accomodate the widest and the tallest items in
+     * the list.  In the Preset dialogs we show more of an item in the list than in the editor, so
+     * the editor becomes too big.  With this method we can set the editor height to a fixed value.
+     * <p>
+     * Set this to -1 to get the default behaviour back.
+     *
+     * See also: #6157
+     *
+     * @param height the preferred height or -1
+     * @return the old preferred height
+     * @see #setPreferredSize
+     * @since xxx
+     */
+    public int setPreferredHeight(int height) {
+        int old = preferredHeight;
+        preferredHeight = height;
+        return old;
+    }
 
-        private JTextComponent component;
-        private PopupMenuLauncher launcher;
-
-        @Override
-        public void propertyChange(PropertyChangeEvent evt) {
-            if ("editable".equals(evt.getPropertyName())) {
-                if (evt.getNewValue().equals(Boolean.TRUE)) {
-                    enableMenu();
-                } else {
-                    disableMenu();
-                }
-            } else if ("editor".equals(evt.getPropertyName())) {
-                disableMenu();
-                if (isEditable()) {
-                    enableMenu();
-                }
-            }
+    /**
+     * Get the dropdown list component
+     *
+     * @return the list or null
+     */
+    @SuppressWarnings("unchecked")
+    public JList<E> getList() {
+        Object popup = getUI().getAccessibleChild(this, 0);
+        if (popup != null && popup instanceof javax.swing.plaf.basic.ComboPopup) {
+            return ((javax.swing.plaf.basic.ComboPopup) popup).getList();
         }
+        return null;
+    }
 
-        private void enableMenu() {
-            if (launcher == null && editor != null) {
-                Component editorComponent = editor.getEditorComponent();
-                if (editorComponent instanceof JTextComponent) {
-                    component = (JTextComponent) editorComponent;
-                    component.addMouseListener(this);
-                    launcher = TextContextualPopupMenu.enableMenuFor(component, true);
-                }
-            }
-        }
+    // get the popup list
 
-        private void disableMenu() {
-            if (launcher != null) {
-                TextContextualPopupMenu.disableMenuFor(component, launcher);
-                launcher = null;
-                component.removeMouseListener(this);
-                component = null;
+    /**
+     * Draw the hint text for read-only comboboxes.
+     * <p>
+     * The obvious way -- to call {@code setText(hint)} and {@code setForeground(gray)} on the
+     * {@code JLabel} returned by the list cell renderer -- unfortunately does not work out well
+     * because many UIs change the foreground color or the enabled state of the {@code JLabel} after
+     * the list cell renderer has returned ({@code BasicComboBoxUI}).  Other UIs don't honor the
+     * label color at all ({@code SynthLabelUI}).
+     * <p>
+     * We use the same approach as in {@link JosmTextField}. The only problem we face is to get the
+     * coordinates of the text inside the combobox.  Fortunately even read-only comboboxes have a
+     * (partially configured) editor component, although they don't use it.  We configure that editor
+     * just enough to call {@link JTextField#modelToView modelToView} and
+     * {@link javax.swing.JComponent#getBaseline getBaseline} on it, thus obtaining the text
+     * coordinates.
+     *
+     * @see javax.swing.plaf.basic.BasicComboBoxUI#paintCurrentValue
+     * @see javax.swing.plaf.synth.SynthLabelUI#paint
+     */
+    @Override
+    protected void paintComponent(Graphics g) {
+        super.paintComponent(g);
+        JosmTextField editor = getEditorComponent();
+        if (displayHint() && editor != null) {
+            if (editor.getSize().width == 0) {
+                Dimension dimen = getSize();
+                Insets insets = getInsets();
+                // a fake configuration not too far from reality
+                editor.setSize(dimen.width - insets.left - insets.right,
+                               dimen.height - insets.top - insets.bottom);
             }
+            editor.drawHint(g);
         }
+    }
 
-        private void discardAllUndoableEdits() {
-            if (launcher != null) {
-                launcher.discardAllUndoableEdits();
-            }
-        }
+    /**
+     * Empties the internal undo manager, if any.
+     * <p>
+     * Used in the {@link org.openstreetmap.josm.gui.io.UploadDialog UploadDialog}.
+     * @since 14977
+     */
+    public final void discardAllUndoableEdits() {
+        getEditorComponent().discardAllUndoableEdits();
+    }
 
-        @Override
-        public void mousePressed(MouseEvent e) {
-            processEvent(e);
-        }
+    /**
+     * Limits the popup height.
+     * <p>
+     * Limits the popup height to the available screen space either below or above the combobox,
+     * whichever is bigger. To find the maximum number of rows that fit the screen, it does the
+     * reverse of the calculation done in
+     * {@link javax.swing.plaf.basic.BasicComboPopup#getPopupLocation}.
+     *
+     * @see javax.swing.plaf.basic.BasicComboBoxUI#getAccessibleChild
+     */
+    @Override
+    public void popupMenuWillBecomeVisible(PopupMenuEvent ev) {
+        // Get the combobox bounds.
+        Rectangle bounds = new Rectangle(getLocationOnScreen(), getSize());
 
-        @Override
-        public void mouseReleased(MouseEvent e) {
-            processEvent(e);
+        // Get the screen bounds of the screen (of a multi-screen setup) we are on.
+        Rectangle screenBounds;
+        GraphicsConfiguration gc = getGraphicsConfiguration();
+        Toolkit toolkit = Toolkit.getDefaultToolkit();
+        if (gc != null) {
+            Insets screenInsets = toolkit.getScreenInsets(gc);
+            screenBounds = gc.getBounds();
+            screenBounds.x += screenInsets.left;
+            screenBounds.y += screenInsets.top;
+            screenBounds.width -= (screenInsets.left + screenInsets.right);
+            screenBounds.height -= (screenInsets.top + screenInsets.bottom);
+        } else {
+            screenBounds = new Rectangle(new Point(), toolkit.getScreenSize());
         }
+        int freeAbove = bounds.y - screenBounds.y;
+        int freeBelow = (screenBounds.y + screenBounds.height) - (bounds.y + bounds.height);
 
-        private void processEvent(MouseEvent e) {
-            if (launcher != null && !e.isPopupTrigger() && launcher.getMenu().isShowing()) {
-                launcher.getMenu().setVisible(false);
+        try {
+            // First try an implementation-dependent method to get the exact number.
+            JList<E> jList = getList();
+
+            // 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
+            // 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();
+                Border border = scroller.getViewportBorder();
+                if (border != null) {
+                    insets = border.getBorderInsets(null);
+                    free -= insets.top + insets.bottom;
+                }
+                border = scroller.getBorder();
+                if (border != null) {
+                    insets = border.getBorderInsets(null);
+                    free -= insets.top + insets.bottom;
+                }
             }
+
+            // Calculate how many rows fit into the free space.  Rows may have variable heights.
+            int rowCount = Math.min(configMaximumRowCount, getItemCount());
+            ListCellRenderer<? super E> r = jList.getCellRenderer();  // must take this from list, not combo: flatlaf bug
+            int i, h = 0;
+            for (i = 0; i < rowCount; ++i) {
+                Component c = r.getListCellRendererComponent(jList, getModel().getElementAt(i), i, false, false);
+                h += c.getPreferredSize().height;
+                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
         }
     }
 
-    /**
-     * Reinitializes this {@link JosmComboBox} to the specified values. This may be needed if a custom renderer is used.
-     * @param values The values displayed in the combo box.
-     * @since 5558
-     */
-    public final void reinitialize(Collection<E> values) {
-        init(findPrototypeDisplayValue(values), false);
-        discardAllUndoableEdits();
+    @Override
+    public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
+        // Who cares?
     }
 
-    /**
-     * Empties the internal undo manager, if any.
-     * @since 14977
-     */
-    public final void discardAllUndoableEdits() {
-        handler.discardAllUndoableEdits();
+    @Override
+    public void popupMenuCanceled(PopupMenuEvent e) {
+        // Who cares?
     }
+
+    @Override
+    public void propertyChange(PropertyChangeEvent evt) {
+        // follow our editor's orientation
+        if ("componentOrientation".equals(evt.getPropertyName())) {
+            setComponentOrientation((ComponentOrientation) evt.getNewValue());
+        }
+    }
 }
Index: src/org/openstreetmap/josm/gui/widgets/JosmComboBoxEditor.java
===================================================================
--- src/org/openstreetmap/josm/gui/widgets/JosmComboBoxEditor.java	(nonexistent)
+++ src/org/openstreetmap/josm/gui/widgets/JosmComboBoxEditor.java	(working copy)
@@ -0,0 +1,26 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.widgets;
+
+import javax.swing.plaf.basic.BasicComboBoxEditor;
+
+/**
+ * A {@link javax.swing.ComboBoxEditor} that uses an {@link JosmTextField}.
+ * <p>
+ * This lets us stick a {@code JosmTextField} into a {@link javax.swing.JComboBox}.
+ * Used in {@link JosmComboBox}.
+ *
+ * @since xxx
+ */
+public class JosmComboBoxEditor extends BasicComboBoxEditor {
+
+    @Override
+    protected JosmTextField createEditorComponent() {
+        return new JosmTextField();
+    }
+
+    @Override
+    public JosmTextField getEditorComponent() {
+        // this cast holds unless somebody overrides createEditorComponent()
+        return (JosmTextField) editor;
+    }
+}
Index: src/org/openstreetmap/josm/gui/widgets/JosmComboBoxModel.java
===================================================================
--- src/org/openstreetmap/josm/gui/widgets/JosmComboBoxModel.java	(nonexistent)
+++ src/org/openstreetmap/josm/gui/widgets/JosmComboBoxModel.java	(working copy)
@@ -0,0 +1,322 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.widgets;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.function.Function;
+
+import javax.swing.AbstractListModel;
+import javax.swing.MutableComboBoxModel;
+
+import org.openstreetmap.josm.data.preferences.ListProperty;
+import org.openstreetmap.josm.spi.preferences.Config;
+
+/**
+ * A data model for the {@link JosmComboBox}
+ *
+ * @author marcello@perathoner.de
+ * @param <E> The element type.
+ * @since xxx
+ */
+public class JosmComboBoxModel<E> extends AbstractListModel<E> implements MutableComboBoxModel<E>, Iterable<E> {
+
+    /** The maximum number of elements to hold, -1 for no limit. Used for histories. */
+    private int maxSize = -1;
+
+    /** the elements shown in the dropdown */
+    protected ArrayList<E> elements = new ArrayList<>();
+    /** the selected element in the dropdown or null */
+    protected Object selected;
+
+    /**
+     * Sets the maximum number of elements.
+     *
+     * @param size The maximal number of elements in the model.
+     */
+    public void setSize(int size) {
+        maxSize = size;
+    }
+
+    /**
+     * Returns a copy of the element list.
+     * @return a copy of the data
+     */
+    public Collection<E> asCollection() {
+        return new ArrayList<>(elements);
+    }
+
+    /**
+     * Returns the index of the specified element
+     *
+     * Note: This is not part of the {@link javax.swing.ComboBoxModel} interface but is defined in
+     * {@link javax.swing.DefaultComboBoxModel}.
+     *
+     * @param element the element to get the index of
+     * @return an int representing the index position, where 0 is the first position
+     */
+    public int getIndexOf(E element) {
+        return elements.indexOf(element);
+    }
+
+    //
+    // interface java.lang.Iterable
+    //
+
+    @Override
+    public Iterator<E> iterator() {
+        return elements.iterator();
+    }
+
+    //
+    // interface javax.swing.MutableComboBoxModel
+    //
+
+    /**
+     * Adds an element to the end of the model. Does nothing if max size is already reached.
+     */
+    @Override
+    public void addElement(E element) {
+        if (element != null && (maxSize == -1 || getSize() < maxSize)) {
+            elements.add(element);
+        }
+    }
+
+    @Override
+    public void removeElement(Object elem) {
+        elements.remove(elem);
+    }
+
+    @Override
+    public void removeElementAt(int index) {
+        Object elem = getElementAt(index);
+        if (elem == selected) {
+            if (index == 0) {
+                setSelectedItem(getSize() == 1 ? null : getElementAt(index + 1));
+            } else {
+                setSelectedItem(getElementAt(index - 1));
+            }
+        }
+        elements.remove(index);
+        fireIntervalRemoved(this, index, index);
+    }
+
+    /**
+     * Adds an element at a specific index.
+     *
+     * @param element The element to add
+     * @param index Location to add the element
+     */
+    @Override
+    public void insertElementAt(E element, int index) {
+        if (maxSize != -1 && maxSize <= getSize()) {
+            removeElementAt(getSize() - 1);
+        }
+        elements.add(index, element);
+    }
+
+    //
+    // javax.swing.ComboBoxModel
+    //
+
+    /**
+     * Set the value of the selected item. The selected item may be null.
+     *
+     * @param elem The combo box value or null for no selection.
+     */
+    @Override
+    public void setSelectedItem(Object elem) {
+        if ((selected != null && !selected.equals(elem)) ||
+            (selected == null && elem != null)) {
+            selected = elem;
+            fireContentsChanged(this, -1, -1);
+        }
+    }
+
+    @Override
+    public Object getSelectedItem() {
+        return selected;
+    }
+
+    //
+    // javax.swing.ListModel
+    //
+
+    @Override
+    public int getSize() {
+        return elements.size();
+    }
+
+    @Override
+    public E getElementAt(int index) {
+        if (index >= 0 && index < elements.size())
+            return elements.get(index);
+        else
+            return null;
+    }
+
+    //
+    // end interfaces
+    //
+
+    /**
+     * Adds all elements from the collection.
+     *
+     * @param elems The elements to add.
+     */
+    public void addAllElements(Collection<E> elems) {
+        elems.forEach(e -> addElement(e));
+    }
+
+    /**
+     * Adds all elements from the collection of string representations.
+     *
+     * @param strings The string representation of the elements to add.
+     * @param buildE A {@link java.util.function.Function} that builds an {@code <E>} from a
+     *               {@code String}.
+     */
+    public void addAllElements(Collection<String> strings, Function<String, E> buildE) {
+        strings.forEach(s -> addElement(buildE.apply(s)));
+    }
+
+    /**
+     * 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,
+     * deletes the last element.
+     *
+     * @param newElement the element to add
+     * @return The element that is at the top now.
+     */
+    public E addTopElement(E newElement) {
+        // if the element is already at the top, do nothing
+        if (newElement.equals(getElementAt(0)))
+            return getElementAt(0);
+
+        removeElement(newElement);
+        insertElementAt(newElement, 0);
+        return newElement;
+    }
+
+    /**
+     * Empties the list.
+     */
+    public void removeAllElements() {
+        if (!elements.isEmpty()) {
+            int firstIndex = 0;
+            int lastIndex = elements.size() - 1;
+            elements.clear();
+            selected = null;
+            fireIntervalRemoved(this, firstIndex, lastIndex);
+        } else {
+            selected = null;
+        }
+    }
+
+    /**
+     * Finds the item that matches string.
+     * <p>
+     * Looks in the model for an element whose {@code toString()} matches {@code s}.
+     *
+     * @param s The string to match.
+     * @return The item or null
+     */
+    public E find(String s) {
+        return elements.stream().filter(o -> o.toString().equals(s)).findAny().orElse(null);
+    }
+
+    /**
+     * Gets a preference loader and saver.
+     *
+     * @param readE A {@link Function} that builds an {@code <E>} from a {@link String}.
+     * @param writeE A {@code Function} that serializes an {@code <E>} to a {@code String}
+     * @return The {@link Preferences} instance.
+     */
+    public Preferences prefs(Function<String, E> readE, Function<E, String> writeE) {
+        return new Preferences(readE, writeE);
+    }
+
+    /**
+     * Loads and saves the model to the JOSM preferences.
+     * <p>
+     * Obtainable through {@link #prefs}.
+     */
+    public final class Preferences {
+
+        /** A {@link Function} that builds an {@code <E>} from a {@code String}. */
+        private Function<String, E> readE;
+        /** A {@code Function} that serializes {@code <E>} to a {@code String}. */
+        private Function<E, String> writeE;
+
+        /**
+         * Private constructor
+         *
+         * @param readE A {@link Function} that builds an {@code <E>} from a {@code String}.
+         * @param writeE A {@code Function} that serializes an {@code <E>} to a {@code String}
+         */
+        private Preferences(Function<String, E> readE, Function<E, String> writeE) {
+            this.readE = readE;
+            this.writeE = writeE;
+        }
+
+        /**
+         * Loads the model from the JOSM preferences.
+         * @param key The preferences key
+         */
+        public void load(String key) {
+            removeAllElements();
+            addAllElements(Config.getPref().getList(key), readE);
+        }
+
+        /**
+         * Loads the model from the JOSM preferences.
+         *
+         * @param key The preferences key
+         * @param defaults A list of default values.
+         */
+        public void load(String key, List<String> defaults) {
+            removeAllElements();
+            addAllElements(Config.getPref().getList(key, defaults), readE);
+        }
+
+        /**
+         * Loads the model from the JOSM preferences.
+         *
+         * @param prop The property holding the strings.
+         */
+        public void load(ListProperty prop) {
+            removeAllElements();
+            addAllElements(prop.get(), readE);
+        }
+
+        /**
+         * Returns the model elements as list of strings.
+         *
+         * @return a list of strings
+         */
+        public List<String> asStringList() {
+            List<String> list = new ArrayList<>(getSize());
+            forEach(element -> list.add(writeE.apply(element)));
+            return list;
+        }
+
+        /**
+         * Saves the model to the JOSM preferences.
+         *
+        * @param key The preferences key
+        */
+        public void save(String key) {
+            Config.getPref().putList(key, asStringList());
+        }
+
+        /**
+         * Saves the model to the JOSM preferences.
+         *
+         * @param prop The property to write to.
+         */
+        public void save(ListProperty prop) {
+            prop.put(asStringList());
+        }
+    }
+}
Index: src/org/openstreetmap/josm/gui/widgets/JosmListCellRenderer.java
===================================================================
--- src/org/openstreetmap/josm/gui/widgets/JosmListCellRenderer.java	(nonexistent)
+++ src/org/openstreetmap/josm/gui/widgets/JosmListCellRenderer.java	(working copy)
@@ -0,0 +1,34 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.widgets;
+
+import java.awt.Component;
+
+import javax.swing.JList;
+import javax.swing.ListCellRenderer;
+
+/**
+ * A convenience list cell renderer to override.
+ *
+ * @param <E> The type of the ListCellRenderer
+ */
+public class JosmListCellRenderer<E> implements ListCellRenderer<E> {
+    protected ListCellRenderer<? super E> renderer;
+    protected Component component;
+
+    /**
+     * Constructor.
+     * @param component the component
+     * @param renderer The inner renderer
+     */
+    public JosmListCellRenderer(Component component, ListCellRenderer<? super E> renderer) {
+        this.component = component;
+        this.renderer = renderer;
+    }
+
+    @Override
+    public Component getListCellRendererComponent(JList<? extends E> list, E value, int index, boolean isSelected, boolean cellHasFocus) {
+        Component l = renderer.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
+        l.setComponentOrientation(component.getComponentOrientation());
+        return l;
+    }
+}
Index: src/org/openstreetmap/josm/gui/widgets/JosmTextField.java
===================================================================
--- src/org/openstreetmap/josm/gui/widgets/JosmTextField.java	(revision 18220)
+++ src/org/openstreetmap/josm/gui/widgets/JosmTextField.java	(working copy)
@@ -2,18 +2,28 @@
 package org.openstreetmap.josm.gui.widgets;
 
 import java.awt.Color;
+import java.awt.ComponentOrientation;
+import java.awt.Font;
 import java.awt.FontMetrics;
 import java.awt.Graphics;
 import java.awt.Graphics2D;
 import java.awt.Insets;
+import java.awt.Point;
 import java.awt.RenderingHints;
+import java.awt.event.ComponentEvent;
+import java.awt.event.ComponentListener;
 import java.awt.event.FocusEvent;
 import java.awt.event.FocusListener;
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
 
-import javax.swing.BorderFactory;
 import javax.swing.Icon;
 import javax.swing.JTextField;
-import javax.swing.border.Border;
+import javax.swing.RepaintManager;
+import javax.swing.JMenuItem;
+import javax.swing.JPopupMenu;
+import javax.swing.UIManager;
+import javax.swing.text.BadLocationException;
 import javax.swing.text.Document;
 
 import org.openstreetmap.josm.gui.MainApplication;
@@ -30,12 +40,14 @@
  * </ul><br>This class must be used everywhere in core and plugins instead of {@code JTextField}.
  * @since 5886
  */
-public class JosmTextField extends JTextField implements Destroyable, FocusListener {
+public class JosmTextField extends JTextField implements Destroyable, ComponentListener, FocusListener, PropertyChangeListener {
 
     private final PopupMenuLauncher launcher;
     private String hint;
     private Icon icon;
-    private int leftInsets;
+    private Point iconPos;
+    private Insets originalMargin;
+    private OrientationAction orientationAction;
 
     /**
      * Constructs a new <code>JosmTextField</code> that uses the given text
@@ -77,13 +89,30 @@
     public JosmTextField(Document doc, String text, int columns, boolean undoRedo) {
         super(doc, text, columns);
         launcher = TextContextualPopupMenu.enableMenuFor(this, undoRedo);
+
+        // There seems to be a bug in Swing 8 that components with Bidi enabled are smaller than
+        // without. (eg. 23px vs 21px in height, maybe a font thing).  Usually Bidi starts disabled
+        // but gets enabled whenever RTL text is loaded.  To avoid trashing the layout we enable
+        // Bidi by default.  See also {@link #drawHint()}.
+        getDocument().putProperty("i18n", Boolean.TRUE);
+
+        // the menu and hotkey to change text orientation
+        orientationAction = new OrientationAction(this);
+        orientationAction.addPropertyChangeListener(this);
+        JPopupMenu menu = launcher.getMenu();
+        menu.addSeparator();
+        menu.add(new JMenuItem(orientationAction));
+        getInputMap().put(OrientationAction.getShortcutKey(), orientationAction);
+
         // Fix minimum size when columns are specified
         if (columns > 0) {
             setMinimumSize(getPreferredSize());
         }
         addFocusListener(this);
+        addComponentListener(this);
         // Workaround for Java bug 6322854
         JosmPasswordField.workaroundJdkBug6322854(this);
+        originalMargin = getMargin();
     }
 
     /**
@@ -147,13 +176,26 @@
     /**
      * Sets the hint to display when no text has been entered.
      * @param hint the hint to set
-     * @since 7505
+     * @return the old hint
+     * @since xxx (signature)
      */
-    public final void setHint(String hint) {
+    public String setHint(String hint) {
+        String old = hint;
         this.hint = hint;
+        return old;
     }
 
     /**
+     * Return true if the textfield should display the hint text.
+     *
+     * @return whether to display the hint text
+     * @since xxx
+     */
+    public boolean displayHint() {
+        return !Utils.isEmpty(hint) && getText().isEmpty() && !isFocusOwner();
+    }
+
+    /**
      * Returns the icon to display
      * @return the icon to display
      * @since 17768
@@ -169,14 +211,35 @@
      */
     public void setIcon(Icon icon) {
         this.icon = icon;
+        if (icon == null) {
+            setMargin(originalMargin);
+        }
+        positionIcon();
+    }
+
+    private void positionIcon() {
         if (icon != null) {
-            this.leftInsets = getInsets().left;
-            Border original = getBorder();
-            Border margin = BorderFactory.createEmptyBorder(0, icon.getIconWidth(), 0, 0);
-            setBorder(original == null ? margin : BorderFactory.createCompoundBorder(original, margin));
+            Insets margin = (Insets) originalMargin.clone();
+            int hGap = (getHeight() - icon.getIconHeight()) / 2;
+            if (getComponentOrientation() == ComponentOrientation.RIGHT_TO_LEFT) {
+                margin.right += icon.getIconWidth() + 2 * hGap;
+                iconPos = new Point(getWidth() - icon.getIconWidth() - hGap, hGap);
+            } else {
+                margin.left += icon.getIconWidth() + 2 * hGap;
+                iconPos = new Point(hGap, hGap);
+            }
+            setMargin(margin);
         }
     }
 
+    @Override
+    public void setComponentOrientation(ComponentOrientation o) {
+        if (o.isLeftToRight() != getComponentOrientation().isLeftToRight()) {
+            super.setComponentOrientation(o);
+            positionIcon();
+        }
+    }
+
     /**
      * Empties the internal undo manager.
      * @since 14977
@@ -185,31 +248,71 @@
         launcher.discardAllUndoableEdits();
     }
 
+    /**
+     * Returns the color for hint texts.
+     * @return the Color for hint texts
+     */
+    public static Color getHintTextColor() {
+        Color color = UIManager.getColor("TextField[Disabled].textForeground"); // Nimbus?
+        if (color == null)
+            color = UIManager.getColor("TextField.inactiveForeground");
+        if (color == null)
+            color = Color.GRAY;
+        return color;
+    }
+
+    /**
+     * Returns the font for hint texts.
+     * @return the font for hint texts
+     */
+    public static Font getHintFont() {
+        return UIManager.getFont("TextField.font");
+    }
+
     @Override
-    public void paint(Graphics g) {
-        super.paint(g);
+    public void paintComponent(Graphics g) {
+        super.paintComponent(g);
         if (icon != null) {
-            int h = getHeight() - icon.getIconHeight();
-            icon.paintIcon(this, g, Math.min(leftInsets, h / 2), h / 2);
+            icon.paintIcon(this, g, iconPos.x, iconPos.y);
         }
-        if (!Utils.isEmpty(hint) && getText().isEmpty() && !isFocusOwner()) {
-            // Taken from http://stackoverflow.com/a/24571681/2257172
-            int h = getHeight();
-            if (g instanceof Graphics2D) {
-                ((Graphics2D) g).setRenderingHint(
-                        RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
-            }
-            Insets ins = getInsets();
-            FontMetrics fm = g.getFontMetrics();
-            int c0 = getBackground().getRGB();
-            int c1 = getForeground().getRGB();
-            int m = 0xfefefefe;
-            int c2 = ((c0 & m) >>> 1) + ((c1 & m) >>> 1);
-            g.setColor(new Color(c2, true));
-            g.drawString(hint, ins.left, h / 2 + fm.getAscent() / 2 - 2);
+        if (displayHint()) {
+            // Logging.debug("drawing textfield hint: {0}", getHint());
+            drawHint(g);
         }
     }
 
+    /**
+     * Draws the hint text over the editor component.
+     *
+     * @param g the graphics context
+     */
+    public void drawHint(Graphics g) {
+        int x;
+        try {
+            x = modelToView(0).x;
+        } catch (BadLocationException exc) {
+            return; // can't happen
+        }
+        // Taken from http://stackoverflow.com/a/24571681/2257172
+        if (g instanceof Graphics2D) {
+            ((Graphics2D) g).setRenderingHint(
+                    RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
+        }
+        g.setColor(getHintTextColor());
+        g.setFont(getHintFont());
+        if (getComponentOrientation().isLeftToRight()) {
+            g.drawString(getHint(), x, getBaseline(getWidth(), getHeight()));
+        } else {
+            FontMetrics metrics = g.getFontMetrics(g.getFont());
+            int dx = metrics.stringWidth(getHint());
+            g.drawString(getHint(), x - dx, getBaseline(getWidth(), getHeight()));
+        }
+        // Needed to avoid endless repaint loop if we accidentally draw over the insets.  This may
+        // easily happen because a change in text orientation invalidates the textfield and
+        // following that the preferred size gets smaller. (Bug in Swing?)
+        RepaintManager.currentManager(this).markCompletelyClean(this);
+    }
+
     @Override
     public void focusGained(FocusEvent e) {
         MapFrame map = MainApplication.getMap();
@@ -216,7 +319,13 @@
         if (map != null) {
             map.keyDetector.setEnabled(false);
         }
-        repaint();
+        if (e != null && e.getOppositeComponent() != null) {
+            // Select all characters when the change of focus occurs inside JOSM only.
+            // When switching from another application, it is annoying, see #13747
+            selectAll();
+        }
+        positionIcon();
+        repaint(); // get rid of hint
     }
 
     @Override
@@ -225,7 +334,7 @@
         if (map != null) {
             map.keyDetector.setEnabled(true);
         }
-        repaint();
+        repaint(); // paint hint
     }
 
     @Override
@@ -233,4 +342,29 @@
         removeFocusListener(this);
         TextContextualPopupMenu.disableMenuFor(this, launcher);
     }
+
+    @Override
+    public void componentResized(ComponentEvent e) {
+        positionIcon();
+    }
+
+    @Override
+    public void componentMoved(ComponentEvent e) {
+    }
+
+    @Override
+    public void componentShown(ComponentEvent e) {
+    }
+
+    @Override
+    public void componentHidden(ComponentEvent e) {
+    }
+
+    @Override
+    public void propertyChange(PropertyChangeEvent evt) {
+        // command from the menu / shortcut key
+        if ("orientationAction".equals(evt.getPropertyName())) {
+            setComponentOrientation((ComponentOrientation) evt.getNewValue());
+        }
+    }
 }
Index: src/org/openstreetmap/josm/gui/widgets/OrientationAction.java
===================================================================
--- src/org/openstreetmap/josm/gui/widgets/OrientationAction.java	(nonexistent)
+++ src/org/openstreetmap/josm/gui/widgets/OrientationAction.java	(working copy)
@@ -0,0 +1,210 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.widgets;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.Component;
+import java.awt.ComponentOrientation;
+import java.awt.event.ActionEvent;
+import java.awt.event.KeyEvent;
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.swing.AbstractAction;
+import javax.swing.Action;
+import javax.swing.ImageIcon;
+import javax.swing.KeyStroke;
+
+import org.openstreetmap.josm.data.preferences.ListProperty;
+import org.openstreetmap.josm.gui.MainApplication;
+import org.openstreetmap.josm.spi.preferences.Config;
+import org.openstreetmap.josm.tools.ImageProvider;
+import org.openstreetmap.josm.tools.PlatformManager;
+
+/**
+ * An action that toggles text orientation.
+ */
+public class OrientationAction extends AbstractAction implements PropertyChangeListener {
+    /** Default for {@link #RTL_LANGUAGES} */
+    public static final List<String> DEFAULT_RTL_LANGUAGES = Arrays.asList("ar", "he", "fa", "iw", "ur", "lld");
+
+    /** Default for {@link #LOCALIZED_KEYS} */
+    public static final List<String> DEFAULT_LOCALIZED_KEYS = Arrays.asList(
+        "(\\p{Alnum}+_)?name", "addr", "description", "fixme", "note", "source", "strapline", "operator");
+
+    /**
+     * Language codes of languages that are right-to-left
+     *
+     * @see #getValueOrientation
+     */
+    public static final ListProperty RTL_LANGUAGES = new ListProperty("properties.rtl-languages", DEFAULT_RTL_LANGUAGES);
+    /**
+     * Keys whose values are localized
+     *
+     * Regex fractions are allowed. The items will be merged into a regular expression.
+     *
+     * @see #getValueOrientation
+     */
+    public static final ListProperty LOCALIZED_KEYS = new ListProperty("properties.localized-keys", DEFAULT_LOCALIZED_KEYS);
+
+    private static final Pattern LANG_PATTERN = Pattern.compile(":([a-z]{2,3})$");
+
+    private Component component;
+    private ImageIcon iconRTL;
+    private ImageIcon iconLTR;
+    protected static Set<String> RTLLanguages = new HashSet<>(RTL_LANGUAGES.get());
+    protected static Pattern localizedKeys = compile_localized_keys();
+
+    /**
+     * Constructs a new {@code OrientationAction}.
+     *
+     * @param component The component to toggle
+     */
+    public OrientationAction(Component component) {
+        super(null);
+        this.component = component;
+        setEnabled(true);
+        if (Config.getPref().getBoolean("text.popupmenu.useicons", true)) {
+            iconLTR = new ImageProvider("dialogs/next").setSize(ImageProvider.ImageSizes.SMALLICON).get();
+            iconRTL = new ImageProvider("dialogs/previous").setSize(ImageProvider.ImageSizes.SMALLICON).get();
+        }
+        component.addPropertyChangeListener(this);
+        putValue(Action.ACCELERATOR_KEY, getShortcutKey());
+        updateState();
+    }
+
+    @Override
+    public void actionPerformed(ActionEvent e) {
+        firePropertyChange("orientationAction", null, getValue("newState"));
+    }
+
+    /**
+     * Updates the text and the icon.
+     */
+    public void updateState() {
+        if (component.getComponentOrientation().isLeftToRight()) {
+            putValue(Action.NAME, tr("Right to Left"));
+            putValue(Action.SMALL_ICON, iconRTL);
+            putValue(Action.SHORT_DESCRIPTION, tr("Switch the text orientation to Right-to-Left."));
+            putValue("newState", ComponentOrientation.RIGHT_TO_LEFT);
+        } else {
+            putValue(Action.NAME, tr("Left to Right"));
+            putValue(Action.SMALL_ICON, iconLTR);
+            putValue(Action.SHORT_DESCRIPTION, tr("Switch the text orientation to Left-to-Right."));
+            putValue("newState", ComponentOrientation.LEFT_TO_RIGHT);
+        }
+    }
+
+    @Override
+    public void propertyChange(PropertyChangeEvent evt) {
+        if ("componentOrientation".equals(evt.getPropertyName())) {
+            updateState();
+        }
+    }
+
+    /**
+     * Returns the shortcut key to assign to this action.
+     *
+     * @return the shortcut key
+     */
+    public static KeyStroke getShortcutKey() {
+        return KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, PlatformManager.getPlatform().getMenuShortcutKeyMaskEx());
+    }
+
+    /**
+     * Returns the default component orientation by the user's locale
+     *
+     * @return the default component orientation
+     */
+    public static ComponentOrientation getDefaultComponentOrientation() {
+        Component main = MainApplication.getMainFrame();
+        // is null while testing
+        return main != null ? main.getComponentOrientation() : ComponentOrientation.LEFT_TO_RIGHT;
+    }
+
+    /**
+     * Returns the text orientation of the value for the given key.
+     *
+     * This is intended for Preset Dialog comboboxes. The choices in the dropdown list are
+     * typically translated. Ideally the user needs not see the English value.
+     *
+     * The algorithm is as follows:
+     * <ul>
+     * <li>If the key has an explicit language suffix, return the text orientation for that
+     * language.
+     * <li>Else return the text orientation of the user's locale.
+     * </ul>
+     *
+     * You can configure which languages are RTL with the list property: {@code rtl-languages}.
+     *
+     * @param key the key
+     * @return the text orientation of the value
+     */
+    public static ComponentOrientation getValueOrientation(String key) {
+        if (key == null || key.isEmpty())
+            return ComponentOrientation.LEFT_TO_RIGHT;
+
+        // if the key has an explicit language suffix, use it
+        Matcher m = LANG_PATTERN.matcher(key);
+        if (m.find()) {
+            if (RTLLanguages.contains(m.group(1))) {
+                return ComponentOrientation.RIGHT_TO_LEFT;
+            }
+            return ComponentOrientation.LEFT_TO_RIGHT;
+        }
+        // return the user's locale
+        return ComponentOrientation.getOrientation(Locale.getDefault());
+    }
+
+    /**
+     * Returns the text orientation of the value for the given key.
+     *
+     * This expansion of {@link #getValueOrientation} is intended for Preset Dialog textfields and
+     * for the Add Tag and Edit Tag dialog comboboxes.
+     *
+     * The algorithm is as follows:
+     * <ul>
+     * <li>If the key has an explicit language suffix, return the text orientation for that
+     * language.
+     * <li>If the key is usually localized, return the text orientation of the user's locale.
+     * <li>Else return left to right.
+     * </ul>
+     *
+     * You can configure which keys are localized with the list property: {@code localized-keys}.
+     * You can configure which languages are RTL with the list property: {@code rtl-languages}.
+     *
+     * @param key the key
+     * @return the text orientation of the value
+     */
+    public static ComponentOrientation getNamelikeOrientation(String key) {
+        if (key == null || key.isEmpty())
+            return ComponentOrientation.LEFT_TO_RIGHT;
+
+        // if the key has an explicit language suffix, use it
+        Matcher m = LANG_PATTERN.matcher(key);
+        if (m.find()) {
+            if (RTLLanguages.contains(m.group(1))) {
+                return ComponentOrientation.RIGHT_TO_LEFT;
+            }
+            return ComponentOrientation.LEFT_TO_RIGHT;
+        }
+        // if the key is usually localized, use the user's locale
+        m = localizedKeys.matcher(key);
+        if (m.find()) {
+            return ComponentOrientation.getOrientation(Locale.getDefault());
+        }
+        // all other keys are LTR
+        return ComponentOrientation.LEFT_TO_RIGHT;
+    }
+
+    private static Pattern compile_localized_keys() {
+        return Pattern.compile("^(" + String.join("|", LOCALIZED_KEYS.get()) + ")$");
+    }
+}
Index: src/org/openstreetmap/josm/tools/Logging.java
===================================================================
--- src/org/openstreetmap/josm/tools/Logging.java	(revision 18220)
+++ src/org/openstreetmap/josm/tools/Logging.java	(working copy)
@@ -436,6 +436,17 @@
     }
 
     /**
+     * Returns a stack trace as string
+     *
+     * @return the stack trace
+     */
+    public static String getStackTrace() {
+        StringWriter sb = new StringWriter();
+        new Throwable().printStackTrace(new PrintWriter(sb));
+        return sb.toString();
+    }
+
+    /**
      * Returns a human-readable message of error, also usable for developers.
      * @param t The error
      * @return The human-readable error message
Index: test/unit/org/openstreetmap/josm/gui/conflict/tags/MultiValueCellEditorTest.java
===================================================================
--- test/unit/org/openstreetmap/josm/gui/conflict/tags/MultiValueCellEditorTest.java	(revision 18220)
+++ test/unit/org/openstreetmap/josm/gui/conflict/tags/MultiValueCellEditorTest.java	(working copy)
@@ -4,10 +4,12 @@
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 
 import org.junit.jupiter.api.Test;
+import org.openstreetmap.josm.testutils.annotations.BasicPreferences;
 
 /**
  * Unit tests of {@link MultiValueCellEditor} class.
  */
+@BasicPreferences
 class MultiValueCellEditorTest {
     /**
      * Unit test of {@link MultiValueCellEditor#MultiValueCellEditor}.
Index: test/unit/org/openstreetmap/josm/gui/tagging/presets/items/PresetListEntryTest.java
===================================================================
--- test/unit/org/openstreetmap/josm/gui/tagging/presets/items/PresetListEntryTest.java	(revision 18220)
+++ test/unit/org/openstreetmap/josm/gui/tagging/presets/items/PresetListEntryTest.java	(working copy)
@@ -1,7 +1,7 @@
 // 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 org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Test;
@@ -25,6 +25,6 @@
      */
     @Test
     void testTicket12416() {
-        assertEquals("&nbsp;", new PresetListEntry("").getListDisplay());
+        assertTrue(new PresetListEntry("").getListDisplay(200).contains(" "));
     }
 }
